diff --git a/.changeset/fix-null-arguments-internal-error.md b/.changeset/fix-null-arguments-internal-error.md new file mode 100644 index 000000000..73d217542 --- /dev/null +++ b/.changeset/fix-null-arguments-internal-error.md @@ -0,0 +1,5 @@ +--- +'@modelcontextprotocol/core': patch +--- + +Accept null arguments in tools/call requests and return -32602 (InvalidParams) instead of -32603 (InternalError) for request validation failures. Clients that serialize missing fields as null (common in Go, Java, C# JSON libraries) no longer get an opaque internal error when calling tools. diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 361bd6fc7..550ff8023 100644 --- a/packages/core/src/shared/protocol.ts +++ b/packages/core/src/shared/protocol.ts @@ -388,7 +388,10 @@ export abstract class Protocol { const schema = getRequestSchema(method as RequestMethod); this._requestHandlers.set(method, (request, ctx) => { // Validate request params via Zod (strips jsonrpc/id, so we pass original to handler) - schema.parse(request); + const result = schema.safeParse(request); + if (!result.success) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid ${method} request: ${result.error.message}`); + } return handler(request, ctx); }); }, diff --git a/packages/core/src/types/schemas.ts b/packages/core/src/types/schemas.ts index a243c1b82..3fd8839fc 100644 --- a/packages/core/src/types/schemas.ts +++ b/packages/core/src/types/schemas.ts @@ -1417,7 +1417,7 @@ export const CallToolRequestParamsSchema = TaskAugmentedRequestParamsSchema.exte /** * Arguments to pass to the tool. */ - arguments: z.record(z.string(), z.unknown()).optional() + arguments: z.preprocess(val => val ?? undefined, z.record(z.string(), z.unknown()).optional()) }); /** diff --git a/packages/core/test/types.test.ts b/packages/core/test/types.test.ts index 9383f7d5e..64009c82b 100644 --- a/packages/core/test/types.test.ts +++ b/packages/core/test/types.test.ts @@ -1,4 +1,5 @@ import { + CallToolRequestSchema, CallToolResultSchema, ClientCapabilitiesSchema, CompleteRequestSchema, @@ -985,6 +986,58 @@ describe('Types', () => { }); }); + describe('CallToolRequest arguments handling', () => { + test('should accept tools/call with arguments as object', () => { + const request = { + method: 'tools/call', + params: { + name: 'echo', + arguments: { message: 'hello' } + } + }; + const result = CallToolRequestSchema.safeParse(request); + expect(result.success).toBe(true); + }); + + test('should accept tools/call with arguments omitted', () => { + const request = { + method: 'tools/call', + params: { + name: 'echo' + } + }; + const result = CallToolRequestSchema.safeParse(request); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.params.arguments).toBeUndefined(); + } + }); + + test('should accept tools/call with arguments as null', () => { + const request = { + method: 'tools/call', + params: { + name: 'echo', + arguments: null + } + }; + const result = CallToolRequestSchema.safeParse(request); + expect(result.success).toBe(true); + }); + + test('should accept tools/call with arguments as empty object', () => { + const request = { + method: 'tools/call', + params: { + name: 'echo', + arguments: {} + } + }; + const result = CallToolRequestSchema.safeParse(request); + expect(result.success).toBe(true); + }); + }); + describe('ElicitRequestFormParamsSchema', () => { test('accepts requestedSchema with extra JSON Schema metadata keys', () => { // Mirrors what z.toJSONSchema() emits — includes $schema, additionalProperties, etc.