MCP: no connection supervision — dead servers stay 'ready' until a call fails, no auto-reconnect or list_changed
- Dominant language
- Rust
- Stars
- 41k
- Forks
- 3.6k
- Avg merge
- 13h 59m
- Merged PRs (30d)
- 299
Description
## Problem
A configured MCP server that dies mid-session shows "connected" until the
next tool call hits EOF — nothing watches the transport between calls, and
recovery only happens lazily at the next `get_or_connect`. User report:
"we don't have anything supporting the servers staying connected or
making it easier for them to reconnect … it just feels clunky."
Concrete state today (`crates/tui/src/mcp.rs`):
- `McpConnection::is_ready()` (~2254) is a state flag — `state == Ready`
with no liveness observation. The stdio read side only runs per-request
(`mcp/stdio.rs:334` sees EOF when a call is in flight), so a crashed
child is invisible until the model calls a tool and it fails.
- `connect_backoff` + `get_or_connect` give reconnect-on-use with
cooldown, and `call_tool` retries exactly once on
`is_mcp_stale_session_error` — but general transport failures do not
get the drop→reconnect→retry path.
- No `notifications/tools/list_changed` (or resources/prompts) handling —
a server that changes its tool set mid-session is invisible until a
manual reload.
- And there are two MCP client stacks: `tui/src/mcp/` (~13.2k lines) vs
`crates/mcp` (~4.5k) — #6142. Lifecycle work should land once, on the
reconciled stack.
## What the reference harnesses do that we don't
From `refs/` (verified against source/docs in-tree):
**deepseek-harness** (`refs/dsh/.agents/notes/archived/feature/2026-08-06-mcp-client-auto-reconnect.md`):
- A per-server **connection supervisor** arms `client.onclose` per
generation — a crashed stdio child is observed at exit, not at next
call, and a reconnect loop starts immediately.
- **Outage budget with a stability window**: backoff doubles to a cap;
the budget resets only if a connection survives past `maxDelayMs`, so
an occasionally-crashing server recovers forever while a crash loop
exhausts (~2.5min default) and stops — the uptime gate prevents a
restart storm.
- **Generation fencing**: an `isCurrent` fence makes stale generations
inert so overlapping reconnects cannot double-register or leak tools.
- **Tool-list stability during outage**: the last good generation stays
registered (calls fail, but the model-visible schema never flaps);
tools unregister only at final failure.
**oh-my-pi** (`refs/omp/oh-my-pi/docs/mcp-runtime-lifecycle.md`):
- `transport.onClose` → automatic reconnect with 500ms/1s/2s/4s backoff;
a **crash-storm circuit breaker** suspends auto-reconnect after >5
attempts in 30s; manual `/mcp reconnect` resets the history.
- Tool calls on retriable connection errors attempt **one reconnect +
retry** for any transport failure, not only stale-session.
- `tools/list_changed`, `resources/list_changed`, `prompts/list_changed`
fan out to per-server refresh.
- **Fast-startup gate**: 250ms race — fulfilled connects become live
tools, pending ones return cached `DeferredMCPTool`s (tool-definition
cache), and late registrations rebind via `onToolsChanged`.
- `getConnectionStatus` derives connected/connecting/disconnected from
pending/connection maps — the UI never shows a dead server as ready.
**codex** (`refs/codex/codex-rs/codex-mcp/`):
- `defer_startup` + `startup_trigger` — registration without connection
(already filed as #6033, which names this design).
- Per-server `startup_timeout`, concurrent connects in a JoinSet,
`reconnect_failed_startup` for boot-time stragglers.
## Suggested shape
1. Per-server supervisor armed on transport close/child exit (stdio) —
observe death between calls; SSE/HTTP transports keep their own
recovery and surface per-call failures (DSH's recorded limitation).
2. Bounded backoff with an uptime-gated outage budget + crash-loop
breaker; manual `/mcp reconnect ` resets it (already exists as
`retry_connection`).
3. Reconnect+single-retry on any retriable transport error in
`call_tool`, not only stale-session.
4. Handle `notifications/tools/list_changed` (and resources/prompts) →
refresh that server's catalog slice.
5. Keep the last-good tool catalog registered during an outage so the
model-visible tool set doesn't flap; reflect liveness in the status
surface (Extensions MCP rows / launch block) instead of a stale
"ready".
6. Land it once on the reconciled stack — sequence after/with #6142 and
#6033 per #6142's own ordering note.
## Related
- #6033 lazy connect at point of use (codex `defer_startup` design)
- #6142 reconcile the two MCP stacks
- #6131 one negotiated protocol adapter
- #6186 plugin-contributed rows advertise a remove that 404s
- #6030 mid-session auth attach + `/mcp reload` crash (partially
subsumed by lifecycle work)
Contributor guide
Research direction
Start with crates/tui/src/mcp.rs, especially McpConnection::is_ready(), get_or_connect(), and call_tool(), then inspect mcp/stdio.rs around the EOF handling. Read the reconciled-stack work in #6142 and the lazy-connect design in #6033 before choosing an implementation path. Done means liveness is surfaced between calls, retriable failures reconnect once, catalogs refresh on change, and the work lands on one MCP stack.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- backend-api-design, cli
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100