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
5 changes: 5 additions & 0 deletions .changeset/mcperror-double-prefix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@modelcontextprotocol/sdk': patch
---

Fix v1 server sending McpError messages with a doubled prefix on the wire
5 changes: 4 additions & 1 deletion src/shared/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,10 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
id: request.id,
error: {
code: Number.isSafeInteger(error['code']) ? error['code'] : ErrorCode.InternalError,
message: error.message ?? 'Internal error',
// McpError.message already carries the `MCP error <code>:` prefix, which is
// reconstructed by the receiving peer; send the original message instead of
// leaking the local prefixed form onto the wire (#2786).
message: error instanceof McpError ? error.originalMessage : (error.message ?? 'Internal error'),
...(error['data'] !== undefined && { data: error['data'] })
}
};
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2305,13 +2305,21 @@ export const ServerResultSchema = z.union([
]);

export class McpError extends Error {
/**
* The message exactly as passed to the constructor, without the
* `MCP error <code>:` prefix that `.message` carries. Peers reconstruct
* the prefixed form from `code`, so this is what belongs on the wire.
*/
public readonly originalMessage: string;

constructor(
public readonly code: number,
message: string,
public readonly data?: unknown
) {
super(`MCP error ${code}: ${message}`);
this.name = 'McpError';
this.originalMessage = message;
}

/**
Expand Down
49 changes: 49 additions & 0 deletions test/issues/test_2786_mcp_error_wire_message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Client } from '../../src/client/index.js';
import { InMemoryTransport } from '../../src/inMemory.js';
import { Server } from '../../src/server/index.js';
import { CallToolRequestSchema, ErrorCode, McpError, type JSONRPCError } from '../../src/types.js';

describe('Issue #2786: a handler-thrown McpError must not be double-prefixed', () => {
test('wire message carries the original message; client reconstructs a single prefix', async () => {
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const server = new Server({ name: 'test-server', version: '1.0.0' }, { capabilities: { tools: {} } });
const client = new Client({ name: 'test-client', version: '1.0.0' });

server.setRequestHandler(CallToolRequestSchema, async () => {
throw new McpError(ErrorCode.MethodNotFound, 'Unknown tool: nope');
});

// Capture the raw JSON-RPC error the server puts on the wire.
const wireErrors: JSONRPCError[] = [];
const originalSend = serverTransport.send.bind(serverTransport);
serverTransport.send = async message => {
if ('error' in message) {
wireErrors.push(message);
}
return originalSend(message);
};

await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);

let caught: unknown;
try {
await client.callTool({ name: 'nope', arguments: {} });
} catch (error) {
caught = error;
} finally {
await Promise.all([client.close(), server.close()]);
}

// The wire carries the original message, without the local `MCP error <code>:` prefix...
expect(wireErrors).toHaveLength(1);
expect(wireErrors[0]?.error.code).toBe(ErrorCode.MethodNotFound);
expect(wireErrors[0]?.error.message).toBe('Unknown tool: nope');

// ...and the client reconstructs exactly one prefix.
expect(caught).toBeInstanceOf(McpError);
const mcpError = caught as McpError;
expect(mcpError.code).toBe(ErrorCode.MethodNotFound);
expect(mcpError.message).toBe('MCP error -32601: Unknown tool: nope');
expect(mcpError.originalMessage).toBe('Unknown tool: nope');
});
});
Loading