Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/spec-method-invalid-params-32602.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 9 additions & 4 deletions packages/core-internal/src/shared/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1710,10 +1710,15 @@ export abstract class Protocol<ContextT extends BaseContext> {
// 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));
};
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -9,5 +9,5 @@
}
},
"expect": "error-response",
"errorCode": -32603
"errorCode": -32602
}
43 changes: 43 additions & 0 deletions packages/server/test/server/specMethodInvalidParams.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
8 changes: 1 addition & 7 deletions test/e2e/requirements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -956,13 +956,7 @@ export const REQUIREMENTS: Record<string, Requirement> = {
'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,
Expand Down
Loading