/**
* 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
};
}
}