diff --git a/packages/server/src/server/mcp.ts b/packages/server/src/server/mcp.ts index 05136f5b6..dc803939d 100644 --- a/packages/server/src/server/mcp.ts +++ b/packages/server/src/server/mcp.ts @@ -213,8 +213,8 @@ export class McpServer { await this.validateToolOutput(tool, result, request.params.name); return result; } catch (error) { - if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) { - throw error; // Return the error to the caller without wrapping in CallToolResult + if (error instanceof ProtocolError) { + throw error; // Keep protocol-level failures as JSON-RPC errors. } return this.createToolError(error instanceof Error ? error.message : String(error)); } diff --git a/packages/server/src/server/streamableHttp.ts b/packages/server/src/server/streamableHttp.ts index 74e689892..090fc953e 100644 --- a/packages/server/src/server/streamableHttp.ts +++ b/packages/server/src/server/streamableHttp.ts @@ -635,7 +635,8 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { if (options?.parsedBody === undefined) { try { rawMessage = await req.json(); - } catch { + } catch (error) { + this.onerror?.(error instanceof Error ? error : new Error(String(error))); return this.createJsonErrorResponse(400, -32_700, 'Parse error: Invalid JSON'); } } else { @@ -649,7 +650,8 @@ export class WebStandardStreamableHTTPServerTransport implements Transport { messages = Array.isArray(rawMessage) ? rawMessage.map(msg => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(rawMessage)]; - } catch { + } catch (error) { + this.onerror?.(error instanceof Error ? error : new Error(String(error))); return this.createJsonErrorResponse(400, -32_700, 'Parse error: Invalid JSON-RPC message'); } diff --git a/packages/server/test/server/streamableHttp.test.ts b/packages/server/test/server/streamableHttp.test.ts index ab6f22342..d07a2cb75 100644 --- a/packages/server/test/server/streamableHttp.test.ts +++ b/packages/server/test/server/streamableHttp.test.ts @@ -209,6 +209,54 @@ describe('Zod v4', () => { }); describe('POST Requests', () => { + it('should call onerror when request JSON is invalid', async () => { + const errorSpy = vi.fn(); + transport.onerror = errorSpy; + + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json' + }, + body: '{invalid-json' + }); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32_700, /Invalid JSON/); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy.mock.calls[0]?.[0]).toBeInstanceOf(Error); + }); + + it('should call onerror when request JSON-RPC message is invalid', async () => { + const errorSpy = vi.fn(); + transport.onerror = errorSpy; + + const request = new Request('http://localhost/mcp', { + method: 'POST', + headers: { + Accept: 'application/json, text/event-stream', + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + jsonrpc: '2.0', + method: 123, + params: {} + }) + }); + + const response = await transport.handleRequest(request); + + expect(response.status).toBe(400); + const errorData = await response.json(); + expectErrorResponse(errorData, -32_700, /Invalid JSON-RPC message/); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(errorSpy.mock.calls[0]?.[0]).toBeInstanceOf(Error); + }); + it('should handle post requests via SSE response correctly', async () => { sessionId = await initializeServer(); @@ -271,6 +319,31 @@ describe('Zod v4', () => { }); }); + it('should return a JSON-RPC error when calling an unknown tool', async () => { + sessionId = await initializeServer(); + + const toolCallMessage: JSONRPCMessage = { + jsonrpc: '2.0', + method: 'tools/call', + params: { + name: 'missing-tool', + arguments: {} + }, + id: 'call-missing' + }; + + const request = createRequest('POST', toolCallMessage, { sessionId }); + const response = await transport.handleRequest(request); + + expect(response.status).toBe(200); + + const text = await readSSEEvent(response); + const eventData = parseSSEData(text); + + expectErrorResponse(eventData, -32_602, /Tool missing-tool not found/); + expect((eventData as JSONRPCErrorResponse).id).toBe('call-missing'); + }); + it('should reject requests without a valid session ID', async () => { const request = createRequest('POST', TEST_MESSAGES.toolsList); const response = await transport.handleRequest(request);