diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 5e9115391d..8b6d44257c 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -1116,7 +1116,9 @@ export type RegisteredTool = { /** * Creates an executor that invokes the handler with the appropriate arguments. * When `inputSchema` is defined, the handler is called with `(args, ctx)`. - * When `inputSchema` is undefined, the handler is called with just `(ctx)`. + * When `inputSchema` is undefined, single-argument callbacks are called with `(ctx)`. + * Two-argument callbacks are called with `({}, ctx)` so handlers that need both + * values receive the context in the correct argument slot. */ function createToolExecutor( inputSchema: StandardSchemaWithJSON | undefined, @@ -1127,7 +1129,12 @@ function createToolExecutor( return async (args, ctx) => callback(args, ctx); } - // When no inputSchema, call with just ctx (the handler expects (ctx) signature) + if (handler.length >= 2) { + const callback = handler as ToolCallbackInternal; + return async (_args, ctx) => callback({}, ctx); + } + + // When no inputSchema, one-argument handlers expect just ctx. const callback = handler as (ctx: ServerContext) => CallToolResult | Promise; return async (_args, ctx) => callback(ctx); } diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 8c844b11cb..9657c7ebae 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1,5 +1,5 @@ import { Client } from '@modelcontextprotocol/client'; -import type { Notification, TextContent } from '@modelcontextprotocol/core'; +import type { CallToolResult, Notification, ServerContext, TextContent } from '@modelcontextprotocol/core'; import { getDisplayName, InMemoryTransport, ProtocolErrorCode, UriTemplate, UrlElicitationRequiredError } from '@modelcontextprotocol/core'; import { completable, McpServer, ResourceTemplate } from '@modelcontextprotocol/server'; import { afterEach, beforeEach, describe, expect, test } from 'vitest'; @@ -6282,6 +6282,43 @@ describe('Zod v4', () => { }); }); + test('should pass empty args before context for no-schema two-argument tool callbacks', async () => { + const server = new McpServer({ + name: 'test', + version: '1.0.0' + }); + + const client = new Client({ + name: 'test-client', + version: '1.0.0' + }); + + let receivedArgs: unknown; + let receivedRequestId: unknown; + + const twoArgumentHandler = ((args: unknown, ctx: ServerContext): CallToolResult => { + receivedArgs = args; + receivedRequestId = ctx.mcpReq.id; + return { + content: [{ type: 'text' as const, text: 'Success' }] + }; + }) as unknown as (ctx: ServerContext) => CallToolResult; + + server.registerTool('no-schema-two-arg', {}, twoArgumentHandler); + + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + + const result = await client.callTool({ + name: 'no-schema-two-arg' + }); + + expect(result.content).toEqual([{ type: 'text', text: 'Success' }]); + expect(receivedArgs).toEqual({}); + expect(receivedRequestId).toBeDefined(); + }); + // SEP-2663: `taskSupport: 'required'` enforcement and the automatic-polling wrapper // depended on the client sending `params.task`. Under the server-directed model the // tool handler decides to return `{resultType:'task', task}`; there is no per-tool