diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index fdfbbd23de..d330a67fe0 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -1816,6 +1816,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 0441063f81..439e24fb71 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -2123,6 +2123,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) { @@ -2133,7 +2149,7 @@ function createUrlTransport( } return new StreamableHTTPClientTransport( new URL(mcpServerConfig.httpUrl), - transportOptions, + httpOptions, ); } @@ -2142,7 +2158,7 @@ function createUrlTransport( if (mcpServerConfig.type === 'http') { return new StreamableHTTPClientTransport( new URL(mcpServerConfig.url), - transportOptions, + httpOptions, ); } else if (mcpServerConfig.type === 'sse') { return new SSEClientTransport( @@ -2156,7 +2172,7 @@ function createUrlTransport( if (mcpServerConfig.url) { return new StreamableHTTPClientTransport( new URL(mcpServerConfig.url), - transportOptions, + httpOptions, ); }