refactor(mcp): keep the passive status RPC, drop the SDK contract and reason codes

mcp.servers.status now rides the shared _mcp_rpc decorator (profile scope, 4064,
5024 with the real message) instead of a hand-rolled try/finally with a blanket
except. Drop the _MCPConnectErrorText str subclass and reason taxonomy: the
existing status/error fields already carry the state, and a whitelist on the RPC
keeps error text out of the wire. The Desktop connections.health contribution
contract is held back until its consumer plugin is public. Tests trimmed to the
scope invariants (per-profile runtime visibility, scoped shutdown clears only its
own status, launch runtime never leaks into another profile).
This commit is contained in:
Teknium
2026-09-06 12:06:12 -07:00
parent a6699d60f4
commit 0d8a1575c5
5 changed files with 30 additions and 223 deletions
+2 -73
View File
@@ -4,11 +4,9 @@ All tests use mocks -- no real MCP servers or subprocesses are started.
"""
import asyncio
import copy
import json
import logging
import os
import pickle
import sys
import threading
import time
@@ -274,19 +272,6 @@ class TestMCPStatus:
import tools.mcp_tool as mcp_tool
from tools import mcp_tool_config as _mcp_config
from tools import mcp_tool_discovery as _mcp_discovery
from tools import mcp_tool_errors as _mcp_errors
from tools.mcp_oauth import OAuthNonInteractiveError
assert _mcp_errors._connect_failure_reason(RuntimeError("network")) == "check_failed"
assert _mcp_errors._connect_failure_reason(
OAuthNonInteractiveError("browser required")
) == "auth_required"
tagged = _mcp_errors._connect_error_text(
"Connection closed", OAuthNonInteractiveError("browser required")
)
for cloned in (copy.copy(tagged), copy.deepcopy(tagged), pickle.loads(pickle.dumps(tagged))):
assert cloned == "Connection closed"
assert getattr(cloned, "reason") == "auth_required"
monkeypatch.setattr(
_mcp_config, "_load_mcp_config",
@@ -299,17 +284,13 @@ class TestMCPStatus:
)
with mcp_tool._lock:
saved_servers = dict(mcp_tool._servers)
saved_scopes = dict(mcp_tool._server_scope_keys)
saved_connecting = set(mcp_tool._server_connecting)
saved_errors = dict(mcp_tool._server_connect_errors)
mcp_tool._servers.clear()
mcp_tool._server_scope_keys.clear()
mcp_tool._server_connecting.clear()
mcp_tool._server_connect_errors.clear()
mcp_tool._server_connecting.add("connecting")
mcp_tool._server_connect_errors["failed"] = _mcp_errors._connect_error_text(
"Connection closed", OAuthNonInteractiveError("browser required")
)
mcp_tool._server_connect_errors["failed"] = "Connection closed"
try:
statuses = {
@@ -320,8 +301,6 @@ class TestMCPStatus:
with mcp_tool._lock:
mcp_tool._servers.clear()
mcp_tool._servers.update(saved_servers)
mcp_tool._server_scope_keys.clear()
mcp_tool._server_scope_keys.update(saved_scopes)
mcp_tool._server_connecting.clear()
mcp_tool._server_connecting.update(saved_connecting)
mcp_tool._server_connect_errors.clear()
@@ -331,10 +310,8 @@ class TestMCPStatus:
assert statuses["configured"]["connected"] is False
assert statuses["configured"]["disabled"] is False
assert statuses["connecting"]["status"] == "connecting"
assert statuses["connecting"]["reason"] == "stale"
assert statuses["failed"]["status"] == "failed"
assert statuses["failed"]["error"] == "Connection closed"
assert statuses["failed"]["reason"] == "auth_required"
assert statuses["disabled"]["status"] == "disabled"
assert statuses["disabled"]["disabled"] is True
@@ -372,10 +349,8 @@ class TestMCPStatus:
mcp_tool._server_scope_keys.update(saved_scopes)
assert status["status"] == "configured"
assert status["reason"] == "stale"
assert status["tools"] == 0
assert unscoped_status["status"] == "configured"
assert unscoped_status["reason"] == "stale"
def test_scoped_shutdown_clears_only_its_connection_status(self, monkeypatch):
@@ -448,6 +423,7 @@ class TestMCPStatus:
mcp_tool._server_connect_failures.update(saved_failures)
class TestLifecycleConfig:
def test_get_lifecycle_seconds_accepts_top_level_and_nested_values(self):
from tools.mcp_tool_common import _get_lifecycle_seconds
@@ -2820,53 +2796,6 @@ class TestSanitizeMcpNameComponent:
class TestRegisterMcpServers:
def test_records_profile_scope_before_connect(self):
import tools.mcp_tool as mcp_tool
from tools import mcp_tool_lifecycle, mcp_tool_loop, mcp_tool_discovery
seen = []
async def fake_register(name, _cfg):
with mcp_tool._lock:
seen.append((
name in mcp_tool._server_connecting,
mcp_tool._server_scope_keys.get(name),
))
raise RuntimeError("expected connect failure")
with mcp_tool._lock:
saved_servers = dict(mcp_tool._servers)
saved_scopes = dict(mcp_tool._server_scope_keys)
saved_connecting = set(mcp_tool._server_connecting)
saved_errors = dict(mcp_tool._server_connect_errors)
mcp_tool._servers.clear()
mcp_tool._server_scope_keys.clear()
mcp_tool._server_connecting.clear()
mcp_tool._server_connect_errors.clear()
try:
with patch("tools.mcp_tool._MCP_AVAILABLE", True), \
patch("tools.mcp_tool_config._filter_suspicious_mcp_servers", side_effect=lambda value: value), \
patch("tools.mcp_tool_discovery._connect_cooldown_active", return_value=False), \
patch("tools.mcp_tool._mcp_registry_scope", return_value="profile:work"), \
patch("tools.mcp_tool_discovery._discover_and_register_server", side_effect=fake_register):
mcp_tool_discovery.register_mcp_servers({"scoped": {"command": "test"}})
assert seen == [(True, "profile:work")]
with mcp_tool._lock:
assert mcp_tool._server_connect_errors["scoped"].reason == "check_failed"
assert mcp_tool._server_scope_keys["scoped"] == "profile:work"
finally:
with mcp_tool._lock:
mcp_tool._servers.clear()
mcp_tool._servers.update(saved_servers)
mcp_tool._server_scope_keys.clear()
mcp_tool._server_scope_keys.update(saved_scopes)
mcp_tool._server_connecting.clear()
mcp_tool._server_connecting.update(saved_connecting)
mcp_tool._server_connect_errors.clear()
mcp_tool._server_connect_errors.update(saved_errors)
"""Verify the new register_mcp_servers() public API."""
def test_mcp_not_available_returns_empty(self):
@@ -136,68 +136,11 @@ def test_status_is_profile_scoped_and_credential_safe(hermes_root):
"connected": False,
"disabled": False,
"status": "configured",
"reason": "stale",
}
]
assert "error" not in str(payload)
def test_status_redacts_internal_failures(hermes_root, monkeypatch):
from tools import mcp_tool_discovery
def fail(*args, **kwargs):
raise RuntimeError("token=must-not-leak")
monkeypatch.setattr(mcp_tool_discovery, "get_mcp_status", fail)
response = _call("mcp.servers.status", {"profile": "work"})
assert response["error"]["message"] == "MCP status unavailable"
assert "must-not-leak" not in str(response)
def test_status_omits_raw_server_errors(hermes_root, monkeypatch):
from tools import mcp_tool_discovery
monkeypatch.setattr(
mcp_tool_discovery,
"get_mcp_status",
lambda configured, include_runtime: [{
"name": "svc",
"transport": "stdio",
"tools": 0,
"connected": False,
"disabled": False,
"status": "failed",
"reason": "auth_required",
"error": "token=must-not-leak",
}],
)
payload = _result(_call("mcp.servers.status"))
assert payload["servers"][0]["reason"] == "auth_required"
assert "error" not in payload["servers"][0]
assert "must-not-leak" not in str(payload)
def test_status_does_not_run_the_runtime_config_loader(hermes_root, monkeypatch):
from tools import mcp_tool_config
_result(
_call(
"mcp.servers.add",
{"profile": "work", "name": "svc-a", "config": {"command": "svc-a-bin"}},
)
)
def forbidden():
raise AssertionError("runtime config loader executed")
monkeypatch.setattr(mcp_tool_config, "_load_mcp_config", forbidden)
payload = _result(_call("mcp.servers.status", {"profile": "work"}))
assert [server["name"] for server in payload["servers"]] == ["svc-a"]
def test_status_does_not_mix_launch_runtime_into_another_profile(hermes_root):
import tools.mcp_tool as mcp_tool
+16 -41
View File
@@ -112,7 +112,7 @@ def _note_connect_failure(name: str, exc: BaseException) -> str:
message = _errors._format_connect_error(exc)
with _core._lock:
_core._server_connecting.discard(name)
_core._server_connect_errors[name] = _errors._connect_error_text(message, exc)
_core._server_connect_errors[name] = message
_record_connect_failure(name)
return message
@@ -329,9 +329,7 @@ def _run_discovery_pass(new_servers: Dict[str, dict]) -> None:
"connecting set: %s", how, len(stale), ", ".join(stale))
_core._server_connecting.difference_update(stale)
for _sn in stale:
message = f"Connection attempt {how} during discovery"
_core._server_connect_errors.setdefault(
_sn, _errors._MCPConnectErrorText(message, "check_failed"))
_core._server_connect_errors.setdefault(_sn, f"Connection attempt {how} during discovery")
raise
finally:
if _was_interrupted:
@@ -457,36 +455,24 @@ def is_mcp_tool_parallel_safe(tool_name: str) -> bool:
return bool(server_name and server_name in _core._parallel_safe_servers)
def get_mcp_status(
configured: Optional[Dict[str, dict]] = None,
*,
include_runtime: bool = True,
) -> List[dict]:
"""Per-server cached status without initiating connections."""
def get_mcp_status(configured: Optional[Dict[str, dict]] = None, *, include_runtime: bool = True) -> List[dict]:
"""Per-server status dicts for banner/TUI: name, transport, tools, connected, disabled,
status (connected / disabled / connecting / failed / configured) and error for failed.
Reads cached runtime state only; never connects."""
configured = _config._load_mcp_config() if configured is None else dict(configured)
if not configured:
return []
current_scope = _core._mcp_registry_scope()
with _core._lock:
scope_keys = dict(_core._server_scope_keys) if include_runtime else {}
def visible(name: str) -> bool:
# Runtime state belongs to the profile that adopted it; under a multiplexer only that
# profile's view may show it, and ``include_runtime=False`` hides the launch profile's
# servers from a status read scoped to a different profile.
return include_runtime and _core._server_scope_keys.get(name, None) == current_scope
def visible_in_current_scope(name: str) -> bool:
if name in scope_keys:
return scope_keys[name] == current_scope
return current_scope is None
active_servers = {
name: server for name, server in _core._servers.items()
if include_runtime and visible_in_current_scope(name)
}
connecting = {
name for name in _core._server_connecting
if include_runtime and visible_in_current_scope(name)
}
connect_errors = {
name: error for name, error in _core._server_connect_errors.items()
if include_runtime and visible_in_current_scope(name)
}
active_servers = {n: s for n, s in _core._servers.items() if visible(n)}
connecting = {n for n in _core._server_connecting if visible(n)}
connect_errors = {n: e for n, e in _core._server_connect_errors.items() if visible(n)}
result: List[dict] = []
for name, cfg in configured.items():
@@ -495,13 +481,8 @@ def get_mcp_status(
live = server is not None and server.session is not None
status = ("connected" if live else "disabled" if not enabled else "connecting" if name in connecting
else "failed" if name in connect_errors else "configured")
reason = {
"connected": "healthy", "disabled": "not_configured",
"connecting": "stale", "configured": "stale",
}.get(status, "check_failed")
entry = {"name": name, "transport": cfg.get("transport", "http") if "url" in cfg else "stdio",
"tools": 0, "connected": False, "disabled": status == "disabled",
"status": status, "reason": reason}
"tools": 0, "connected": False, "disabled": status == "disabled", "status": status}
if live:
entry["connected"] = True
entry["tools"] = (len(server._registered_tool_names) if hasattr(server, "_registered_tool_names")
@@ -509,13 +490,7 @@ def get_mcp_status(
if server._sampling:
entry["sampling"] = dict(server._sampling.metrics)
elif status == "failed":
stored_error = connect_errors[name]
failure = getattr(server, "_error", None) if server is not None else None
failure_reason = getattr(stored_error, "reason", None)
if failure_reason is None and isinstance(failure, BaseException):
failure_reason = _errors._connect_failure_reason(failure)
entry["reason"] = failure_reason or "check_failed"
entry["error"] = str(stored_error)
entry["error"] = connect_errors[name]
result.append(entry)
return result
-23
View File
@@ -283,29 +283,6 @@ def _is_auth_error(exc: BaseException) -> bool:
return getattr(exc.response, "status_code", None) == 401 if isinstance(exc, http_types) else True
def _connect_failure_reason(exc: BaseException) -> str:
"""Reduce an MCP connect exception to a credential-safe health reason."""
return "auth_required" if _is_auth_error(_unwrap_exception_group(exc)) else "check_failed"
class _MCPConnectErrorText(str):
"""Backward-compatible error text carrying a credential-safe reason."""
reason: str
def __new__(cls, message: str, reason: str):
value = super().__new__(cls, message)
value.reason = reason
return value
def __reduce__(self):
return type(self), (str(self), self.reason)
def _connect_error_text(message: str, exc: BaseException) -> str:
return _MCPConnectErrorText(message, _connect_failure_reason(exc))
# Lower-cased substrings meaning the transport session expired / was GC'd (OAuth token still valid).
# Substrings (lower-cased match) that indicate the MCP server rejected the request because its server-side
# transport session expired / was garbage-collected. See #13383.
+12 -29
View File
@@ -1171,37 +1171,20 @@ def _(rid, params: dict) -> dict:
return _ok(rid, {"servers": [_mcp_summarize_server(name, cfg) for name, cfg in sorted(servers.items())]})
@method("mcp.servers.status")
@_mcp_rpc("status", required=())
def _(rid, params: dict) -> dict:
"""Return credential-safe cached MCP runtime state without connecting."""
"""``{servers: [{name, transport, tools, connected, disabled, status}], checked_at}`` from cached
runtime state; never connects, probes, or starts auth. Under a multiplexer the runtime view is the
scoped profile's; otherwise it is shown only when ``profile`` is the launch profile."""
import time
hc = _tools_mod("hermes_constants")
runtime_home_key = hc.hermes_home_key()
token = None
try:
if profile := _str_arg(params, "profile"):
profile_dir = _tools_mod("hermes_cli.profiles").get_profile_dir(profile)
if not profile_dir or not profile_dir.is_dir():
return _err(rid, 4064, f"profile '{profile}' not found")
token = hc.set_hermes_home_override(str(profile_dir))
import time
from agent.secret_scope import is_multiplex_active
configured = _tools_mod("hermes_cli.mcp_config")._get_mcp_servers()
get_status = _tools_mod("tools.mcp_tool_discovery").get_mcp_status
safe_fields = ("name", "transport", "tools", "connected", "disabled", "status", "reason")
servers = get_status(
configured,
include_runtime=is_multiplex_active() or hc.hermes_home_key() == runtime_home_key,
)
return _ok(rid, {
"servers": [{key: entry[key] for key in safe_fields if key in entry} for entry in servers],
"checked_at": int(time.time() * 1000),
})
except Exception:
return _err(rid, 5024, "MCP status unavailable")
finally:
_mcp_reset_profile(token)
configured = _tools_mod("hermes_cli.mcp_config")._get_mcp_servers()
include_runtime = (_tools_mod("agent.secret_scope").is_multiplex_active()
or hc.hermes_home_key() == hc.hermes_home_key(hc.get_process_hermes_home()))
safe = ("name", "transport", "tools", "connected", "disabled", "status")
servers = _tools_mod("tools.mcp_tool_discovery").get_mcp_status(configured, include_runtime=include_runtime)
return _ok(rid, {"servers": [{k: e[k] for k in safe if k in e} for e in servers],
"checked_at": int(time.time() * 1000)})
@_mcp_rpc("add")