Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions packages/core/src/tools/mcp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
22 changes: 19 additions & 3 deletions packages/core/src/tools/mcp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -2133,7 +2149,7 @@ function createUrlTransport(
}
return new StreamableHTTPClientTransport(
new URL(mcpServerConfig.httpUrl),
transportOptions,
httpOptions,
);
}

Expand All @@ -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(
Expand All @@ -2156,7 +2172,7 @@ function createUrlTransport(
if (mcpServerConfig.url) {
return new StreamableHTTPClientTransport(
new URL(mcpServerConfig.url),
transportOptions,
httpOptions,
);
}

Expand Down
Loading