MCP: tolerate/opt out of standalone GET/SSE notification stream on hostile gateways
@jhrozek is already working on this.
Since Aug 5, 2026.
- Dominant language
- Go
- Stars
- 152
- Forks
- 16
- Avg merge
- 14h 48m
- Merged PRs (30d)
- 536
Description
Split out from #391 (claim 1).
Claim
Some authenticated Streamable HTTP gateways close or reject the optional standalone GET/SSE notification stream after initialization while continuing to accept normal POST request/response traffic, and Mecatl then loses the MCP session.
Investigation (verified against the codebase + an empirical repro)
Verdict: partially confirmed — the mechanism is real, but "lost session" overstates the impact. Mecatl's ADR 0056 reconnect layer already re-establishes the session transparently on the next tool call.
The mechanism (confirmed)
- The transport is built at
internal/adapter/mcp/mcp.go:362asmcpsdk.StreamableClientTransportwith the standalone GET stream enabled (ADR 0057 — it carriesnotifications/{tools,prompts,resources}/list_changed);DisableStandaloneSSEis never set. - In the pinned
go-sdk v1.6.1(mcp/streamable.go): the initial GET tolerates 405/wrong-content-type/any 4xx gracefully (:1701-1722), but once established,handleSSE(:1985-2047) retries reconnects on drop and callsc.fail()aftermaxRetries(default 5) without progress (:2015-2020).c.fail()(:1739-1746) is connection-wide — it poisons the healthy POST channel along with the GET. Bumping the pin does not help: v1.7.0's fail behavior is byte-equivalent.
What actually happens (empirically verified)
Repro tests against a server that 200-then-closes every GET while POST stays healthy: 6 GETs over ~12s → session fails → the next CallTool rides withSession→reconnect (internal/adapter/mcp/reconnect.go:116-168) and succeeds (exactly one reconnect diagnostic pair). An initial-GET 401 doesn't kill anything (the 4xx-tolerant path); an initial-GET 500 triggers a transparent reconnect on first call.
The honest residual cost
- Reconnect = fresh
initialize→ newMcp-Session-Id→ all server-side session state is lost (subscriptions, elicitation state). - Reconnect deliberately does not re-fetch tool/resource/prompt lists (
reconnect.go:112-115) — stale specs persist. - Against a GET-hostile gateway the new session re-opens the GET, so the kill→reconnect→kill cycle repeats, each cycle adding up to
cfg.Timeout(default 30s) latency to one unlucky tool call. Not "permanently lost", but "transparent reconnect with state loss + latency spike + chronic churn".
Proposed fix
Per-server opt-out (recommended — exactly the transport option the issue asks for): add ServerConfig.DisableNotifications bool (mcp.go:66-80) mapping to transport.DisableStandaloneSSE at mcp.go:362, threaded through the four construction sites: internal/cliconfig/mcpserver.go:84 (shared --mcp-server flag), internal/adapter/mcp/source/toolhive.go:184, internal/adapter/acp/agent.go:758, internal/app/agentdefs.go:608. ~15-30 LoC + tests. Trade-off: ADR 0057 list-changed notifications stop on that server (the pre-0057 behavior); acceptable when the alternative is churn.
The root cause is upstream: the SDK should degrade standalone-GET failure to "no notifications" instead of c.fail() — worth filing/fixing in modelcontextprotocol/go-sdk separately.
Tests
- Land the repro as a characterization test locking in the mitigation: "GET hostile, POST healthy → transparent reconnect succeeds" (passes today;
reconnect_test.goalready has therestartableServer/recordingDiagharness, lines 95-231). - Fix test: connect with the opt-out against a GET-hostile server → assert zero GETs, tool call succeeds, no reconnect diagnostic.
Constraints
- Internal-only — no api-compat gate (
ServerConfigisinternal/), no proto change. - ADR 0057 mandates the stream unconditionally on, so it must be amended by a new ADR (ADRs are frozen, never edited in place).
- ADR 0027 List 1 (SSE goroutine inventory) needs an "only when enabled" note.
- Same-PR
user-docs/mention; any WARN viaport.Diagnostics, never slog.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.