fix(mcp): stream pinned transport under Bun (providers + self-hosted-private MCP)#5901
Conversation
…ceptor Extends the Bun undici-streaming fix to createPinnedFetchWithDispatcher (providers, A2A, self-hosted-private MCP over SSE). It now routes through undiciRequestAsResponse like the guarded builder, so streaming bodies deliver under Bun. Unlike the guarded path it has no followRedirectsGuarded wrapper (it's handed straight to provider SDKs), so redirects are followed via undici's redirect interceptor composed onto the pinned Agent — every hop still dispatches through the pinned connect.lookup (resolvedIP), so a redirect can't escape to another address, matching the old fetch guarantee. secureFetchWithPinnedIP (raw Node http, tools path) is untouched.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryHigh Risk Overview Redirect behavior is implemented in the pinned layer instead of undici auto-follow: SSRF redirect checks gain Tests in Reviewed by Cursor Bugbot for commit 8b376a0. Configure here. |
Greptile SummaryThis PR moves pinned outbound requests onto the Bun-compatible streaming transport while preserving redirect and SSRF protections.
Confidence Score: 5/5The PR appears safe to merge. No blocking failures remain in the fix for cross-origin redirect credential handling. Important Files Changed
Sequence DiagramsequenceDiagram
participant SDK as Provider / MCP SDK
participant Fetch as Pinned Fetch
participant Guard as Guarded Redirect Loop
participant Agent as Pinned Undici Agent
participant Server as Remote Server
SDK->>Fetch: fetch(input, init)
Fetch->>Fetch: Lift Request fields and inspect redirect mode
alt redirect is manual or error
Fetch->>Agent: undici.request
Agent->>Server: Connect through pinned lookup
Server-->>Fetch: Response
else redirect is follow
Fetch->>Guard: followRedirectsGuarded
loop Each permitted hop
Guard->>Agent: undici.request with redirect manual
Agent->>Server: Connect through pinned lookup
Server-->>Guard: Response
Guard->>Guard: Validate target and strip cross-origin headers
end
Guard-->>Fetch: Streaming final Response
end
Fetch-->>SDK: Web-compatible Response
Reviews (5): Last reviewed commit: "fix(mcp): permit the pinned IP as a redi..." | Re-trigger Greptile |
…ed fetch Replaces the always-on redirect interceptor with redirect-mode-aware handling: - redirect:'manual' returns the 3xx without following (detectMcpAuthType inspects it) - redirect:'error' throws on a 3xx - default 'follow' uses followRedirectsGuarded, which drops ALL headers on a cross-origin hop (so a redirect can't disclose a provider api-key to another origin — Greptile P1) and stamps the final response.url + redirected flag. Extracts the shared Request-lift helper used by both guarded and pinned builders.
|
@cursor review |
Routing the pinned fetch through followRedirectsGuarded added an initial assertGuardedRedirectTarget check the old undici.fetch path never ran, which would block a self-hosted MCP configured with a private IP-literal URL (e.g. http://10.0.0.5:3000/mcp) — its own transport. The pinned path's callers already validate the target and the private carve-out intentionally pins to a private IP, so skip the initial-target check (validateInitialTarget: false) while still validating every redirect hop. Adds a regression test.
|
@cursor review |
liftFetchArgs copied method/headers/body/signal from a Request but omitted
redirect, so a Request({ redirect: 'manual' }) on the pinned path defaulted to
'follow' and was transparently followed. Copy input.redirect (explicit init still
wins). Adds a Request-input redirect-mode test.
|
@cursor review |
… block other private IPs Consolidates the pinned-path redirect policy into one mechanism. followRedirectsGuarded took validateInitialTarget to skip the initial private-IP check, but per-hop checks still blocked a self-hosted MCP redirecting to its own pinned private IP (e.g. a trailing-slash 301 to http://10.0.0.5/mcp/). Replace it with allowRedirectToIp: the pinned fetch permits exactly its own validated IP as a target — initial URL and any hop that stays on it — while every OTHER private target (e.g. the 169.254.169.254 metadata IP) stays blocked. Tests cover the same-IP hop (followed) and the metadata-IP escape (still refused).
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 8b376a0. Configure here.
Follow-up to #5897 (now merged). #5897 fixed the Bun undici-streaming hang on the guarded path (public MCP servers like GitHub). This extends the same fix to the pinned path (
createPinnedFetchWithDispatcher) — the remaining streaming consumers that were still onundici.fetch:createPinnedFetchto the SDK asfetchand read streamed completion bodies)createPinnedPrivateMcpFetch)How
undiciRequestAsResponsehelper (undici.request+ Node→Web bridge), inheriting fix(mcp): stream guarded transport via undici.request so SSE responses work under Bun #5897's hardening for free: Content-Encoding decode, URLSearchParams bodies, chunk-copy, decoder-crash guard, abort,maxResponseSize.followRedirectsGuarded, and those SDKs rely onfetch's auto-redirect. So redirects use undici's redirect interceptor composed onto the pinnedAgent— every hop still dispatches through the pinnedconnect.lookup(forcesresolvedIP), so a redirect can't escape to another address (matches the oldundici.fetchguarantee).secureFetchWithPinnedIP(tools path, raw Nodehttp/httpswith its own redirect loop) is untouched.Type of Change
Testing
pinned-fetch.server.test.tsfor the new path (pinning/allowH2/IPv6 assertions kept; behavior assertsundici.requestthrough the composed redirect dispatcher, init preservation, streaming Response). 93 security/MCP unit tests + 436 provider/a2a/mcp tests green; typecheck + lint clean.Checklist