From efa7190041cad86beb49c36e7177a9a73a915cb8 Mon Sep 17 00:00:00 2001 From: Jason Walker <913443@dadeschools.net> Date: Thu, 9 Jul 2026 11:55:42 -0400 Subject: [PATCH] docs: add MCP namespace `client is closing: EOF` recovery runbook (#543) Adds docs/mcp-namespace-eof-recovery.md documenting the correct recovery path when a Gitea MCP namespace (gitea-author/reviewer/merger/tools) returns `client is closing: EOF`. Satisfies acceptance criterion 7 of #543: symptom, root cause (closed client transport vs a live-but-stale process), the sanctioned reconnect/relaunch sequence, diagnostics to capture, and how test_mcp_conn.py reproduces the registered-vs-callable gap. Distinguishes this transport-close failure from the ps-based stale-runtime family in #531/#544 and reinforces the no-direct-import guard (#558). Docs-only; no code or test behavior changed. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/mcp-namespace-eof-recovery.md | 109 +++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 docs/mcp-namespace-eof-recovery.md diff --git a/docs/mcp-namespace-eof-recovery.md b/docs/mcp-namespace-eof-recovery.md new file mode 100644 index 0000000..7f79023 --- /dev/null +++ b/docs/mcp-namespace-eof-recovery.md @@ -0,0 +1,109 @@ +# Recovering from `client is closing: EOF` on a Gitea MCP namespace (#543) + +## Symptom + +A tool call through a Gitea MCP namespace — `gitea-author`, `gitea-reviewer`, +`gitea-merger`, or the shared `gitea-tools` namespace — fails immediately with: + +``` +client is closing: EOF +``` + +Every subsequent call to that same namespace returns the same error, including +cheap read tools such as `gitea_whoami` and `gitea_list_profiles`. Other MCP +servers registered with the same client (for example `context7`) keep working, +so this is **not** a global MCP-client outage. + +## Why this is not a code defect + +This failure is a **transport-level** condition in the IDE / MCP client manager, +not a missing or broken tool: + +- The tool can be present and registered in the Python `FastMCP` tool manager. +- Direct Python inspection of the server confirms the tool exists. +- Running the server manually and sending JSON-RPC over stdio works fine. + +The client manager entered a closed state after the backing subprocess for that +namespace terminated (or was killed) behind its back. Once closed, the client +does **not** re-spawn the child on the next tool call — it just replays +`client is closing: EOF`. The OS process may even still be alive if a parent +language-server process is holding the stdio pipes open. + +This is the canonical "registered in FastMCP ≠ callable through the namespace" +false-ready state. It is distinct from the **stale-runtime** family in #531 / +#544, where the process is reachable but running behind `master`; that case is +detected by the `ps`-based `_check_mcp_runtimes_diagnostics` in +`gitea_mcp_server.py`. The EOF case is a dead/closed transport, not a stale one, +so the `ps` check alone will not surface it. + +## Recovery path + +Do the steps in order. Stop as soon as a live namespace call succeeds. + +1. **Confirm the blast radius.** Call a cheap read tool on the failing namespace + (`gitea_whoami` or `gitea_list_profiles`). Then call the same tool on a + different MCP server (e.g. `context7`). + - Only the Gitea namespace fails → single-namespace transport close. Continue. + - Every server fails → restart the whole MCP client, not just one namespace. + +2. **Reconnect the namespace through the client, not the shell.** Use the IDE / + client MCP-reconnect action for that server entry (in Claude Code: + `/mcp` → reconnect the affected `gitea-*` server). Reconnecting forces the + client to spawn a fresh subprocess and re-open the pipe. This clears the + closed-client state that a bare `kill`/respawn from a terminal does **not**. + +3. **Do not "fix" it by importing the server or poking the process.** Reaching + for `python -c 'import gitea_mcp_server ...'`, raw JSON-RPC from a shell, or + killing PIDs to force a respawn does not restore the *client's* view of the + namespace and violates the daemon-import guard (#558, + `docs/mcp-daemon-import-guard.md`). The only sanctioned repair is a client + reconnect / relaunch. + +4. **Verify through the same path the workflow will use.** After reconnect, call + the specific tool the blocked workflow needs — not just any tool — through + the target namespace. For a merge that means calling the merger-authorized + adoption/merge tool through `gitea-merger`. A green `gitea_whoami` on one + namespace does **not** prove another namespace or another tool is callable. + +5. **If reconnect does not clear it,** relaunch the client entirely, then repeat + step 4. If EOF persists after a full relaunch, the backing subprocess is + failing to start — inspect its stderr / launch config (command path, venv, + `*_MCP_CONFIG`, `*_MCP_PROFILE` env) rather than retrying the call. + +## Diagnostics to capture when reporting EOF + +Include all of these so the failure is actionable and reproducible: + +- **Namespace name** that returned EOF (`gitea-author` / `gitea-reviewer` / + `gitea-merger` / `gitea-tools`). +- **Tool** that was called and the **exact** error string. +- **PID** of the backing process (if any) and whether it was still alive. +- **Profile / env** for that namespace (execution profile, `*_MCP_PROFILE`, + worktree binding such as `GITEA_AUTHOR_WORKTREE`). +- **Config path** the client launched the server from. +- Result of the **cross-server control** call (did `context7` succeed?). + +## Reproducing the registered-vs-callable gap + +`test_mcp_conn.py` performs a full JSON-RPC handshake +(`initialize` → `initialized` → `tools/list`) plus a direct tool call against a +namespace, which is what distinguishes "registered in FastMCP" from "callable +through the client." Use it to confirm a namespace is genuinely reachable after +a reconnect. It probes a namespace end-to-end; a namespace that returns EOF at +the client fails this handshake, while `ps`-only checks still report the process +as present. + +## Do-not list during EOF recovery + +- Do **not** retry a blocked merge/adoption until the required tool is confirmed + callable through the merger-authorized namespace (see #543 canonical handoff). +- Do **not** clean, reset, or rebind a **foreign** worktree to work around the + error. +- Do **not** bypass the namespace with direct imports, raw API/curl, or + in-memory state restoration. + +## Related + +- #531 / #544 — stale-runtime detection (`ps`-based); sibling failure mode. +- #558 / `docs/mcp-daemon-import-guard.md` — why shell imports are not a repair. +- `docs/mcp-client-registration.md` — per-server registration contract.