feat(mcp): detect Connected-but-namespaces-missing attachment failures (Closes #708)
This commit is contained in:
@@ -86,3 +86,26 @@ When a namespace returns EOF, follow
|
||||
|
||||
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).
|
||||
|
||||
|
||||
@@ -51,6 +51,14 @@ EOF_PATTERNS = (
|
||||
"eof",
|
||||
)
|
||||
|
||||
ERROR_CONNECTED_NAMESPACES_MISSING = "mcp_connected_namespaces_missing"
|
||||
|
||||
UNSAFE_FALLBACK_WARNING = (
|
||||
"Workflow Safety Hard Stop (#708): Connected-but-namespaces-missing recovery must "
|
||||
"NEVER use direct imports, Gitea API mutations, profile hopping, session-state "
|
||||
"overrides, PID kills, or config mtime touches. Use client reconnect only."
|
||||
)
|
||||
|
||||
SAFE_ENV_KEYS = (
|
||||
"GITEA_MCP_PROFILE",
|
||||
"GITEA_PROFILE_NAME",
|
||||
@@ -60,6 +68,83 @@ SAFE_ENV_KEYS = (
|
||||
)
|
||||
|
||||
|
||||
def assess_connected_namespace_attachment(
|
||||
*,
|
||||
connected_servers: list[str] | tuple[str, ...] | set[str] | None = None,
|
||||
attached_session_namespaces: list[str] | tuple[str, ...] | set[str] | None = None,
|
||||
required_namespaces: list[str] | tuple[str, ...] | set[str] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Assess whether host-connected MCP servers have attached tool namespaces in the active session (#708).
|
||||
|
||||
Addresses the Connected-but-namespaces-missing defect: CLI/host status may report Connected
|
||||
while the active LLM session tool surface exposes 0 attached tool namespaces.
|
||||
|
||||
Returns structured telemetry and detection details.
|
||||
"""
|
||||
connected = [str(s).strip() for s in (connected_servers or []) if str(s).strip()]
|
||||
attached = set(str(ns).strip() for ns in (attached_session_namespaces or []) if str(ns).strip())
|
||||
req = [str(r).strip() for r in (required_namespaces or DEFAULT_NAMESPACES) if str(r).strip()]
|
||||
|
||||
proof: dict[str, dict[str, bool]] = {}
|
||||
missing: list[str] = []
|
||||
|
||||
for s in connected:
|
||||
is_attached = s in attached
|
||||
proof[s] = {"connected": True, "attached": is_attached}
|
||||
if not is_attached and s in req:
|
||||
missing.append(s)
|
||||
|
||||
for r in req:
|
||||
if r not in proof:
|
||||
proof[r] = {"connected": r in connected, "attached": r in attached}
|
||||
if r in connected and r not in attached and r not in missing:
|
||||
missing.append(r)
|
||||
|
||||
attachment_healthy = len(missing) == 0 and len(connected) > 0
|
||||
discovery_status = (
|
||||
"namespaces_attached" if attachment_healthy
|
||||
else ("connected_but_namespaces_missing" if len(connected) > 0 else "disconnected")
|
||||
)
|
||||
|
||||
reasons: list[str] = []
|
||||
remediation: list[str] = []
|
||||
if missing:
|
||||
reasons.append(
|
||||
f"MCP server(s) {missing} report Connected at host/CLI layer but tool namespaces "
|
||||
f"are missing from active session attached tools (Connected ≠ attached tools, #708)."
|
||||
)
|
||||
remediation.append(
|
||||
"Reconnect the IDE/client MCP session to attach namespaces to the active session. "
|
||||
"Do not use direct imports, CLI API mutations, profile hopping, or session file overrides."
|
||||
)
|
||||
elif not connected:
|
||||
reasons.append("No MCP servers reported Connected.")
|
||||
remediation.append("Start or reconnect Gitea MCP servers in client config.")
|
||||
else:
|
||||
reasons.append("All connected MCP server namespaces are attached to the active session.")
|
||||
|
||||
return {
|
||||
"success": attachment_healthy,
|
||||
"attachment_healthy": attachment_healthy,
|
||||
"discovery_status": discovery_status,
|
||||
"connected_servers": connected,
|
||||
"attached_session_namespaces": list(attached),
|
||||
"missing_namespaces": missing,
|
||||
"proof_of_connected_vs_attached": proof,
|
||||
"error_type": None if attachment_healthy else ERROR_CONNECTED_NAMESPACES_MISSING,
|
||||
"reasons": reasons,
|
||||
"remediation": remediation,
|
||||
"exact_next_action": (
|
||||
"Reconnect the IDE/client MCP session so tool namespaces attach to the active session. "
|
||||
"Do not use direct imports, CLI API mutations, profile hopping, or session-state overrides."
|
||||
if not attachment_healthy
|
||||
else "None; session tool namespaces attached."
|
||||
),
|
||||
"unsafe_fallback_policy": UNSAFE_FALLBACK_WARNING,
|
||||
}
|
||||
|
||||
|
||||
|
||||
def _as_list(value: Any) -> list[str] | None:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Unit regression tests for Issue #708: Connected-but-namespaces-missing detection and attachment safety."""
|
||||
|
||||
import mcp_namespace_health
|
||||
|
||||
|
||||
def test_assess_connected_namespace_attachment_success():
|
||||
connected = ["gitea-author", "gitea-reviewer", "gitea-merger"]
|
||||
attached = ["gitea-author", "gitea-reviewer", "gitea-merger"]
|
||||
res = mcp_namespace_health.assess_connected_namespace_attachment(
|
||||
connected_servers=connected,
|
||||
attached_session_namespaces=attached,
|
||||
)
|
||||
assert res["success"] is True
|
||||
assert res["attachment_healthy"] is True
|
||||
assert res["discovery_status"] == "namespaces_attached"
|
||||
assert res["error_type"] is None
|
||||
assert res["missing_namespaces"] == []
|
||||
assert res["exact_next_action"] == "None; session tool namespaces attached."
|
||||
|
||||
|
||||
def test_assess_connected_namespace_attachment_missing():
|
||||
connected = ["gitea-author", "gitea-reviewer", "gitea-merger"]
|
||||
attached = ["gitea-author"]
|
||||
res = mcp_namespace_health.assess_connected_namespace_attachment(
|
||||
connected_servers=connected,
|
||||
attached_session_namespaces=attached,
|
||||
)
|
||||
assert res["success"] is False
|
||||
assert res["attachment_healthy"] is False
|
||||
assert res["discovery_status"] == "connected_but_namespaces_missing"
|
||||
assert res["error_type"] == "mcp_connected_namespaces_missing"
|
||||
assert "gitea-reviewer" in res["missing_namespaces"]
|
||||
assert "gitea-merger" in res["missing_namespaces"]
|
||||
assert "Reconnect the IDE/client MCP session" in res["exact_next_action"]
|
||||
|
||||
|
||||
def test_assess_connected_namespace_attachment_disconnected():
|
||||
res = mcp_namespace_health.assess_connected_namespace_attachment(
|
||||
connected_servers=[],
|
||||
attached_session_namespaces=[],
|
||||
)
|
||||
assert res["success"] is False
|
||||
assert res["attachment_healthy"] is False
|
||||
assert res["discovery_status"] == "disconnected"
|
||||
|
||||
|
||||
def test_proof_of_connected_vs_attached_mapping():
|
||||
connected = ["gitea-author", "gitea-reviewer"]
|
||||
attached = ["gitea-author"]
|
||||
res = mcp_namespace_health.assess_connected_namespace_attachment(
|
||||
connected_servers=connected,
|
||||
attached_session_namespaces=attached,
|
||||
)
|
||||
proof = res["proof_of_connected_vs_attached"]
|
||||
assert proof["gitea-author"] == {"connected": True, "attached": True}
|
||||
assert proof["gitea-reviewer"] == {"connected": True, "attached": False}
|
||||
|
||||
|
||||
def test_unsafe_fallback_policy_enforcement():
|
||||
res = mcp_namespace_health.assess_connected_namespace_attachment(
|
||||
connected_servers=["gitea-author"],
|
||||
attached_session_namespaces=[],
|
||||
)
|
||||
policy = res["unsafe_fallback_policy"]
|
||||
assert "Workflow Safety Hard Stop (#708)" in policy
|
||||
assert "direct imports" in policy
|
||||
assert "API mutations" in policy
|
||||
assert "profile hopping" in policy
|
||||
assert "session-state overrides" in policy
|
||||
Reference in New Issue
Block a user