fix(transport): avoid stall when legacy SSE headers stay pending - #282
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed August 9, 2026, 9:41 PM ET / August 10, 2026, 01:41 UTC. ClawSweeper reviewWhat this changesCaps legacy standalone-SSE startup waiting at 250 ms so HTTP MCP list and call operations continue when the receive stream has not yet returned headers. Regression provenancePossible regression — probable (reviewed change; known regression link). No predecessor PR is attributed. Merge readiness⛔ Blocked until stronger real behavior proof is added - 3 items remain Keep open: current main still waits unboundedly for legacy standalone-SSE response headers, while this PR supplies a focused bounded-wait repair and regression coverage. The collaborator’s explicit request for an authenticated Mobbin run from the final head remains the merge gate. Priority: P2 Review scores
Verification
How this fits togetherMCPorter’s HTTP transport connects configured MCP servers and then feeds the resulting client to CLI list and call operations. For legacy protocol servers, the SDK opens a long-lived SSE receive channel after initialization, and its startup timing affects both the first request and later server-initiated messages. flowchart TD
A[Configured HTTP MCP server] --> B[Legacy connection setup]
B --> C[Standalone SSE receive request]
C --> D{Headers arrive during grace?}
D -->|Yes| E[Start list or call]
D -->|No| F[Continue after bounded grace]
F --> E
C --> G[Long-lived server message stream]
E --> H[CLI tool result]
G --> H
Before merge
Agent review detailsSecurityNone. Review metrics
Merge-risk optionsMaintainer options:
Technical reviewBest possible solution: Publish a redacted final-head built-CLI Mobbin Do we have a high-confidence way to reproduce the issue? Yes, at source level: a legacy server that accepts the standalone SSE GET but withholds headers leaves current main awaiting the tracked fetch before list or call. This read-only review did not execute that fixture. Is this the best way to solve the issue? Yes, the bounded grace preserves normal early readiness while preventing the proven unbounded wait, and the branch covers a later-header elicitation path; final-head provider proof is still required before merge. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against e404ed575a47. LabelsLabel justifications:
EvidenceWhat I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (42 earlier review cycles; latest 8 shown)
|
|
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
What Problem This Solves
MCPorter 0.13.0 can finish OAuth successfully and still report a legacy HTTP MCP server as offline. After
notifications/initialized, the SDK v2 client starts a long-lived standalone SSEGET; MCPorter waits for that fetch promise before allowinglistorcallto continue. A fetch promise does not settle until response headers arrive, so a server that accepts the SSE request but leaves its response completely idle can hold MCPorter at startup until the outer command timeout.That is the live Mobbin failure behind this PR: authentication completes, but
mcporter list mobbintimes out after 30 seconds. This is distinct from #234, which isolates a byte-idle SSE response that has already sent headers from the ordinary request connection pool. The unbounded pre-header wait was added with the SDK v2 migration in #255.Why This Change Was Made
The repair is deliberately narrow. Legacy connections still give the standalone SSE receive channel a chance to establish, but that readiness wait is capped at 250 ms:
Removing the readiness wait entirely made the existing legacy HTTP elicitation/sampling fixture race its first tool call ahead of the receive channel. Keeping the wait unbounded reproduces the reported stall. The bounded grace passes both paths without adding configuration, dependencies, or changes to OAuth behavior.
The pending-header regression fixture accepts the standalone SSE request without flushing response headers, matching the observed hosted-server behavior. A second compatibility test routes a real legacy fixture through a proxy that forwards the SSE request immediately but withholds its response headers for 500 ms—twice the startup grace. Its first tool call begins after the grace, receives a later
elicitation/createrequest on the still-open stream, returns the headless decline, and completes successfully. This directly covers the delayed-header ordering boundary raised by ClawSweeper.The production change remains confined to
src/runtime/http-transport.ts; the other changes are focused integration coverage and the 0.13.1 changelog entry.User Impact
Legacy hosted MCP servers such as Mobbin can list and call tools even when their standalone SSE response does not immediately send headers. Servers that establish the receive channel normally retain the existing readiness ordering. When headers arrive after the grace, the receive stream remains active and can still deliver server-initiated elicitation, as verified with a 500 ms delay. Header-idle servers incur at most the 250 ms startup grace per new connection instead of hanging until the command timeout.
No configuration, credentials, OAuth persistence, protocol selection, or public API changes.
Evidence
Exact PR head
8412b06, macOS, Node 24.6.0, pnpm 10.33.2.Live authenticated Mobbin run from the built PR head (redacted transcript; no token or credential output):
Non-vacuous regression proof: with only the production await reset to current
main's unboundedawait transportOptions.standaloneSseStarted, the new pending-header fixture fails as offline at its 2-second command timeout:Restoring the bounded wait makes the same fixture pass:
Delayed-header compatibility proof requested by ClawSweeper: the proxy withholds standalone SSE response headers for 500 ms, beyond the 250 ms grace, while the first legacy call completes a server-initiated elicitation exchange:
Repository gates:
Scope: four files,
+93 / -5; no dependency, configuration, or public API changes.