Skip to content

fix(mcp): bound and tear down one-shot OAuth fetches so auth can't hang#5789

Merged
waleedlatif1 merged 1 commit into
stagingfrom
fix/mcp-oauth-fetch-lifecycle
Jul 21, 2026
Merged

fix(mcp): bound and tear down one-shot OAuth fetches so auth can't hang#5789
waleedlatif1 merged 1 commit into
stagingfrom
fix/mcp-oauth-fetch-lifecycle

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Root-caused the Gauge MCP OAuth "Connecting… forever" hang on staging: the SSRF-guarded fetch used for all one-shot OAuth legs (discovery, dynamic client registration, token exchange/refresh, RFC 7009 revocation) created a fresh pinned undici Agent per request and never destroyed it, and returned the live response so the MCP SDK read the body outside any deadline. The SDK sets no timeout on OAuth legs, so a stalled body read or an accumulating leaked keep-alive socket could leave the callback (and the browser waiting on it) pending indefinitely.
  • Empirically confirmed via a staging diagnostic + DB: the callback burned its state, completed discovery, then hung before the token POST — no token exchange, no error, tokens never persisted. An isolated repro proved the SDK/flow itself reaches the token POST in <1s; the guard's lifecycle was the fault.

What changed

  • Buffer the (always-small) OAuth JSON body inside the guard, under the composed deadline/caller AbortSignal, then return a detached in-memory Response. undici's bodyTimeout only measures idle gaps between chunks and cannot bound a slow-drip/stalled body — the AbortSignal is the only true wall-clock deadline over the body read, so the read now lives inside it.
  • destroy() (not close()) the per-request pinned Agent on every path so a one-shot leg can't strand its keep-alive socket, and teardown itself can't hang on an in-flight request.
  • Fixed the same per-request Agent leak in the auth-type probe (tears the Agent down after best-effort session cleanup settles).
  • Returning a normalized global Response also makes provider token-endpoint errors surface cleanly instead of the SDK's opaque "[object Response]" parse failure.

Aligned with the official MCP TypeScript SDK's contract (all lifecycle/deadline enforcement lives in the injected fetch) and undici best practice (per-request pinned Agent → buffer body → destroy(); AbortSignal as the authoritative deadline).

Type of Change

  • Bug fix

Testing

  • Unit: 14 guard tests (pinning, teardown on success/failure, detached-body readability, deadline over a stalled body read, DNS/abort composition) + updated probe/revoke suites — full lib/mcp suite green (366 tests).
  • Integration against live Gauge: drove the real SDK auth() with the new guard behavior — reaches the token POST, returns in ~1.9s, 0 leaked Agents (8 created / 8 destroyed), and surfaces a clean invalid_grant message.
  • tsc clean; biome clean.
  • Not yet re-run through the full staging OAuth flow end-to-end (the temporary McpHttpDiag logging from diag(mcp): comprehensive HTTP logging for OAuth + streamable-HTTP transport #5782 is still live and will confirm the token POST now fires; a follow-up removes it once verified).

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)

@vercel

vercel Bot commented Jul 21, 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 21, 2026 1:52am

Request Review

@cursor

cursor Bot commented Jul 21, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches security-sensitive MCP OAuth/SSRF fetch paths and connection lifecycle; behavior changes are well-tested but affect all OAuth discovery/token/revocation legs.

Overview
Fixes MCP OAuth flows that could hang indefinitely (“Connecting… forever”) because one-shot legs used a per-request pinned undici Agent that was never destroyed and returned live responses so the MCP SDK read bodies outside any deadline.

createSsrfGuardedMcpFetch now buffers small OAuth JSON under the composed timeout/caller AbortSignal and returns a detached in-memory Response, so SSRF validation, the HTTP request, and the body read all respect the wall-clock deadline. Per-request pinned Agents are destroy()ed on every path (with a 1 MB maxResponseSize DoS cap via createPinnedFetchWithDispatcher). text/event-stream probe replies stay streaming (tee + drain) so auth classification from headers still works.

