Feature: #6351 - Agent selection, tool resolution & framework integration

Made-with: Cursor
This commit is contained in:
Fernando Mano
2026-03-24 22:34:52 -03:00
parent 45aafbc52b
commit c7d0afc775
11 changed files with 107 additions and 170 deletions
+4 -3
View File
@@ -13,6 +13,10 @@ out/
.env .env
.env.local .env.local
.env.*.local .env.*.local
.venv
/venv
tools/src/uv.lock
# User configuration (copied from .example) # User configuration (copied from .example)
config.yaml config.yaml
@@ -69,9 +73,6 @@ exports/*
.claude/settings.local.json .claude/settings.local.json
.venv
/venv
docs/github-issues/* docs/github-issues/*
core/tests/*dumps/* core/tests/*dumps/*
@@ -584,11 +584,19 @@ class CredentialTesterAgent:
self._tool_registry.load_mcp_config(mcp_config_path) self._tool_registry.load_mcp_config(mcp_config_path)
try: try:
agent_dir = Path(__file__).parent
registry = MCPRegistry() registry = MCPRegistry()
registry.initialize() registry.initialize()
registry_configs = registry.load_agent_selection(Path(__file__).parent) if (agent_dir / "mcp_registry.json").is_file():
self._tool_registry.set_mcp_registry_agent_path(agent_dir)
registry_configs, selection_max_tools = registry.load_agent_selection(agent_dir)
if registry_configs: if registry_configs:
self._tool_registry.load_registry_servers(registry_configs) self._tool_registry.load_registry_servers(
registry_configs,
preserve_existing_tools=True,
log_collisions=True,
max_tools=selection_max_tools,
)
except Exception: except Exception:
logger.warning("MCP registry config failed to load", exc_info=True) logger.warning("MCP registry config failed to load", exc_info=True)
@@ -1,4 +0,0 @@
{
"profile": "all"
}
@@ -1,44 +0,0 @@
{
"servers": [
{
"name": "hive-tools",
"version": "1.0.0",
"tags": ["core", "productivity"],
"profiles": ["all", "core", "productivity"],
"mcp_config": {
"transport": "stdio",
"command": "uv",
"args": ["run", "python", "mcp_server.py", "--stdio"],
"cwd": "tools",
"description": "Hive tools MCP server providing core utilities"
}
},
{
"name": "coder-tools",
"version": "1.0.0",
"tags": ["coding", "productivity"],
"profiles": ["all", "coding", "productivity"],
"mcp_config": {
"transport": "stdio",
"command": "uv",
"args": ["run", "python", "coder_tools_server.py", "--stdio"],
"cwd": "tools",
"description": "Unsandboxed file/code tools for code generation"
}
},
{
"name": "tools",
"version": "1.0.0",
"tags": ["web", "productivity"],
"profiles": ["all", "general"],
"mcp_config": {
"transport": "stdio",
"command": "uv",
"args": ["run", "python", "mcp_server.py", "--stdio"],
"cwd": "tools",
"description": "Aden tools MCP server providing web/file utilities"
}
}
]
}
+12 -7
View File
@@ -401,14 +401,16 @@ class MCPRegistry:
# ── load_agent_selection ──────────────────────────────────────── # ── load_agent_selection ────────────────────────────────────────
def load_agent_selection(self, agent_path: Path) -> list[dict[str, Any]]: def load_agent_selection(self, agent_path: Path) -> tuple[list[dict[str, Any]], int | None]:
"""Load mcp_registry.json from an agent directory and resolve servers. """Load mcp_registry.json from an agent directory and resolve servers.
Returns list of plain dicts compatible with ToolRegistry.register_mcp_server(). Returns:
(server_config_dicts, max_tools) for :meth:`ToolRegistry.load_registry_servers`.
``max_tools`` is ``None`` when omitted or invalid in JSON.
""" """
registry_json_path = agent_path / "mcp_registry.json" registry_json_path = agent_path / "mcp_registry.json"
if not registry_json_path.exists(): if not registry_json_path.exists():
return [] return [], None
selection = json.loads(registry_json_path.read_text(encoding="utf-8")) selection = json.loads(registry_json_path.read_text(encoding="utf-8"))
@@ -437,15 +439,16 @@ class MCPRegistry:
continue continue
validated[field] = value validated[field] = value
max_tools = validated.get("max_tools")
configs = self.resolve_for_agent( configs = self.resolve_for_agent(
include=validated.get("include"), include=validated.get("include"),
tags=validated.get("tags"), tags=validated.get("tags"),
exclude=validated.get("exclude"), exclude=validated.get("exclude"),
profile=validated.get("profile"), profile=validated.get("profile"),
max_tools=validated.get("max_tools"), max_tools=max_tools,
versions=validated.get("versions"), versions=validated.get("versions"),
) )
return [self._server_config_to_dict(c) for c in configs] return [self._server_config_to_dict(c) for c in configs], max_tools
# ── resolve_for_agent ─────────────────────────────────────────── # ── resolve_for_agent ───────────────────────────────────────────
@@ -552,12 +555,14 @@ class MCPRegistry:
) )
continue continue
# Check tool count cap before adding (FR-56) # Check tool count cap before adding (FR-56), using manifest tool list when present.
# When ``tools`` is empty (e.g. ``add_local``), counts are unknown here—callers should
# pass the same ``max_tools`` to ToolRegistry.load_registry_servers to cap registration.
manifest_tools = manifest.get("tools", []) manifest_tools = manifest.get("tools", [])
server_tool_count = len(manifest_tools) server_tool_count = len(manifest_tools)
if max_tools is not None and server_tool_count == 0: if max_tools is not None and server_tool_count == 0:
logger.debug( logger.debug(
"Server '%s' has no declared tools in manifest, skipping max_tools check", "Server '%s' has no tools list in manifest; max_tools enforced at registration",
name, name,
) )
elif max_tools is not None and total_tools + server_tool_count > max_tools: elif max_tools is not None and total_tools + server_tool_count > max_tools:
+13 -2
View File
@@ -1429,12 +1429,18 @@ class AgentRunner:
def _load_registry_mcp_servers(self, agent_path: Path) -> None: def _load_registry_mcp_servers(self, agent_path: Path) -> None:
"""Load and register MCP servers selected via ``mcp_registry.json``.""" """Load and register MCP servers selected via ``mcp_registry.json``."""
registry_json = agent_path / "mcp_registry.json"
if registry_json.is_file():
self._tool_registry.set_mcp_registry_agent_path(agent_path)
else:
self._tool_registry.set_mcp_registry_agent_path(None)
from framework.runner.mcp_registry import MCPRegistry from framework.runner.mcp_registry import MCPRegistry
try: try:
registry = MCPRegistry() registry = MCPRegistry()
registry.initialize() registry.initialize()
server_configs = registry.load_agent_selection(agent_path) server_configs, selection_max_tools = registry.load_agent_selection(agent_path)
except Exception as exc: except Exception as exc:
logger.warning( logger.warning(
"Failed to load MCP registry servers for '%s': %s", "Failed to load MCP registry servers for '%s': %s",
@@ -1446,7 +1452,12 @@ class AgentRunner:
if not server_configs: if not server_configs:
return return
results = self._tool_registry.load_registry_servers(server_configs) results = self._tool_registry.load_registry_servers(
server_configs,
preserve_existing_tools=True,
log_collisions=True,
max_tools=selection_max_tools,
)
loaded = [result for result in results if result["status"] == "loaded"] loaded = [result for result in results if result["status"] == "loaded"]
skipped = [result for result in results if result["status"] != "loaded"] skipped = [result for result in results if result["status"] != "loaded"]
+9 -2
View File
@@ -90,9 +90,16 @@ async def create_queen(
try: try:
registry = MCPRegistry() registry = MCPRegistry()
registry.initialize() registry.initialize()
registry_configs = registry.load_agent_selection(queen_pkg_dir) if (queen_pkg_dir / "mcp_registry.json").is_file():
queen_registry.set_mcp_registry_agent_path(queen_pkg_dir)
registry_configs, selection_max_tools = registry.load_agent_selection(queen_pkg_dir)
if registry_configs: if registry_configs:
results = queen_registry.load_registry_servers(registry_configs) results = queen_registry.load_registry_servers(
registry_configs,
preserve_existing_tools=True,
log_collisions=True,
max_tools=selection_max_tools,
)
logger.info("Queen: loaded MCP registry servers: %s", results) logger.info("Queen: loaded MCP registry servers: %s", results)
except Exception: except Exception:
logger.warning("Queen: MCP registry config failed to load", exc_info=True) logger.warning("Queen: MCP registry config failed to load", exc_info=True)
+5 -5
View File
@@ -32,7 +32,7 @@ class _FakeRegistry:
def load_agent_selection(self, agent_path: Path): def load_agent_selection(self, agent_path: Path):
self.loaded_paths.append(agent_path) self.loaded_paths.append(agent_path)
return list(self._returned_configs) return list(self._returned_configs), None
def test_agent_runner_loads_registry_selected_servers(tmp_path, monkeypatch): def test_agent_runner_loads_registry_selected_servers(tmp_path, monkeypatch):
@@ -61,7 +61,7 @@ def test_agent_runner_loads_registry_selected_servers(tmp_path, monkeypatch):
monkeypatch.setattr(AgentRunner, "_resolve_default_model", staticmethod(lambda: "test-model")) monkeypatch.setattr(AgentRunner, "_resolve_default_model", staticmethod(lambda: "test-model"))
monkeypatch.setattr( monkeypatch.setattr(
"framework.runner.tool_registry.ToolRegistry.register_mcp_server", "framework.runner.tool_registry.ToolRegistry.register_mcp_server",
lambda self, server_config, use_connection_manager=True: ( lambda self, server_config, use_connection_manager=True, **kwargs: (
registered.append(server_config) or 1 registered.append(server_config) or 1
), ),
) )
@@ -95,7 +95,7 @@ def test_agent_runner_skips_registry_when_no_servers_selected(tmp_path, monkeypa
monkeypatch.setattr(AgentRunner, "_resolve_default_model", staticmethod(lambda: "test-model")) monkeypatch.setattr(AgentRunner, "_resolve_default_model", staticmethod(lambda: "test-model"))
monkeypatch.setattr( monkeypatch.setattr(
"framework.runner.tool_registry.ToolRegistry.register_mcp_server", "framework.runner.tool_registry.ToolRegistry.register_mcp_server",
lambda self, server_config, use_connection_manager=True: ( lambda self, server_config, use_connection_manager=True, **kwargs: (
registered.append(server_config) or 1 registered.append(server_config) or 1
), ),
) )
@@ -135,7 +135,7 @@ def test_agent_runner_logs_actual_registry_load_results(tmp_path, monkeypatch):
monkeypatch.setattr(AgentRunner, "_resolve_default_model", staticmethod(lambda: "test-model")) monkeypatch.setattr(AgentRunner, "_resolve_default_model", staticmethod(lambda: "test-model"))
monkeypatch.setattr( monkeypatch.setattr(
"framework.runner.tool_registry.ToolRegistry.load_registry_servers", "framework.runner.tool_registry.ToolRegistry.load_registry_servers",
lambda self, server_configs: [ lambda self, server_configs, **kwargs: [
{"server": "jira", "status": "loaded", "tools_loaded": 2, "skipped_reason": None}, {"server": "jira", "status": "loaded", "tools_loaded": 2, "skipped_reason": None},
{ {
"server": "slack", "server": "slack",
@@ -223,7 +223,7 @@ def test_integration_real_registry_to_agent_runner(tmp_path, monkeypatch):
registered: list[dict] = [] registered: list[dict] = []
monkeypatch.setattr( monkeypatch.setattr(
"framework.runner.tool_registry.ToolRegistry.register_mcp_server", "framework.runner.tool_registry.ToolRegistry.register_mcp_server",
lambda self, server_config, use_connection_manager=True: ( lambda self, server_config, use_connection_manager=True, **kwargs: (
registered.append(server_config) or 1 registered.append(server_config) or 1
), ),
) )
+52 -96
View File
@@ -6,6 +6,39 @@ from framework.runner.mcp_client import MCPTool
from framework.runner.tool_registry import ToolRegistry from framework.runner.tool_registry import ToolRegistry
def _patch_connection_manager_for_fake_stdio(monkeypatch, tool_map: dict[str, list[str]]) -> None:
"""Avoid spawning real stdio MCP processes; return in-memory clients per server name."""
class FakeMCPClient:
def __init__(self, config: Any):
self.config = config
def connect(self) -> None:
return
def disconnect(self) -> None:
return
def list_tools(self) -> list[MCPTool]:
names = tool_map.get(self.config.name, [])
return [_make_tool(n, self.config.name) for n in names]
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
raise NotImplementedError
class FakeManager:
def acquire(self, config: Any) -> FakeMCPClient:
return FakeMCPClient(config)
def release(self, _server_name: str) -> None:
return
monkeypatch.setattr(
"framework.runner.mcp_connection_manager.MCPConnectionManager.get_instance",
lambda: FakeManager(),
)
def _make_tool(name: str, server_name: str) -> MCPTool: def _make_tool(name: str, server_name: str) -> MCPTool:
return MCPTool( return MCPTool(
name=name, name=name,
@@ -25,46 +58,21 @@ def test_registry_first_wins_collisions(monkeypatch):
"s1": ["tool_common", "tool_hive"], "s1": ["tool_common", "tool_hive"],
"s2": ["tool_common", "tool_coder"], "s2": ["tool_common", "tool_coder"],
} }
_patch_connection_manager_for_fake_stdio(monkeypatch, tool_map)
from framework.runner import mcp_client as mcp_client_mod
class FakeMCPClient:
def __init__(self, config: Any):
self.config = config
self._tools: list[MCPTool] = []
def connect(self) -> None:
return
def disconnect(self) -> None:
return
def list_tools(self) -> list[MCPTool]:
names = tool_map.get(self.config.name, [])
return [_make_tool(n, self.config.name) for n in names]
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
raise NotImplementedError
monkeypatch.setattr(mcp_client_mod, "MCPClient", FakeMCPClient)
from framework.runner import mcp_registry_resolver as resolver_mod
# Return server configs in the desired deterministic order.
resolved_servers = [ resolved_servers = [
{"name": "s1", "transport": "stdio", "command": "fake", "args": [], "cwd": None}, {"name": "s1", "transport": "stdio", "command": "fake", "args": [], "cwd": None},
{"name": "s2", "transport": "stdio", "command": "fake", "args": [], "cwd": None}, {"name": "s2", "transport": "stdio", "command": "fake", "args": [], "cwd": None},
] ]
monkeypatch.setattr(
resolver_mod,
"resolve_registry_servers",
lambda **kwargs: resolved_servers,
)
registry = ToolRegistry() registry = ToolRegistry()
added = registry.load_registry_servers(include=["s1"], tags=None, exclude=None, profile=None) registry.load_registry_servers(
resolved_servers,
log_summary=False,
preserve_existing_tools=True,
log_collisions=True,
)
assert added == 3 # tool_common + tool_hive + tool_coder
assert registry.has_tool("tool_common") is True assert registry.has_tool("tool_common") is True
assert registry.has_tool("tool_hive") is True assert registry.has_tool("tool_hive") is True
assert registry.has_tool("tool_coder") is True assert registry.has_tool("tool_coder") is True
@@ -81,48 +89,25 @@ def test_registry_precedence_over_existing_mcp_servers(monkeypatch):
"s1": ["tool_common", "tool_hive"], "s1": ["tool_common", "tool_hive"],
"s2": ["tool_common", "tool_coder"], "s2": ["tool_common", "tool_coder"],
} }
_patch_connection_manager_for_fake_stdio(monkeypatch, tool_map)
from framework.runner import mcp_client as mcp_client_mod
class FakeMCPClient:
def __init__(self, config: Any):
self.config = config
def connect(self) -> None:
return
def disconnect(self) -> None:
return
def list_tools(self) -> list[MCPTool]:
names = tool_map.get(self.config.name, [])
return [_make_tool(n, self.config.name) for n in names]
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
raise NotImplementedError
monkeypatch.setattr(mcp_client_mod, "MCPClient", FakeMCPClient)
from framework.runner import mcp_registry_resolver as resolver_mod
resolved_servers = [ resolved_servers = [
{"name": "s1", "transport": "stdio", "command": "fake", "args": [], "cwd": None}, {"name": "s1", "transport": "stdio", "command": "fake", "args": [], "cwd": None},
{"name": "s2", "transport": "stdio", "command": "fake", "args": [], "cwd": None}, {"name": "s2", "transport": "stdio", "command": "fake", "args": [], "cwd": None},
] ]
monkeypatch.setattr(
resolver_mod,
"resolve_registry_servers",
lambda **kwargs: resolved_servers,
)
registry = ToolRegistry() registry = ToolRegistry()
registry.register_mcp_server( registry.register_mcp_server(
{"name": "pre", "transport": "stdio", "command": "fake", "args": [], "cwd": None} {"name": "pre", "transport": "stdio", "command": "fake", "args": [], "cwd": None}
) )
added = registry.load_registry_servers(include=None, tags=None, exclude=None, profile=None) registry.load_registry_servers(
resolved_servers,
log_summary=False,
preserve_existing_tools=True,
log_collisions=True,
)
assert added == 2 # only tool_hive + tool_coder; tool_common is preserved from "pre"
assert registry.get_server_tool_names("pre") == {"tool_common", "tool_pre"} assert registry.get_server_tool_names("pre") == {"tool_common", "tool_pre"}
assert registry.get_server_tool_names("s1") == {"tool_hive"} assert registry.get_server_tool_names("s1") == {"tool_hive"}
assert registry.get_server_tool_names("s2") == {"tool_coder"} assert registry.get_server_tool_names("s2") == {"tool_coder"}
@@ -135,50 +120,21 @@ def test_registry_max_tools_cap(monkeypatch):
"s1": ["tool_a", "tool_b"], "s1": ["tool_a", "tool_b"],
"s2": ["tool_c"], "s2": ["tool_c"],
} }
_patch_connection_manager_for_fake_stdio(monkeypatch, tool_map)
from framework.runner import mcp_client as mcp_client_mod
class FakeMCPClient:
def __init__(self, config: Any):
self.config = config
def connect(self) -> None:
return
def disconnect(self) -> None:
return
def list_tools(self) -> list[MCPTool]:
names = tool_map.get(self.config.name, [])
return [_make_tool(n, self.config.name) for n in names]
def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
raise NotImplementedError
monkeypatch.setattr(mcp_client_mod, "MCPClient", FakeMCPClient)
from framework.runner import mcp_registry_resolver as resolver_mod
resolved_servers = [ resolved_servers = [
{"name": "s1", "transport": "stdio", "command": "fake", "args": [], "cwd": None}, {"name": "s1", "transport": "stdio", "command": "fake", "args": [], "cwd": None},
{"name": "s2", "transport": "stdio", "command": "fake", "args": [], "cwd": None}, {"name": "s2", "transport": "stdio", "command": "fake", "args": [], "cwd": None},
] ]
monkeypatch.setattr(
resolver_mod,
"resolve_registry_servers",
lambda **kwargs: resolved_servers,
)
registry = ToolRegistry() registry = ToolRegistry()
added = registry.load_registry_servers( registry.load_registry_servers(
include=None, resolved_servers,
tags=None, log_summary=False,
exclude=None, preserve_existing_tools=True,
profile=None,
max_tools=2, max_tools=2,
) )
assert added == 2
assert registry.has_tool("tool_a") is True assert registry.has_tool("tool_a") is True
assert registry.has_tool("tool_b") is True assert registry.has_tool("tool_b") is True
assert registry.has_tool("tool_c") is False assert registry.has_tool("tool_c") is False
+1 -1
View File
@@ -214,7 +214,7 @@ def test_load_registry_servers_retries_when_registration_returns_zero(monkeypatc
registry = ToolRegistry() registry = ToolRegistry()
attempts = {"count": 0} attempts = {"count": 0}
def fake_register(server_config, use_connection_manager=True): def fake_register(server_config, use_connection_manager=True, **kwargs):
attempts["count"] += 1 attempts["count"] += 1
return 0 if attempts["count"] == 1 else 2 return 0 if attempts["count"] == 1 else 2
@@ -1,4 +1 @@
{ { "include": ["hive-tools"] }
"include": ["jira"],
"max_tools": 10
}