From 03a22648c81ba5ab0bee63a553abc117852570fa Mon Sep 17 00:00:00 2001 From: Gauravtiwari31 <205016146+Gauravtiwari31@users.noreply.github.com> Date: Fri, 2 Oct 2026 21:52:02 +0530 Subject: [PATCH] [v1.x] fix(protocol): answer -32602 for requests whose params fail the schema Protocol.setRequestHandler parsed the request with parseWithCompat, which throws the raw ZodError. It has no numeric code, so _onrequest answered -32603 Internal error with the Zod issue dump as the message. Parse with safeParse instead and throw McpError(InvalidParams) on failure, so callers get -32602 "Invalid params for : ...". This also makes the existing InvalidParams guards in the Client and Server wrappers (elicitation, sampling, tools/call) reachable again, since they run after this parse. Drops the three e2e knownFailures that pinned the old behaviour, and lets the mcpserver logging/setLevel scenario send its invalid level past the wire sniffer, as it already does on main. Backport of the fix for #2916 / #2284. --- .changeset/spec-method-invalid-params.md | 6 ++++++ src/shared/protocol.ts | 11 ++++++++--- test/e2e/requirements.ts | 22 +++------------------ test/e2e/scenarios/logging.test.ts | 3 ++- test/shared/protocol.test.ts | 25 ++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 23 deletions(-) create mode 100644 .changeset/spec-method-invalid-params.md diff --git a/.changeset/spec-method-invalid-params.md b/.changeset/spec-method-invalid-params.md new file mode 100644 index 0000000000..b20e5ab930 --- /dev/null +++ b/.changeset/spec-method-invalid-params.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/sdk': patch +--- + +A request whose params fail the schema it was registered with (for example `prompts/get` without a `name`, `logging/setLevel` with an unknown level, or `tools/call` with a non-string `name`) is now answered with `-32602` Invalid params instead of `-32603` Internal error. The +message reads `Invalid params for : …`. diff --git a/src/shared/protocol.ts b/src/shared/protocol.ts index b3e547222a..0f7c9d730d 100644 --- a/src/shared/protocol.ts +++ b/src/shared/protocol.ts @@ -1,4 +1,4 @@ -import { AnySchema, AnyObjectSchema, SchemaOutput, safeParse } from '../server/zod-compat.js'; +import { AnySchema, AnyObjectSchema, SchemaOutput, getParseErrorMessage, safeParse } from '../server/zod-compat.js'; import { CancelledNotificationSchema, ClientCapabilities, @@ -1459,8 +1459,13 @@ export abstract class Protocol { - const parsed = parseWithCompat(requestSchema, request) as SchemaOutput; - return Promise.resolve(handler(parsed, extra)); + const parsed = safeParse(requestSchema, request); + if (!parsed.success) { + // Params that fail the request schema are the caller's error, so answer + // -32602 Invalid params rather than letting the parse error become -32603. + throw new McpError(ErrorCode.InvalidParams, `Invalid params for ${method}: ${getParseErrorMessage(parsed.error)}`); + } + return Promise.resolve(handler(parsed.data, extra)); }); } diff --git a/test/e2e/requirements.ts b/test/e2e/requirements.ts index a45752d7fd..3709849c66 100644 --- a/test/e2e/requirements.ts +++ b/test/e2e/requirements.ts @@ -163,12 +163,7 @@ export const REQUIREMENTS: Record = { }, 'protocol:error:invalid-params': { source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic#responses', - behavior: 'A request with malformed params is answered with JSON-RPC error -32602 Invalid params.', - knownFailures: [ - { - note: 'Protocol wraps schema-parse failures as -32603 (InternalError), not -32602 (InvalidParams) as required by JSON-RPC 2.0.' - } - ] + behavior: 'A request with malformed params is answered with JSON-RPC error -32602 Invalid params.' }, 'protocol:error:method-not-found': { source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic#responses', @@ -748,13 +743,7 @@ export const REQUIREMENTS: Record = { }, 'logging:set-level:invalid-level': { source: 'https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging#error-handling', - behavior: 'logging/setLevel with an invalid level value returns JSON-RPC error -32602 (Invalid params).', - knownFailures: [ - { - test: 'mcpserver', - note: 'Protocol wraps schema-parse failures as -32603 (InternalError), not -32602 (InvalidParams) as required by JSON-RPC 2.0.' - } - ] + behavior: 'logging/setLevel with an invalid level value returns JSON-RPC error -32602 (Invalid params).' }, 'logging:out-of-band:basic': { transports: STATEFUL_TRANSPORTS, @@ -1023,12 +1012,7 @@ export const REQUIREMENTS: Record = { behavior: 'Form-mode requested schemas are flat objects with primitive-typed properties only; nested structures and arrays of objects are not used.', transports: ['inMemory', 'stdio', 'streamableHttp'], - note: 'Stateless hosting creates a fresh server per request and has no standalone GET stream, so there is no server→client channel to deliver/observe these.', - knownFailures: [ - { - note: 'The client rejects a nested requestedSchema before the user handler, but via the raw ZodError from parseWithCompat in Protocol.setRequestHandler (src/shared/protocol.ts:1428-1431), which _onrequest maps to -32603 InternalError (~line 823) instead of -32602 InvalidParams; the InvalidParams guard in src/client/index.ts (~363-368) is unreachable.' - } - ] + note: 'Stateless hosting creates a fresh server per request and has no standalone GET stream, so there is no server→client channel to deliver/observe these.' }, // Roots diff --git a/test/e2e/scenarios/logging.test.ts b/test/e2e/scenarios/logging.test.ts index 74915109cb..f3fd9e917f 100644 --- a/test/e2e/scenarios/logging.test.ts +++ b/test/e2e/scenarios/logging.test.ts @@ -164,7 +164,8 @@ verifies( 'logging:set-level:invalid-level', async ({ transport }: TestArgs) => { const client = newClient(); - await using _ = await wire(transport, loggingServer, client); + // strictValidation off: the invalid level must actually reach the server for it to reject. + await using _ = await wire(transport, loggingServer, client, { strictValidation: false }); const tap = tapWire(client); diff --git a/test/shared/protocol.test.ts b/test/shared/protocol.test.ts index 733146f29b..396e91b75c 100644 --- a/test/shared/protocol.test.ts +++ b/test/shared/protocol.test.ts @@ -3,11 +3,13 @@ import { CallToolRequestSchema, ClientCapabilities, ErrorCode, + GetPromptRequestSchema, JSONRPCMessage, McpError, RELATED_TASK_META_KEY, RequestId, ServerCapabilities, + SetLevelRequestSchema, Task, TaskCreationParams, type Request, @@ -197,6 +199,29 @@ describe('protocol tests', () => { expect(onmessageMock).toHaveBeenCalled(); }); + test.each([ + { schema: GetPromptRequestSchema, method: 'prompts/get', params: {} }, + { schema: SetLevelRequestSchema, method: 'logging/setLevel', params: { level: 'loud' } } + ])('should answer $method with invalid params as InvalidParams without calling the handler', async ({ schema, method, params }) => { + const handler = vi.fn(); + protocol.setRequestHandler(schema, handler); + await protocol.connect(transport); + + transport.onmessage?.({ jsonrpc: '2.0', id: 1, method, params }); + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(handler).not.toHaveBeenCalled(); + expect(sendSpy).toHaveBeenCalledWith( + expect.objectContaining({ + id: 1, + error: expect.objectContaining({ + code: ErrorCode.InvalidParams, + message: expect.stringContaining(`Invalid params for ${method}:`) + }) + }) + ); + }); + describe('_meta preservation with onprogress', () => { test('should preserve existing _meta when adding progressToken', async () => { await protocol.connect(transport);