diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 316074e2d..a4bb8f846 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -166,15 +166,16 @@ export class McpServer { ); this.server.setRequestHandler('tools/call', async (request, ctx): Promise => { - try { - const tool = this._registeredTools[request.params.name]; - if (!tool) { - throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`); - } - if (!tool.enabled) { - throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`); - } + // Tool lookup errors are JSON-RPC errors, not tool errors + const tool = this._registeredTools[request.params.name]; + if (!tool) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} not found`); + } + if (!tool.enabled) { + throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Tool ${request.params.name} disabled`); + } + try { const isTaskRequest = !!request.params.task; const taskSupport = tool.execution?.taskSupport; const isTaskHandler = 'createTask' in (tool.handler as AnyToolHandler); diff --git a/packages/server/src/server/stdio.ts b/packages/server/src/server/stdio.ts index 562c6861c..c004de60e 100644 --- a/packages/server/src/server/stdio.ts +++ b/packages/server/src/server/stdio.ts @@ -37,6 +37,14 @@ export class StdioServerTransport implements Transport { _onerror = (error: Error) => { this.onerror?.(error); }; + _onstdouterror = (error: Error) => { + // Handle stdout errors (e.g., EPIPE when client disconnects) + // Trigger close to clean up gracefully + this.close().catch(() => { + // Ignore errors during close + }); + this.onerror?.(error); + }; /** * Starts listening for messages on `stdin`. @@ -51,6 +59,7 @@ export class StdioServerTransport implements Transport { this._started = true; this._stdin.on('data', this._ondata); this._stdin.on('error', this._onerror); + this._stdout.on('error', this._onstdouterror); } private processReadBuffer() { @@ -72,6 +81,7 @@ export class StdioServerTransport implements Transport { // Remove our event listeners first this._stdin.off('data', this._ondata); this._stdin.off('error', this._onerror); + this._stdout.off('error', this._onstdouterror); // Check if we were the only data listener const remainingDataListeners = this._stdin.listenerCount('data'); @@ -87,12 +97,25 @@ export class StdioServerTransport implements Transport { } send(message: JSONRPCMessage): Promise { - return new Promise(resolve => { + return new Promise((resolve, reject) => { const json = serializeMessage(message); + + // Handle write errors (e.g., EPIPE when client disconnects) + const onError = (error: Error) => { + this._stdout.off('error', onError); + reject(error); + }; + + this._stdout.once('error', onError); + if (this._stdout.write(json)) { + this._stdout.off('error', onError); resolve(); } else { - this._stdout.once('drain', resolve); + this._stdout.once('drain', () => { + this._stdout.off('error', onError); + resolve(); + }); } }); } diff --git a/test/integration/test/server/mcp.test.ts b/test/integration/test/server/mcp.test.ts index 091e4ac21..ce4072006 100644 --- a/test/integration/test/server/mcp.test.ts +++ b/test/integration/test/server/mcp.test.ts @@ -1837,25 +1837,17 @@ describe('Zod v4', () => { await Promise.all([client.connect(clientTransport), mcpServer.server.connect(serverTransport)]); - const result = await client.request( - { - method: 'tools/call', - params: { - name: 'nonexistent-tool' - } - }, - CallToolResultSchema - ); - - expect(result.isError).toBe(true); - expect(result.content).toEqual( - expect.arrayContaining([ + await expect( + client.request( { - type: 'text', - text: expect.stringContaining('Tool nonexistent-tool not found') - } - ]) - ); + method: 'tools/call', + params: { + name: 'nonexistent-tool' + } + }, + CallToolResultSchema + ) + ).rejects.toThrow(/Tool nonexistent-tool not found/); }); /***