Skip to main content
Glama
supabase.ts17.4 kB
/** * Supabase namespace - Deep Supabase integration */ import axios from 'axios'; import { MCPServer } from '../core/server.js'; import { ConfigMissingError, InvalidArgError, wrapError } from '../core/errors.js'; import { SupabaseInstance, SupabaseSqlResponse, SupabaseRestResponse, SupabaseRpcResponse, SupabaseStoragePutResponse, SupabaseStorageSignedUrlResponse, SupabaseAuthAdminResponse, SupabaseFunctionResponse, SupabaseProjectInfo, SupabaseHealthResponse, AuthAdminCommand } from '../types/supabase.js'; export class SupabaseNamespace { private mcpServer: MCPServer; private instances = new Map<string, SupabaseInstance>(); constructor(mcpServer: MCPServer) { this.mcpServer = mcpServer; this.initializeInstances(); this.registerTools(); } private initializeInstances(): void { const env = this.mcpServer.getEnvConfig(); // Initialize default instance from environment if (env.SUPABASE_URL) { this.instances.set('default', { instance: 'default', url: env.SUPABASE_URL, service_role_key: env.SUPABASE_SERVICE_ROLE, anon_key: env.SUPABASE_ANON }); } } private getInstance(name: string): SupabaseInstance { const instance = this.instances.get(name); if (!instance) { throw new ConfigMissingError(`SUPABASE_URL for instance '${name}'`); } return instance; } private registerTools(): void { const registry = this.mcpServer.getRegistry(); registry.registerTool( 'supabase.sql', { name: 'supabase.sql', description: 'Execute SQL query on Supabase', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, text: { type: 'string' }, params: { type: 'array' }, tx: { type: 'string', enum: ['none', 'begin', 'commit', 'rollback'] } }, required: ['instance', 'text'] } }, this.sql.bind(this) ); registry.registerTool( 'supabase.rest', { name: 'supabase.rest', description: 'Make REST API call to Supabase', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, method: { type: 'string', enum: ['GET', 'POST', 'PATCH', 'DELETE'] }, path: { type: 'string' }, query: { type: 'object' }, body: { type: ['object', 'array', 'null'] }, headers: { type: 'object' } }, required: ['instance', 'method', 'path'] } }, this.rest.bind(this) ); registry.registerTool( 'supabase.rpc', { name: 'supabase.rpc', description: 'Call Supabase RPC function', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, fn_name: { type: 'string' }, args: { type: 'object' } }, required: ['instance', 'fn_name'] } }, this.rpc.bind(this) ); registry.registerTool( 'supabase.storage_put', { name: 'supabase.storage_put', description: 'Upload file to Supabase Storage', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, bucket: { type: 'string' }, key: { type: 'string' }, data_base64: { type: 'string' }, content_type: { type: 'string' } }, required: ['instance', 'bucket', 'key', 'data_base64'] } }, this.storagePut.bind(this) ); registry.registerTool( 'supabase.storage_get_signed_url', { name: 'supabase.storage_get_signed_url', description: 'Get signed URL for Supabase Storage object', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, bucket: { type: 'string' }, key: { type: 'string' }, expires_sec: { type: 'number' } }, required: ['instance', 'bucket', 'key', 'expires_sec'] } }, this.storageGetSignedUrl.bind(this) ); registry.registerTool( 'supabase.storage_delete', { name: 'supabase.storage_delete', description: 'Delete file from Supabase Storage', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, bucket: { type: 'string' }, key: { type: 'string' } }, required: ['instance', 'bucket', 'key'] } }, this.storageDelete.bind(this) ); registry.registerTool( 'supabase.auth_admin', { name: 'supabase.auth_admin', description: 'Supabase Auth admin operations', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, cmd: { type: 'string', enum: ['invite', 'create_user', 'delete_user', 'get_user', 'list_users', 'set_role'] }, args: { type: 'object' } }, required: ['instance', 'cmd', 'args'] } }, this.authAdmin.bind(this) ); registry.registerTool( 'supabase.functions_invoke', { name: 'supabase.functions_invoke', description: 'Invoke Supabase Edge Function', inputSchema: { type: 'object', properties: { instance: { type: 'string' }, name: { type: 'string' }, body: { type: ['object', 'array', 'string', 'null'] }, headers: { type: 'object' } }, required: ['instance', 'name'] } }, this.functionsInvoke.bind(this) ); registry.registerTool( 'supabase.projects_info', { name: 'supabase.projects_info', description: 'Get Supabase project information', inputSchema: { type: 'object', properties: { instance: { type: 'string' } }, required: ['instance'] } }, this.projectsInfo.bind(this) ); registry.registerTool( 'supabase.health', { name: 'supabase.health', description: 'Check Supabase health status', inputSchema: { type: 'object', properties: { instance: { type: 'string' } }, required: ['instance'] } }, this.health.bind(this) ); } private async sql(params: { instance: string; text: string; params?: any[]; tx?: string; }): Promise<SupabaseSqlResponse> { const instance = this.getInstance(params.instance); if (!instance.service_role_key) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE'); } try { // Use Supabase REST API to execute SQL const response = await axios.post( `${instance.url}/rest/v1/rpc/sql`, { query: params.text, params: params.params || [] }, { headers: { 'apikey': instance.service_role_key, 'Authorization': `Bearer ${instance.service_role_key}`, 'Content-Type': 'application/json' } } ); // Parse response based on query type const isSelect = params.text.trim().toLowerCase().startsWith('select'); if (isSelect) { const rows = response.data || []; const cols = rows.length > 0 ? Object.keys(rows[0]) : []; return { rows, cols }; } else { return { rows: [], cols: [] }; } } catch (error) { throw wrapError(error); } } private async rest(params: { instance: string; method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; path: string; query?: object; body?: any; headers?: object; }): Promise<SupabaseRestResponse> { const instance = this.getInstance(params.instance); const apiKey = instance.service_role_key || instance.anon_key; if (!apiKey) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE or SUPABASE_ANON'); } try { const response = await axios({ method: params.method, url: `${instance.url}/rest/v1${params.path}`, params: params.query, data: params.body, headers: { 'apikey': apiKey, 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', 'Prefer': 'return=representation', ...(params.headers || {}) } }); return { status: response.status, headers: response.headers as Record<string, string>, json: response.data, text: typeof response.data === 'string' ? response.data : JSON.stringify(response.data) }; } catch (error) { throw wrapError(error); } } private async rpc(params: { instance: string; fn_name: string; args?: object; }): Promise<SupabaseRpcResponse> { const instance = this.getInstance(params.instance); const apiKey = instance.service_role_key || instance.anon_key; if (!apiKey) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE or SUPABASE_ANON'); } try { const response = await axios.post( `${instance.url}/rest/v1/rpc/${params.fn_name}`, params.args || {}, { headers: { 'apikey': apiKey, 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' } } ); return { status: 'ok', result: response.data }; } catch (error) { throw wrapError(error); } } private async storagePut(params: { instance: string; bucket: string; key: string; data_base64: string; content_type?: string; }): Promise<SupabaseStoragePutResponse> { const instance = this.getInstance(params.instance); if (!instance.service_role_key) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE'); } const buffer = Buffer.from(params.data_base64, 'base64'); try { const response = await axios.post( `${instance.url}/storage/v1/object/${params.bucket}/${params.key}`, buffer, { headers: { 'apikey': instance.service_role_key, 'Authorization': `Bearer ${instance.service_role_key}`, 'Content-Type': params.content_type || 'application/octet-stream' } } ); return { url: `${instance.url}/storage/v1/object/public/${params.bucket}/${params.key}`, etag: response.headers['etag'] }; } catch (error) { throw wrapError(error); } } private async storageGetSignedUrl(params: { instance: string; bucket: string; key: string; expires_sec: number; }): Promise<SupabaseStorageSignedUrlResponse> { const instance = this.getInstance(params.instance); if (!instance.service_role_key) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE'); } try { const response = await axios.post( `${instance.url}/storage/v1/object/sign/${params.bucket}/${params.key}`, { expiresIn: params.expires_sec }, { headers: { 'apikey': instance.service_role_key, 'Authorization': `Bearer ${instance.service_role_key}`, 'Content-Type': 'application/json' } } ); return { url: `${instance.url}/storage/v1${response.data.signedURL}` }; } catch (error) { throw wrapError(error); } } private async storageDelete(params: { instance: string; bucket: string; key: string; }): Promise<{ ok: true }> { const instance = this.getInstance(params.instance); if (!instance.service_role_key) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE'); } try { await axios.delete( `${instance.url}/storage/v1/object/${params.bucket}/${params.key}`, { headers: { 'apikey': instance.service_role_key, 'Authorization': `Bearer ${instance.service_role_key}` } } ); return { ok: true }; } catch (error) { throw wrapError(error); } } private async authAdmin(params: { instance: string; cmd: AuthAdminCommand; args: any; }): Promise<SupabaseAuthAdminResponse> { const instance = this.getInstance(params.instance); if (!instance.service_role_key) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE'); } let endpoint = ''; let method: 'GET' | 'POST' | 'DELETE' | 'PATCH' = 'POST'; let data: any = params.args; switch (params.cmd) { case 'invite': endpoint = '/auth/v1/invite'; break; case 'create_user': endpoint = '/auth/v1/admin/users'; break; case 'delete_user': endpoint = `/auth/v1/admin/users/${params.args.id}`; method = 'DELETE'; data = undefined; break; case 'get_user': endpoint = `/auth/v1/admin/users/${params.args.id}`; method = 'GET'; data = undefined; break; case 'list_users': endpoint = '/auth/v1/admin/users'; method = 'GET'; data = undefined; break; case 'set_role': endpoint = `/auth/v1/admin/users/${params.args.id}`; method = 'PATCH'; data = { role: params.args.role }; break; default: throw new InvalidArgError('cmd', `Unknown auth command: ${params.cmd}`); } try { const response = await axios({ method, url: `${instance.url}${endpoint}`, data, headers: { 'apikey': instance.service_role_key, 'Authorization': `Bearer ${instance.service_role_key}`, 'Content-Type': 'application/json' } }); return { result: response.data }; } catch (error) { throw wrapError(error); } } private async functionsInvoke(params: { instance: string; name: string; body?: any; headers?: object; }): Promise<SupabaseFunctionResponse> { const instance = this.getInstance(params.instance); const apiKey = instance.service_role_key || instance.anon_key; if (!apiKey) { throw new ConfigMissingError('SUPABASE_SERVICE_ROLE or SUPABASE_ANON'); } try { const response = await axios.post( `${instance.url}/functions/v1/${params.name}`, params.body, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', ...(params.headers || {}) } } ); return { status: response.status, json: response.data, text: typeof response.data === 'string' ? response.data : JSON.stringify(response.data) }; } catch (error) { throw wrapError(error); } } private async projectsInfo(params: { instance: string; }): Promise<SupabaseProjectInfo> { const instance = this.getInstance(params.instance); // Parse Supabase URL to extract project info const url = new URL(instance.url); const projectId = url.hostname.split('.')[0]; const region = url.hostname.split('.')[1] || 'us-east-1'; return { project_id: projectId, url: instance.url, region, db: { host: `db.${projectId}.supabase.co`, port: 5432, dbname: 'postgres', user: 'postgres' }, anon_key_present: !!instance.anon_key, service_role_present: !!instance.service_role_key }; } private async health(params: { instance: string; }): Promise<SupabaseHealthResponse> { const instance = this.getInstance(params.instance); const healthChecks = { db: 'down' as 'ok' | 'down', storage: 'down' as 'ok' | 'down', functions: 'down' as 'ok' | 'down' }; // Check database health try { await axios.get(`${instance.url}/rest/v1/`, { headers: { 'apikey': instance.anon_key || instance.service_role_key || '' }, timeout: 5000 }); healthChecks.db = 'ok'; } catch (error) { // Database is down } // Check storage health try { await axios.get(`${instance.url}/storage/v1/`, { timeout: 5000 }); healthChecks.storage = 'ok'; } catch (error) { // Storage is down } // Check functions health try { await axios.get(`${instance.url}/functions/v1/`, { timeout: 5000 }); healthChecks.functions = 'ok'; } catch (error) { // Functions are down } // Calculate overall status const healthyComponents = Object.values(healthChecks).filter(s => s === 'ok').length; let overallStatus: 'ok' | 'degraded' | 'down'; if (healthyComponents === 3) { overallStatus = 'ok'; } else if (healthyComponents > 0) { overallStatus = 'degraded'; } else { overallStatus = 'down'; } return { status: overallStatus, components: healthChecks }; } }

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/JacobFV/mcp-fullstack'

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