Skip to content

Commit 6cdba06

Browse files
committed
docs(planning): reconcile connection-resilience docs and add leftovers inventory
Backend-resilience and pool-invalidation docs still described shipped work (dcc2977) as pending, and clone-auth had drifted the same way. Update all five to reflect what actually landed, add a dedicated leftovers doc for still-parked/open items, and scope web-admin clone parity as required work (Phase 5) instead of a deferred stub. Signed-off-by: crimsonsunset <jsangio1@gmail.com>
1 parent 508930e commit 6cdba06

7 files changed

Lines changed: 174 additions & 109 deletions

docs/manual/cursor-workspace-bridge.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ On first connect, McpMux may show **Name this machine** — approve it.
4141
Check the McpMux log (macOS:
4242
`~/Library/Application Support/com.mcpmux.desktop/logs/mcpmux.<date>.log`):
4343

44-
- `[SessionRoots] pinned explicit workspace root from X-Mcpmux-Workspace header`
45-
with the correct path per session.
44+
- `[SessionRoots] X-Mcpmux-Workspace held until mcp-session-id exists` on
45+
initialize, then `pinned explicit workspace root from X-Mcpmux-Workspace header`
46+
with the correct path per session. A non-empty header without a session id
47+
is held, not skipped.
4648
- `[FeatureSetResolver] resolved via WorkspaceBinding workspace_root=…` matching
4749
each window's folder.
4850

docs/planning/backend-connection-resilience-test.md

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
| C HA idle | **SKIPPED** |
1717
| D unmatched | **SKIPPED** — grant-layer "did you mean", never reached classifier |
1818

19-
Confounders: Tauri-watch rebuild wiped inbound sessions (`POST /mcp` → 404 until MCP reload). After reload, 6 unpinned roots required one `mcpmux_set_workspace_root` to *this* repo before invoke was possible. That pin was session disambiguation, not the reconnect path.
19+
Confounders (A/B run, before `dcc2977`): Tauri-watch rebuild wiped inbound sessions (`POST /mcp` → 404 until MCP reload). After reload, 6 unpinned roots required one `mcpmux_set_workspace_root` to *this* repo. That pin was session disambiguation, not the reconnect path.
2020

21-
**Inbound 404 after gateway rebuild (Decision 3, follow-on):** process death drops `LocalSessionManager`. `POST /mcp` with a stale `Mcp-Session-Id` is a spec-correct 404. [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) and the [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk/issues/1708) do **not** re-`initialize` on that 404 (they stay stuck on the dead session id). Do not persist sessions. Recovery is Reload MCP once. `/health` staying 200 means the gateway is up; 404 is not "gateway is down."
21+
Follow-on (`dcc2977`) playbook: [`pool-invalidation-and-session-survival-test.md`](./pool-invalidation-and-session-survival-test.md) — E/F/H PASS (hold-then-pin, no `set_workspace_root`), G BLOCKED.
2222

23-
**Header pin after reload (Decision 4):** a non-empty `X-Mcpmux-Workspace` is held across initialize (no session id yet) and applied when `mcp-session-id` appears. After Reload MCP, `mcpmux_list_servers` should be bound for this repo without `set_workspace_root`. Empty `${workspaceFolder}` is still the Agents-window hole.
23+
**Inbound 404 after gateway rebuild:** process death drops `LocalSessionManager`. `POST /mcp` with a stale `Mcp-Session-Id` is a spec-correct 404. [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) and the [TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk/issues/1708) do **not** re-`initialize` on that 404. Do not persist sessions. Recovery is Reload MCP once. `/health` staying 200 means the gateway is up.
24+
25+
**Header pin after reload:** a non-empty `X-Mcpmux-Workspace` is held across initialize and applied when `mcp-session-id` appears. Empty `${workspaceFolder}` is still the Agents-window hole.
2426

2527
---
2628

@@ -168,19 +170,14 @@ A permission / not-found error that never hits the backend is also acceptable
168170

169171
## Follow-on cases (pool-invalidation-and-session-survival)
170172

171-
Playbook for [`pool-invalidation-and-session-survival.md`](./pool-invalidation-and-session-survival.md). Same mux-only / no-`pkill` rules.
172-
173-
### Config save reconnect
174-
175-
Change an enabled server's header or stdio arg in Configure, **do not** click Retry Connection, invoke a tool on that server. Expect `reconnect_fresh completed ok=true` from `[ServerConfigHandler]` and the new value in use.
176-
177-
### Rebuild 404
173+
Dedicated playbook + Aug 20 results: [`pool-invalidation-and-session-survival-test.md`](./pool-invalidation-and-session-survival-test.md). Same mux-only / no-`pkill` rules.
178174

