diff --git a/.changeset/spec-method-invalid-params-32602.md b/.changeset/spec-method-invalid-params-32602.md new file mode 100644 index 0000000000..7d193f30bc --- /dev/null +++ b/.changeset/spec-method-invalid-params-32602.md @@ -0,0 +1,10 @@ +--- +'@modelcontextprotocol/core-internal': patch +'@modelcontextprotocol/client': patch +'@modelcontextprotocol/core': patch +'@modelcontextprotocol/server': patch +--- + +Spec-method requests whose params fail the method's wire schema are now answered with `-32602` Invalid params instead of `-32603` Internal error. Per JSON-RPC 2.0, invalid method parameters are the caller's error — the previous surface told callers the server broke rather than that their params were wrong (e.g. `logging/setLevel` with `{ level: 'not-a-level' }`). + +This is a wire-visible behavior change: peers that matched on `-32603` for schema-invalid spec-method params will now observe `-32602`. Custom handlers registered with a params schema already answered `-32602`; spec methods now match that surface. The dispatch-rejection corpus fixture pinning this outcome is updated in the same change. diff --git a/packages/core-internal/src/shared/protocol.ts b/packages/core-internal/src/shared/protocol.ts index ffab6e8df8..ac86e5075a 100644 --- a/packages/core-internal/src/shared/protocol.ts +++ b/packages/core-internal/src/shared/protocol.ts @@ -1710,10 +1710,15 @@ export abstract class Protocol { // handler runs. throw new ProtocolError(ProtocolErrorCode.InternalError, `No wire schema for ${method} in the resolved era`); } - // Preserves the pre-function-only error surface: the Zod - // parse threw out of the handler and the funnel mapped it - // to −32603 Internal error (specCorpusDispatch pins this). - throw new Error(outcome.message); + // Spec-method params that fail the wire schema are the + // caller's error, not the server's: answer −32602 Invalid + // params (JSON-RPC 2.0), matching the custom-handler path + // below (specCorpusDispatch pins this). Before the + // function-only WireCodec contract, the Zod parse threw + // out of the handler and the funnel mapped it to −32603 + // Internal error — callers were told the server broke + // rather than that their params were wrong. + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params for ${method}: ${outcome.message}`); } return Promise.resolve(schemasOrHandler(outcome.value, ctx)); }; diff --git a/packages/core-internal/test/corpus/fixtures/rejection/invalid-spec-params.json b/packages/core-internal/test/corpus/fixtures/rejection/invalid-spec-params.json index 5bf8c693f3..cc6bf38cf3 100644 --- a/packages/core-internal/test/corpus/fixtures/rejection/invalid-spec-params.json +++ b/packages/core-internal/test/corpus/fixtures/rejection/invalid-spec-params.json @@ -1,5 +1,5 @@ { - "description": "A spec request with params that fail the method schema is answered with an error response (current dispatch surfaces the parse failure as -32603 Internal error).", + "description": "A spec request with params that fail the method schema is answered with -32602 Invalid params (the caller's error, not the server's).", "message": { "jsonrpc": "2.0", "id": 3, @@ -9,5 +9,5 @@ } }, "expect": "error-response", - "errorCode": -32603 + "errorCode": -32602 } diff --git a/packages/server/test/server/specMethodInvalidParams.test.ts b/packages/server/test/server/specMethodInvalidParams.test.ts new file mode 100644 index 0000000000..bfadfb11c7 --- /dev/null +++ b/packages/server/test/server/specMethodInvalidParams.test.ts @@ -0,0 +1,43 @@ +import { InMemoryTransport } from '@modelcontextprotocol/core-internal'; +import { describe, expect, test } from 'vitest'; + +import { Server } from '../../src/server/server'; + +/** + * Spec-method requests with schema-invalid params are the caller's error: + * they answer -32602 Invalid params, not -32603 Internal error. + * (modelcontextprotocol/typescript-sdk#2284 — the Zod parse used to throw + * out of the handler and the funnel surfaced it as an internal error.) + */ +describe('spec-method invalid params surface', () => { + test('logging/setLevel with an invalid level answers -32602 Invalid params', async () => { + const [peerTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const server = new Server({ name: 'test server', version: '1.0.0' }, { capabilities: { logging: {} } }); + server.setRequestHandler('logging/setLevel', async () => ({})); + await server.connect(serverTransport); + + const responses: Array<{ id?: number; error?: { code: number; message: string } }> = []; + peerTransport.onmessage = message => void responses.push(message as (typeof responses)[number]); + await peerTransport.start(); + + await peerTransport.send({ + jsonrpc: '2.0', + id: 1, + method: 'initialize', + params: { protocolVersion: '2025-11-25', capabilities: {}, clientInfo: { name: 'client', version: '1.0.0' } } + } as never); + await new Promise(resolve => setTimeout(resolve, 25)); + await peerTransport.send({ jsonrpc: '2.0', method: 'notifications/initialized' } as never); + await new Promise(resolve => setTimeout(resolve, 25)); + + await peerTransport.send({ jsonrpc: '2.0', id: 2, method: 'logging/setLevel', params: { level: 'not-a-level' } } as never); + await new Promise(resolve => setTimeout(resolve, 50)); + + const response = responses.find(message => message.id === 2); + expect(response?.error).toBeDefined(); + expect(response?.error?.code).toBe(-32602); + expect(response?.error?.message).toContain('Invalid params for logging/setLevel'); + + await server.close(); + }); +}); diff --git a/test/e2e/requirements.ts b/test/e2e/requirements.ts index 4fda9e661f..40623b2c63 100644 --- a/test/e2e/requirements.ts +++ b/test/e2e/requirements.ts @@ -956,13 +956,7 @@ export const REQUIREMENTS: Record = { 'logging:set-level:invalid-level': { entryExclusions: [{ arm: 'entryModern', reason: 'method-not-in-modern-registry' }], 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,