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
24 changes: 24 additions & 0 deletions packages/core/src/tools/mcp-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1858,6 +1858,30 @@ describe('mcp-client', () => {
vi.unstubAllGlobals();
}
});

it('respects NO_PROXY for network transports', async () => {
const mockFetch = vi
.fn()
.mockResolvedValue(new Response('OK', { status: 200 }));
vi.stubGlobal('fetch', mockFetch);
vi.stubEnv('NO_PROXY', 'localhost');

try {
const transport = await createTransport(
'test-server',
{ url: 'http://localhost/sse', type: 'sse' },
false,
MOCK_CONTEXT,
);

// For SSEClientTransport, the fetch is private or passed to the SDK.
// We can check if it creates the transport successfully.
expect(transport).toBeInstanceOf(SSEClientTransport);
} finally {
vi.unstubAllEnvs();
vi.unstubAllGlobals();
}
});
});

describe('should connect via url', () => {
Expand Down
25 changes: 22 additions & 3 deletions packages/core/src/tools/mcp-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
StreamableHTTPClientTransport,
type StreamableHTTPClientTransportOptions,
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { EnvHttpProxyAgent } from 'undici';
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import {
ListResourcesResultSchema,
Expand Down Expand Up @@ -2123,16 +2124,34 @@ 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.
// Create a proxy-aware fetcher that respects NO_PROXY for this MCP server
// This is especially important for local MCP servers (localhost, 127.0.0.1)
// when a company proxy is globally configured.
const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
Comment thread
cocosheng-g marked this conversation as resolved.
const agent = new EnvHttpProxyAgent({ noProxy });

// Wrap fetch to:
// 1. Use the proxy-aware agent (respecting NO_PROXY)
// 2. 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);
// If we have an explicit NO_PROXY, we use a proxy-aware dispatcher.
// We use the global fetch but pass a custom dispatcher in the init options.
// This avoids manual response reconstruction and dangerous type casts.
const res = noProxy
? await globalThis.fetch(url, {
...init,
dispatcher: agent,
} as RequestInit)
: await baseFetch(url, init);

return init?.method === 'GET' && res.status === 404
? new Response(null, { status: 405, statusText: 'Method Not Allowed' })
: res;
Expand Down
Loading