From 1f2a33f4acf253b58157ed0c87652884bc8e0558 Mon Sep 17 00:00:00 2001 From: HexLab98 Date: Sun, 5 Jul 2026 18:36:43 +0700 Subject: [PATCH] fix(mcp): gate probe prompts/resources on config + advertised capabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Test server" probe (`_probe_single_server`, used by the Desktop/dashboard MCP tab, `hermes mcp add`, and `hermes mcp test`) called `prompts/list` and `resources/list` on every server unconditionally whenever `details` was requested. This ignored the user's `tools.prompts` / `tools.resources` config and the server's own advertised capabilities. Servers that don't implement those optional families (e.g. Unreal Engine's MCP server, which answers `Call to unknown method "prompts/list"`) therefore logged a hard error during discovery, and setting `tools.prompts: false` — the documented workaround — had no effect because the probe never consulted it. Mirror the runtime gating in `tools.mcp_tool._select_utility_schemas`: only probe a family when it is enabled in config AND advertised in the server's `initialize` capabilities. Falls back to the previous always-try behaviour when no capability info was captured. --- hermes_cli/mcp_config.py | 52 ++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/hermes_cli/mcp_config.py b/hermes_cli/mcp_config.py index 6d8646bec7e..657d315c13c 100644 --- a/hermes_cli/mcp_config.py +++ b/hermes_cli/mcp_config.py @@ -267,6 +267,7 @@ def _probe_single_server( _run_on_mcp_loop, _connect_server, _stop_mcp_loop_if_idle, + _parse_boolish, ) config = _resolve_mcp_server_config(config) @@ -287,18 +288,49 @@ def _probe_single_server( desc = desc[:77] + "..." tools_found.append((t.name, desc)) if details is not None: + # Gate the capability probes exactly like runtime utility-tool + # registration (tools.mcp_tool._select_utility_schemas): + # 1. honour the user's tools.prompts / tools.resources config + # 2. only call a family the server actually advertises. + # Without this the "Test server" probe fired prompts/list and + # resources/list at every server unconditionally — so a server + # that rejects those methods (e.g. Unreal's MCP server, which + # answers "Call to unknown method 'prompts/list'") logged a hard + # error, and setting tools.prompts: false did NOT suppress it. + tools_filter = config.get("tools") or {} + prompts_enabled = _parse_boolish( + tools_filter.get("prompts"), default=True + ) + resources_enabled = _parse_boolish( + tools_filter.get("resources"), default=True + ) + advertised_caps = getattr( + getattr(server, "initialize_result", None), + "capabilities", + None, + ) + + def _advertises(cap_attr: str) -> bool: + # When no capability info was captured (legacy fixtures / + # older servers) preserve the old always-try behaviour. + if advertised_caps is None: + return True + return getattr(advertised_caps, cap_attr, None) is not None + # Capability probes are best-effort: servers without the # capability raise, which just means "0". - try: - result = await server.session.list_prompts() - details["prompts"] = len(result.prompts) - except Exception: - pass - try: - result = await server.session.list_resources() - details["resources"] = len(result.resources) - except Exception: - pass + if prompts_enabled and _advertises("prompts"): + try: + result = await server.session.list_prompts() + details["prompts"] = len(result.prompts) + except Exception: + pass + if resources_enabled and _advertises("resources"): + try: + result = await server.session.list_resources() + details["resources"] = len(result.resources) + except Exception: + pass finally: await server.shutdown()