Skip to content

fix(mcp): stream pinned transport under Bun (providers + self-hosted-private MCP)#5901

Merged
waleedlatif1 merged 5 commits into
stagingfrom
fix/mcp-bun-undici-pinned-streaming
Jul 23, 2026
Merged

fix(mcp): stream pinned transport under Bun (providers + self-hosted-private MCP)#5901
waleedlatif1 merged 5 commits into
stagingfrom
fix/mcp-bun-undici-pinned-streaming

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

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 on undici.fetch:

  • LLM provider streaming — Azure OpenAI, vLLM, Azure Anthropic (they pass createPinnedFetch to the SDK as fetch and read streamed completion bodies)
  • A2A client
  • Self-hosted / private-IP MCP over SSE (createPinnedPrivateMcpFetch)

How

  • Pinned builder now routes through the same undiciRequestAsResponse helper (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.
  • Unlike the guarded path, the pinned fetch is handed straight to provider/A2A SDKs with no followRedirectsGuarded, and those SDKs rely on fetch's auto-redirect. So redirects use undici's redirect interceptor composed onto the pinned Agent — every hop still dispatches through the pinned connect.lookup (forces resolvedIP), so a redirect can't escape to another address (matches the old undici.fetch guarantee).
  • secureFetchWithPinnedIP (tools path, raw Node http/https with its own redirect loop) is untouched.

Type of Change

  • Bug fix

Testing

  • Rewrote pinned-fetch.server.test.ts for the new path (pinning/allowH2/IPv6 assertions kept; behavior asserts undici.request through the composed redirect dispatcher, init preservation, streaming Response). 93 security/MCP unit tests + 436 provider/a2a/mcp tests green; typecheck + lint clean.
  • e2e on both Node and Bun: pinned streaming SSE, 302→SSE followed via the interceptor, auth header + body forwarded across the redirect. Decode/abort/cap covered by the shared helper's own suite.
  • Pending: staging validation of an actual Azure/vLLM stream + a self-hosted MCP server in the Bun container.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…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.
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 23, 2026 10:08pm

Request Review

@cursor

cursor Bot commented Jul 23, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes core outbound fetch used by LLM providers, A2A, and private MCP—security-sensitive redirect, SSRF, and credential-forwarding behavior—with manual redirect logic replacing undici’s built-in follower.

Overview
Pinned fetch (createPinnedFetchWithDispatcher) no longer calls undici.fetch; it routes through the same undiciRequestAsResponse path as the SSRF-guarded fetch so streaming response bodies work under Bun (provider SSE, private MCP, etc.).

Redirect behavior is implemented in the pinned layer instead of undici auto-follow: redirect: manual returns 3xx (e.g. MCP auth probing), error throws on redirect, and default follow uses followRedirectsGuarded—cross-origin hops drop custom headers (no api-key leak), final response.url / redirected match fetch semantics, and every hop still uses the pinned Agent. liftFetchArgs centralizes lifting Request inputs (including redirect mode) for both guarded and pinned builders.

SSRF redirect checks gain allowRedirectToIp: only the validated pinned IP may appear as a private IP literal on the initial URL or a redirect hop (self-hosted MCP on 10.x), while redirects to other private targets (e.g. metadata IP) remain blocked.

Tests in pinned-fetch.server.test.ts are updated to mock undici.request and cover redirect modes, header stripping, private carve-out, and streaming bodies.

Reviewed by Cursor Bugbot for commit 8b376a0. Configure here.

Comment thread apps/sim/lib/core/security/input-validation.server.ts Outdated
Comment thread apps/sim/lib/core/security/input-validation.server.ts Outdated
@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves pinned outbound requests onto the Bun-compatible streaming transport while preserving redirect and SSRF protections.

  • Routes pinned provider and private MCP requests through undici.request and the Node-to-Web stream bridge.
  • Adds guarded redirect handling with redirect-mode preservation, cross-origin header removal, and a validated private-IP carve-out.
  • Updates pinned-fetch tests for streaming responses, redirects, dispatcher reuse, and private-address restrictions.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failures remain in the fix for cross-origin redirect credential handling.

Important Files Changed

Filename Overview
apps/sim/lib/core/security/input-validation.server.ts Replaces pinned undici.fetch calls with the shared streaming response bridge and adds guarded, mode-aware redirect handling.
apps/sim/lib/core/security/pinned-fetch.server.test.ts Updates pinned-fetch coverage for the request transport, streaming bodies, redirect behavior, header removal, and private-IP restrictions.

Sequence Diagram

sequenceDiagram
  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
Loading

Reviews (5): Last reviewed commit: "fix(mcp): permit the pinned IP as a redi..." | Re-trigger Greptile

Comment thread apps/sim/lib/core/security/input-validation.server.ts Outdated
…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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/core/security/input-validation.server.ts
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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@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.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/core/security/input-validation.server.ts Outdated
Comment thread apps/sim/lib/core/security/input-validation.server.ts
Comment thread apps/sim/lib/core/security/input-validation.server.ts Outdated
… 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).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

@waleedlatif1
waleedlatif1 merged commit 6f90d89 into staging Jul 23, 2026
20 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/mcp-bun-undici-pinned-streaming branch July 23, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant