From fbebb8b7dbd47a0453d2f1a512f5e085ffc90681 Mon Sep 17 00:00:00 2001 From: Greg Richardson Date: Mon, 28 Apr 2025 16:02:39 -0600 Subject: [PATCH] fix: missing cost tools in project scoped mode --- .../mcp-server-supabase/src/server.test.ts | 3 - packages/mcp-server-supabase/src/server.ts | 66 ++--- .../src/tools/project-management-tools.ts | 273 +++++++++--------- 3 files changed, 170 insertions(+), 172 deletions(-) diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index f0a41ec..b0a10ec 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -1933,9 +1933,6 @@ describe('project scoped tools', () => { 'list_organizations', 'get_organization', 'list_projects', - 'get_project', - 'get_cost', - 'confirm_cost', 'create_project', 'pause_project', 'restore_project', diff --git a/packages/mcp-server-supabase/src/server.ts b/packages/mcp-server-supabase/src/server.ts index 6eabfdd..66a78fd 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -68,45 +68,33 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) { } ); }, - tools: () => { - const tools: Record = {}; - - // Add account-level tools only if projectId is not provided - if (!projectId) { - Object.assign( - tools, - getProjectManagementTools({ managementApiClient }) - ); - } - - // Add project-level tools - Object.assign( - tools, - getDatabaseOperationTools({ - managementApiClient, - projectId, - readOnly, - }), - getEdgeFunctionTools({ - managementApiClient, - projectId, - }), - getDebuggingTools({ - managementApiClient, - projectId, - }), - getDevelopmentTools({ - managementApiClient, - projectId, - }), - getBranchingTools({ - managementApiClient, - projectId, - }) - ); - - return tools; - }, + tools: () => ({ + ...getProjectManagementTools({ + managementApiClient, + projectId, + }), + ...getDatabaseOperationTools({ + managementApiClient, + projectId, + readOnly, + }), + ...getEdgeFunctionTools({ + managementApiClient, + projectId, + }), + ...getDebuggingTools({ + managementApiClient, + projectId, + }), + ...getDevelopmentTools({ + managementApiClient, + projectId, + }), + ...getBranchingTools({ + managementApiClient, + projectId, + }), + }), }); return server; diff --git a/packages/mcp-server-supabase/src/tools/project-management-tools.ts b/packages/mcp-server-supabase/src/tools/project-management-tools.ts index e696fe5..2f7828c 100644 --- a/packages/mcp-server-supabase/src/tools/project-management-tools.ts +++ b/packages/mcp-server-supabase/src/tools/project-management-tools.ts @@ -1,4 +1,4 @@ -import { tool } from '@supabase/mcp-utils'; +import { tool, type Tool } from '@supabase/mcp-utils'; import { z } from 'zod'; import { assertSuccess, @@ -13,71 +13,31 @@ import { getCountryCoordinates, } from '../regions.js'; import { hashObject } from '../util.js'; +import { injectableTool } from './util.js'; export type ProjectManagementToolsOptions = { managementApiClient: ManagementApiClient; + projectId?: string; }; export function getProjectManagementTools({ managementApiClient, + projectId, }: ProjectManagementToolsOptions) { async function getClosestRegion() { return getClosestAwsRegion(getCountryCoordinates(await getCountryCode())) .code; } - return { - list_organizations: tool({ - description: 'Lists all organizations that the user is a member of.', - parameters: z.object({}), - execute: async () => { - const response = await managementApiClient.GET('/v1/organizations'); + const tools: Record = {}; - assertSuccess(response, 'Failed to fetch organizations'); - - return response.data; - }, - }), - get_organization: tool({ - description: - 'Gets details for an organization. Includes subscription plan.', - parameters: z.object({ - id: z.string().describe('The organization ID'), - }), - execute: async ({ id: organizationId }) => { - const response = await managementApiClient.GET( - '/v1/organizations/{slug}', - { - params: { - path: { - slug: organizationId, - }, - }, - } - ); - - assertSuccess(response, 'Failed to fetch organization'); - - return response.data; - }, - }), - list_projects: tool({ - description: - '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 () => { - const response = await managementApiClient.GET('/v1/projects'); - - assertSuccess(response, 'Failed to fetch projects'); - - return response.data; - }, - }), - get_project: tool({ + Object.assign(tools, { + get_project: injectableTool({ description: 'Gets details for a Supabase project.', parameters: z.object({ id: z.string().describe('The project ID'), }), + inject: { id: projectId }, execute: async ({ id }) => { const response = await managementApiClient.GET('/v1/projects/{ref}', { params: { @@ -132,97 +92,150 @@ export function getProjectManagementTools({ return await hashObject(cost); }, }), - create_project: tool({ - description: - 'Creates a new Supabase project. Always ask the user which organization to create the project in. The project can take a few minutes to initialize - use `get_project` to check the status.', - parameters: z.object({ - name: z.string().describe('The name of the project'), - region: z.optional( - z - .enum(AWS_REGION_CODES) - .describe( - 'The region to create the project in. Defaults to the closest region.' - ) - ), - organization_id: z.string(), - confirm_cost_id: z - .string({ - required_error: - 'User must confirm understanding of costs before creating a project.', - }) - .describe('The cost confirmation ID. Call `confirm_cost` first.'), + }); + + if (!projectId) { + Object.assign(tools, { + list_organizations: tool({ + description: 'Lists all organizations that the user is a member of.', + parameters: z.object({}), + execute: async () => { + const response = await managementApiClient.GET('/v1/organizations'); + + assertSuccess(response, 'Failed to fetch organizations'); + + return response.data; + }, }), - execute: async ({ name, region, organization_id, confirm_cost_id }) => { - const cost = await getNextProjectCost( - managementApiClient, - organization_id - ); - const costHash = await hashObject(cost); - if (costHash !== confirm_cost_id) { - throw new Error( - 'Cost confirmation ID does not match the expected cost of creating a project.' + get_organization: tool({ + description: + 'Gets details for an organization. Includes subscription plan.', + parameters: z.object({ + id: z.string().describe('The organization ID'), + }), + execute: async ({ id: organizationId }) => { + const response = await managementApiClient.GET( + '/v1/organizations/{slug}', + { + params: { + path: { + slug: organizationId, + }, + }, + } ); - } - const response = await managementApiClient.POST('/v1/projects', { - body: { - name, - region: region ?? (await getClosestRegion()), - organization_id, - db_pass: generatePassword({ - length: 16, - numbers: true, - uppercase: true, - lowercase: true, - }), - }, - }); + assertSuccess(response, 'Failed to fetch organization'); - assertSuccess(response, 'Failed to create project'); + return response.data; + }, + }), + list_projects: tool({ + description: + '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 () => { + const response = await managementApiClient.GET('/v1/projects'); - return response.data; - }, - }), - pause_project: tool({ - description: 'Pauses a Supabase project.', - parameters: z.object({ - project_id: z.string(), + assertSuccess(response, 'Failed to fetch projects'); + + return response.data; + }, }), - execute: async ({ project_id }) => { - const response = await managementApiClient.POST( - '/v1/projects/{ref}/pause', - { - params: { - path: { - ref: project_id, - }, - }, + create_project: tool({ + description: + 'Creates a new Supabase project. Always ask the user which organization to create the project in. The project can take a few minutes to initialize - use `get_project` to check the status.', + parameters: z.object({ + name: z.string().describe('The name of the project'), + region: z.optional( + z + .enum(AWS_REGION_CODES) + .describe( + 'The region to create the project in. Defaults to the closest region.' + ) + ), + organization_id: z.string(), + confirm_cost_id: z + .string({ + required_error: + 'User must confirm understanding of costs before creating a project.', + }) + .describe('The cost confirmation ID. Call `confirm_cost` first.'), + }), + execute: async ({ name, region, organization_id, confirm_cost_id }) => { + const cost = await getNextProjectCost( + managementApiClient, + organization_id + ); + const costHash = await hashObject(cost); + if (costHash !== confirm_cost_id) { + throw new Error( + 'Cost confirmation ID does not match the expected cost of creating a project.' + ); } - ); - assertSuccess(response, 'Failed to pause project'); - }, - }), - restore_project: tool({ - description: 'Restores a Supabase project.', - parameters: z.object({ - project_id: z.string(), + const response = await managementApiClient.POST('/v1/projects', { + body: { + name, + region: region ?? (await getClosestRegion()), + organization_id, + db_pass: generatePassword({ + length: 16, + numbers: true, + uppercase: true, + lowercase: true, + }), + }, + }); + + assertSuccess(response, 'Failed to create project'); + + return response.data; + }, }), - execute: async ({ project_id }) => { - const response = await managementApiClient.POST( - '/v1/projects/{ref}/restore', - { - params: { - path: { - ref: project_id, + pause_project: tool({ + description: 'Pauses a Supabase project.', + parameters: z.object({ + project_id: z.string(), + }), + execute: async ({ project_id }) => { + const response = await managementApiClient.POST( + '/v1/projects/{ref}/pause', + { + params: { + path: { + ref: project_id, + }, }, - }, - body: {}, - } - ); + } + ); - assertSuccess(response, 'Failed to restore project'); - }, - }), - }; + assertSuccess(response, 'Failed to pause project'); + }, + }), + restore_project: tool({ + description: 'Restores a Supabase project.', + parameters: z.object({ + project_id: z.string(), + }), + execute: async ({ project_id }) => { + const response = await managementApiClient.POST( + '/v1/projects/{ref}/restore', + { + params: { + path: { + ref: project_id, + }, + }, + body: {}, + } + ); + + assertSuccess(response, 'Failed to restore project'); + }, + }), + }); + } + + return tools; }