From e3f2d3e1ef792c11040b09a8c509c2d8ba1c13b0 Mon Sep 17 00:00:00 2001 From: Coco Sheng Date: Thu, 14 May 2026 13:11:17 -0400 Subject: [PATCH] fix(core): respect NO_PROXY for network-based MCP servers (#27012) --- packages/core/src/tools/mcp-client.test.ts | 24 +++++++++++++++++++++ packages/core/src/tools/mcp-client.ts | 25 +++++++++++++++++++--- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts index be137f548e..7f0b837cba 100644 --- a/packages/core/src/tools/mcp-client.test.ts +++ b/packages/core/src/tools/mcp-client.test.ts @@ -2066,6 +2066,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', () => { diff --git a/packages/core/src/tools/mcp-client.ts b/packages/core/src/tools/mcp-client.ts index 61e2db17a8..3cadad99be 100644 --- a/packages/core/src/tools/mcp-client.ts +++ b/packages/core/src/tools/mcp-client.ts @@ -21,6 +21,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, @@ -2137,16 +2138,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']; + 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;