Skip to content

feat(mcp): /mcp reconnect <name> for identity drift (closes part of #110)#115

Merged
esengine merged 1 commit intomainfrom
feat/mcp-reconnect-identity
May 2, 2026
Merged

feat(mcp): /mcp reconnect <name> for identity drift (closes part of #110)#115
esengine merged 1 commit intomainfrom
feat/mcp-reconnect-identity

Conversation

@esengine
Copy link
Copy Markdown
Owner

@esengine esengine commented May 2, 2026

Closes part of #110.

Scope

/mcp reconnect <name> for the identity-drift case only. Append / edit / reorder / remove drift surface a clear "restart Reasonix to apply" message instead of mutating the registry or prefix mid-session.

Why split this way: the graduated-permissive policy from the empirical spike (#113) needs API work on ImmutablePrefix (replaceTool / removeTool) before the other drift kinds can take effect mid-session. That's a follow-up PR. This one delivers the most common reconnect case (server crashed / restarted with same tool surface, user wants to retry without restarting Reasonix).

Touch

Indirection layer:

  • src/mcp/registry.ts — new McpClientHost = { client: McpClient } exposed via BridgeOptions.host. Tool closures resolve host.client at call time, so swapping the client doesn't require re-bridging tools.

New module:

Slash:

  • src/cli/ui/slash/handlers/mcp.ts — third subcommand reconnect <name>, fires async, reports via ctx.postInfo with ↻ reconnect… / ✓ connected / ✖ failed.
  • src/cli/ui/mcp-lifecycle.tsreconnect state added to the union.

Type / consumer churn:

  • src/cli/ui/slash/types.tsMcpServerSummary.client?: McpClient replaced by host: McpClientHost. The mutable wrapper IS the swap point.
  • src/cli/ui/mcp-browse.ts/resource and /prompt read through server.host.client. Dropped the "server not connected" branches — host always carries a client now.
  • src/cli/commands/chat.tsx — builds host at bridge time, stores on summary.

Tests

  • tests/mcp-reconnect.test.ts (new) — 2 cases for spec_parse early returns
  • tests/mcp-integration.test.ts — live test that bridge + host indirection routes a swapped client correctly through registry.dispatch
  • tests/slash.test.ts — 3 cases for slash dispatch (lifecycle line emission, unknown-name rejection with hint, no-arg usage)
  • tests/mcp-browse.test.tsserver() helper updated to accept client and wrap in host shape

npm run verify green: 1778 tests.

Deferred to follow-up

  • Append-drift mid-session: needs ImmutablePrefix.addTool to be safely callable from outside the loop, plus ToolRegistry mutation that doesn't break the prefix invariant
  • Edit-drift mid-session: needs ImmutablePrefix.replaceTool (doesn't exist yet)
  • Reorder/remove mid-session: requires ImmutablePrefix.removeTool + handling the cache reset announcement
  • r keybind in McpBrowser — reuse this slash once landed
  • --strict flag — opt-in to refusing even identity reconnects (specialised use case)

Each of those is its own PR with its own design call.

Test plan

  • npm run verify passes (1778 tests, +6 new)
  • Spec-parse failures surface spec_parse reason
  • Live demo MCP server reconnect via host indirection works (integration test)
  • Eyeball: launch with a real MCP server, type /mcp reconnect <name>, confirm the lifecycle lines appear and tools keep working

C2b implementation per RFC #110. Identity-drift only — append / edit /
reorder / remove drift cases surface a clear "restart Reasonix to apply"
message instead of mutating the registry or prefix mid-session. The
graduated permissive policy from the empirical spike (#113) needs API
work on `ImmutablePrefix` (replaceTool / removeTool) before the other
drift kinds can take effect mid-session; that's a follow-up PR.

Touch:

- `src/mcp/registry.ts`: new `McpClientHost = { client: McpClient }`
  indirection on BridgeOptions. Tool closures resolve the live client
  via `host.client` at call time, so reconnect can swap the underlying
  socket without re-bridging tools.
- `src/mcp/reconnect.ts` (new): `reconnectMcpServer({ host, spec,
  beforeTools })` re-handshakes a fresh transport, classifies drift,
  swaps `host.client` only on identity, closes the new client cleanly
  on refusal so the old one stays untouched.
- `src/cli/ui/slash/handlers/mcp.ts`: third subcommand `reconnect <name>`,
  fires async, reports via `ctx.postInfo` with the lifecycle
  `↻ reconnect…` / `✓ connected` / `✖ failed` formatter.
- `src/cli/ui/mcp-lifecycle.ts`: `reconnect` state added to the union.
- `src/cli/ui/slash/types.ts`: `McpServerSummary.client?` replaced by
  `host: McpClientHost`. `McpClient` import dropped (now via host).
- `src/cli/ui/mcp-browse.ts`: `/resource` and `/prompt` read through
  `server.host.client`. Disconnected-server warnings dropped — host
  always carries a client now.
- `src/cli/commands/chat.tsx`: builds the host at bridge time, stores
  it on the summary.

Tests:

- `tests/mcp-reconnect.test.ts`: 2 cases for spec_parse early returns.
- `tests/mcp-integration.test.ts`: live test that bridge + host
  indirection routes a swapped client correctly through registry.dispatch.
- `tests/slash.test.ts`: 3 cases for the slash dispatch (lifecycle line
  emission, unknown-name rejection with hint, no-arg usage).
- `tests/mcp-browse.test.ts`: server() helper updated to accept `client`
  and wrap in host shape.

Closes part of #110 (identity case only). Append/edit/reorder/remove
mid-session handling deferred — needs ImmutablePrefix surgery.
@esengine esengine merged commit 05d1efb into main May 2, 2026
1 check passed
@esengine esengine deleted the feat/mcp-reconnect-identity branch May 2, 2026 09:22
esengine added a commit that referenced this pull request May 2, 2026
Stage E2 of the C2b follow-ups: when /mcp reconnect finds the server
added new tools at the END of its tool list, register them
mid-session and addTool the prefix instead of refusing.

Drift kinds the reconnect now accepts:

- identity → free swap, ~95% cache hit (was already shipped in #115)
- append → register new tools, ~95% cache hit (the new chunks land
  in cache too, per benchmarks/spike-mcp-reconnect data)
- edit / reorder / remove → still refused with "restart Reasonix"

Touch:

- src/mcp/registry.ts — extract `registerSingleMcpTool(mcpTool, env)`
  + new `BridgeEnv` type (resolved bridge environment captured at
  first-bridge time). bridgeMcpTools' return shape gains `env` so
  reconnect can re-use the same options.
- src/cli/ui/slash/types.ts — McpServerSummary gains `bridgeEnv:
  BridgeEnv` so the append handler has everything it needs to
  register a new tool without redoing the whole bridge.
- src/cli/commands/chat.tsx — captures bridge.env onto the summary.
- src/mcp/reconnect.ts — accepts identity (always) plus append (if
  caller passes accept: ["identity", "append"]). On append, returns
  addedTools so the caller can register them. Identity is forced-
  accepted regardless of `accept` because it's free.
- src/cli/ui/mcp-append.ts (new) — `applyMcpAppend(loop, target,
  addedTools)` calls registerSingleMcpTool + prefix.addTool +
  refreshes target.report. Accepted-tools-only counting handles the
  unnamed-tool defensive case.
- src/cli/ui/mcp-reconnect-kickoff.ts — gains optional `applyAppend`
  callback. When set, opts the reconnect into ["identity",
  "append"]; when missing, behaviour is unchanged from #115.
- src/cli/ui/slash/handlers/mcp.ts + src/cli/ui/McpBrowser.tsx +
  src/cli/ui/App.tsx — both surfaces wire applyMcpAppend through.
- tests/mcp-append.test.ts (new) — 4 cases: registry registration,
  fingerprint invalidation, summary refresh, defensive skip on
  unnamed tools.

Closes most of #110. Edit / reorder / remove mid-session remain as
follow-up issues — each requires new ImmutablePrefix API
(replaceTool / removeTool) + cache-reset announcement.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant