- Distinguish client_namespace vs offline_spawn probe sources; only IDE client probes prove namespace health and feed mutation gates. - Record assessments in session; gate gitea_submit_pr_review and gitea_merge_pr when client-namespace health is unhealthy/non-proven. - Mark test_mcp_conn.py offline-only; align recovery docs to client reconnect (no PID-kill/config-touch as canonical recovery). - Rebase onto current master so #590 ledger isolation keeps TestMergePR.test_unknown_profile_blocks green.
119 lines
5.8 KiB
Markdown
119 lines
5.8 KiB
Markdown
# 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
|
|
(offline spawn) — that path does **not** prove the IDE namespace is healthy.
|
|
|
|
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 (canonical — client reconnect only)
|
|
|
|
Do the steps in order. Stop as soon as a live **client-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,
|
|
killing PIDs to force a respawn, or touching MCP config mtimes 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.
|
|
Record success with:
|
|
|
|
```text
|
|
gitea_assess_mcp_namespace_health(..., probe_source="client_namespace")
|
|
```
|
|
|
|
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. Still
|
|
do not use PID kill or config-touch as the primary recovery.
|
|
|
|
## 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
|
|
(informational only — not a recovery action).
|
|
- **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?).
|
|
|
|
## Offline spawn probe (non-authoritative)
|
|
|
|
`test_mcp_conn.py` performs a full JSON-RPC handshake against a **fresh
|
|
subprocess** (`initialize` → `initialized` → `tools/list` → `tools/call`) and
|
|
classifies with `probe_source=offline_spawn`. That is useful for offline
|
|
launch/registration debugging. It is **not** proof the IDE-managed namespace is
|
|
healthy. See `docs/mcp-namespace-health.md`.
|
|
|
|
## Do-not list during EOF recovery
|
|
|
|
- Do **not** retry a blocked merge/adoption until the required tool is confirmed
|
|
callable through the merger-authorized **client** namespace (see #543).
|
|
- 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.
|
|
- Do **not** kill MCP PIDs or touch config mtimes as a substitute for client
|
|
reconnect.
|
|
|
|
## 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.
|
|
- `docs/mcp-namespace-health.md` — probe sources and mutation enforcement.
|