From fd73777da5f3d746d22cefad7baa5b4cb5c83070 Mon Sep 17 00:00:00 2001 From: Dayna Blackwell Date: Sat, 2 May 2026 05:06:41 -0700 Subject: [PATCH 1/3] fix: accept null arguments in tools/call and return -32602 for validation errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues: 1. CallToolRequestParamsSchema uses .optional() on the arguments field, which accepts undefined but rejects null. Clients that serialize missing fields as null (common in Go, Java, C# JSON libraries) get an internal error. Fixed with z.preprocess() to coerce null to undefined before validation, preserving the existing type signature. 2. The protocol request handler uses schema.parse() which throws on validation failure. The thrown ZodError lacks a numeric "code" property, so the error handler defaults to -32603 (InternalError). Changed to schema.safeParse() with an explicit ProtocolError using -32602 (InvalidParams) on failure. Before: null arguments → -32603 with raw Zod error message. After: null arguments → accepted (coerced to undefined). Any remaining validation errors → -32602 with structured message. --- packages/core/src/shared/protocol.ts | 5 ++- packages/core/src/types/schemas.ts | 2 +- packages/core/test/types.test.ts | 53 ++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/packages/core/src/shared/protocol.ts b/packages/core/src/shared/protocol.ts index 361bd6fc7c..550ff8023d 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 a243c1b829..3fd8839fc0 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 9383f7d5ec..64009c82b9 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. From c599594b02afabc146214c9fa5043caacf5d2120 Mon Sep 17 00:00:00 2001 From: Dayna Blackwell Date: Sat, 2 May 2026 05:14:57 -0700 Subject: [PATCH 2/3] chore: add changeset for null arguments fix --- .changeset/fix-null-arguments-internal-error.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-null-arguments-internal-error.md diff --git a/.changeset/fix-null-arguments-internal-error.md b/.changeset/fix-null-arguments-internal-error.md new file mode 100644 index 0000000000..73d2175425 --- /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. From b33d687d8f39fa1e2c6f2bbbc9f34636a742d61b Mon Sep 17 00:00:00 2001 From: Dayna Blackwell Date: Sat, 2 May 2026 05:34:38 -0700 Subject: [PATCH 3/3] ci: retrigger CI (flaky cloudflareWorkers test on Node 20)