From 1977bb774c98cf2a05600a86029cade5f46812f4 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 23 Jun 2025 13:59:00 -0600 Subject: [PATCH 1/6] feat: conditional tools based on platform implementation --- README.md | 8 +- .../mcp-server-supabase/src/index.test.ts | 12 +- .../src/platform/api-platform.ts | 363 ++++++++++-------- .../mcp-server-supabase/src/platform/types.ts | 40 +- packages/mcp-server-supabase/src/pricing.ts | 8 +- .../mcp-server-supabase/src/server.test.ts | 83 ++-- packages/mcp-server-supabase/src/server.ts | 86 +++-- .../src/tools/account-tools.ts | 24 +- .../src/tools/branching-tools.ts | 18 +- .../src/tools/database-operation-tools.ts | 18 +- .../src/tools/debugging-tools.ts | 12 +- .../src/tools/development-tools.ts | 12 +- .../src/tools/edge-function-tools.ts | 10 +- .../src/tools/storage-tools.ts | 12 +- packages/mcp-server-supabase/src/types.ts | 14 + packages/mcp-server-supabase/src/util.ts | 59 ++- 16 files changed, 449 insertions(+), 330 deletions(-) create mode 100644 packages/mcp-server-supabase/src/types.ts diff --git a/README.md b/README.md index b25fc37..b6970eb 100644 --- a/README.md +++ b/README.md @@ -156,9 +156,9 @@ You can enable or disable specific tool groups by passing the `--features` flag npx -y @supabase/mcp-server-supabase@latest --features=database,docs ``` -Available groups are: [`account`](#account), [`docs`](#knowledge-base), [`database`](#database), [`debug`](#debug), [`development`](#development), [`functions`](#edge-functions), [`storage`](#storage), and [`branching`](#branching-experimental-requires-a-paid-plan). +Available groups are: [`account`](#account), [`docs`](#knowledge-base), [`database`](#database), [`debugging`](#debugging), [`development`](#development), [`functions`](#edge-functions), [`storage`](#storage), and [`branching`](#branching-experimental-requires-a-paid-plan). -If this flag is not passed, the default feature groups are: `account`, `database`, `debug`, `development`, `docs`, `functions`, and `branching`. +If this flag is not passed, the default feature groups are: `account`, `database`, `debugging`, `development`, `docs`, `functions`, and `branching`. ## Tools @@ -198,9 +198,9 @@ Enabled by default. Use `database` to target this group of tools with the [`--fe - `apply_migration`: Applies a SQL migration to the database. SQL passed to this tool will be tracked within the database, so LLMs should use this for DDL operations (schema changes). - `execute_sql`: Executes raw SQL in the database. LLMs should use this for regular queries that don't change the schema. -#### Debug +#### Debugging -Enabled by default. Use `debug` to target this group of tools with the [`--features`](#feature-groups) option. +Enabled by default. Use `debugging` to target this group of tools with the [`--features`](#feature-groups) option. - `get_logs`: Gets logs for a Supabase project by service type (api, postgres, edge functions, auth, storage, realtime). LLMs can use this to help with debugging and monitoring service performance. - `get_advisors`: Gets a list of advisory notices for a Supabase project. LLMs can use this to check for security vulnerabilities or performance issues. diff --git a/packages/mcp-server-supabase/src/index.test.ts b/packages/mcp-server-supabase/src/index.test.ts index 6a8c0f2..3c7110f 100644 --- a/packages/mcp-server-supabase/src/index.test.ts +++ b/packages/mcp-server-supabase/src/index.test.ts @@ -7,7 +7,7 @@ import { MCP_CLIENT_NAME, MCP_CLIENT_VERSION, } from '../test/mocks.js'; -import { createSupabaseMcpServer } from './index.js'; +import { createSupabaseApiPlatform, createSupabaseMcpServer } from './index.js'; type SetupOptions = { accessToken?: string; @@ -34,11 +34,13 @@ async function setup(options: SetupOptions = {}) { } ); + const platform = createSupabaseApiPlatform({ + apiUrl: API_URL, + accessToken, + }); + const server = createSupabaseMcpServer({ - platform: { - apiUrl: API_URL, - accessToken, - }, + platform, projectId, readOnly, features, diff --git a/packages/mcp-server-supabase/src/platform/api-platform.ts b/packages/mcp-server-supabase/src/platform/api-platform.ts index 8315a67..9e2d5ba 100644 --- a/packages/mcp-server-supabase/src/platform/api-platform.ts +++ b/packages/mcp-server-supabase/src/platform/api-platform.ts @@ -21,15 +21,22 @@ import { executeSqlOptionsSchema, getLogsOptionsSchema, resetBranchOptionsSchema, + type AccountOperations, type ApplyMigrationOptions, + type BranchingOperations, type CreateBranchOptions, type CreateProjectOptions, + type DatabaseOperations, + type DebuggingOperations, type DeployEdgeFunctionOptions, + type DevelopmentOperations, type EdgeFunction, + type EdgeFunctionsOperations, type ExecuteSqlOptions, type GetLogsOptions, type ResetBranchOptions, type StorageConfig, + type StorageOperations, type SupabasePlatform, } from './index.js'; @@ -62,22 +69,103 @@ export function createSupabaseApiPlatform( accessToken ); - const platform: SupabasePlatform = { - async init(info: InitData) { - const { clientInfo } = info; - if (!clientInfo) { - throw new Error('Client info is required'); - } + const account: AccountOperations = { + async listOrganizations() { + const response = await managementApiClient.GET('/v1/organizations'); - // Re-initialize the management API client with the user agent - managementApiClient = createManagementApiClient( - managementApiUrl, - accessToken, + assertSuccess(response, 'Failed to fetch organizations'); + + return response.data; + }, + async getOrganization(organizationId: string) { + const response = await managementApiClient.GET( + '/v1/organizations/{slug}', { - 'User-Agent': `supabase-mcp/${version} (${clientInfo.name}/${clientInfo.version})`, + params: { + path: { + slug: organizationId, + }, + }, + } + ); + + assertSuccess(response, 'Failed to fetch organization'); + + return response.data; + }, + async listProjects() { + const response = await managementApiClient.GET('/v1/projects'); + + assertSuccess(response, 'Failed to fetch projects'); + + return response.data; + }, + async getProject(projectId: string) { + const response = await managementApiClient.GET('/v1/projects/{ref}', { + params: { + path: { + ref: projectId, + }, + }, + }); + assertSuccess(response, 'Failed to fetch project'); + return response.data; + }, + async createProject(options: CreateProjectOptions) { + const { name, organization_id, region, db_pass } = + createProjectOptionsSchema.parse(options); + + const response = await managementApiClient.POST('/v1/projects', { + body: { + name, + region: region ?? (await getClosestRegion()), + organization_id, + db_pass: + db_pass ?? + generatePassword({ + length: 16, + numbers: true, + uppercase: true, + lowercase: true, + }), + }, + }); + + assertSuccess(response, 'Failed to create project'); + + return response.data; + }, + async pauseProject(projectId: string) { + const response = await managementApiClient.POST( + '/v1/projects/{ref}/pause', + { + params: { + path: { + ref: projectId, + }, + }, + } + ); + + assertSuccess(response, 'Failed to pause project'); + }, + async restoreProject(projectId: string) { + const response = await managementApiClient.POST( + '/v1/projects/{ref}/restore', + { + params: { + path: { + ref: projectId, + }, + }, } ); + + assertSuccess(response, 'Failed to restore project'); }, + }; + + const database: DatabaseOperations = { async executeSql(projectId: string, options: ExecuteSqlOptions) { const { query, read_only } = executeSqlOptionsSchema.parse(options); @@ -140,88 +228,100 @@ export function createSupabaseApiPlatform( // to avoid prompt injection attacks. If the migration failed, // it will throw an error. }, - async listOrganizations() { - const response = await managementApiClient.GET('/v1/organizations'); + }; - assertSuccess(response, 'Failed to fetch organizations'); + const debugging: DebuggingOperations = { + async getLogs(projectId: string, options: GetLogsOptions) { + const { sql, iso_timestamp_start, iso_timestamp_end } = + getLogsOptionsSchema.parse(options); - return response.data; - }, - async getOrganization(organizationId: string) { const response = await managementApiClient.GET( - '/v1/organizations/{slug}', + '/v1/projects/{ref}/analytics/endpoints/logs.all', { params: { path: { - slug: organizationId, + ref: projectId, + }, + query: { + sql, + iso_timestamp_start, + iso_timestamp_end, }, }, } ); - assertSuccess(response, 'Failed to fetch organization'); + assertSuccess(response, 'Failed to fetch logs'); return response.data; }, - async listProjects() { - const response = await managementApiClient.GET('/v1/projects'); + async getSecurityAdvisors(projectId: string) { + const response = await managementApiClient.GET( + '/v1/projects/{ref}/advisors/security', + { + params: { + path: { + ref: projectId, + }, + }, + } + ); - assertSuccess(response, 'Failed to fetch projects'); + assertSuccess(response, 'Failed to fetch security advisors'); return response.data; }, - async getProject(projectId: string) { - const response = await managementApiClient.GET('/v1/projects/{ref}', { - params: { - path: { - ref: projectId, + async getPerformanceAdvisors(projectId: string) { + const response = await managementApiClient.GET( + '/v1/projects/{ref}/advisors/performance', + { + params: { + path: { + ref: projectId, + }, }, - }, - }); - assertSuccess(response, 'Failed to fetch project'); - return response.data; - }, - async createProject(options: CreateProjectOptions) { - const { name, organization_id, region, db_pass } = - createProjectOptionsSchema.parse(options); - - const response = await managementApiClient.POST('/v1/projects', { - body: { - name, - region: region ?? (await getClosestRegion()), - organization_id, - db_pass: - db_pass ?? - generatePassword({ - length: 16, - numbers: true, - uppercase: true, - lowercase: true, - }), - }, - }); + } + ); - assertSuccess(response, 'Failed to create project'); + assertSuccess(response, 'Failed to fetch performance advisors'); return response.data; }, - async pauseProject(projectId: string) { - const response = await managementApiClient.POST( - '/v1/projects/{ref}/pause', + }; + + const development: DevelopmentOperations = { + async getProjectUrl(projectId: string): Promise { + const apiUrl = new URL(managementApiUrl); + return `https://${projectId}.${getProjectDomain(apiUrl.hostname)}`; + }, + async getAnonKey(projectId: string): Promise { + const response = await managementApiClient.GET( + '/v1/projects/{ref}/api-keys', { params: { path: { ref: projectId, }, + query: { + reveal: false, + }, }, } ); - assertSuccess(response, 'Failed to pause project'); + assertSuccess(response, 'Failed to fetch API keys'); + + const anonKey = response.data?.find((key) => key.name === 'anon'); + + if (!anonKey) { + throw new Error('Anonymous key not found'); + } + + return anonKey.api_key; }, - async restoreProject(projectId: string) { - const response = await managementApiClient.POST( - '/v1/projects/{ref}/restore', + async generateTypescriptTypes(projectId: string) { + const response = await managementApiClient.GET( + '/v1/projects/{ref}/types/typescript', { params: { path: { @@ -231,8 +331,13 @@ export function createSupabaseApiPlatform( } ); - assertSuccess(response, 'Failed to restore project'); + assertSuccess(response, 'Failed to fetch TypeScript types'); + + return response.data; }, + }; + + const functions: EdgeFunctionsOperations = { async listEdgeFunctions(projectId: string) { const response = await managementApiClient.GET( '/v1/projects/{ref}/functions', @@ -250,7 +355,10 @@ export function createSupabaseApiPlatform( // Fetch files for each Edge Function return await Promise.all( response.data.map(async (listedFunction) => { - return await platform.getEdgeFunction(projectId, listedFunction.slug); + return await functions.getEdgeFunction( + projectId, + listedFunction.slug + ); }) ); }, @@ -345,7 +453,7 @@ export function createSupabaseApiPlatform( let existingEdgeFunction: EdgeFunction | undefined; try { - existingEdgeFunction = await platform.getEdgeFunction(projectId, name); + existingEdgeFunction = await functions.getEdgeFunction(projectId, name); } catch (error) {} const import_map_file = inputFiles.find((file) => @@ -398,107 +506,9 @@ export function createSupabaseApiPlatform( return response.data; }, - async getLogs(projectId: string, options: GetLogsOptions) { - const { sql, iso_timestamp_start, iso_timestamp_end } = - getLogsOptionsSchema.parse(options); - - const response = await managementApiClient.GET( - '/v1/projects/{ref}/analytics/endpoints/logs.all', - { - params: { - path: { - ref: projectId, - }, - query: { - sql, - iso_timestamp_start, - iso_timestamp_end, - }, - }, - } - ); - - assertSuccess(response, 'Failed to fetch logs'); - - return response.data; - }, - async getSecurityAdvisors(projectId: string) { - const response = await managementApiClient.GET( - '/v1/projects/{ref}/advisors/security', - { - params: { - path: { - ref: projectId, - }, - }, - } - ); - - assertSuccess(response, 'Failed to fetch security advisors'); - - return response.data; - }, - async getPerformanceAdvisors(projectId: string) { - const response = await managementApiClient.GET( - '/v1/projects/{ref}/advisors/performance', - { - params: { - path: { - ref: projectId, - }, - }, - } - ); - - assertSuccess(response, 'Failed to fetch performance advisors'); - - return response.data; - }, - async getProjectUrl(projectId: string): Promise { - const apiUrl = new URL(managementApiUrl); - return `https://${projectId}.${getProjectDomain(apiUrl.hostname)}`; - }, - async getAnonKey(projectId: string): Promise { - const response = await managementApiClient.GET( - '/v1/projects/{ref}/api-keys', - { - params: { - path: { - ref: projectId, - }, - query: { - reveal: false, - }, - }, - } - ); - - assertSuccess(response, 'Failed to fetch API keys'); - - const anonKey = response.data?.find((key) => key.name === 'anon'); - - if (!anonKey) { - throw new Error('Anonymous key not found'); - } - - return anonKey.api_key; - }, - async generateTypescriptTypes(projectId: string) { - const response = await managementApiClient.GET( - '/v1/projects/{ref}/types/typescript', - { - params: { - path: { - ref: projectId, - }, - }, - } - ); - - assertSuccess(response, 'Failed to fetch TypeScript types'); + }; - return response.data; - }, + const branching: BranchingOperations = { async listBranches(projectId: string) { const response = await managementApiClient.GET( '/v1/projects/{ref}/branches', @@ -601,7 +611,9 @@ export function createSupabaseApiPlatform( assertSuccess(response, 'Failed to rebase branch'); }, + }; + const storage: StorageOperations = { // Storage methods async listAllBuckets(project_id: string) { const response = await managementApiClient.GET( @@ -666,6 +678,31 @@ export function createSupabaseApiPlatform( }, }; + const platform: SupabasePlatform = { + async init(info: InitData) { + const { clientInfo } = info; + if (!clientInfo) { + throw new Error('Client info is required'); + } + + // Re-initialize the management API client with the user agent + managementApiClient = createManagementApiClient( + managementApiUrl, + accessToken, + { + 'User-Agent': `supabase-mcp/${version} (${clientInfo.name}/${clientInfo.version})`, + } + ); + }, + account, + database, + debugging, + development, + functions, + branching, + storage, + }; + return platform; } diff --git a/packages/mcp-server-supabase/src/platform/types.ts b/packages/mcp-server-supabase/src/platform/types.ts index 58a3f1c..86fcfb0 100644 --- a/packages/mcp-server-supabase/src/platform/types.ts +++ b/packages/mcp-server-supabase/src/platform/types.ts @@ -155,18 +155,16 @@ export type GenerateTypescriptTypesResult = z.infer< export type StorageConfig = z.infer; export type StorageBucket = z.infer; -export type SupabasePlatform = { - init?(info: InitData): Promise; - - // Database operations +export type DatabaseOperations = { executeSql(projectId: string, options: ExecuteSqlOptions): Promise; listMigrations(projectId: string): Promise; applyMigration( projectId: string, options: ApplyMigrationOptions ): Promise; +}; - // Account +export type AccountOperations = { listOrganizations(): Promise[]>; getOrganization(organizationId: string): Promise; listProjects(): Promise; @@ -174,8 +172,9 @@ export type SupabasePlatform = { createProject(options: CreateProjectOptions): Promise; pauseProject(projectId: string): Promise; restoreProject(projectId: string): Promise; +}; - // Edge functions +export type EdgeFunctionsOperations = { listEdgeFunctions(projectId: string): Promise; getEdgeFunction( projectId: string, @@ -185,20 +184,29 @@ export type SupabasePlatform = { projectId: string, options: DeployEdgeFunctionOptions ): Promise>; +}; - // Debugging +export type DebuggingOperations = { getLogs(projectId: string, options: GetLogsOptions): Promise; getSecurityAdvisors(projectId: string): Promise; getPerformanceAdvisors(projectId: string): Promise; +}; - // Development +export type DevelopmentOperations = { getProjectUrl(projectId: string): Promise; getAnonKey(projectId: string): Promise; generateTypescriptTypes( projectId: string ): Promise; +}; - // Branching +export type StorageOperations = { + getStorageConfig(projectId: string): Promise; + updateStorageConfig(projectId: string, config: StorageConfig): Promise; + listAllBuckets(projectId: string): Promise; +}; + +export type BranchingOperations = { listBranches(projectId: string): Promise; createBranch( projectId: string, @@ -208,9 +216,15 @@ export type SupabasePlatform = { mergeBranch(branchId: string): Promise; resetBranch(branchId: string, options: ResetBranchOptions): Promise; rebaseBranch(branchId: string): Promise; +}; - // Storage - getStorageConfig(projectId: string): Promise; - updateStorageConfig(projectId: string, config: StorageConfig): Promise; - listAllBuckets(projectId: string): Promise; +export type SupabasePlatform = { + init?(info: InitData): Promise; + account?: AccountOperations; + database?: DatabaseOperations; + functions?: EdgeFunctionsOperations; + debugging?: DebuggingOperations; + development?: DevelopmentOperations; + storage?: StorageOperations; + branching?: BranchingOperations; }; diff --git a/packages/mcp-server-supabase/src/pricing.ts b/packages/mcp-server-supabase/src/pricing.ts index 7c13d0c..960bbae 100644 --- a/packages/mcp-server-supabase/src/pricing.ts +++ b/packages/mcp-server-supabase/src/pricing.ts @@ -1,4 +1,4 @@ -import type { SupabasePlatform } from './platform/types.js'; +import type { AccountOperations } from './platform/types.js'; export const PROJECT_COST_MONTHLY = 10; export const BRANCH_COST_HOURLY = 0.01344; @@ -21,11 +21,11 @@ export type Cost = ProjectCost | BranchCost; * Gets the cost of the next project in an organization. */ export async function getNextProjectCost( - platform: SupabasePlatform, + account: AccountOperations, orgId: string ): Promise { - const org = await platform.getOrganization(orgId); - const projects = await platform.listProjects(); + const org = await account.getOrganization(orgId); + const projects = await account.listProjects(); const activeProjects = projects.filter( (project) => diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index 533f17b..2c9a514 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -24,7 +24,7 @@ import { } from '../test/mocks.js'; import { createSupabaseApiPlatform } from './platform/api-platform.js'; import { BRANCH_COST_HOURLY, PROJECT_COST_MONTHLY } from './pricing.js'; -import { createSupabaseMcpServer, type FeatureGroup } from './server.js'; +import { createSupabaseMcpServer } from './server.js'; beforeEach(async () => { mockOrgs.clear(); @@ -39,7 +39,7 @@ type SetupOptions = { accessToken?: string; projectId?: string; readOnly?: boolean; - features?: FeatureGroup[]; + features?: string[]; }; /** @@ -2097,14 +2097,14 @@ describe('tools', () => { describe('feature groups', () => { test('account tools', async () => { - const { client: accountClient } = await setup({ + const { client } = await setup({ features: ['account'], }); - const { tools: accountTools } = await accountClient.listTools(); - const accountToolNames = accountTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(accountToolNames).toEqual([ + expect(toolNames).toEqual([ 'list_organizations', 'get_organization', 'list_projects', @@ -2118,14 +2118,14 @@ describe('feature groups', () => { }); test('database tools', async () => { - const { client: databaseClient } = await setup({ + const { client } = await setup({ features: ['database'], }); - const { tools: databaseTools } = await databaseClient.listTools(); - const databaseToolNames = databaseTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(databaseToolNames).toEqual([ + expect(toolNames).toEqual([ 'list_tables', 'list_extensions', 'list_migrations', @@ -2134,26 +2134,26 @@ describe('feature groups', () => { ]); }); - test('debug tools', async () => { - const { client: debugClient } = await setup({ - features: ['debug'], + test('debugging tools', async () => { + const { client } = await setup({ + features: ['debugging'], }); - const { tools: debugTools } = await debugClient.listTools(); - const debugToolNames = debugTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(debugToolNames).toEqual(['get_logs', 'get_advisors']); + expect(toolNames).toEqual(['get_logs', 'get_advisors']); }); test('development tools', async () => { - const { client: developmentClient } = await setup({ + const { client } = await setup({ features: ['development'], }); - const { tools: developmentTools } = await developmentClient.listTools(); - const developmentToolNames = developmentTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(developmentToolNames).toEqual([ + expect(toolNames).toEqual([ 'get_project_url', 'get_anon_key', 'generate_typescript_types', @@ -2161,39 +2161,36 @@ describe('feature groups', () => { }); test('docs tools', async () => { - const { client: docsClient } = await setup({ + const { client } = await setup({ features: ['docs'], }); - const { tools: docsTools } = await docsClient.listTools(); - const docsToolNames = docsTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(docsToolNames).toEqual(['search_docs']); + expect(toolNames).toEqual(['search_docs']); }); test('functions tools', async () => { - const { client: functionsClient } = await setup({ + const { client } = await setup({ features: ['functions'], }); - const { tools: functionsTools } = await functionsClient.listTools(); - const functionsToolNames = functionsTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(functionsToolNames).toEqual([ - 'list_edge_functions', - 'deploy_edge_function', - ]); + expect(toolNames).toEqual(['list_edge_functions', 'deploy_edge_function']); }); test('branching tools', async () => { - const { client: branchingClient } = await setup({ + const { client } = await setup({ features: ['branching'], }); - const { tools: branchingTools } = await branchingClient.listTools(); - const branchingToolNames = branchingTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(branchingToolNames).toEqual([ + expect(toolNames).toEqual([ 'create_branch', 'list_branches', 'delete_branch', @@ -2204,14 +2201,14 @@ describe('feature groups', () => { }); test('storage tools', async () => { - const { client: storageClient } = await setup({ + const { client } = await setup({ features: ['storage'], }); - const { tools: storageTools } = await storageClient.listTools(); - const storageToolNames = storageTools.map((tool) => tool.name); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(storageToolNames).toEqual([ + expect(toolNames).toEqual([ 'list_storage_buckets', 'get_storage_config', 'update_storage_config', @@ -2220,7 +2217,7 @@ describe('feature groups', () => { test('invalid group fails', async () => { const setupPromise = setup({ - features: ['my-invalid-group' as FeatureGroup], + features: ['my-invalid-group'], }); await expect(setupPromise).rejects.toThrow('Invalid enum value'); @@ -2231,10 +2228,10 @@ describe('feature groups', () => { features: ['account', 'account'], }); - const { tools: duplicateTools } = await duplicateClient.listTools(); - const duplicateToolNames = duplicateTools.map((tool) => tool.name); + const { tools } = await duplicateClient.listTools(); + const toolNames = tools.map((tool) => tool.name); - expect(duplicateToolNames).toEqual([ + expect(toolNames).toEqual([ 'list_organizations', 'get_organization', 'list_projects', diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts index 8f713a5..a713719 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -1,16 +1,17 @@ import { createMcpServer, type Tool } from '@supabase/mcp-utils'; -import { z } from 'zod'; import packageJson from '../package.json' with { type: 'json' }; import { createContentApiClient } from './content-api/index.js'; import type { SupabasePlatform } from './platform/types.js'; import { getAccountTools } from './tools/account-tools.js'; import { getBranchingTools } from './tools/branching-tools.js'; -import { getDatabaseOperationTools } from './tools/database-operation-tools.js'; +import { getDatabaseTools } from './tools/database-operation-tools.js'; import { getDebuggingTools } from './tools/debugging-tools.js'; import { getDevelopmentTools } from './tools/development-tools.js'; import { getDocsTools } from './tools/docs-tools.js'; import { getEdgeFunctionTools } from './tools/edge-function-tools.js'; import { getStorageTools } from './tools/storage-tools.js'; +import type { FeatureGroup } from './types.js'; +import { parseFeatureGroups } from './util.js'; const { version } = packageJson; @@ -52,34 +53,23 @@ export type SupabaseMcpServerOptions = { /** * Features to enable. - * Options: 'account', 'branching', 'database', 'debug', 'development', 'docs', 'functions', 'storage' + * Options: 'account', 'branching', 'database', 'debugging', 'development', 'docs', 'functions', 'storage' */ features?: string[]; }; -const featureGroupSchema = z.enum([ - 'docs', - 'account', - 'database', - 'debug', - 'development', - 'functions', - 'branching', - 'storage', -]); - -export type FeatureGroup = z.infer; - const DEFAULT_FEATURES: FeatureGroup[] = [ 'docs', 'account', 'database', - 'debug', + 'debugging', 'development', 'functions', 'branching', ]; +export const PLATFORM_INDEPENDENT_FEATURES: FeatureGroup[] = ['docs']; + /** * Creates an MCP server for interacting with Supabase. */ @@ -94,9 +84,18 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) { const contentApiClientPromise = createContentApiClient(contentApiUrl); - const enabledFeatures = z - .set(featureGroupSchema) - .parse(new Set(features ?? DEFAULT_FEATURES)); + // Filter the default features based on the platform's capabilities + const availableDefaultFeatures = DEFAULT_FEATURES.filter( + (key) => + PLATFORM_INDEPENDENT_FEATURES.includes(key) || + Object.keys(platform).includes(key) + ); + + // Validate the desired features against the platform's available features + const enabledFeatures = parseFeatureGroups( + platform, + features ?? availableDefaultFeatures + ); const server = createMcpServer({ name: 'supabase', @@ -110,40 +109,53 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) { const contentApiClient = await contentApiClientPromise; const tools: Record = {}; - // Add feature-based tools - if (!projectId && enabledFeatures.has('account')) { - Object.assign(tools, getAccountTools({ platform })); + const { + account, + database, + functions, + debugging, + development, + storage, + branching, + } = platform; + + if (enabledFeatures.has('docs')) { + Object.assign(tools, getDocsTools({ contentApiClient })); } - if (enabledFeatures.has('branching')) { - Object.assign(tools, getBranchingTools({ platform, projectId })); + if (!projectId && account && enabledFeatures.has('account')) { + Object.assign(tools, getAccountTools({ account })); } - if (enabledFeatures.has('database')) { + if (database && enabledFeatures.has('database')) { Object.assign( tools, - getDatabaseOperationTools({ platform, projectId, readOnly }) + getDatabaseTools({ + database, + projectId, + readOnly, + }) ); } - if (enabledFeatures.has('debug')) { - Object.assign(tools, getDebuggingTools({ platform, projectId })); + if (debugging && enabledFeatures.has('debugging')) { + Object.assign(tools, getDebuggingTools({ debugging, projectId })); } - if (enabledFeatures.has('development')) { - Object.assign(tools, getDevelopmentTools({ platform, projectId })); + if (development && enabledFeatures.has('development')) { + Object.assign(tools, getDevelopmentTools({ development, projectId })); } - if (enabledFeatures.has('docs')) { - Object.assign(tools, getDocsTools({ contentApiClient })); + if (functions && enabledFeatures.has('functions')) { + Object.assign(tools, getEdgeFunctionTools({ functions, projectId })); } - if (enabledFeatures.has('functions')) { - Object.assign(tools, getEdgeFunctionTools({ platform, projectId })); + if (branching && enabledFeatures.has('branching')) { + Object.assign(tools, getBranchingTools({ branching, projectId })); } - if (enabledFeatures.has('storage')) { - Object.assign(tools, getStorageTools({ platform, projectId })); + if (storage && enabledFeatures.has('storage')) { + Object.assign(tools, getStorageTools({ storage, projectId })); } return tools; diff --git a/packages/mcp-server-supabase/src/tools/account-tools.ts b/packages/mcp-server-supabase/src/tools/account-tools.ts index 7d9d1d5..470b16b 100644 --- a/packages/mcp-server-supabase/src/tools/account-tools.ts +++ b/packages/mcp-server-supabase/src/tools/account-tools.ts @@ -1,21 +1,21 @@ import { tool } from '@supabase/mcp-utils'; import { z } from 'zod'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { AccountOperations } from '../platform/types.js'; import { type Cost, getBranchCost, getNextProjectCost } from '../pricing.js'; import { AWS_REGION_CODES } from '../regions.js'; import { hashObject } from '../util.js'; export type AccountToolsOptions = { - platform: SupabasePlatform; + account: AccountOperations; }; -export function getAccountTools({ platform }: AccountToolsOptions) { +export function getAccountTools({ account }: AccountToolsOptions) { return { list_organizations: tool({ description: 'Lists all organizations that the user is a member of.', parameters: z.object({}), execute: async () => { - return await platform.listOrganizations(); + return await account.listOrganizations(); }, }), get_organization: tool({ @@ -25,7 +25,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { id: z.string().describe('The organization ID'), }), execute: async ({ id: organizationId }) => { - return await platform.getOrganization(organizationId); + return await account.getOrganization(organizationId); }, }), list_projects: tool({ @@ -33,7 +33,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { 'Lists all Supabase projects for the user. Use this to help discover the project ID of the project that the user is working on.', parameters: z.object({}), execute: async () => { - return await platform.listProjects(); + return await account.listProjects(); }, }), get_project: tool({ @@ -42,7 +42,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { id: z.string().describe('The project ID'), }), execute: async ({ id }) => { - return await platform.getProject(id); + return await account.getProject(id); }, }), get_cost: tool({ @@ -60,7 +60,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { } switch (type) { case 'project': { - const cost = await getNextProjectCost(platform, organization_id); + const cost = await getNextProjectCost(account, organization_id); return generateResponse(cost); } case 'branch': { @@ -105,7 +105,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { .describe('The cost confirmation ID. Call `confirm_cost` first.'), }), execute: async ({ name, region, organization_id, confirm_cost_id }) => { - const cost = await getNextProjectCost(platform, organization_id); + const cost = await getNextProjectCost(account, organization_id); const costHash = await hashObject(cost); if (costHash !== confirm_cost_id) { throw new Error( @@ -113,7 +113,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { ); } - return await platform.createProject({ + return await account.createProject({ name, region, organization_id, @@ -126,7 +126,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { project_id: z.string(), }), execute: async ({ project_id }) => { - return await platform.pauseProject(project_id); + return await account.pauseProject(project_id); }, }), restore_project: tool({ @@ -135,7 +135,7 @@ export function getAccountTools({ platform }: AccountToolsOptions) { project_id: z.string(), }), execute: async ({ project_id }) => { - return await platform.restoreProject(project_id); + return await account.restoreProject(project_id); }, }), }; diff --git a/packages/mcp-server-supabase/src/tools/branching-tools.ts b/packages/mcp-server-supabase/src/tools/branching-tools.ts index 453aba9..8504a99 100644 --- a/packages/mcp-server-supabase/src/tools/branching-tools.ts +++ b/packages/mcp-server-supabase/src/tools/branching-tools.ts @@ -1,17 +1,17 @@ import { tool } from '@supabase/mcp-utils'; import { z } from 'zod'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { BranchingOperations } from '../platform/types.js'; import { getBranchCost } from '../pricing.js'; import { hashObject } from '../util.js'; import { injectableTool } from './util.js'; export type BranchingToolsOptions = { - platform: SupabasePlatform; + branching: BranchingOperations; projectId?: string; }; export function getBranchingTools({ - platform, + branching, projectId, }: BranchingToolsOptions) { const project_id = projectId; @@ -42,7 +42,7 @@ export function getBranchingTools({ 'Cost confirmation ID does not match the expected cost of creating a branch.' ); } - return await platform.createBranch(project_id, { name }); + return await branching.createBranch(project_id, { name }); }, }), list_branches: injectableTool({ @@ -53,7 +53,7 @@ export function getBranchingTools({ }), inject: { project_id }, execute: async ({ project_id }) => { - return await platform.listBranches(project_id); + return await branching.listBranches(project_id); }, }), delete_branch: tool({ @@ -62,7 +62,7 @@ export function getBranchingTools({ branch_id: z.string(), }), execute: async ({ branch_id }) => { - return await platform.deleteBranch(branch_id); + return await branching.deleteBranch(branch_id); }, }), merge_branch: tool({ @@ -72,7 +72,7 @@ export function getBranchingTools({ branch_id: z.string(), }), execute: async ({ branch_id }) => { - return await platform.mergeBranch(branch_id); + return await branching.mergeBranch(branch_id); }, }), reset_branch: tool({ @@ -88,7 +88,7 @@ export function getBranchingTools({ ), }), execute: async ({ branch_id, migration_version }) => { - return await platform.resetBranch(branch_id, { + return await branching.resetBranch(branch_id, { migration_version, }); }, @@ -100,7 +100,7 @@ export function getBranchingTools({ branch_id: z.string(), }), execute: async ({ branch_id }) => { - return await platform.rebaseBranch(branch_id); + return await branching.rebaseBranch(branch_id); }, }), }; diff --git a/packages/mcp-server-supabase/src/tools/database-operation-tools.ts b/packages/mcp-server-supabase/src/tools/database-operation-tools.ts index b55ccb1..a0e2db5 100644 --- a/packages/mcp-server-supabase/src/tools/database-operation-tools.ts +++ b/packages/mcp-server-supabase/src/tools/database-operation-tools.ts @@ -5,17 +5,17 @@ import { postgresExtensionSchema, postgresTableSchema, } from '../pg-meta/types.js'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { DatabaseOperations } from '../platform/types.js'; import { injectableTool } from './util.js'; export type DatabaseOperationToolsOptions = { - platform: SupabasePlatform; + database: DatabaseOperations; projectId?: string; readOnly?: boolean; }; -export function getDatabaseOperationTools({ - platform, +export function getDatabaseTools({ + database, projectId, readOnly, }: DatabaseOperationToolsOptions) { @@ -34,7 +34,7 @@ export function getDatabaseOperationTools({ inject: { project_id }, execute: async ({ project_id, schemas }) => { const query = listTablesSql(schemas); - const data = await platform.executeSql(project_id, { + const data = await database.executeSql(project_id, { query, read_only: readOnly, }); @@ -50,7 +50,7 @@ export function getDatabaseOperationTools({ inject: { project_id }, execute: async ({ project_id }) => { const query = listExtensionsSql(); - const data = await platform.executeSql(project_id, { + const data = await database.executeSql(project_id, { query, read_only: readOnly, }); @@ -67,7 +67,7 @@ export function getDatabaseOperationTools({ }), inject: { project_id }, execute: async ({ project_id }) => { - return await platform.listMigrations(project_id); + return await database.listMigrations(project_id); }, }), apply_migration: injectableTool({ @@ -84,7 +84,7 @@ export function getDatabaseOperationTools({ throw new Error('Cannot apply migration in read-only mode.'); } - await platform.applyMigration(project_id, { + await database.applyMigration(project_id, { name, query, }); @@ -101,7 +101,7 @@ export function getDatabaseOperationTools({ }), inject: { project_id }, execute: async ({ query, project_id }) => { - const result = await platform.executeSql(project_id, { + const result = await database.executeSql(project_id, { query, read_only: readOnly, }); diff --git a/packages/mcp-server-supabase/src/tools/debugging-tools.ts b/packages/mcp-server-supabase/src/tools/debugging-tools.ts index 67a3ee6..cb61617 100644 --- a/packages/mcp-server-supabase/src/tools/debugging-tools.ts +++ b/packages/mcp-server-supabase/src/tools/debugging-tools.ts @@ -1,15 +1,15 @@ import { z } from 'zod'; import { getLogQuery } from '../logs.js'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { DebuggingOperations } from '../platform/types.js'; import { injectableTool } from './util.js'; export type DebuggingToolsOptions = { - platform: SupabasePlatform; + debugging: DebuggingOperations; projectId?: string; }; export function getDebuggingTools({ - platform, + debugging, projectId, }: DebuggingToolsOptions) { const project_id = projectId; @@ -42,7 +42,7 @@ export function getDebuggingTools({ ? new Date(Date.now() - 5 * 60 * 1000) : undefined; - return platform.getLogs(project_id, { + return debugging.getLogs(project_id, { sql: getLogQuery(service), iso_timestamp_start: startTimestamp?.toISOString(), }); @@ -61,9 +61,9 @@ export function getDebuggingTools({ execute: async ({ project_id, type }) => { switch (type) { case 'security': - return platform.getSecurityAdvisors(project_id); + return debugging.getSecurityAdvisors(project_id); case 'performance': - return platform.getPerformanceAdvisors(project_id); + return debugging.getPerformanceAdvisors(project_id); default: throw new Error(`Unknown advisor type: ${type}`); } diff --git a/packages/mcp-server-supabase/src/tools/development-tools.ts b/packages/mcp-server-supabase/src/tools/development-tools.ts index f37d291..1cc8baa 100644 --- a/packages/mcp-server-supabase/src/tools/development-tools.ts +++ b/packages/mcp-server-supabase/src/tools/development-tools.ts @@ -1,14 +1,14 @@ import { z } from 'zod'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { DevelopmentOperations } from '../platform/types.js'; import { injectableTool } from './util.js'; export type DevelopmentToolsOptions = { - platform: SupabasePlatform; + development: DevelopmentOperations; projectId?: string; }; export function getDevelopmentTools({ - platform, + development, projectId, }: DevelopmentToolsOptions) { const project_id = projectId; @@ -21,7 +21,7 @@ export function getDevelopmentTools({ }), inject: { project_id }, execute: async ({ project_id }) => { - return platform.getProjectUrl(project_id); + return development.getProjectUrl(project_id); }, }), get_anon_key: injectableTool({ @@ -31,7 +31,7 @@ export function getDevelopmentTools({ }), inject: { project_id }, execute: async ({ project_id }) => { - return platform.getAnonKey(project_id); + return development.getAnonKey(project_id); }, }), generate_typescript_types: injectableTool({ @@ -41,7 +41,7 @@ export function getDevelopmentTools({ }), inject: { project_id }, execute: async ({ project_id }) => { - return platform.generateTypescriptTypes(project_id); + return development.generateTypescriptTypes(project_id); }, }), }; diff --git a/packages/mcp-server-supabase/src/tools/edge-function-tools.ts b/packages/mcp-server-supabase/src/tools/edge-function-tools.ts index 06ac741..bb96df6 100644 --- a/packages/mcp-server-supabase/src/tools/edge-function-tools.ts +++ b/packages/mcp-server-supabase/src/tools/edge-function-tools.ts @@ -1,15 +1,15 @@ import { z } from 'zod'; import { edgeFunctionExample } from '../edge-function.js'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { EdgeFunctionsOperations } from '../platform/types.js'; import { injectableTool } from './util.js'; export type EdgeFunctionToolsOptions = { - platform: SupabasePlatform; + functions: EdgeFunctionsOperations; projectId?: string; }; export function getEdgeFunctionTools({ - platform, + functions, projectId, }: EdgeFunctionToolsOptions) { const project_id = projectId; @@ -22,7 +22,7 @@ export function getEdgeFunctionTools({ }), inject: { project_id }, execute: async ({ project_id }) => { - return await platform.listEdgeFunctions(project_id); + return await functions.listEdgeFunctions(project_id); }, }), deploy_edge_function: injectableTool({ @@ -57,7 +57,7 @@ export function getEdgeFunctionTools({ import_map_path, files, }) => { - return await platform.deployEdgeFunction(project_id, { + return await functions.deployEdgeFunction(project_id, { name, entrypoint_path, import_map_path, diff --git a/packages/mcp-server-supabase/src/tools/storage-tools.ts b/packages/mcp-server-supabase/src/tools/storage-tools.ts index 07231f9..24a6b07 100644 --- a/packages/mcp-server-supabase/src/tools/storage-tools.ts +++ b/packages/mcp-server-supabase/src/tools/storage-tools.ts @@ -1,13 +1,13 @@ import { z } from 'zod'; -import type { SupabasePlatform } from '../platform/types.js'; +import type { StorageOperations } from '../platform/types.js'; import { injectableTool } from './util.js'; export type StorageToolsOptions = { - platform: SupabasePlatform; + storage: StorageOperations; projectId?: string; }; -export function getStorageTools({ platform, projectId }: StorageToolsOptions) { +export function getStorageTools({ storage, projectId }: StorageToolsOptions) { const project_id = projectId; return { @@ -18,7 +18,7 @@ export function getStorageTools({ platform, projectId }: StorageToolsOptions) { }), inject: { project_id }, execute: async ({ project_id }) => { - return await platform.listAllBuckets(project_id); + return await storage.listAllBuckets(project_id); }, }), get_storage_config: injectableTool({ @@ -28,7 +28,7 @@ export function getStorageTools({ platform, projectId }: StorageToolsOptions) { }), inject: { project_id }, execute: async ({ project_id }) => { - return await platform.getStorageConfig(project_id); + return await storage.getStorageConfig(project_id); }, }), update_storage_config: injectableTool({ @@ -45,7 +45,7 @@ export function getStorageTools({ platform, projectId }: StorageToolsOptions) { }), inject: { project_id }, execute: async ({ project_id, config }) => { - await platform.updateStorageConfig(project_id, config); + await storage.updateStorageConfig(project_id, config); return { success: true }; }, }), diff --git a/packages/mcp-server-supabase/src/types.ts b/packages/mcp-server-supabase/src/types.ts new file mode 100644 index 0000000..9ca7a6e --- /dev/null +++ b/packages/mcp-server-supabase/src/types.ts @@ -0,0 +1,14 @@ +import { z } from 'zod'; + +export const featureGroupSchema = z.enum([ + 'docs', + 'account', + 'database', + 'debugging', + 'development', + 'functions', + 'branching', + 'storage', +]); + +export type FeatureGroup = z.infer; diff --git a/packages/mcp-server-supabase/src/util.ts b/packages/mcp-server-supabase/src/util.ts index 0b63e55..3a9a1ed 100644 --- a/packages/mcp-server-supabase/src/util.ts +++ b/packages/mcp-server-supabase/src/util.ts @@ -1,20 +1,24 @@ +import { z } from 'zod'; +import { featureGroupSchema, type FeatureGroup } from './types.js'; +import type { SupabasePlatform } from './platform/types.js'; +import { PLATFORM_INDEPENDENT_FEATURES } from './server.js'; + export type ValueOf = T[keyof T]; // UnionToIntersection = A & B export type UnionToIntersection = ( - U extends unknown - ? (arg: U) => 0 - : never + U extends unknown ? (arg: U) => 0 : never ) extends (arg: infer I) => 0 ? I : never; // LastInUnion = B -export type LastInUnion = UnionToIntersection< - U extends unknown ? (x: U) => 0 : never -> extends (x: infer L) => 0 - ? L - : never; +export type LastInUnion = + UnionToIntersection 0 : never> extends ( + x: infer L + ) => 0 + ? L + : never; // UnionToTuple = [A, B] export type UnionToTuple> = [T] extends [never] @@ -71,3 +75,42 @@ export async function hashObject( const base64Hash = btoa(String.fromCharCode(...new Uint8Array(buffer))); return base64Hash.slice(0, length); } + +/** + * Parses and validates feature groups based on the platform's available features. + */ +export function parseFeatureGroups( + platform: SupabasePlatform, + features: string[] +) { + // First pass: validate that all features are valid + const desiredFeatures = z.set(featureGroupSchema).parse(new Set(features)); + + // The platform implementation can define a subset of features + const availableFeatures: FeatureGroup[] = [ + ...PLATFORM_INDEPENDENT_FEATURES, + ...featureGroupSchema.options.filter((key) => + Object.keys(platform).includes(key) + ), + ]; + + const availableFeaturesSchema = z.enum( + availableFeatures as [string, ...string[]], + { + description: 'Available features based on platform implementation', + errorMap: (issue, ctx) => { + switch (issue.code) { + case 'invalid_enum_value': + return { + message: `This platform does not support the '${issue.received}' feature group. Supported groups are: ${availableFeatures.join(', ')}`, + }; + default: + return { message: ctx.defaultError }; + } + }, + } + ); + + // Second pass: validate the desired features against this platform's available features + return z.set(availableFeaturesSchema).parse(desiredFeatures); +} From 80a6dcf3628442ee9566d9a5601d1c5dc236bbc9 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 23 Jun 2025 14:20:41 -0600 Subject: [PATCH 2/6] feat: test tool filtering by platform ops --- .../mcp-server-supabase/src/server.test.ts | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index 2c9a514..a32deda 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -25,6 +25,7 @@ import { import { createSupabaseApiPlatform } from './platform/api-platform.js'; import { BRANCH_COST_HOURLY, PROJECT_COST_MONTHLY } from './pricing.js'; import { createSupabaseMcpServer } from './server.js'; +import type { SupabasePlatform } from './platform/types.js'; beforeEach(async () => { mockOrgs.clear(); @@ -38,6 +39,7 @@ beforeEach(async () => { type SetupOptions = { accessToken?: string; projectId?: string; + platform?: SupabasePlatform; readOnly?: boolean; features?: string[]; }; @@ -63,10 +65,12 @@ async function setup(options: SetupOptions = {}) { } ); - const platform = createSupabaseApiPlatform({ - accessToken, - apiUrl: API_URL, - }); + const platform = + options.platform ?? + createSupabaseApiPlatform({ + accessToken, + apiUrl: API_URL, + }); const server = createSupabaseMcpServer({ platform, @@ -2243,6 +2247,35 @@ describe('feature groups', () => { 'restore_project', ]); }); + + test('tools filtered to available platform operations', async () => { + const platform: SupabasePlatform = { + database: { + executeSql() { + throw new Error('Not implemented'); + }, + listMigrations() { + throw new Error('Not implemented'); + }, + applyMigration() { + throw new Error('Not implemented'); + }, + }, + }; + + const { client } = await setup({ platform }); + const { tools } = await client.listTools(); + const toolNames = tools.map((tool) => tool.name); + + expect(toolNames).toEqual([ + 'search_docs', + 'list_tables', + 'list_extensions', + 'list_migrations', + 'apply_migration', + 'execute_sql', + ]); + }); }); describe('project scoped tools', () => { From b1ab5a0b5017f1dd94f75be15b3b1b87a8980939 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 23 Jun 2025 14:38:49 -0600 Subject: [PATCH 3/6] feat: add test for custom error message on unimplemented feature group --- .../mcp-server-supabase/src/server.test.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index a32deda..513fabe 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -2276,6 +2276,28 @@ describe('feature groups', () => { 'execute_sql', ]); }); + + test('unimplemented feature group produces custom error message', async () => { + const platform: SupabasePlatform = { + database: { + executeSql() { + throw new Error('Not implemented'); + }, + listMigrations() { + throw new Error('Not implemented'); + }, + applyMigration() { + throw new Error('Not implemented'); + }, + }, + }; + + const setupPromise = setup({ platform, features: ['account'] }); + + await expect(setupPromise).rejects.toThrow( + "This platform does not support the 'account' feature group" + ); + }); }); describe('project scoped tools', () => { From b2657b8b7475df6dda9311bf2434c50d4120c017 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Wed, 25 Jun 2025 14:31:45 -0600 Subject: [PATCH 4/6] fix: split api platform into separate package export --- packages/mcp-server-supabase/package.json | 9 +++++++-- packages/mcp-server-supabase/src/index.test.ts | 3 ++- packages/mcp-server-supabase/src/index.ts | 1 - packages/mcp-server-supabase/tsup.config.ts | 7 ++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/mcp-server-supabase/package.json b/packages/mcp-server-supabase/package.json index bb74437..7a64cce 100644 --- a/packages/mcp-server-supabase/package.json +++ b/packages/mcp-server-supabase/package.json @@ -23,14 +23,19 @@ }, "exports": { ".": { - "import": "./dist/index.js", "types": "./dist/index.d.ts", + "import": "./dist/index.js", "default": "./dist/index.cjs" }, "./platform": { - "import": "./dist/platform/index.js", "types": "./dist/platform/index.d.ts", + "import": "./dist/platform/index.js", "default": "./dist/platform/index.cjs" + }, + "./platform/api": { + "types": "./dist/platform/api-platform.d.ts", + "import": "./dist/platform/api-platform.js", + "default": "./dist/platform/api-platform.cjs" } }, "dependencies": { diff --git a/packages/mcp-server-supabase/src/index.test.ts b/packages/mcp-server-supabase/src/index.test.ts index 3c7110f..8d0c6b0 100644 --- a/packages/mcp-server-supabase/src/index.test.ts +++ b/packages/mcp-server-supabase/src/index.test.ts @@ -7,7 +7,8 @@ import { MCP_CLIENT_NAME, MCP_CLIENT_VERSION, } from '../test/mocks.js'; -import { createSupabaseApiPlatform, createSupabaseMcpServer } from './index.js'; +import { createSupabaseMcpServer } from './index.js'; +import { createSupabaseApiPlatform } from './platform/api-platform.js'; type SetupOptions = { accessToken?: string; diff --git a/packages/mcp-server-supabase/src/index.ts b/packages/mcp-server-supabase/src/index.ts index 912e243..9921d15 100644 --- a/packages/mcp-server-supabase/src/index.ts +++ b/packages/mcp-server-supabase/src/index.ts @@ -1,3 +1,2 @@ -export * from './platform/api-platform.js'; export type { SupabasePlatform } from './platform/index.js'; export * from './server.js'; diff --git a/packages/mcp-server-supabase/tsup.config.ts b/packages/mcp-server-supabase/tsup.config.ts index c7b955b..b647276 100644 --- a/packages/mcp-server-supabase/tsup.config.ts +++ b/packages/mcp-server-supabase/tsup.config.ts @@ -2,7 +2,12 @@ import { defineConfig } from 'tsup'; export default defineConfig([ { - entry: ['src/index.ts', 'src/transports/stdio.ts', 'src/platform/index.ts'], + entry: [ + 'src/index.ts', + 'src/transports/stdio.ts', + 'src/platform/index.ts', + 'src/platform/api-platform.ts', + ], format: ['cjs', 'esm'], outDir: 'dist', sourcemap: true, From 20e0c9c6058664696afa675d2733dc2354cb08aa Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 26 Jun 2025 22:06:16 -0600 Subject: [PATCH 5/6] chore: remove unused type --- packages/mcp-server-supabase/src/server.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts index a713719..9113a7d 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -15,18 +15,6 @@ import { parseFeatureGroups } from './util.js'; const { version } = packageJson; -export type SupabasePlatformOptions = { - /** - * The access token for the Supabase Management API. - */ - accessToken: string; - - /** - * The API URL for the Supabase Management API. - */ - apiUrl?: string; -}; - export type SupabaseMcpServerOptions = { /** * Platform implementation for Supabase. From f3a655a164d2585a673a3330b4e007416750391c Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Thu, 26 Jun 2025 22:06:39 -0600 Subject: [PATCH 6/6] feat: export feature group zod schema/type --- packages/mcp-server-supabase/src/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/mcp-server-supabase/src/index.ts b/packages/mcp-server-supabase/src/index.ts index 9921d15..39c49f3 100644 --- a/packages/mcp-server-supabase/src/index.ts +++ b/packages/mcp-server-supabase/src/index.ts @@ -1,2 +1,6 @@ export type { SupabasePlatform } from './platform/index.js'; -export * from './server.js'; +export { + createSupabaseMcpServer, + type SupabaseMcpServerOptions, +} from './server.js'; +export { featureGroupSchema, type FeatureGroup } from './types.js';