stacklok / stacklok/mecatl

MCP: tolerate/opt out of standalone GET/SSE notification stream on hostile gateways

Open
#395 0 comments 0 reactions 1 assignee View on GitHub

@jhrozek is already working on this.

Since Aug 5, 2026.

bug
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:362 as mcpsdk.StreamableClientTransport with the standalone GET stream enabled (ADR 0057 — it carries notifications/{tools,prompts,resources}/list_changed); DisableStandaloneSSE is 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 calls c.fail() after maxRetries (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 withSessionreconnect (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
  1. Reconnect = fresh initializenew Mcp-Session-Id → all server-side session state is lost (subscriptions, elicitation state).
  2. Reconnect deliberately does not re-fetch tool/resource/prompt lists (reconnect.go:112-115) — stale specs persist.
  3. 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.go already has the restartableServer/recordingDiag harness, 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 (ServerConfig is internal/), 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 via port.Diagnostics, never slog.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.