fix(mcp): reconcile an emptied profile config on reload; list portable plugin tools
Review gate on #7720 (two regressions reproduced against master with the installed Agent): 1. Deleting a profile's last MCP server left ADOPTED tools callable after /reload-mcp. The scoped teardown only stops connections the profile owns, and the Agent's discover_mcp_tools() returns before its reconcile step when the profile's config has no server, so an adopted shared connection was never judged against the config. /reload-mcp now calls register_mcp_servers({}) under the profile's scope before rediscovery: the Agent's overlay heal detaches only this profile's overlay for servers its config no longer has; the owner's connection keeps running (master's wildcard shutdown stopped the owner too). 2. Portable plugin MCP servers are merged by the Agent at runtime and never appear in the profile's raw ``mcp_servers``; the registry fallback required a config entry and dropped every plugin tool from /api/mcp/tools and the Notes inventory. The profile's own registry slot (registry_tool_owned_by_view) is the isolation check; the config is not an allowlist any more. Tests (red before, green after): agent-free unit tests for both (the fake agent now models the overlay heal and discovery's empty-config early return), and a real-Agent integration test for the adopted-then-deleted case (owner connection survives, adopter slot emptied, "Removed" reported). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
dbf0179c53
commit
3b7b5cbe7c
4 files changed
+125
-5
No files matched your search
@@ -286,6 +286,12 @@ def _run_reload_mcp_command() -> str:
|
||||
discover_mcp_tools = agent_attr(
|
||||
"tools.mcp_tool", "discover_mcp_tools", "tools.mcp_tool_discovery"
|
||||
)
|
||||
try:
|
||||
register_mcp_servers = agent_attr(
|
||||
"tools.mcp_tool", "register_mcp_servers", "tools.mcp_tool_discovery"
|
||||
)
|
||||
except Exception:
|
||||
register_mcp_servers = None # older agent: no overlay reconciliation
|
||||
except Exception as exc:
|
||||
logger.warning("Failed to import MCP runtime for /reload-mcp", exc_info=True)
|
||||
raise RuntimeError("MCP runtime unavailable") from exc
|
||||
@@ -332,6 +338,15 @@ def _run_reload_mcp_command() -> str:
|
||||
# The wildcard also dropped connect backoff; keep that for this
|
||||
# profile's own failed servers so the reload retries them now.
|
||||
clear_profile_connect_cooldowns(_core, view)
|
||||
# A connection this profile ADOPTED (another profile's identical
|
||||
# route) is not torn down above, and ``discover_mcp_tools()``
|
||||
# returns before its reconcile step when the profile's config
|
||||
# has no server left, so the adopted tools would stay callable.
|
||||
# ``register_mcp_servers({})`` judges every server serving this
|
||||
# scope against the profile's config on disk and detaches only
|
||||
# this profile's overlay; the owner's connection keeps running.
|
||||
if register_mcp_servers is not None and view.registry_scope is not None:
|
||||
register_mcp_servers({})
|
||||
new_tools = discover_mcp_tools()
|
||||
|
||||
with _lock:
|
||||
|
||||
+7
-5
@@ -29559,8 +29559,11 @@ def _mcp_tools_from_runtime_status(runtime_by_name, server_summaries):
|
||||
def _mcp_tools_from_registry(server_summaries, view=None):
|
||||
"""Read already-registered MCP tool schemas without probing MCP servers.
|
||||
|
||||
With a profile ``view`` (see ``api.mcp_runtime``), only tools of servers the
|
||||
profile configures, registered in that profile's own registry slot, are listed.
|
||||
With a profile ``view`` (see ``api.mcp_runtime``), only tools registered in
|
||||
that profile's own registry slot are listed. The slot is the isolation check;
|
||||
the raw ``mcp_servers`` config is not an allowlist: the agent merges portable
|
||||
plugin servers into the running config at runtime, and their tools are
|
||||
registered in the same slot without a ``config.yaml`` entry.
|
||||
"""
|
||||
try:
|
||||
from tools.registry import registry
|
||||
@@ -29580,9 +29583,8 @@ def _mcp_tools_from_registry(server_summaries, view=None):
|
||||
if not isinstance(toolset, str) or not toolset.startswith("mcp-"):
|
||||
continue
|
||||
server_name = toolset[len("mcp-"):]
|
||||
if view is not None and not view.legacy and (
|
||||
server_name not in server_summaries
|
||||
or not registry_tool_owned_by_view(registry, tool_name, view)
|
||||
if view is not None and not view.legacy and not registry_tool_owned_by_view(
|
||||
registry, tool_name, view
|
||||
):
|
||||
continue
|
||||
schema = registry.get_schema(tool_name) or {}
|
||||
|
||||
Reference in new issue
Block a user