fix: address PR #587 REQUEST_CHANGES for MCP namespace health (#543)

- 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.
This commit is contained in:
2026-07-09 18:44:04 -04:00
parent af9f1c5944
commit ebd06b73c9
7 changed files with 341 additions and 94 deletions
+28 -19
View File
@@ -21,7 +21,8 @@ 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.
- 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
@@ -36,9 +37,9 @@ 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
## Recovery path (canonical — client reconnect only)
Do the steps in order. Stop as soon as a live namespace call succeeds.
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
@@ -53,22 +54,28 @@ Do the steps in order. Stop as soon as a live namespace call succeeds.
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.
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.
`*_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
@@ -77,33 +84,35 @@ 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.
- **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?).
## Reproducing the registered-vs-callable gap
## Offline spawn probe (non-authoritative)
`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.
`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 namespace (see #543 canonical handoff).
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.
+61 -49
View File
@@ -1,4 +1,4 @@
# MCP namespace EOF recovery
# MCP namespace health diagnostics (#543)
Gitea MCP tools can be registered in the Python FastMCP server while the IDE's
live MCP namespace is still unusable. The failure usually appears as
@@ -6,19 +6,54 @@ live MCP namespace is still unusable. The failure usually appears as
a tool such as `gitea_whoami`.
Do not treat static tool registration as proof that review or merge workflows
can proceed. A reviewer or merger flow must have live namespace evidence that
the required tool is callable through the configured namespace.
can proceed. A reviewer or merger flow must have **client-namespace** evidence
that the required tool is callable through the configured IDE MCP namespace.
## Required namespace probes
## Probe sources (do not confuse them)
Run the health check after changing MCP config, switching branches that affect
server code, or seeing EOF from any Gitea namespace:
| Source | How obtained | Proves IDE namespace? |
| --- | --- | --- |
| `client_namespace` | Tool call through the IDE-managed MCP client | **Yes** |
| `offline_spawn` | `test_mcp_conn.py` subprocess JSON-RPC handshake | **No** (offline only) |
`gitea_assess_mcp_namespace_health` accepts `probe_source` and only sets
`ide_namespace_proven=true` for `client_namespace` success. Offline spawn
success never clears review/merge mutation gates.
## Client-namespace health check (canonical)
1. Through the IDE client, call a cheap tool on the target namespace
(`gitea_whoami` or `gitea_list_profiles`).
2. Feed the live result into:
```text
gitea_assess_mcp_namespace_health(
namespace="gitea-merger", # or reviewer / author / tools
registered_tools=[...], # optional static list
probe_result={"success": true, "result": {...}},
probe_source="client_namespace",
)
```
3. A healthy client-namespace assessment is recorded in the MCP session and
consulted by **live** `gitea_submit_pr_review` / `gitea_merge_pr` gates.
4. If the probe fails with EOF, recover via **client reconnect only** — see
`docs/mcp-namespace-eof-recovery.md`. Do **not** kill PIDs or touch MCP
config mtimes as a recovery procedure.
## Offline spawn probe (non-authoritative)
```bash
python3 test_mcp_conn.py --config ~/.gemini/config/mcp_config.json
```
By default the script checks these namespaces and required tools:
This script spawns a **separate** server process from config, performs
JSON-RPC `initialize``tools/list``tools/call`, and classifies the
result with `probe_source=offline_spawn`. Use it for offline debugging of
launch command / registration. It does **not** prove the IDE-managed
namespace is healthy.
By default the script checks:
| Namespace | Required tool |
| --- | --- |
@@ -27,50 +62,27 @@ By default the script checks these namespaces and required tools:
| `gitea-merger` | `gitea_whoami` |
| `gitea-tools` | `gitea_list_profiles` |
Use `--namespace gitea-reviewer` to test only one namespace. A passing probe
requires:
## Recovery (canonical)
1. The namespace exists in the MCP config.
2. JSON-RPC initialize succeeds.
3. `tools/list` returns the required tool.
4. `tools/call` successfully invokes the required tool.
When a namespace returns EOF, follow
`docs/mcp-namespace-eof-recovery.md` in order:
## Recovery steps
1. Confirm blast radius (Gitea namespace vs all MCP servers).
2. **Reconnect the namespace through the client** (IDE reconnect / relaunch).
3. Do not repair via shell imports, raw JSON-RPC, PID kills, or config mtime
touches — those do not restore the client's closed transport.
4. Re-verify the **specific** required tool through the target namespace.
5. Resume review/merge only after a successful `client_namespace` assessment.
When a namespace returns EOF:
## Enforcement
1. Note the reported namespace, required tool, PID, profile, environment
summary, and config path.
2. Stop any stale MCP server process for that PID.
3. Reload or touch the MCP config so the IDE reconnects the namespace.
4. Verify the namespace command, args, profile env, and checkout path point at
the current repository.
5. Re-run `python3 test_mcp_conn.py --namespace <name>`.
6. Resume review or merge work only after the required tool call passes.
1. **State machine (read-only):** feed `blocks_merge_workflow` from a
`client_namespace` assessment into
`gitea_assess_review_merge_state_machine(live_namespace_broken=...)`.
2. **Live mutations:** `gitea_submit_pr_review` and `gitea_merge_pr` call
`_live_namespace_health_gate` and fail closed when the session has a
recorded unhealthy or non-client probe for the required namespace
(`gitea-reviewer` for review, `gitea-merger` for merge).
The read-only `gitea_assess_mcp_namespace_health` tool can classify probe
evidence supplied by a client. It intentionally distinguishes
`required_tool_registered=true` from `required_tool_callable=false`; the latter
must block reviewer and merger workflows until the live namespace is repaired.
## Enforcement in the review/merge state machine
The block is not advisory. Feed the `blocks_merge_workflow` verdict from
`gitea_assess_mcp_namespace_health` into
`gitea_assess_review_merge_state_machine` as `live_namespace_broken`:
```text
health = gitea_assess_mcp_namespace_health(namespace="gitea-merger", ...)
state = gitea_assess_review_merge_state_machine(
state_completion=...,
pre_merge_gates=...,
live_namespace_broken=health["blocks_merge_workflow"],
)
# state["merge"]["allowed"] is False whenever the live namespace is broken,
# even when every review state and pre-merge gate is otherwise satisfied.
```
When `live_namespace_broken=True`, `assess_workflow_blockers`,
`can_approve`, `can_merge`, and `workflow_status` all fail closed. This is the
guard that stops a merge from proceeding on a false-ready namespace, as in the
PR #418 halt that motivated this work (#543).
When blocked, repair the IDE namespace and re-record a healthy
`client_namespace` assessment before retrying the mutation.