Repository navigation
core: honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY in the Node transport #1563
Description
Activity
- linked a pull request that will close this issuefeat(core): honor HTTPS_PROXY/HTTP_PROXY/NO_PROXY in the Node transport #1590
on Jul 1, 2026 Smoke test & AGENTS.md audit — PR #1590
Reviewed
v2/1563-node-proxy(core: honorHTTPS_PROXY/HTTP_PROXY/NO_PROXYin the Node transport via undici). Node used: v25.9.0 (well above the new>=22.19.0floor, so the real proxy path was exercised, not just the tests).1. Smoke test (real output, no Playwright)
Wrote a standalone
tsxscript that imports the real exportedreadProxyEnv()/withProxyDispatcher()fromcore/mcp/node/transport.tsand drives them directly (script in the collapsible below). The undici-missing branch is reproduced faithfully with a module-resolution hook that makesimport("undici")reject only fortransport.ts, hitting the actualcatchinwithProxyDispatcher(no source edits).=== 1. readProxyEnv() reads the standard env vars === no proxy vars set -> readProxyEnv() = undefined HTTP_PROXY set -> readProxyEnv() = http://proxy.example:3128 + HTTPS_PROXY (precedence)-> readProxyEnv() = http://secure.example:3128 whitespace-only value -> readProxyEnv() = undefined === 2. Passthrough: no dispatcher injected when no proxy is set === withProxyDispatcher === original fetch (unchanged)? true === 3. Injection: dispatcher added to RequestInit when a proxy IS set === withProxyDispatcher returned a NEW wrapped fetch? true request input preserved? true request init.method preserved? true dispatcher injected into RequestInit? true dispatcher constructor name EnvHttpProxyAgent 2nd call reuses same lazy singleton dispatcher? true === 4. Actionable error when a proxy is set but undici is unavailable === threw actionable error: message: HTTPS_PROXY / HTTP_PROXY is set but the `undici` package is not available. Install it (it ships with the CLI client) or unset the proxy env var. cause: Error: Cannot find module 'undici' (blocked by smoke hook)Confirmed behaviors: env precedence (HTTPS before HTTP, lowercase forms, whitespace-only ignored), passthrough returns the identical fetch reference when no proxy is set (zero cost), an
EnvHttpProxyAgentdispatcheris injected intoRequestInitwith input/init preserved, the dispatcher is a lazy singleton reused across calls, and the actionable error fires (withcausechained) when a proxy is configured but undici can't load.2. AGENTS.md compliance audit
-
Mantine: N/A — this is a
core/transport change with no UI/component surface. -
TypeScript: clean. No
any, noeslint-disable/@ts-*/ suppressions in the added lines (grepped the full diff). Uses a preciseNodeRequestInit = RequestInit & { dispatcher?: Dispatcher }type,import type { Dispatcher } from "undici"(erased at runtime), andcause: unknownin the import-failure handler. -
Coverage —
core/mcp/node/transport.ts(all four dims, ≥90 gate):Metric % Statements 100 Branch 93.61 Functions 100 Lines 100 All four clear the 90 gate. (Uncovered: only branch partials on lines 92/119/129.)
-
Integration suite:
npm run test:integration→ 804 passed (38 files), including the 5 new proxy tests (readProxyEnvprecedence, passthrough, dispatcher injection + lazy singleton,createTransportNodestreamable-http wrapping, and the undici-missing actionable error viavi.doMock). -
Node-floor consistency (5 touch-points, all aligned):
.github/workflows/main.yml— CInode-version20.x→22.xpackage.json—engines.node>=22.7.5→>=22.19.0package-lock.json(root) —engines.node→>=22.19.0clients/cli/package.json+ lock — addsundici@^8.5.0(whose ownengines.nodeis>=22.19.0, the reason for the bump)specification/v2_cli_tui_launcher.md— doc floor>=22.7.5→>=22.19.0
No stray
22.7.5or20.xreferences remain (grepped repo-wide). CLI + web READMEs document the proxy support and the Node floor.
Notes
- A full
cd clients/web && npm run test:coverage(unit + integration) showed 2 pre-existing, unrelated happy-dom timeouts under coverage instrumentation (ResourcesScreen,ServerConfigModal) — no connection to the transport/proxy change; thetransport.tsnumbers above are from a clean integration-project coverage run.
Verdict
APPROVE. The change is correct, well-typed, well-tested, and cheap when no proxy is set (identical-reference passthrough, lazy undici import). Real end-to-end smoke confirms dispatcher injection, passthrough, and the actionable undici-missing error. Coverage clears the 90 gate on all four dims; the Node
>=22.19.0floor is consistent across all five touch-points.Smoke script (tsx) + undici-block module hook
proxy-smoke.mts(sections 1–3):const TRANSPORT = "<repo>/core/mcp/node/transport.ts"; const PROXY_VARS = ["HTTPS_PROXY","https_proxy","HTTP_PROXY","http_proxy","NO_PROXY","no_proxy"]; const clearProxy = () => { for (const v of PROXY_VARS) delete process.env[v]; }; const line = (s = "") => process.stdout.write(s + "\n"); async function main() { const { readProxyEnv, withProxyDispatcher } = await import(TRANSPORT); line("=== 1. readProxyEnv() reads the standard env vars ==="); clearProxy(); line(`no proxy vars set -> readProxyEnv() = ${String(readProxyEnv())}`); process.env.HTTP_PROXY = "http://proxy.example:3128"; line(`HTTP_PROXY set -> readProxyEnv() = ${readProxyEnv()}`); process.env.HTTPS_PROXY = "http://secure.example:3128"; line(`+ HTTPS_PROXY (precedence)-> readProxyEnv() = ${readProxyEnv()}`); clearProxy(); process.env.https_proxy = " "; line(`whitespace-only value -> readProxyEnv() = ${String(readProxyEnv())}`); line("\n=== 2. Passthrough: no dispatcher injected when no proxy is set ==="); clearProxy(); const passBase = (async () => new Response("ok")) as unknown as typeof fetch; line(`withProxyDispatcher === original fetch (unchanged)? ${withProxyDispatcher(passBase) === passBase}`); line("\n=== 3. Injection: dispatcher added to RequestInit when a proxy IS set ==="); clearProxy(); process.env.HTTPS_PROXY = "http://proxy.example:3128"; const calls: Array<{ input: unknown; init: Record<string, unknown> | undefined }> = []; const base = (async (input: unknown, init?: Record<string, unknown>) => { calls.push({ input, init }); return new Response("ok"); }) as unknown as typeof fetch; const proxied = withProxyDispatcher(base); line(`withProxyDispatcher returned a NEW wrapped fetch? ${proxied !== base}`); await proxied("https://example.com/mcp", { method: "POST" } as RequestInit); const d1 = (calls[0].init as { dispatcher?: { constructor: { name: string } } }).dispatcher; line(`request input preserved? ${calls[0].input === "https://example.com/mcp"}`); line(`request init.method preserved? ${(calls[0].init as { method?: string }).method === "POST"}`); line(`dispatcher injected into RequestInit? ${d1 !== undefined}`); line(`dispatcher constructor name ${d1?.constructor.name}`); await proxied("https://example.com/mcp"); line(`2nd call reuses same lazy singleton dispatcher? ${(calls[1].init as { dispatcher?: unknown }).dispatcher === d1}`); } main();
proxy-undici-missing.mts(section 4 — self-registers the hook, then imports the transport under tsx):import { register } from "node:module"; import { pathToFileURL } from "node:url"; const TRANSPORT = "<repo>/core/mcp/node/transport.ts"; register(pathToFileURL("<scratch>/undici-block-hook.mjs")); const { withProxyDispatcher } = await import(TRANSPORT); process.env.HTTPS_PROXY = "http://proxy.example:3128"; const proxied = withProxyDispatcher((async () => new Response("ok")) as unknown as typeof fetch); try { await proxied("https://example.com"); } catch (err) { process.stdout.write(" message: " + (err as Error).message + "\n"); process.stdout.write(" cause: " + String((err as Error).cause) + "\n"); }
undici-block-hook.mjs(resolve hook — failsimport("undici")only for transport.ts):export async function resolve(specifier, context, next) { if (specifier === "undici" && String(context.parentURL).includes("core/mcp/node/transport")) { throw new Error("Cannot find module 'undici' (blocked by smoke hook)"); } return next(specifier, context); }
Run:
clients/tui/node_modules/.bin/tsx proxy-smoke.mtsthen… tsx proxy-undici-missing.mts. Not committed.-
🔗 Implemented by PR #1590. Its code is already integrated into the Wave 1 rollup branch
1579-wave-1(see rollup PR #1601 / verification #1600). #1590 currently targetsv2/mainand will auto-show as Merged once the rollup lands inv2/main. The Development panel doesn't link it now because the closing keyword is on a non-default base branch.- added a commit that references this issue
on Jul 28, 2026
Summary
Make the Node transport factory honor the standard
HTTPS_PROXY/HTTP_PROXY/NO_PROXYenvironment variables (CLI and web backend) by wrapping fetch with undici'sEnvHttpProxyAgent.Why
Users behind corporate proxies currently can't reach remote MCP servers from the CLI or the web backend without inspector-specific configuration. Honoring the conventional env vars fixes that with zero new flags.
Reference implementation (PR #1510)
Re-implement informed by these changes at
33fac3f:readProxyEnv()(checksHTTPS_PROXY/https_proxy/HTTP_PROXY/http_proxy),withProxyDispatcher()wrapping baseFetch, lazy undici import with an actionable error when a proxy is set but undici is missing (+63)undici@^8.5.0Depends on
Nothing — Wave 1 foundation, parallel-safe.
Notes
undici@^8.x's Node engine floor against our supported Node range and document it in the README (dependency + proxy behavior) per AGENTS.md's docs rule.clients/web/src/test/integration/mcp/node/(auto-picked-up by theintegrationvitest project).Part of the PR #1510 decomposition (see tracking issue).