Skip to main content
Glama
dj-pearson

Supabase Coolify MCP Server

by dj-pearson
supabase-client.ts22 kB
/** * Supabase Client Wrapper for self-hosted Supabase management */ import { createClient, SupabaseClient } from '@supabase/supabase-js'; import axios, { AxiosInstance } from 'axios'; import type { SupabaseConfig, SupabaseDatabaseConfig, Migration, EdgeFunction, EdgeFunctionLog, StorageBucket, SupabaseAuthConfig, RealtimeConfig, SupabaseServiceStatus, ToolResponse, } from './types.js'; export class SupabaseManager { private client: SupabaseClient; private adminClient: AxiosInstance; private config: SupabaseConfig; private dbConfig?: SupabaseDatabaseConfig; constructor(config: SupabaseConfig, dbConfig?: SupabaseDatabaseConfig) { this.config = config; this.dbConfig = dbConfig; // Initialize Supabase client this.client = createClient(config.url, config.serviceRoleKey, { auth: { autoRefreshToken: false, persistSession: false, }, }); // Initialize admin API client this.adminClient = axios.create({ baseURL: config.url, headers: { 'Authorization': `Bearer ${config.serviceRoleKey}`, 'Content-Type': 'application/json', 'apikey': config.serviceRoleKey, }, }); } // ==================== Database Migrations ==================== /** * List all migrations with their status */ async listMigrations(): Promise<ToolResponse<Migration[]>> { try { const { data, error } = await this.client .from('supabase_migrations.schema_migrations') .select('*') .order('version', { ascending: false }); if (error) throw error; const migrations: Migration[] = (data || []).map((row: any) => ({ version: row.version, name: row.name || `Migration ${row.version}`, executed_at: row.inserted_at, status: 'applied' as const, })); return { success: true, data: migrations, }; } catch (error) { return this.handleError(error, 'Failed to list migrations'); } } /** * Deploy a new migration */ async deployMigration(sql: string, name: string): Promise<ToolResponse<Migration>> { try { const version = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14); // Execute the migration SQL directly using the admin client // PostgREST doesn't support arbitrary SQL execution, so we need to use direct execution const { error } = await this.client.rpc('exec', { query: sql }); if (error) { // If rpc doesn't work, try using pg_stat_statements or direct query // For self-hosted Supabase, we'll need to execute via admin API try { await this.adminClient.post('/rest/v1/rpc/query', { q: sql }); } catch { // As a last resort, record the migration without executing // (user will need to execute SQL manually via CLI or dashboard) console.error('Warning: Could not execute migration SQL automatically.'); console.error('Please execute the following SQL manually in your Supabase dashboard:'); console.error(sql); // Still record it as pending const migration: Migration = { version, name, executed_at: new Date().toISOString(), status: 'pending', sql, }; return { success: false, error: `Migration created but SQL execution failed: ${error.message}. Execute SQL manually in Supabase dashboard, then use CLI: supabase migration list`, data: migration, }; } } const migration: Migration = { version, name, executed_at: new Date().toISOString(), status: 'applied', sql, }; return { success: true, data: migration, message: `Migration ${name} (${version}) created. Note: For self-hosted Supabase, you may need to apply migrations using the Supabase CLI: supabase db push`, }; } catch (error) { return this.handleError(error, 'Failed to deploy migration'); } } /** * Execute raw SQL query * Note: Self-hosted Supabase may not have exec_sql RPC function * Users should use Supabase CLI or dashboard for direct SQL execution */ async executeSQL(sql: string): Promise<ToolResponse<any>> { try { // Try using pg_stat_statements extension if available const { data, error } = await this.client.rpc('exec', { query: sql }); if (error) { return { success: false, error: `SQL execution not supported via REST API. Please use one of these alternatives:\n` + `1. Supabase CLI: supabase db execute --file your-file.sql\n` + `2. Supabase Dashboard: SQL Editor\n` + `3. Direct psql connection: psql $SUPABASE_DB_URL\n` + `Error: ${error.message}`, }; } return { success: true, data, message: 'SQL executed successfully', }; } catch (error) { return this.handleError(error, 'Failed to execute SQL'); } } /** * Get migration status */ async getMigrationStatus(version: string): Promise<ToolResponse<Migration>> { try { const { data, error } = await this.client .from('supabase_migrations.schema_migrations') .select('*') .eq('version', version) .single(); if (error) throw error; const migration: Migration = { version: data.version, name: data.name || `Migration ${data.version}`, executed_at: data.inserted_at, status: 'applied', }; return { success: true, data: migration, }; } catch (error) { return this.handleError(error, `Failed to get migration status for version ${version}`); } } /** * Rollback a specific migration */ async rollbackMigration(version: string, downSql?: string): Promise<ToolResponse<void>> { try { // If down SQL is provided, execute it if (downSql) { const { error: execError } = await this.client.rpc('exec_sql', { sql_query: downSql }); if (execError) { const rawResult = await this.executeSQL(downSql); if (!rawResult.success) throw new Error(rawResult.error); } } // Remove migration from history const { error: deleteError } = await this.client .from('supabase_migrations.schema_migrations') .delete() .eq('version', version); if (deleteError) { // Try alternative approach await this.executeSQL( `DELETE FROM supabase_migrations.schema_migrations WHERE version = '${version}'` ); } return { success: true, message: `Migration ${version} rolled back successfully`, }; } catch (error) { return this.handleError(error, `Failed to rollback migration ${version}`); } } /** * Rollback to a specific migration version (rolls back all migrations after this version) */ async rollbackToVersion(targetVersion: string): Promise<ToolResponse<{ rolledBack: string[] }>> { try { // Get all migrations after the target version const { data: migrations, error } = await this.client .from('supabase_migrations.schema_migrations') .select('version, name') .gt('version', targetVersion) .order('version', { ascending: false }); if (error) throw error; if (!migrations || migrations.length === 0) { return { success: true, data: { rolledBack: [] }, message: 'No migrations to rollback', }; } const rolledBackVersions: string[] = []; // Note: This is a simplified rollback - in production, you'd want to store // down migration SQL with each migration for proper rollback for (const migration of migrations) { // Remove from migration history const { error: deleteError } = await this.client .from('supabase_migrations.schema_migrations') .delete() .eq('version', migration.version); if (!deleteError) { rolledBackVersions.push(migration.version); } } return { success: true, data: { rolledBack: rolledBackVersions }, message: `Rolled back ${rolledBackVersions.length} migration(s) to version ${targetVersion}`, }; } catch (error) { return this.handleError(error, `Failed to rollback to version ${targetVersion}`); } } /** * Rollback last N migrations */ async rollbackLastMigrations(count: number = 1): Promise<ToolResponse<{ rolledBack: string[] }>> { try { // Get the last N migrations const { data: migrations, error } = await this.client .from('supabase_migrations.schema_migrations') .select('version, name') .order('version', { ascending: false }) .limit(count); if (error) throw error; if (!migrations || migrations.length === 0) { return { success: true, data: { rolledBack: [] }, message: 'No migrations to rollback', }; } const rolledBackVersions: string[] = []; for (const migration of migrations) { const { error: deleteError } = await this.client .from('supabase_migrations.schema_migrations') .delete() .eq('version', migration.version); if (!deleteError) { rolledBackVersions.push(migration.version); } } return { success: true, data: { rolledBack: rolledBackVersions }, message: `Rolled back ${rolledBackVersions.length} migration(s)`, }; } catch (error) { return this.handleError(error, 'Failed to rollback migrations'); } } /** * Create a migration with both up and down SQL for proper rollback support */ async createMigrationWithRollback( name: string, upSql: string, downSql: string ): Promise<ToolResponse<Migration>> { try { const version = new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14); // Execute the up migration const deployResult = await this.deployMigration(upSql, name); if (!deployResult.success) { return deployResult; } // Store the down migration SQL for future rollback // This would typically be stored in a migrations table or file system const { error: storeError } = await this.client .from('_migration_rollbacks') .insert({ version, name, down_sql: downSql, created_at: new Date().toISOString(), }); if (storeError) { // Table might not exist, create it await this.executeSQL(` CREATE TABLE IF NOT EXISTS _migration_rollbacks ( version TEXT PRIMARY KEY, name TEXT, down_sql TEXT, created_at TIMESTAMP DEFAULT NOW() ); `); // Try again await this.client.from('_migration_rollbacks').insert({ version, name, down_sql: downSql, created_at: new Date().toISOString(), }); } return { success: true, data: deployResult.data!, message: `Migration ${name} deployed with rollback support`, }; } catch (error) { return this.handleError(error, 'Failed to create migration with rollback'); } } /** * Rollback a migration using its stored down SQL */ async rollbackMigrationWithDownSql(version: string): Promise<ToolResponse<void>> { try { // Get the down SQL for this migration const { data: rollbackData, error: fetchError } = await this.client .from('_migration_rollbacks') .select('down_sql') .eq('version', version) .single(); if (fetchError) { return { success: false, error: `No rollback SQL found for migration ${version}. Use rollbackMigration() with manual down SQL instead.`, }; } // Execute the down SQL return await this.rollbackMigration(version, rollbackData.down_sql); } catch (error) { return this.handleError(error, `Failed to rollback migration ${version}`); } } // ==================== Edge Functions ==================== /** * List all edge functions * Note: Edge functions listing via REST API may not be available on self-hosted */ async listEdgeFunctions(): Promise<ToolResponse<EdgeFunction[]>> { try { // Try to list functions using admin API const functionsUrl = this.config.functionsUrl || `${this.config.url}/functions/v1`; const response = await axios.get(functionsUrl, { headers: { 'Authorization': `Bearer ${this.config.serviceRoleKey}`, 'apikey': this.config.serviceRoleKey, }, validateStatus: (status) => status < 500, // Accept 4xx responses }); if (response.status === 404) { return { success: false, error: 'Edge Functions not configured on this Supabase instance. ' + 'For self-hosted Supabase, edge functions require additional setup. ' + 'Use Supabase CLI instead: supabase functions deploy', }; } if (response.status >= 400) { return { success: false, error: `Edge Functions API returned ${response.status}. ` + 'Edge functions may not be enabled or configured. ' + 'Use Supabase CLI for edge function management: supabase functions list', }; } return { success: true, data: response.data || [], }; } catch (error) { return this.handleError(error, 'Failed to list edge functions. Consider using Supabase CLI: supabase functions list'); } } /** * Deploy an edge function */ async deployEdgeFunction( name: string, code: string, verifyJWT: boolean = true ): Promise<ToolResponse<EdgeFunction>> { try { const functionsUrl = this.config.functionsUrl || `${this.config.url}/functions/v1`; await axios.post( `${functionsUrl}/${name}`, { code, verify_jwt: verifyJWT }, { headers: { 'Authorization': `Bearer ${this.config.serviceRoleKey}`, 'Content-Type': 'application/json', }, } ); const edgeFunction: EdgeFunction = { name, status: 'deployed', verify_jwt: verifyJWT, created_at: new Date().toISOString(), }; return { success: true, data: edgeFunction, message: `Edge function ${name} deployed successfully`, }; } catch (error) { return this.handleError(error, `Failed to deploy edge function ${name}`); } } /** * Delete an edge function */ async deleteEdgeFunction(name: string): Promise<ToolResponse<void>> { try { const functionsUrl = this.config.functionsUrl || `${this.config.url}/functions/v1`; await axios.delete(`${functionsUrl}/${name}`, { headers: { 'Authorization': `Bearer ${this.config.serviceRoleKey}`, }, }); return { success: true, message: `Edge function ${name} deleted successfully`, }; } catch (error) { return this.handleError(error, `Failed to delete edge function ${name}`); } } /** * Get edge function logs */ async getEdgeFunctionLogs(name: string, limit: number = 100): Promise<ToolResponse<EdgeFunctionLog[]>> { try { const { data, error } = await this.client .from('edge_logs') .select('*') .eq('function_name', name) .order('timestamp', { ascending: false }) .limit(limit); if (error) throw error; return { success: true, data: data || [], }; } catch (error) { return this.handleError(error, `Failed to get logs for edge function ${name}`); } } /** * Invoke an edge function */ async invokeEdgeFunction(name: string, payload?: any): Promise<ToolResponse<any>> { try { const { data, error } = await this.client.functions.invoke(name, { body: payload, }); if (error) throw error; return { success: true, data, }; } catch (error) { return this.handleError(error, `Failed to invoke edge function ${name}`); } } // ==================== Storage Management ==================== /** * List all storage buckets */ async listStorageBuckets(): Promise<ToolResponse<StorageBucket[]>> { try { const { data, error } = await this.client.storage.listBuckets(); if (error) throw error; return { success: true, data: data as StorageBucket[], }; } catch (error) { return this.handleError(error, 'Failed to list storage buckets'); } } /** * Create a storage bucket */ async createStorageBucket( id: string, isPublic: boolean = false, fileSizeLimit?: number ): Promise<ToolResponse<StorageBucket>> { try { const { error } = await this.client.storage.createBucket(id, { public: isPublic, fileSizeLimit, }); if (error) throw error; const bucket: StorageBucket = { id, name: id, public: isPublic, file_size_limit: fileSizeLimit, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; return { success: true, data: bucket, message: `Storage bucket ${id} created successfully`, }; } catch (error) { return this.handleError(error, `Failed to create storage bucket ${id}`); } } /** * Delete a storage bucket */ async deleteStorageBucket(id: string): Promise<ToolResponse<void>> { try { const { error } = await this.client.storage.deleteBucket(id); if (error) throw error; return { success: true, message: `Storage bucket ${id} deleted successfully`, }; } catch (error) { return this.handleError(error, `Failed to delete storage bucket ${id}`); } } // ==================== Auth Configuration ==================== /** * Get auth configuration */ async getAuthConfig(): Promise<ToolResponse<SupabaseAuthConfig>> { try { const response = await this.adminClient.get('/auth/v1/settings'); return { success: true, data: response.data, }; } catch (error) { return this.handleError(error, 'Failed to get auth configuration'); } } /** * Update auth configuration */ async updateAuthConfig(config: Partial<SupabaseAuthConfig>): Promise<ToolResponse<void>> { try { await this.adminClient.patch('/auth/v1/settings', config); return { success: true, message: 'Auth configuration updated successfully', }; } catch (error) { return this.handleError(error, 'Failed to update auth configuration'); } } // ==================== Realtime Configuration ==================== /** * Get realtime configuration */ async getRealtimeConfig(): Promise<ToolResponse<RealtimeConfig>> { try { const response = await this.adminClient.get('/realtime/v1/settings'); return { success: true, data: response.data, }; } catch (error) { return this.handleError(error, 'Failed to get realtime configuration'); } } // ==================== Health & Status ==================== /** * Check health of all Supabase services */ async checkHealth(): Promise<ToolResponse<SupabaseServiceStatus[]>> { try { const services: SupabaseServiceStatus[] = []; // Check Database try { await this.client.from('_health').select('*').limit(1); services.push({ name: 'Database', status: 'healthy' }); } catch { services.push({ name: 'Database', status: 'unhealthy' }); } // Check Auth try { await this.adminClient.get('/auth/v1/health'); services.push({ name: 'Auth', status: 'healthy' }); } catch { services.push({ name: 'Auth', status: 'unhealthy' }); } // Check Storage try { await this.client.storage.listBuckets(); services.push({ name: 'Storage', status: 'healthy' }); } catch { services.push({ name: 'Storage', status: 'unhealthy' }); } // Check Realtime try { await this.adminClient.get('/realtime/v1/health'); services.push({ name: 'Realtime', status: 'healthy' }); } catch { services.push({ name: 'Realtime', status: 'unhealthy' }); } return { success: true, data: services, }; } catch (error) { return this.handleError(error, 'Failed to check health'); } } /** * Get Supabase version information */ async getVersion(): Promise<ToolResponse<string>> { try { const response = await this.adminClient.get('/rest/v1/'); const version = response.headers['x-supabase-version'] || 'unknown'; return { success: true, data: version, }; } catch (error) { return this.handleError(error, 'Failed to get version'); } } /** * Handle errors uniformly */ private handleError(error: unknown, message: string): ToolResponse<never> { if (axios.isAxiosError(error)) { return { success: false, error: `${message}: ${error.response?.data?.message || error.message}`, }; } return { success: false, error: `${message}: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dj-pearson/supabase-coolify-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server