fix(mcp): gate probe prompts/resources on config + advertised capabilities

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.
This commit is contained in:
HexLab98 2026-07-05 18:36:43 +07:00 committed by Teknium
parent de7e0a8875
commit 1f2a33f4ac

View file

@ -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()