detectMcpAuthType switches to createPinnedFetchWithDispatcher when a pre-validated IP is supplied and tears down the Agent after best-effort session DELETE completes.

Tests expand coverage for teardown, buffered bodies, stalled body reads, oversized responses, and SSE vs JSON handling.

Reviewed by Cursor Bugbot for commit 66c6f33. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR prevents one-shot MCP OAuth requests from hanging indefinitely. The main changes are:

  • Buffers OAuth response bodies within the request deadline.
  • Returns detached in-memory responses to SDK callers.
  • Destroys per-request pinned Agents after requests and probes.
  • Adds a response-size limit for pinned OAuth fetches.
  • Expands tests for timeouts, teardown, streaming responses, and revocation.

Confidence Score: 5/5

The latest changes appear safe to merge.

  • No additional blocking issue qualifies for this follow-up review.

Important Files Changed

Filename Overview
apps/sim/lib/mcp/pinned-fetch.ts Adds deadline-bound body handling, detached responses, streaming cleanup, response limits, and Agent teardown.
apps/sim/lib/core/security/input-validation.server.ts Adds optional response-size limits to pinned undici Agents.
apps/sim/lib/mcp/oauth/probe.ts Keeps the pinned Agent alive through session cleanup and then destroys it.

Reviews (3): Last reviewed commit: "fix(mcp): bound and tear down one-shot O..." | Re-trigger Greptile

Comment thread apps/sim/lib/mcp/pinned-fetch.ts
@waleedlatif1
waleedlatif1 force-pushed the fix/mcp-oauth-fetch-lifecycle branch from 4d6ff77 to 96bbd06 Compare July 21, 2026 01:39
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/lib/mcp/pinned-fetch.ts
The SSRF-guarded fetch used for MCP OAuth (discovery, DCR, token exchange/
refresh, RFC 7009 revocation) created a fresh pinned undici Agent per request
and never tore it down, and returned the live response so the SDK's lazy body
read happened outside any deadline. The MCP SDK sets no timeout on OAuth legs,
so a stalled body read or a leaked keep-alive socket could leave the flow — and
the browser waiting on it — pending indefinitely ("Connecting… forever").

- Buffer the (always-small) OAuth JSON body inside the guard, under the composed
  deadline/caller AbortSignal, then return a detached in-memory Response. undici's
  bodyTimeout only measures idle gaps between chunks and cannot bound a slow-drip
  body; the AbortSignal is the only true wall-clock deadline over the body read.
- Destroy (not close) the per-request pinned Agent on every path so a one-shot
  leg can't strand its keep-alive socket, and teardown itself can't hang.
- Fix the same per-request Agent leak in the auth-type probe.
- Returning a normalized global Response also surfaces provider token-endpoint
  errors cleanly instead of the SDK's opaque parse failure.
@waleedlatif1
waleedlatif1 force-pushed the fix/mcp-oauth-fetch-lifecycle branch from 96bbd06 to 66c6f33 Compare July 21, 2026 01:52
@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 66c6f33. Configure here.

@waleedlatif1
waleedlatif1 merged commit 4187225 into staging Jul 21, 2026
15 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/mcp-oauth-fetch-lifecycle branch July 21, 2026 01:55
waleedlatif1 added a commit that referenced this pull request Jul 21, 2026
Reverts the temporary McpHttpDiag instrumentation from #5782 now that it has
served its purpose: it confirmed the OAuth fetch fix (#5789) makes the flow reach
and complete the token exchange, and it isolated a separate, pre-existing
transport-initialize stall (Gauge returns 200 + session then no body for 30s,
reproducible only from the staging egress — not from a fresh IP, and not with the
SDK/h2/pinning/tee in isolation).

- Delete apps/sim/lib/mcp/http-diagnostics.ts.
- Restore client.ts transport fetch to `...(pinned ? { fetch: pinned.fetch } : {})`.
- Restore oauth/auth.ts fetchFn to the plain SSRF-guarded default.
- Drop the diagnostic-specific test assertion.

Removes the body tee() from the transport hot path so the streamable-HTTP
connection runs clean while the transport stall is investigated separately.
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