Skip to content

Commit e3f2d3e

Browse files
authored
fix(core): respect NO_PROXY for network-based MCP servers (google-gemini#27012)
1 parent b705505 commit e3f2d3e

2 files changed

Lines changed: 46 additions & 3 deletions

File tree

packages/core/src/tools/mcp-client.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2066,6 +2066,30 @@ describe('mcp-client', () => {
20662066
vi.unstubAllGlobals();
20672067
}
20682068
});
2069+
2070+
it('respects NO_PROXY for network transports', async () => {
2071+
const mockFetch = vi
2072+
.fn()
2073+
.mockResolvedValue(new Response('OK', { status: 200 }));
2074+
vi.stubGlobal('fetch', mockFetch);
2075+
vi.stubEnv('NO_PROXY', 'localhost');
2076+
2077+
try {
2078+
const transport = await createTransport(
2079+
'test-server',
2080+
{ url: 'http://localhost/sse', type: 'sse' },
2081+
false,
2082+
MOCK_CONTEXT,
2083+
);
2084+
2085+
// For SSEClientTransport, the fetch is private or passed to the SDK.
2086+
// We can check if it creates the transport successfully.
2087+
expect(transport).toBeInstanceOf(SSEClientTransport);
2088+
} finally {
2089+
vi.unstubAllEnvs();
2090+
vi.unstubAllGlobals();
2091+
}
2092+
});
20692093
});
20702094

20712095
describe('should connect via url', () => {

packages/core/src/tools/mcp-client.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
StreamableHTTPClientTransport,
2222
type StreamableHTTPClientTransportOptions,
2323
} from '@modelcontextprotocol/sdk/client/streamableHttp.js';
24+
import { EnvHttpProxyAgent } from 'undici';
2425
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
2526
import {
2627
ListResourcesResultSchema,
@@ -2137,16 +2138,34 @@ function createUrlTransport(
21372138
| StreamableHTTPClientTransportOptions
21382139
| SSEClientTransportOptions,
21392140
): StreamableHTTPClientTransport | SSEClientTransport {
2140-
// Wrap fetch to treat GET 404 as 405 so servers that do not support the
2141-
// optional SSE GET stream (e.g. n8n native MCP) are handled gracefully.
2141+
// Create a proxy-aware fetcher that respects NO_PROXY for this MCP server
2142+
// This is especially important for local MCP servers (localhost, 127.0.0.1)
2143+
// when a company proxy is globally configured.
2144+
const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'];
2145+
const agent = new EnvHttpProxyAgent({ noProxy });
2146+
2147+
// Wrap fetch to:
2148+
// 1. Use the proxy-aware agent (respecting NO_PROXY)
2149+
// 2. Treat GET 404 as 405 so servers that do not support the
2150+
// optional SSE GET stream (e.g. n8n native MCP) are handled gracefully.
21422151
// The SDK already silently ignores 405; 404 is semantically equivalent here.
21432152
const baseFetch =
21442153
(transportOptions as StreamableHTTPClientTransportOptions).fetch ??
21452154
globalThis.fetch;
2155+
21462156
const httpOptions: StreamableHTTPClientTransportOptions = {
21472157
...transportOptions,
21482158
fetch: async (url, init) => {
2149-
const res = await baseFetch(url, init);
2159+
// If we have an explicit NO_PROXY, we use a proxy-aware dispatcher.
2160+
// We use the global fetch but pass a custom dispatcher in the init options.
2161+
// This avoids manual response reconstruction and dangerous type casts.
2162+
const res = noProxy
2163+
? await globalThis.fetch(url, {
2164+
...init,
2165+
dispatcher: agent,
2166+
} as RequestInit)
2167+
: await baseFetch(url, init);
2168+
21502169
return init?.method === 'GET' && res.status === 404
21512170
? new Response(null, { status: 405, statusText: 'Method Not Allowed' })
21522171
: res;

0 commit comments

Comments
 (0)