fix bridge mcp tool discovery (#1139)
This commit is contained in:
@@ -286,6 +286,43 @@ def _json_line_bytes(value: Any) -> bytes:
|
||||
return payload.encode("utf-8")
|
||||
|
||||
|
||||
def _bridge_log(event: str, payload: dict[str, Any]) -> None:
|
||||
try:
|
||||
body = {"event": event, **payload}
|
||||
print(
|
||||
"[hermes_bridge] " + json.dumps(_sanitize_surrogates(body), ensure_ascii=False, default=_json_default),
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
except Exception:
|
||||
print(f"[hermes_bridge] {event}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def _tool_names_from_definitions(tools: Any) -> list[str]:
|
||||
if not isinstance(tools, list):
|
||||
return []
|
||||
names: list[str] = []
|
||||
for tool in tools:
|
||||
name = ""
|
||||
if isinstance(tool, dict):
|
||||
function = tool.get("function")
|
||||
if isinstance(function, dict):
|
||||
name = str(function.get("name") or "")
|
||||
if not name:
|
||||
name = str(tool.get("name") or "")
|
||||
else:
|
||||
name = str(getattr(tool, "name", "") or "")
|
||||
if name:
|
||||
names.append(name)
|
||||
return names
|
||||
|
||||
|
||||
def _mcp_tool_names_from_names(tool_names: Any) -> list[str]:
|
||||
if not isinstance(tool_names, list):
|
||||
return []
|
||||
return sorted(str(name) for name in tool_names if str(name).startswith("mcp_"))
|
||||
|
||||
|
||||
def _agent_root() -> Path | None:
|
||||
return _find_agent_root(os.environ.get("HERMES_AGENT_ROOT"))
|
||||
|
||||
@@ -627,6 +664,78 @@ def _load_enabled_toolsets() -> list[str] | None:
|
||||
return None
|
||||
|
||||
|
||||
def _discover_bridge_mcp_tools() -> list[str]:
|
||||
_ensure_agent_imports()
|
||||
try:
|
||||
from tools.mcp_tool import discover_mcp_tools
|
||||
|
||||
tools = discover_mcp_tools()
|
||||
return list(tools) if isinstance(tools, list) else []
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[hermes_bridge] MCP tool discovery failed: {exc}",
|
||||
file=sys.stderr,
|
||||
flush=True,
|
||||
)
|
||||
return []
|
||||
|
||||
|
||||
def _log_worker_startup_context(profile: str | None) -> None:
|
||||
profile_name = profile or _worker_profile() or "default"
|
||||
try:
|
||||
cfg = _load_cfg()
|
||||
enabled_toolsets = _load_enabled_toolsets()
|
||||
discovered_mcp_tools = _discover_bridge_mcp_tools()
|
||||
tool_names: list[str] = []
|
||||
tool_error: str | None = None
|
||||
try:
|
||||
from model_tools import get_tool_definitions
|
||||
|
||||
tool_names = _tool_names_from_definitions(
|
||||
get_tool_definitions(
|
||||
enabled_toolsets=enabled_toolsets,
|
||||
quiet_mode=True,
|
||||
)
|
||||
)
|
||||
except Exception as exc:
|
||||
tool_error = str(exc)
|
||||
|
||||
mcp_servers = cfg.get("mcp_servers") if isinstance(cfg.get("mcp_servers"), dict) else {}
|
||||
enabled_mcp_servers: list[str] = []
|
||||
disabled_mcp_servers: list[str] = []
|
||||
for name, server_cfg in mcp_servers.items():
|
||||
enabled = True
|
||||
if isinstance(server_cfg, dict):
|
||||
enabled = str(server_cfg.get("enabled", True)).strip().lower() not in {"0", "false", "no", "off"}
|
||||
(enabled_mcp_servers if enabled else disabled_mcp_servers).append(str(name))
|
||||
|
||||
_bridge_log("bridge.worker.initialized", {
|
||||
"profile": profile_name,
|
||||
"platform": _bridge_platform(),
|
||||
"hermes_home": str(_hermes_home()),
|
||||
"base_hermes_home": str(_base_hermes_home()),
|
||||
"config_path": str(_hermes_home() / "config.yaml"),
|
||||
"model": _resolve_model(cfg),
|
||||
"enabled_toolsets": enabled_toolsets,
|
||||
"tool_count": len(tool_names),
|
||||
"tool_names": tool_names,
|
||||
"tool_error": tool_error,
|
||||
"mcp_server_count": len(mcp_servers),
|
||||
"mcp_servers": sorted(str(name) for name in mcp_servers),
|
||||
"enabled_mcp_servers": sorted(enabled_mcp_servers),
|
||||
"disabled_mcp_servers": sorted(disabled_mcp_servers),
|
||||
"mcp_discovered_tool_count": len(discovered_mcp_tools),
|
||||
"mcp_discovered_tool_names": discovered_mcp_tools,
|
||||
"mcp_tool_count": len(_mcp_tool_names_from_names(tool_names)),
|
||||
"mcp_tool_names": _mcp_tool_names_from_names(tool_names),
|
||||
})
|
||||
except Exception as exc:
|
||||
_bridge_log("bridge.worker.initialized", {
|
||||
"profile": profile_name,
|
||||
"error": str(exc),
|
||||
})
|
||||
|
||||
|
||||
def _load_reasoning_config() -> dict[str, Any] | None:
|
||||
_ensure_agent_imports()
|
||||
try:
|
||||
@@ -766,6 +875,7 @@ class AgentPool:
|
||||
|
||||
with _profile_env(profile):
|
||||
_refresh_worker_profile_env()
|
||||
discovered_mcp_tools = _discover_bridge_mcp_tools()
|
||||
cfg = _load_cfg()
|
||||
resolved_model = requested_model or _resolve_model(cfg)
|
||||
runtime = _resolve_runtime(resolved_model, requested_provider or None)
|
||||
@@ -801,6 +911,7 @@ class AgentPool:
|
||||
)
|
||||
agent.compression_enabled = False
|
||||
self._install_compression_hook(agent, session_id)
|
||||
mcp_tool_names = self._mcp_tool_names(self._agent_tool_names(getattr(agent, "tools", None) or []))
|
||||
|
||||
session = AgentSession(
|
||||
session_id=session_id,
|
||||
@@ -816,6 +927,8 @@ class AgentPool:
|
||||
"platform": _bridge_platform(),
|
||||
"resumed": False,
|
||||
"resumed_message_count": 0,
|
||||
"mcp_tool_count": len(discovered_mcp_tools),
|
||||
"active_mcp_tool_count": len(mcp_tool_names),
|
||||
"db_error": self._db.error,
|
||||
},
|
||||
)
|
||||
@@ -908,22 +1021,10 @@ class AgentPool:
|
||||
return str(system_message or "")
|
||||
|
||||
def _agent_tool_names(self, tools: Any) -> list[str]:
|
||||
if not isinstance(tools, list):
|
||||
return []
|
||||
names: list[str] = []
|
||||
for tool in tools:
|
||||
name = ""
|
||||
if isinstance(tool, dict):
|
||||
function = tool.get("function")
|
||||
if isinstance(function, dict):
|
||||
name = str(function.get("name") or "")
|
||||
if not name:
|
||||
name = str(tool.get("name") or "")
|
||||
else:
|
||||
name = str(getattr(tool, "name", "") or "")
|
||||
if name:
|
||||
names.append(name)
|
||||
return names
|
||||
return _tool_names_from_definitions(tools)
|
||||
|
||||
def _mcp_tool_names(self, tool_names: Any) -> list[str]:
|
||||
return _mcp_tool_names_from_names(tool_names)
|
||||
|
||||
def _estimate_context_info(self, agent: Any, messages: Any, system_message: Any = None) -> dict[str, Any]:
|
||||
try:
|
||||
@@ -935,6 +1036,7 @@ class AgentPool:
|
||||
tools = getattr(agent, "tools", None) or []
|
||||
message_list = messages if isinstance(messages, list) else []
|
||||
try:
|
||||
tool_names = self._agent_tool_names(tools)
|
||||
token_count = estimate_request_tokens_rough(message_list, system_prompt=prompt, tools=tools or None)
|
||||
fixed_context_tokens = estimate_request_tokens_rough([], system_prompt=prompt, tools=tools or None)
|
||||
system_prompt_tokens = estimate_request_tokens_rough([], system_prompt=prompt, tools=None)
|
||||
@@ -946,7 +1048,9 @@ class AgentPool:
|
||||
"tool_tokens": tool_tokens,
|
||||
"message_count": len(message_list),
|
||||
"tool_count": len(tools) if isinstance(tools, list) else 0,
|
||||
"tool_names": self._agent_tool_names(tools),
|
||||
"tool_names": tool_names,
|
||||
"mcp_tool_count": len(self._mcp_tool_names(tool_names)),
|
||||
"mcp_tool_names": self._mcp_tool_names(tool_names),
|
||||
"system_prompt_chars": len(prompt),
|
||||
}
|
||||
except Exception:
|
||||
@@ -3034,6 +3138,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
_ensure_agent_imports()
|
||||
if args.worker_profile:
|
||||
_set_worker_profile_env(str(args.worker_profile or "default"))
|
||||
_log_worker_startup_context(str(args.worker_profile or "default"))
|
||||
BridgeServer(args.endpoint).serve_forever()
|
||||
else:
|
||||
BridgeBroker(args.endpoint, args.agent_root, args.hermes_home).serve_forever()
|
||||
|
||||
@@ -309,6 +309,86 @@ print(json.dumps({
|
||||
})
|
||||
})
|
||||
|
||||
it('discovers MCP tools in the active profile before creating an agent', async () => {
|
||||
const profileHome = join(tempDir, 'profiles', 'work')
|
||||
await mkdir(profileHome, { recursive: true })
|
||||
await writeFile(join(profileHome, 'config.yaml'), 'model:\n default: work-model\n', 'utf-8')
|
||||
const expectedProfileHome = await realpath(profileHome)
|
||||
|
||||
const result = await runBridgeProbe(`
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
spec = importlib.util.spec_from_file_location("hermes_bridge", os.environ["BRIDGE_PATH"])
|
||||
bridge = importlib.util.module_from_spec(spec)
|
||||
sys.modules["hermes_bridge"] = bridge
|
||||
spec.loader.exec_module(bridge)
|
||||
|
||||
root = os.environ["TEST_HERMES_HOME"]
|
||||
os.environ["HERMES_HOME"] = root
|
||||
os.environ["HERMES_AGENT_BRIDGE_BASE_HOME"] = root
|
||||
|
||||
events = []
|
||||
|
||||
tools_pkg = types.ModuleType("tools")
|
||||
tools_pkg.__path__ = []
|
||||
sys.modules["tools"] = tools_pkg
|
||||
|
||||
mcp_tool = types.ModuleType("tools.mcp_tool")
|
||||
def discover_mcp_tools():
|
||||
events.append({"event": "discover", "home": os.environ.get("HERMES_HOME")})
|
||||
return ["mcp_anysearch_search"]
|
||||
mcp_tool.discover_mcp_tools = discover_mcp_tools
|
||||
sys.modules["tools.mcp_tool"] = mcp_tool
|
||||
|
||||
run_agent = types.ModuleType("run_agent")
|
||||
class FakeAgent:
|
||||
def __init__(self, **kwargs):
|
||||
events.append({
|
||||
"event": "agent",
|
||||
"home": os.environ.get("HERMES_HOME"),
|
||||
"enabled_toolsets": kwargs.get("enabled_toolsets"),
|
||||
})
|
||||
self.tools = []
|
||||
run_agent.AIAgent = FakeAgent
|
||||
sys.modules["run_agent"] = run_agent
|
||||
|
||||
class FakeDbHolder:
|
||||
error = None
|
||||
def get_for_profile(self, profile):
|
||||
return None
|
||||
|
||||
bridge._ensure_agent_imports = lambda: None
|
||||
bridge._load_cfg = lambda: {"model": {"default": "work-model"}, "agent": {}}
|
||||
bridge._resolve_runtime = lambda model, provider=None: {"provider": "fake"}
|
||||
bridge._load_enabled_toolsets = lambda: ["mcp-anysearch"]
|
||||
bridge._load_reasoning_config = lambda: None
|
||||
bridge._load_service_tier = lambda: None
|
||||
|
||||
pool = bridge.AgentPool()
|
||||
pool._db = FakeDbHolder()
|
||||
session = pool.get_or_create("session-1", profile="work")
|
||||
|
||||
print(json.dumps({
|
||||
"events": events,
|
||||
"mcp_tool_count": session.config.get("mcp_tool_count"),
|
||||
"restored_home": os.environ.get("HERMES_HOME"),
|
||||
}))
|
||||
`)
|
||||
|
||||
expect(result).toEqual({
|
||||
events: [
|
||||
{ event: 'discover', home: expectedProfileHome },
|
||||
{ event: 'agent', home: expectedProfileHome, enabled_toolsets: ['mcp-anysearch'] },
|
||||
],
|
||||
mcp_tool_count: 1,
|
||||
restored_home: tempDir,
|
||||
})
|
||||
})
|
||||
|
||||
it('handles Windows netstat output decode failures without crashing', async () => {
|
||||
const result = await runBridgeProbe(`
|
||||
import importlib.util
|
||||
|
||||
Reference in New Issue
Block a user