179-
Trigger a Rust rebuild. `curl -sf http://127.0.0.1:45818/health` stays 200. Existing Cursor chats 404 on `/mcp`. Reload MCP once. Do not treat 404 as a dead gateway.
180-
181-
### Header pin
182-
183-
Multi-folder Cursor window with a real `X-Mcpmux-Workspace` pointing at this repo. Reload MCP. `mcpmux_list_servers` is bound, not 6-way `bindable`. `set_workspace_root` is not the success path.
175+
| Case | Result |
176+
| ---- | ------ |
177+
| E header pin | **PASS** — hold then pin; this repo `ready` |
178+
| F stdio refuse / Case B regression | **PASS**`reconnect_fresh`, no OAuth / no HTTP fallback |
179+
| G config-save | **BLOCKED** — admin `:45819` 401 (no CF probe headers). Do not sqlite-edit |
180+
| H rebuild 404 | **PASS** off the 11:36 recycle — `/health` 200, 404 is expected noise, Case E pin ~3s later |
184181

185182
---
186183

docs/planning/backend-connection-resilience.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ Traced live (code reading, not guessed):
1616

1717
- `RoutingService::call_tool`'s error branch only retries when `is_auth_error(&err_str)` matches (`crates/mcpmux-gateway/src/pool/routing.rs:666,816`) — indicators are `"401"`, `"unauthorized"`, etc. A literal `"connection closed"` never matches, so the `else` branch (`routing.rs:776-787`) just returns the raw error with zero retry.
1818
- `ServerInstance::is_healthy()` (`crates/mcpmux-gateway/src/pool/instance.rs:397-399`) is `state == Connected && client.is_some()` — a state flag, not a liveness check. `PoolService::connect_server()` (`crates/mcpmux-gateway/src/pool/service.rs:284-294`) trusts it and reuses the instance without ever touching the transport again. A backend that silently died still reads "healthy" until a real call fails against it.
19-
- `PoolService::reconnect_instance()` (`service.rs:434-459`) unconditionally calls `ConnectionService::reconnect_after_oauth()` (`crates/mcpmux-gateway/src/pool/connection.rs:459`). For a `TransportType::Stdio` instance, that function hits a fallback branch that **warns and builds an `Http` transport anyway** (`connection.rs:499-502`, `"Unexpected STDIO transport for OAuth reconnection, defaulting to HTTP"`) — i.e. today's only reconnect path is not just auth-only, it actively mis-reconnects stdio backends if it's ever invoked for one.
19+
- `PoolService::reconnect_instance()` (`service.rs:434-459`) unconditionally called `ConnectionService::reconnect_after_oauth()` (`crates/mcpmux-gateway/src/pool/connection.rs:459`). For a `TransportType::Stdio` instance, that function hit a fallback that **warned and built an `Http` transport anyway** (`"Unexpected STDIO transport for OAuth reconnection, defaulting to HTTP"`). At investigation that was the only reconnect path and it mis-reconnected stdio. **Shipped since (`dcc2977`):** stdio `Failed` (`stdio cannot reconnect via OAuth; use reconnect_fresh`). HTTP fallback deleted.
2020
- `InstanceKey::stdio()`/`::http()` (`crates/mcpmux-gateway/src/pool/instance.rs:266,279`) take `command`/`args`/`env`/`headers` as **unused, underscore-prefixed params** — only `space_id` and a debug `description` string survive on the stored key. The original transport config is *not* retained on `ServerInstance` after `connect_server()` returns, so any new reconnect path can't rebuild it from the instance alone — it has to re-resolve from DB, the same way `connect_server()`'s caller does today.
21-
- The one place that already does this correctly is `retry_connection` (`apps/desktop/src-tauri/src/commands/server_manager.rs:516-547`): `pool_service.remove_instance()` then `connect_enabled_server()`, which re-resolves transport config fresh from DB and calls `connect_server()` again — transport-agnostic, unlike `reconnect_after_oauth()`. That path is Tauri-desktop-only today; `RoutingService` (gateway-internal, used by both desktop and headless/web-admin) has no equivalent.
21+
- The one place that already did this correctly at investigation was `retry_connection` (`apps/desktop/src-tauri/src/commands/server_manager.rs:516-547`): `remove_instance()` then `connect_enabled_server()`. **Shipped since:** `PoolService::reconnect_fresh` / `reconnect_fresh_from_db`; `RoutingService` uses it for transport-closed; admin `retry_connection` uses the same helper.
2222
- `mcpmux_set_workspace_root` (`crates/mcpmux-gateway/src/services/meta_tools/set_workspace_root.rs:53-114`) only re-seeds the **inbound** `SessionRootsRegistry` and fires `tools/list_changed` — it never touches `PoolService` or any backend instance. The "fix" working is a side effect of Cursor opening a fresh inbound rmcp session (hence the new `session_id`) — nothing in the gateway explicitly reconnects the dead backend.
2323
- Separately: `mcpmux_bind_current_workspace` (`crates/mcpmux-gateway/src/services/meta_tools/bind_workspace.rs:175-181`) loads the FeatureSet name via `feature_set_repo.get(&fs_id.to_string())`, and on `Ok(None)` (nonexistent id) falls back to `fs_id.to_string()` as the display name **instead of rejecting** — then proceeds to `binding_repo.create()`. `WorkspaceBindingRepository::create()` (`crates/mcpmux-storage/src/repositories/workspace_binding_repository.rs:236-268`) wraps the parent `INSERT` and `rewrite_fs_for_binding()`'s junction `INSERT`s (`workspace_binding_repository.rs:142-160`) in one transaction against `workspace_binding_feature_sets.feature_set_id → feature_sets(id)` — a nonexistent `feature_set_id` surfaces as a raw SQLite `FOREIGN KEY constraint failed`, not a clean domain error. The transaction wrapping means it fails atomically (no partial binding), but the error is the wrong shape for an agent to act on.
2424
- **At investigation time the pool already had per-instance telemetry and nothing populated it at call time.** `InstanceStats` carries `connected_at`, `consecutive_failures`, `requests_served`, and `last_error`, and `record_success()` / `record_failure()` existed with **zero call sites**. Phase 1 wired them from `call_tool`. `mark_failed()` (flips `state` to `Failed`) stays connect/reconnect-only — a call-time `-32000` on a `Connected` instance used to leave `is_healthy()` true and `consecutive_failures` at 0. That is no longer true for call stats; the state-flag lie (Decision 6) is still deferred.
@@ -227,7 +227,9 @@ Playbook: [`backend-connection-resilience-test.md`](./backend-connection-resilie
227227
| C HA idle | **SKIPPED** (15–20 min wait) |
228228
| D unmatched | **SKIPPED** — grant-layer "did you mean", never reached the classifier |
229229

230-
Tauri-watch rebuild mid-test wiped Streamable HTTP sessions (`POST /mcp` → 404). Reload MCP once, then pin this repo if `list_servers` reports unpinned roots. Do not use `set_workspace_root` as the reconnect itself.
230+
Tauri-watch rebuild mid-test wiped Streamable HTTP sessions (`POST /mcp` → 404). Reload MCP once. A non-empty `X-Mcpmux-Workspace` is held until `mcp-session-id` exists, then pinned (`dcc2977`). Empty header still skips. Do not use `set_workspace_root` as the reconnect itself.
231+
232+
Follow-on playbook: [`pool-invalidation-and-session-survival-test.md`](./pool-invalidation-and-session-survival-test.md) — E/F/H PASS, G BLOCKED (admin 401).
231233

232234
---
233235

@@ -236,9 +238,9 @@ Tauri-watch rebuild mid-test wiped Streamable HTTP sessions (`POST /mcp` → 404
236238
| File | Notes |
237239
| ---- | ----- |
238240
| [`crates/mcpmux-gateway/src/pool/routing.rs`](../../crates/mcpmux-gateway/src/pool/routing.rs) | `call_tool()` L298; error branch L657-788; `is_auth_error()` L816-824 — Phase 1 target |
239-
| [`crates/mcpmux-gateway/src/pool/service.rs`](../../crates/mcpmux-gateway/src/pool/service.rs) | `connect_server()` L267-341 (health check L284, non-healthy reconnect L297-301); `remove_instance()` L344; `reconnect_instance()` L434-459 (OAuth-only, unchanged) — Phase 1 target |
241+
| [`crates/mcpmux-gateway/src/pool/service.rs`](../../crates/mcpmux-gateway/src/pool/service.rs) | `reconnect_fresh` / `reconnect_fresh_from_db` (transport-closed + config-save + resource/prompt auth retry). `reconnect_instance()` still OAuth-only |
240242
| [`crates/mcpmux-gateway/src/pool/instance.rs`](../../crates/mcpmux-gateway/src/pool/instance.rs) | `is_healthy()` still state-only (Decision 6, deferred); `record_success()`/`record_failure()` wired from `routing.rs` (Decision 9); `mark_failed()` stays connect/reconnect-only (Decision 10) |
241-
| [`crates/mcpmux-gateway/src/pool/connection.rs`](../../crates/mcpmux-gateway/src/pool/connection.rs) | `reconnect_after_oauth()` L459; stdio-to-HTTP fallback warn L499-502 — root of why `reconnect_instance()` is unsafe for the widened trigger |
243+
| [`crates/mcpmux-gateway/src/pool/connection.rs`](../../crates/mcpmux-gateway/src/pool/connection.rs) | `reconnect_after_oauth`: Stdio → `Failed` (`stdio cannot reconnect via OAuth`). HTTP fallback deleted (`dcc2977`) |
242244
| [`crates/mcpmux-gateway/src/pool/transport/resolution.rs`](../../crates/mcpmux-gateway/src/pool/transport/resolution.rs) | `resolve_auto_connection_context()` + `build_transport_config()` — shared re-resolve for `reconnect_fresh` |
243245
| [`apps/desktop/src-tauri/src/commands/server_manager.rs`](../../apps/desktop/src-tauri/src/commands/server_manager.rs) | `retry_connection()` L516-547 — existing evict-then-reconnect pattern `reconnect_fresh()` generalizes into gateway-internal code |
244246
| [`crates/mcpmux-gateway/src/services/meta_tools/bind_workspace.rs`](../../crates/mcpmux-gateway/src/services/meta_tools/bind_workspace.rs) | `feature_set_repo.get()` fallback L175-181 — Phase 2 target |
@@ -257,7 +259,8 @@ Tauri-watch rebuild mid-test wiped Streamable HTTP sessions (`POST /mcp` → 404
257259

258260
- [`docs/planning/backend-connection-resilience-test.md`](./backend-connection-resilience-test.md) — manual playbook + Aug 20 results (A/B pass; C skipped; D never reached classifier)
259261
- [`docs/planning/aug14-gateway-ops-bugs.md`](./aug14-gateway-ops-bugs.md) — this branch's parent investigation (inbound session keep-alive, log noise); marked stale by this doc (Decision 5)
260-
- [`docs/planning/pool-invalidation-and-session-survival.md`](./pool-invalidation-and-session-survival.md) — follow-on after Aug 20 verification: config-save `reconnect_fresh`, stdio OAuth refuse, inbound 404 recovery, trust `X-Mcpmux-Workspace`
261-
- [`docs/planning/clone-auth-header-config-editing.md`](./clone-auth-header-config-editing.md) — separate "Connection closed" bug (stale pool reuse after a config edit), same `PoolService::connect_server()` healthy-reuse code path this doc's Decision 1/2 also touches
262+
- [`docs/planning/pool-invalidation-and-session-survival.md`](./pool-invalidation-and-session-survival.md) — follow-on **shipped** (`dcc2977`): config-save `reconnect_fresh`, stdio OAuth refuse, hold-then-pin
263+
- [`docs/planning/resilience-routing-leftovers.md`](./resilience-routing-leftovers.md) — collects the Decision 5–7 deferrals (matcher freeze, `is_healthy()` heartbeat, unused `ClientPool`) plus other parked/open items in one inventory
264+
- [`docs/planning/clone-auth-header-config-editing.md`](./clone-auth-header-config-editing.md) — clone UI still planning; Decision 4 pool half shipped in the follow-on
262265
- [`docs/planning/deny-by-default-bindable-callers.md`](./deny-by-default-bindable-callers.md) — established rmcp's inbound keepalive as expected client-hang behavior (Jun 29), informed PR #221's Decision 3
263266
- [`docs/planning/cursor-workspace-routing-bridge.md`](./cursor-workspace-routing-bridge.md)`set_workspace_root`/`SessionRootsRegistry` design this doc confirms is inbound-only, not a backend reconnect mechanism

0 commit comments

Comments
 (0)