fix(mcp): treat GET 404 as 405 in StreamableHTTPClientTransport (#24847)

Co-authored-by: Coco Sheng <cocosheng@google.com>
Co-authored-by: Spencer <spencertang@google.com>
Co-authored-by: Tommaso Sciortino <sciortino@gmail.com>
This commit is contained in:
krishdef7
2026-05-09 03:46:08 +05:30
committed by GitHub
parent 1238dcfe91
commit f51391a0f2
2 changed files with 61 additions and 3 deletions
@@ -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<Response>;
}
)._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', () => {
+19 -3
View File
@@ -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,
);
}