The prior #708 slice added assess_connected_namespace_attachment() as a pure
decision function with no call site: nothing invoked it, so a session whose
role namespaces were Connected at the host but absent from the active session
tool surface still passed every mutation gate. Detection existed on paper only.
This makes it load-bearing.
Decision layer (mcp_namespace_health.py)
- assess_connected_namespace_attachment() gains secret-free telemetry
(connected/attached/required/missing counts, discovery cache hit and age,
reconnect_required, auto_attach_attempted, auto_recovered, error_type) and
reports reconnect_required, auto_recovered and startup_ordering_race.
- Startup ordering: a namespace whose connect completed after the session tool
snapshot cannot be in that snapshot, so parallel multi-role startup is
identified as its own race with the affected namespaces listed.
- attachment_gate_from_session() is a fail-closed gate keyed by
ATTACHMENT_GATED_TASKS. An unassessed namespace does not gate, matching #543
semantics, so this never fabricates a block.
- SANCTIONED_ATTACH_RECOVERY_TOOL names gitea_request_mcp_reconnect (#678) as
the only recovery.
Server wiring (gitea_mcp_server.py)
- New tool gitea_assess_mcp_namespace_attachment classifies the condition and
records a per-namespace verdict in _LIVE_NAMESPACE_ATTACHMENT.
- gitea_submit_pr_review and gitea_merge_pr now consult
_namespace_attachment_gate() alongside the existing #543 health gate, so both
fail closed while a required namespace is unattached.
- Watchdog check-in emits status only, never namespace contents.
The typed condition mcp_connected_namespaces_missing stays distinct from config
drift (#672), transport-closed (#584) and resolver EOF (#685). Recovery never
routes through direct imports, CLI or raw API mutation, profile hopping,
session-state overrides, or process kills.
Docs
- docs/mcp-namespace-health.md documents the tool arguments, startup ordering,
the fail-closed gate, and the telemetry contract.
- skills/llm-project-workflow/SKILL.md states Connected is not attached, and
that preflight proof is live tool visibility plus gitea_whoami on the role
namespace rather than host status alone.
- docs/mcp-tool-inventory.md lists the new tool.
- docs/remote-mcp/threat-model-anchors.json and threat-model.md: 21 #956 anchors
restamped for the line shift these additions caused in gitea_mcp_server.py.
Every anchor was re-derived from its recorded expect substring; none guessed.
Tests
- tests/test_issue_708_attachment_wiring.py (19 cases): typed detection, proof
mapping, reconnect-only next action, auto-attach success and failure,
reconnect rediscovery, multi-role startup ordering, telemetry including a
no-secret-leak assertion, fail-closed gate per role, unassessed and unmapped
tasks not gating, partial attachment gating only the affected role, and no
healthy verdict without attachment proof.
Verification
- tests/test_issue_708_attachment_wiring.py + test_issue_708_mcp_namespace_attachment.py: 24 passed
- namespace/session/runtime/review sweep: 427 passed, 12 subtests
- full suite head: 31 failed, 5885 passed, 6 skipped, 1047 subtests
- full suite base 17ba1ff035: 30 failed, 5862 passed, 6 skipped, 1047 subtests
- failing identifier sets match, plus tests/test_mirror_refs.py DryRunBanner,
which fails on the unmodified base in isolation and passes here: flaky, not a
regression from this branch.
- Gate proven by execution, not inspection: registering the assessment blocks
merge_pr and review_pr, and attaching the namespaces clears the block.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
160 lines
7.2 KiB
Markdown
160 lines
7.2 KiB
Markdown
# 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
|
|
`client is closing: EOF`, `transport closed`, or an empty response when calling
|
|
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 **client-namespace** evidence
|
|
that the required tool is callable through the configured IDE MCP namespace.
|
|
|
|
## Probe sources (do not confuse them)
|
|
|
|
| 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
|
|
```
|
|
|
|
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 |
|
|
| --- | --- |
|
|
| `gitea-author` | `gitea_whoami` |
|
|
| `gitea-reviewer` | `gitea_whoami` |
|
|
| `gitea-merger` | `gitea_whoami` |
|
|
| `gitea-tools` | `gitea_list_profiles` |
|
|
|
|
## Recovery (canonical)
|
|
|
|
When a namespace returns EOF, follow
|
|
`docs/mcp-namespace-eof-recovery.md` in order:
|
|
|
|
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.
|
|
|
|
## Enforcement
|
|
|
|
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).
|
|
|
|
When blocked, repair the IDE namespace and re-record a healthy
|
|
`client_namespace` assessment before retrying the mutation.
|
|
|
|
## Connected vs Attached Tool Surface (#708)
|
|
|
|
MCP servers can report **Connected** at the CLI / host inventory layer while the **active LLM session exposes none of their tool namespaces**.
|
|
|
|
### Core principle
|
|
|
|
* **Connected status at host layer ≠ attached tools in active session.**
|
|
* Required preflight proof is **live tool visibility + `gitea_whoami` call** through the target namespace, not host `Connected` status alone.
|
|
* When servers report Connected but namespaces are absent from attached tools, classify as `mcp_connected_namespaces_missing`.
|
|
|
|
### Forbidden unsafe fallbacks
|
|
|
|
When `mcp_connected_namespaces_missing` is detected, workflows must **fail closed** and must **never** encourage or perform:
|
|
|
|
* direct imports of MCP server Python modules
|
|
* CLI or raw Gitea API mutations as a substitute for native tools
|
|
* profile hopping to another MCP profile/namespace to bypass the empty session
|
|
* session-state overrides or hand-edited session/ledger files
|
|
* process kills (`pkill`), config mtime touches, or `.env` edits
|
|
|
|
Only sanctioned recovery: **client reconnect path**, followed by full preflight (`whoami` → capability resolve → task).
|
|
|
|
### Native detection tool
|
|
|
|
`gitea_assess_mcp_namespace_attachment` classifies the condition and records it in
|
|
the session. Pass the namespaces the host reports Connected and the namespaces
|
|
actually attached to the active session tool surface:
|
|
|
|
| Argument | Meaning |
|
|
|---|---|
|
|
| `connected_servers` | Namespaces the host/CLI reports Connected |
|
|
| `attached_session_namespaces` | Namespaces exposed in the active session tool surface |
|
|
| `required_namespaces` | Namespaces this workflow needs (defaults to the role namespaces) |
|
|
| `discovery_cache_hit` / `discovery_cache_age_seconds` | Client tool-discovery cache state |
|
|
| `auto_attach_attempted` / `auto_attach_succeeded` | Whether the runtime auto-attached |
|
|
| `session_tool_snapshot_at` / `namespace_connected_at` | Epoch seconds, to detect startup ordering races |
|
|
|
|
It returns `discovery_status`
|
|
(`namespaces_attached` | `connected_but_namespaces_missing` | `disconnected`),
|
|
`missing_namespaces`, `proof_of_connected_vs_attached` (per namespace
|
|
`{connected, attached}`), `error_type`, `reconnect_required`, `auto_recovered`,
|
|
`startup_ordering_race`, `late_attaching_namespaces`, `sanctioned_recovery_tool`,
|
|
and a reconnect-only `exact_next_action`.
|
|
|
|
### Startup ordering
|
|
|
|
`session_tool_snapshot_at` earlier than a namespace's `namespace_connected_at`
|
|
means that namespace could not have been in the session snapshot, however healthy
|
|
it looks now. That is reported as `startup_ordering_race` with the affected
|
|
namespaces listed — the multi-role parallel-connect case where Connected flips
|
|
true after the session tool list was already captured.
|
|
|
|
### Fail-closed gate
|
|
|
|
The recorded verdict gates mutations, mirroring the #543 health gate: a namespace
|
|
that has not been assessed does not gate, but one recorded Connected-but-unattached
|
|
blocks the mutation whose role namespace it is
|
|
(`review_pr` / `submit_review` → `gitea-reviewer`, `merge_pr` → `gitea-merger`,
|
|
`work_issue` / `create_pr` → `gitea-author`). `gitea_submit_pr_review` and
|
|
`gitea_merge_pr` return the block reason plus the hard-stop policy string, and the
|
|
only offered recovery is `gitea_request_mcp_reconnect`.
|
|
|
|
### Telemetry
|
|
|
|
The `telemetry` block carries `connected_count`, `attached_count`,
|
|
`required_count`, `missing_count`, `discovery_status`, `discovery_cache_hit`,
|
|
`discovery_cache_age_seconds`, `reconnect_required`, `auto_attach_attempted`,
|
|
`auto_recovered`, `startup_ordering_race`, and `error_type`. It contains namespace
|
|
names and counts only — never tokens, endpoints, env values, or filesystem paths.
|
|
|