diff --git a/.changeset/null-tool-call-arguments.md b/.changeset/null-tool-call-arguments.md new file mode 100644 index 0000000000..15d565b958 --- /dev/null +++ b/.changeset/null-tool-call-arguments.md @@ -0,0 +1,6 @@ +--- +"@modelcontextprotocol/core": patch +"@modelcontextprotocol/server": patch +--- + +Accept `null` tool call arguments as omitted. diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts index a243c1b829..2394dd0554 100644 --- a/packages/core/src/types/schemas.ts +++ b/packages/core/src/types/schemas.ts @@ -1417,7 +1417,11 @@ export const CallToolRequestParamsSchema = TaskAugmentedRequestParamsSchema.exte /** * Arguments to pass to the tool. */ - arguments: z.record(z.string(), z.unknown()).optional() + arguments: z + .record(z.string(), z.unknown()) + .nullable() + .transform(args => args ?? undefined) + .optional() }); /** diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 9383f7d5ec..dddbc98a19 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -1,4 +1,5 @@ import { + CallToolRequestSchema, CallToolResultSchema, ClientCapabilitiesSchema, CompleteRequestSchema, @@ -33,6 +34,25 @@ describe('Types', () => { expect(SUPPORTED_PROTOCOL_VERSIONS).toContain('2024-10-07'); }); + describe('CallToolRequest', () => { + test('should treat null arguments as omitted', () => { + const request = { + method: 'tools/call', + params: { + name: 'echo', + arguments: null + } + }; + + const result = CallToolRequestSchema.safeParse(request); + + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.arguments).toBeUndefined(); + } + }); + }); + describe('ResourceLink', () => { test('should validate a minimal ResourceLink', () => { const resourceLink = { diff --git a/packages/server/test/server/mcp.compat.test.ts b/packages/server/test/server/mcp.compat.test.ts index 322b615353..63a3cd70d6 100644 --- a/packages/server/test/server/mcp.compat.test.ts +++ b/packages/server/test/server/mcp.compat.test.ts @@ -119,6 +119,49 @@ describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () = await server.close(); }); + + it('treats null tools/call arguments as omitted', async () => { + const server = new McpServer({ name: 't', version: '1.0.0' }); + + let received: unknown; + server.registerTool('empty', { inputSchema: {} }, async args => { + received = args; + return { content: [{ type: 'text' as const, text: 'ok' }] }; + }); + + const [client, srv] = InMemoryTransport.createLinkedPair(); + await server.connect(srv); + await client.start(); + + const responses: JSONRPCMessage[] = []; + client.onmessage = m => responses.push(m); + + await client.send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + capabilities: {}, + clientInfo: { name: 'c', version: '1.0.0' } + } + } as JSONRPCMessage); + await client.send({ jsonrpc: '2.0', method: 'notifications/initialized' } as JSONRPCMessage); + await client.send({ + jsonrpc: '2.0', + id: 2, + method: 'tools/call', + params: { name: 'empty', arguments: null } + } as JSONRPCMessage); + + await vi.waitFor(() => expect(responses.some(r => 'id' in r && r.id === 2)).toBe(true)); + + expect(received).toEqual({}); + const result = responses.find(r => 'id' in r && r.id === 2) as { result?: { content: Array<{ text: string }> } }; + expect(result.result?.content[0]?.text).toBe('ok'); + + await server.close(); + }); }); describe('InferRawShape', () => {