diff --git a/packages/mcp-server-supabase/src/platform/types.ts b/packages/mcp-server-supabase/src/platform/types.ts index 0d7a896..9d0610e 100644 --- a/packages/mcp-server-supabase/src/platform/types.ts +++ b/packages/mcp-server-supabase/src/platform/types.ts @@ -234,10 +234,7 @@ export type DevelopmentOperations = { export type StorageOperations = { getStorageConfig(projectId: string): Promise; - updateStorageConfig( - projectId: string, - config: StorageConfig - ): Promise; + updateStorageConfig(projectId: string, config: StorageConfig): Promise; listAllBuckets(projectId: string): Promise; }; @@ -249,10 +246,7 @@ export type BranchingOperations = { ): Promise; deleteBranch(branchId: string): Promise; mergeBranch(branchId: string): Promise; - resetBranch( - branchId: string, - options: ResetBranchOptions - ): Promise; + resetBranch(branchId: string, options: ResetBranchOptions): Promise; rebaseBranch(branchId: string): Promise; }; diff --git a/packages/mcp-server-supabase/src/server.test.ts b/packages/mcp-server-supabase/src/server.test.ts index d20ac9e..413b216 100644 --- a/packages/mcp-server-supabase/src/server.test.ts +++ b/packages/mcp-server-supabase/src/server.test.ts @@ -173,16 +173,20 @@ describe('tools', () => { }); const result = await callTool({ - name: 'get_cost', + name: 'get_and_confirm_cost', arguments: { type: 'project', organization_id: freeOrg.id, }, }); - expect(result).toEqual( - 'The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.' - ); + expect(result).toMatchObject({ + type: 'project', + recurrence: 'monthly', + amount: 0, + confirm_cost_id: expect.any(String), + message: expect.stringContaining('The new project is free'), + }); }); test('get next project cost for paid org with 0 projects', async () => { @@ -195,16 +199,20 @@ describe('tools', () => { }); const result = await callTool({ - name: 'get_cost', + name: 'get_and_confirm_cost', arguments: { type: 'project', organization_id: paidOrg.id, }, }); - expect(result).toEqual( - 'The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.' - ); + expect(result).toMatchObject({ + type: 'project', + recurrence: 'monthly', + amount: 0, + confirm_cost_id: expect.any(String), + message: expect.stringContaining('The new project is free'), + }); }); test('get next project cost for paid org with > 0 active projects', async () => { @@ -224,16 +232,20 @@ describe('tools', () => { priorProject.status = 'ACTIVE_HEALTHY'; const result = await callTool({ - name: 'get_cost', + name: 'get_and_confirm_cost', arguments: { type: 'project', organization_id: paidOrg.id, }, }); - expect(result).toEqual( - `The new project will cost $${PROJECT_COST_MONTHLY} monthly. You must repeat this to the user and confirm their understanding.` - ); + expect(result).toMatchObject({ + type: 'project', + recurrence: 'monthly', + amount: PROJECT_COST_MONTHLY, + confirm_cost_id: expect.any(String), + message: expect.stringContaining(`The new project will cost $${PROJECT_COST_MONTHLY} monthly`), + }); }); test('get next project cost for paid org with > 0 inactive projects', async () => { @@ -253,38 +265,39 @@ describe('tools', () => { priorProject.status = 'INACTIVE'; const result = await callTool({ - name: 'get_cost', + name: 'get_and_confirm_cost', arguments: { type: 'project', organization_id: paidOrg.id, }, }); - expect(result).toEqual( - `The new project will cost $0 monthly. You must repeat this to the user and confirm their understanding.` - ); + expect(result).toMatchObject({ + type: 'project', + recurrence: 'monthly', + amount: 0, + confirm_cost_id: expect.any(String), + message: expect.stringContaining('The new project is free'), + }); }); test('get branch cost', async () => { const { callTool } = await setup(); - const paidOrg = await createOrganization({ - name: 'Paid Org', - plan: 'pro', - allowed_release_channels: ['ga'], - }); - const result = await callTool({ - name: 'get_cost', + name: 'get_and_confirm_cost', arguments: { type: 'branch', - organization_id: paidOrg.id, }, }); - expect(result).toEqual( - `The new branch will cost $${BRANCH_COST_HOURLY} hourly. You must repeat this to the user and confirm their understanding.` - ); + expect(result).toMatchObject({ + type: 'branch', + recurrence: 'hourly', + amount: BRANCH_COST_HOURLY, + confirm_cost_id: expect.any(String), + message: expect.stringContaining(`The new branch will cost $${BRANCH_COST_HOURLY} hourly`), + }); }); test('list projects', async () => { @@ -350,12 +363,11 @@ describe('tools', () => { allowed_release_channels: ['ga'], }); - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'project', - recurrence: 'monthly', - amount: 0, + organization_id: freeOrg.id, }, }); @@ -363,7 +375,7 @@ describe('tools', () => { name: 'New Project', region: 'us-east-1', organization_id: freeOrg.id, - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }; const result = await callTool({ @@ -376,6 +388,7 @@ describe('tools', () => { expect(result).toEqual({ ...projectInfo, id: expect.stringMatching(/^.+$/), + message: `Project \"${newProject.name}\" created successfully. Cost: Free`, created_at: expect.stringMatching( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/ ), @@ -392,12 +405,11 @@ describe('tools', () => { allowed_release_channels: ['ga'], }); - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'project', - recurrence: 'monthly', - amount: 0, + organization_id: freeOrg.id, }, }); @@ -405,7 +417,7 @@ describe('tools', () => { name: 'New Project', region: 'us-east-1', organization_id: freeOrg.id, - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }; const result = callTool({ @@ -427,19 +439,18 @@ describe('tools', () => { allowed_release_channels: ['ga'], }); - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'project', - recurrence: 'monthly', - amount: 0, + organization_id: freeOrg.id, }, }); const newProject = { name: 'New Project', organization_id: freeOrg.id, - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }; const createProjectPromise = callTool({ @@ -1833,12 +1844,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -1848,7 +1857,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: branchName, - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -1888,12 +1897,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -1903,7 +1910,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: branchName, - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -1960,12 +1967,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -1974,7 +1979,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: 'test-branch', - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -2115,12 +2120,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -2129,7 +2132,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: 'test-branch', - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -2220,12 +2223,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -2234,7 +2235,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: 'test-branch', - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -2334,12 +2335,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -2348,7 +2347,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: 'test-branch', - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -2436,12 +2435,10 @@ describe('tools', () => { }); project.status = 'ACTIVE_HEALTHY'; - const confirm_cost_id = await callTool({ - name: 'confirm_cost', + const costResult = await callTool({ + name: 'get_and_confirm_cost', arguments: { type: 'branch', - recurrence: 'hourly', - amount: BRANCH_COST_HOURLY, }, }); @@ -2450,7 +2447,7 @@ describe('tools', () => { arguments: { project_id: project.id, name: 'test-branch', - confirm_cost_id, + confirm_cost_id: costResult.confirm_cost_id, }, }); @@ -2582,8 +2579,7 @@ describe('feature groups', () => { 'get_organization', 'list_projects', 'get_project', - 'get_cost', - 'confirm_cost', + 'get_and_confirm_cost', 'create_project', 'pause_project', 'restore_project', @@ -2713,8 +2709,7 @@ describe('feature groups', () => { 'get_organization', 'list_projects', 'get_project', - 'get_cost', - 'confirm_cost', + 'get_and_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 ab8fe0b..d5630ef 100644 --- a/packages/mcp-server-supabase/src/server.ts +++ b/packages/mcp-server-supabase/src/server.ts @@ -134,7 +134,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) { } if (!projectId && account && enabledFeatures.has('account')) { - Object.assign(tools, getAccountTools({ account, readOnly })); + Object.assign(tools, getAccountTools({ account, readOnly, server })); } if (database && enabledFeatures.has('database')) { @@ -144,6 +144,7 @@ export function createSupabaseMcpServer(options: SupabaseMcpServerOptions) { database, projectId, readOnly, + server, }) ); } diff --git a/packages/mcp-server-supabase/src/tools/account-tools.ts b/packages/mcp-server-supabase/src/tools/account-tools.ts index d2e88f0..c4082ae 100644 --- a/packages/mcp-server-supabase/src/tools/account-tools.ts +++ b/packages/mcp-server-supabase/src/tools/account-tools.ts @@ -1,4 +1,6 @@ -import { tool } from '@supabase/mcp-utils'; +import { tool, type ToolExecuteContext } from '@supabase/mcp-utils'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import type { AccountOperations } from '../platform/types.js'; import { type Cost, getBranchCost, getNextProjectCost } from '../pricing.js'; @@ -10,9 +12,14 @@ const SUCCESS_RESPONSE = { success: true }; export type AccountToolsOptions = { account: AccountOperations; readOnly?: boolean; + server?: Server; }; -export function getAccountTools({ account, readOnly }: AccountToolsOptions) { +export function getAccountTools({ + account, + readOnly, + server, +}: AccountToolsOptions) { return { list_organizations: tool({ description: 'Lists all organizations that the user is a member of.', @@ -76,11 +83,16 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) { return await account.getProject(id); }, }), - get_cost: tool({ - description: - 'Gets the cost of creating a new project or branch. Never assume organization as costs can be different for each.', + get_and_confirm_cost: tool({ + description: async () => { + const clientCapabilities = server?.getClientCapabilities(); + if (clientCapabilities?.elicitation) { + return 'Gets the cost of creating a new project or branch and requests user confirmation. Returns a unique ID for this confirmation which must be passed to `create_project` or `create_branch`. Never assume organization as costs can be different for each.'; + } + return 'Gets the cost of creating a new project or branch. You must repeat the cost to the user and confirm their understanding before calling `create_project` or `create_branch`. Returns a unique ID for this confirmation which must be passed to `create_project` or `create_branch`. Never assume organization as costs can be different for each.'; + }, annotations: { - title: 'Get cost of new resources', + title: 'Get and confirm cost', readOnlyHint: true, destructiveHint: false, idempotentHint: true, @@ -90,48 +102,94 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) { type: z.enum(['project', 'branch']), organization_id: z .string() - .describe('The organization ID. Always ask the user.'), + .describe('The organization ID. Always ask the user.') + .optional(), }), execute: async ({ type, organization_id }) => { - function generateResponse(cost: Cost) { - return `The new ${type} will cost $${cost.amount} ${cost.recurrence}. You must repeat this to the user and confirm their understanding.`; - } + // Get the cost + let cost: Cost; switch (type) { case 'project': { - const cost = await getNextProjectCost(account, organization_id); - return generateResponse(cost); + if (!organization_id) { + throw new Error( + 'organization_id is required for project cost calculation' + ); + } + cost = await getNextProjectCost(account, organization_id); + break; } case 'branch': { - const cost = getBranchCost(); - return generateResponse(cost); + cost = getBranchCost(); + break; } default: throw new Error(`Unknown cost type: ${type}`); } - }, - }), - confirm_cost: tool({ - description: - 'Ask the user to confirm their understanding of the cost of creating a new project or branch. Call `get_cost` first. Returns a unique ID for this confirmation which should be passed to `create_project` or `create_branch`.', - annotations: { - title: 'Confirm cost understanding', - readOnlyHint: true, - destructiveHint: false, - idempotentHint: true, - openWorldHint: false, - }, - parameters: z.object({ - type: z.enum(['project', 'branch']), - recurrence: z.enum(['hourly', 'monthly']), - amount: z.number(), - }), - execute: async (cost) => { - return await hashObject(cost); + + let userDeclinedCost = false; + + // Request confirmation via elicitation if supported + const clientCapabilities = server?.getClientCapabilities(); + if (server && clientCapabilities?.elicitation) { + try { + const costMessage = + cost.amount > 0 ? `$${cost.amount} ${cost.recurrence}` : 'Free'; + + const result = await server.request( + { + method: 'elicitation/create', + params: { + message: `You are about to create a new ${type}.\n\nCost: ${costMessage}\n\nDo you want to proceed?`, + requestedSchema: { + type: 'object', + properties: { + confirm: { + type: 'boolean', + title: 'Confirm Cost', + description: `I understand the cost and want to create the ${type}`, + }, + }, + required: ['confirm'], + }, + }, + }, + ElicitResultSchema + ); + + if (result.action !== 'accept' || !result.content?.confirm) { + userDeclinedCost = true; + } + } catch (error) { + // If elicitation fails (client doesn't support it), return cost info for manual confirmation + console.warn( + 'Elicitation not supported by client, returning cost for manual confirmation' + ); + console.warn(error); + } + } + + if (userDeclinedCost) { + throw new Error( + 'The user declined to confirm the cost. Ask the user to confirm if they want to proceed with the operation or do something else.' + ); + } + + // Generate and return confirmation ID + const confirmationId = await hashObject(cost); + + return { + ...cost, + confirm_cost_id: confirmationId, + message: + cost.amount > 0 + ? `The new ${type} will cost $${cost.amount} ${cost.recurrence}. ${clientCapabilities?.elicitation ? 'User has confirmed.' : 'You must confirm this cost with the user before proceeding.'}` + : `The new ${type} is free. ${clientCapabilities?.elicitation ? 'User has confirmed.' : 'You may proceed with creation.'}`, + }; }, }), 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.', + 'Creates a new Supabase project. Always ask the user which organization to create the project in. Call `get_and_confirm_cost` first to verify the cost and get user confirmation. The project can take a few minutes to initialize - use `get_project` to check the status.', annotations: { title: 'Create project', readOnlyHint: false, @@ -150,13 +208,16 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) { required_error: 'User must confirm understanding of costs before creating a project.', }) - .describe('The cost confirmation ID. Call `confirm_cost` first.'), + .describe( + 'The cost confirmation ID. Call `get_and_confirm_cost` first.' + ), }), execute: async ({ name, region, organization_id, confirm_cost_id }) => { if (readOnly) { throw new Error('Cannot create a project in read-only mode.'); } + // Verify the confirmation ID matches the expected cost const cost = await getNextProjectCost(account, organization_id); const costHash = await hashObject(cost); if (costHash !== confirm_cost_id) { @@ -165,11 +226,23 @@ export function getAccountTools({ account, readOnly }: AccountToolsOptions) { ); } - return await account.createProject({ + // Create the project + const project = await account.createProject({ name, region, organization_id, }); + + // Return appropriate message based on cost + const costInfo = + cost.amount > 0 + ? `Cost: $${cost.amount}/${cost.recurrence}` + : 'Cost: Free'; + + return { + ...project, + message: `Project "${name}" created successfully. ${costInfo}`, + }; }, }), pause_project: tool({ diff --git a/packages/mcp-server-supabase/src/tools/branching-tools.ts b/packages/mcp-server-supabase/src/tools/branching-tools.ts index ef458da..c4ef731 100644 --- a/packages/mcp-server-supabase/src/tools/branching-tools.ts +++ b/packages/mcp-server-supabase/src/tools/branching-tools.ts @@ -23,7 +23,7 @@ export function getBranchingTools({ return { create_branch: injectableTool({ description: - 'Creates a development branch on a Supabase project. This will apply all migrations from the main project to a fresh branch database. Note that production data will not carry over. The branch will get its own project_id via the resulting project_ref. Use this ID to execute queries and migrations on the branch.', + 'Creates a development branch on a Supabase project. Call `get_and_confirm_cost` first to verify the cost and get user confirmation. This will apply all migrations from the main project to a fresh branch database. Note that production data will not carry over. The branch will get its own project_id via the resulting project_ref. Use this ID to execute queries and migrations on the branch.', annotations: { title: 'Create branch', readOnlyHint: false, @@ -42,7 +42,9 @@ export function getBranchingTools({ required_error: 'User must confirm understanding of costs before creating a branch.', }) - .describe('The cost confirmation ID. Call `confirm_cost` first.'), + .describe( + 'The cost confirmation ID. Call `get_and_confirm_cost` first.' + ), }), inject: { project_id }, execute: async ({ project_id, name, confirm_cost_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 762d600..e007037 100644 --- a/packages/mcp-server-supabase/src/tools/database-operation-tools.ts +++ b/packages/mcp-server-supabase/src/tools/database-operation-tools.ts @@ -1,4 +1,6 @@ import { source } from 'common-tags'; +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { ElicitResultSchema } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { listExtensionsSql, listTablesSql } from '../pg-meta/index.js'; import { @@ -14,12 +16,14 @@ export type DatabaseOperationToolsOptions = { database: DatabaseOperations; projectId?: string; readOnly?: boolean; + server?: Server; }; export function getDatabaseTools({ database, projectId, readOnly, + server, }: DatabaseOperationToolsOptions) { const project_id = projectId; @@ -215,6 +219,44 @@ export function getDatabaseTools({ throw new Error('Cannot apply migration in read-only mode.'); } + // Try to request user confirmation via elicitation + if (server) { + try { + const result = await server.request( + { + method: 'elicitation/create', + params: { + message: `You are about to apply migration "${name}" to project ${project_id}. This will modify your database schema.\n\nPlease review the SQL:\n\n${query}\n\nDo you want to proceed?`, + requestedSchema: { + type: 'object', + properties: { + confirm: { + type: 'boolean', + title: 'Confirm Migration', + description: + 'I have reviewed the SQL and approve this migration', + }, + }, + required: ['confirm'], + }, + }, + }, + ElicitResultSchema + ); + + // User declined or cancelled + if (result.action !== 'accept' || !result.content?.confirm) { + throw new Error('Migration cancelled by user'); + } + } catch (error) { + // If elicitation fails (client doesn't support it), proceed without confirmation + // This maintains backwards compatibility + console.warn( + 'Elicitation not supported by client, proceeding with migration without confirmation' + ); + } + } + await database.applyMigration(project_id, { name, query, diff --git a/packages/mcp-server-supabase/src/tools/util.ts b/packages/mcp-server-supabase/src/tools/util.ts index a88b5ad..4207829 100644 --- a/packages/mcp-server-supabase/src/tools/util.ts +++ b/packages/mcp-server-supabase/src/tools/util.ts @@ -64,6 +64,6 @@ export function injectableTool< description, annotations, parameters: parameters.omit(mask), - execute: (args) => execute({ ...args, ...inject }), + execute: (args, context) => execute({ ...args, ...inject }, context), }) as Tool, Result>; } diff --git a/packages/mcp-utils/src/server.ts b/packages/mcp-utils/src/server.ts index b42e773..d554689 100644 --- a/packages/mcp-utils/src/server.ts +++ b/packages/mcp-utils/src/server.ts @@ -50,6 +50,10 @@ export type ResourceTemplate = { ): Promise; }; +export type ToolExecuteContext = { + server?: Server; +}; + export type Tool< Params extends z.ZodObject = z.ZodObject, Result = unknown, @@ -57,7 +61,21 @@ export type Tool< description: Prop; annotations?: Annotations; parameters: Params; - execute(params: z.infer): Promise; + /** + * Optional predicate to determine if this tool should be listed/usable for the + * connected client based on its declared capabilities. If omitted, the tool + * is assumed to be supported by all clients. + * + * Example: Only show when the client supports elicitation + * isSupported: (caps) => Boolean(caps?.elicitation) + */ + isSupported?: ( + clientCapabilities: ClientCapabilities | undefined + ) => boolean | Promise; + execute( + params: z.infer, + context?: ToolExecuteContext + ): Promise; }; /** @@ -436,28 +454,41 @@ export function createMcpServer(options: McpServerOptions) { ListToolsRequestSchema, async (): Promise => { const tools = await getTools(); + const clientCapabilities = server.getClientCapabilities(); + + // Filter tools based on client capabilities when a predicate is provided + const supportedToolEntries = ( + await Promise.all( + Object.entries(tools).map(async ([name, tool]) => { + const ok = + typeof tool.isSupported === 'function' + ? await tool.isSupported(clientCapabilities) + : true; + return ok ? ([name, tool] as const) : null; + }) + ) + ).filter(Boolean) as Array; return { tools: await Promise.all( - Object.entries(tools).map( - async ([name, { description, annotations, parameters }]) => { - const inputSchema = zodToJsonSchema(parameters); - - if (!('properties' in inputSchema)) { - throw new Error('tool parameters must be a ZodObject'); - } - - return { - name, - description: - typeof description === 'function' - ? await description() - : description, - annotations, - inputSchema, - }; + supportedToolEntries.map(async ([name, tool]) => { + const { description, annotations, parameters } = tool; + const inputSchema = zodToJsonSchema(parameters); + + if (!('properties' in inputSchema)) { + throw new Error('tool parameters must be a ZodObject'); } - ) + + return { + name, + description: + typeof description === 'function' + ? await description() + : description, + annotations, + inputSchema, + }; + }) ), } satisfies ListToolsResult; } @@ -484,7 +515,7 @@ export function createMcpServer(options: McpServerOptions) { const executeWithCallback = async (tool: Tool) => { // Wrap success or error in a result value const res = await tool - .execute(args) + .execute(args, { server }) .then((data: unknown) => ({ success: true as const, data })) .catch((error) => ({ success: false as const, error }));