diff --git a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py index c76387c..82ae5f8 100755 --- a/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py +++ b/packages/server/src/services/hermes/agent-bridge/hermes_bridge.py @@ -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() diff --git a/tests/server/agent-bridge-profile-env.test.ts b/tests/server/agent-bridge-profile-env.test.ts index bfafd74..4a79e32 100644 --- a/tests/server/agent-bridge-profile-env.test.ts +++ b/tests/server/agent-bridge-profile-env.test.ts @@ -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