From 9c7452872c3b536cf0233f4c04d8386f15b43d50 Mon Sep 17 00:00:00 2001 From: krishdef7 Date: Wed, 8 Apr 2026 00:28:05 +0530 Subject: [PATCH] fix(mcp): treat GET 404 as 405 in StreamableHTTPClientTransport Some MCP server implementations (e.g. n8n native /mcp-server/http) return 404 on GET requests to the SSE endpoint because they only support POST. The SDK already handles 405 gracefully (returns early, no error) since the SSE GET stream is optional per the MCP spec. A 404 is semantically identical in this context. Fixes #24838 --- packages/core/src/tools/mcp-client.test.ts | 42 ++++++++++++++++++++++ packages/core/src/tools/mcp-client.ts | 22 ++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index 4a14b671a0d..4ef1775ea40 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -1791,6 +1791,48 @@ describe('mcp-client', () => { }, }); }); + + it('wraps fetch to convert GET 404 to 405 for POST-only servers (e.g. n8n)', async () => { + const mockFetch = vi + .fn() + .mockResolvedValue( + new Response(null, { status: 404, statusText: 'Not Found' }), + ); + vi.stubGlobal('fetch', mockFetch); + + try { + const transport = await createTransport( + 'test-server', + { httpUrl: 'http://test-server' }, + false, + MOCK_CONTEXT, + ); + + const wrappedFetch = ( + transport as unknown as { + _fetch: ( + url: URL | string, + init?: RequestInit, + ) => Promise; + } + )._fetch; + + // GET 404 → 405: server doesn't support optional SSE GET stream + const getRes = await wrappedFetch('http://test-server', { + method: 'GET', + }); + expect(getRes.status).toBe(405); + expect(getRes.statusText).toBe('Method Not Allowed'); + + // POST 404 → unchanged: real "not found" errors must still propagate + const postRes = await wrappedFetch('http://test-server', { + method: 'POST', + }); + expect(postRes.status).toBe(404); + } finally { + vi.unstubAllGlobals(); + } + }); }); describe('should connect via url', () => { diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index 7e1ba49b894..162db954de3 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -2124,6 +2124,22 @@ function createUrlTransport( | StreamableHTTPClientTransportOptions | SSEClientTransportOptions, ): StreamableHTTPClientTransport | SSEClientTransport { + // Wrap fetch to treat GET 404 as 405 so servers that do not support the + // optional SSE GET stream (e.g. n8n native MCP) are handled gracefully. + // The SDK already silently ignores 405; 404 is semantically equivalent here. + const baseFetch = + (transportOptions as StreamableHTTPClientTransportOptions).fetch ?? + globalThis.fetch; + const httpOptions: StreamableHTTPClientTransportOptions = { + ...transportOptions, + fetch: async (url, init) => { + const res = await baseFetch(url, init); + return init?.method === 'GET' && res.status === 404 + ? new Response(null, { status: 405, statusText: 'Method Not Allowed' }) + : res; + }, + }; + // Priority 1: httpUrl (deprecated) if (mcpServerConfig.httpUrl) { if (mcpServerConfig.url) { @@ -2134,7 +2150,7 @@ function createUrlTransport( } return new StreamableHTTPClientTransport( new URL(mcpServerConfig.httpUrl), - transportOptions, + httpOptions, ); } @@ -2143,7 +2159,7 @@ function createUrlTransport( if (mcpServerConfig.type === 'http') { return new StreamableHTTPClientTransport( new URL(mcpServerConfig.url), - transportOptions, + httpOptions, ); } else if (mcpServerConfig.type === 'sse') { return new SSEClientTransport( @@ -2157,7 +2173,7 @@ function createUrlTransport( if (mcpServerConfig.url) { return new StreamableHTTPClientTransport( new URL(mcpServerConfig.url), - transportOptions, + httpOptions, ); }