Files
coder/agent/x
Jeremy Ruppel c4fabe90ae fix(agent/x/agentmcp): disable standalone SSE for HTTP MCP servers (#30484)
tl;dr is this should fix the storybook mcp server in Coder Agents. the
storybook mcp never responds to the SSE GET, so it never connects, and
reload fails. this disables that SSE GET and should let storybook
reconnect on config reload

---

HTTP workspace MCP servers whose `/mcp` endpoint never answers the
standalone SSE GET (for example, the Storybook dev server) hang the
agent's MCP reload indefinitely. The go-sdk opens that GET inside
`Connect` on a context the agent's 30s connect timeout does not bound,
and because reloads are singleflighted, every later `.mcp.json` change
waits behind the hung one. The server never shows up as connected or
failed, and no MCP config change is picked up until the agent restarts.

Set `DisableStandaloneSSE: true` on the streamable HTTP transport. The
agent's client registers no handlers for server-initiated messages, so
the stream is unused; tool call responses and in-request notifications
still arrive on the POST.

<details>
<summary>Investigation and decision log</summary>

**Symptom:** a `storybook` HTTP server in `.mcp.json` never produced
`storybook__*` tools in chats, while stdio servers worked.

**Findings:**

- Agent log showed the startup connect failing with `connection refused`
(Storybook was not up yet), and a later reload failing with `standalone
SSE request failed (session ID: ...)`, meaning `initialize` had
succeeded and the GET stream was the failure point.
- After touching `.mcp.json` with Storybook running, no reload result
was ever logged (success or warning).
- A standalone probe using go-sdk v1.8.0 and the same transport config
hung in `Connect` past its 30s timeout. Goroutine dump: `Client.Connect
-> sessionUpdated -> connectStandaloneSSE -> connectSSE`, which uses the
connection's lifetime context, not the connect context.
- `curl` against Storybook: `POST initialize` returns a session ID; `GET
/mcp` with that session sends no response headers at all. The spec
requires either `text/event-stream` or 405, so Storybook is
non-compliant, but the agent should not wedge on it.
- With `DisableStandaloneSSE: true`, the same probe connected in ~7ms
and listed all 8 Storybook tools.

**Why disable instead of alternatives:**

- `mcp.NewClient(..., nil)` in `manager.go` registers no
`tools/list_changed`, sampling, elicitation, roots, or logging handlers,
so nothing on the standalone stream is consumed today.
- The spec makes the standalone GET optional for clients; responses to
client requests are never sent on it.
- Bounding `Connect` with our own timeout would require abandoning a
stuck goroutine and session per bad server, since the hang is inside the
SDK call on a context we do not control.
- Tradeoff: live `tools/list_changed` refresh would need the stream
back, opened after `Connect` and off the reload path.

**Out of scope, noted for follow-up:**

- A server that is down when the agent first connects is not retried
until `.mcp.json` changes.
- A server that restarts invalidates the session ID, so the next POST
fails and nothing reconnects the session. Disabling the standalone
stream does not change this.

</details>

---

Generated by Coder Agents on behalf of @jeremyruppel.
2026-10-12 16:01:23 -04:00
..