hermes - ✅(Solved) Fix MCP utility stubs (`list_prompts` / `get_prompt` / `list_resources` / `read_resource`) are registered even when the server doesn't advertise the corresponding capabilities [1 pull requests, 2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#18051Fetched 2026-05-01 05:54:11
View on GitHub
Comments
2
Participants
2
Timeline
7
Reactions
0
Timeline (top)
labeled ×3commented ×2cross-referenced ×2

For every connected MCP server, hermes-agent registers four "utility" tool schemas (mcp_<server>_list_resources, mcp_<server>_read_resource, mcp_<server>_list_prompts, mcp_<server>_get_prompt) regardless of whether the server actually advertises prompts or resources capabilities in its initialize response. Calling those stubs against a tools-only server returns JSON-RPC -32601 Method not found, which leads the agent to incorrectly conclude the MCP server is broken or disconnected.

Root Cause

tools/mcp_tool.py:_select_utility_schemas (around lines 2614–2641) gates the four utility schemas on:

required_method = _UTILITY_CAPABILITY_METHODS[handler_key]
if not hasattr(server.session, required_method):
    ...
    continue

server.session is an mcp.ClientSession, which always has the four method attributes (list_resources, read_resource, list_prompts, get_prompt) defined on the class — independent of what the remote server actually supports. The check therefore never trips, and all four stubs are always registered.

The InitializeResult returned from await session.initialize() (called at lines 1118, 1219, 1242) is the source of truth — its .capabilities.resources / .capabilities.prompts are non-None only when the server advertises support — but hermes-agent currently discards this object instead of inspecting it.

Fix Action

Fix / Workaround

Patch

PR fix notes

PR #18052: fix(mcp): gate utility stubs on server-advertised capabilities

Description (problem / solution / changelog)

Summary

Fixes #18051. The four MCP utility schemas (list_resources / read_resource / list_prompts / get_prompt) are registered for every connected MCP server, even when the server advertises only the tools capability. Today's gate uses hasattr(server.session, required_method), which is always True because mcp.ClientSession defines all four methods on the class regardless of what the remote server actually supports.

The InitializeResult returned from await session.initialize() is the source of truth — result.capabilities.resources and result.capabilities.prompts are non-None only when the server implements those request types — but it was being discarded.

Changes

  • MCPServerTask.__slots__: add "initialize_result".
  • MCPServerTask.__init__: initialise self.initialize_result: Optional[Any] = None.
  • All three await session.initialize() call sites (_run_http, _run_http reconnect path, _run_stdio): assign the result into self.initialize_result.
  • _select_utility_schemas: when initialize_result is present, skip list_resources / read_resource if capabilities.resources is None, and skip list_prompts / get_prompt if capabilities.prompts is None. When initialize_result is None (older test fixtures, no real connect), fall back to the existing hasattr check so behaviour on that path is unchanged.

Verification

Direct unit-style test against a freshly patched tree (venv/bin/python against tools.mcp_tool):

from types import SimpleNamespace
from tools.mcp_tool import _select_utility_schemas, MCPServerTask

# tools-only server (Context7 case)
s1 = MCPServerTask("tools_only")
s1.session = SimpleNamespace(list_resources=lambda: None, read_resource=lambda x: None,
                              list_prompts=lambda: None, get_prompt=lambda *a: None)
s1.initialize_result = SimpleNamespace(capabilities=SimpleNamespace(
    tools=SimpleNamespace(listChanged=True), prompts=None, resources=None))
print([e["handler_key"] for e in _select_utility_schemas("x", s1, {})])
# -> []

# full-caps server
s2 = MCPServerTask("full")
s2.session = s1.session
s2.initialize_result = SimpleNamespace(capabilities=SimpleNamespace(
    tools=True, prompts=SimpleNamespace(), resources=SimpleNamespace()))
print([e["handler_key"] for e in _select_utility_schemas("x", s2, {})])
# -> ['list_resources', 'read_resource', 'list_prompts', 'get_prompt']

# legacy fallback (no initialize_result)
s3 = MCPServerTask("legacy")
s3.session = s1.session
print([e["handler_key"] for e in _select_utility_schemas("x", s3, {})])
# -> ['list_resources', 'read_resource', 'list_prompts', 'get_prompt']

End-to-end: I have this running locally against @upstash/context7-mcp (which is the canonical "tools-only" server). With the patch applied, _register_server_tools registers exactly two LLM-visible tools (mcp_context7_resolve_library_id, mcp_context7_query_docs) instead of the previous six.

Risk

  • The new initialize_result slot is None until the first successful initialize(). The fallback path keeps existing behaviour for code paths that haven't reached that point yet.
  • No production behaviour change for servers that do advertise prompts/resources — they get the same schemas as before.
  • Minimal blast radius: one new slot, three single-line assignments, one if/else block in the existing utility-selection function. No public-API change.

Notes

I haven't added a unit test in this PR because the existing tests in tests/tools/test_mcp_tool.py mock ClientSession rather than the full connect/initialize flow, and adding a capabilities-gating test would require either a new fixture for InitializeResult or relaxing the existing fixtures. Happy to follow up with one in a separate commit if you'd like — let me know which fixture style you prefer.

Changed files

  • tools/mcp_tool.py (modified, +36/-12)

Code Example

mcp_servers:
     context7:
       command: npx
       args: ["-y", "@upstash/context7-mcp"]

---

{"tools": {"listChanged": true}}

---

required_method = _UTILITY_CAPABILITY_METHODS[handler_key]
if not hasattr(server.session, required_method):
    ...
    continue
RAW_BUFFERClick to expand / collapse

Summary

For every connected MCP server, hermes-agent registers four "utility" tool schemas (mcp_<server>_list_resources, mcp_<server>_read_resource, mcp_<server>_list_prompts, mcp_<server>_get_prompt) regardless of whether the server actually advertises prompts or resources capabilities in its initialize response. Calling those stubs against a tools-only server returns JSON-RPC -32601 Method not found, which leads the agent to incorrectly conclude the MCP server is broken or disconnected.

Reproduction

  1. Configure any MCP server that advertises only tools capability — e.g. Context7 (@upstash/context7-mcp):
    mcp_servers:
      context7:
        command: npx
        args: ["-y", "@upstash/context7-mcp"]
  2. Probe Context7 directly to confirm its capabilities (initializeresult.capabilities):
    {"tools": {"listChanged": true}}
    No prompts / resources keys, and the server returns -32601 for prompts/list, prompts/get, resources/list, resources/read.
  3. After hermes-agent connects to it, the LLM is offered six tool names instead of two: mcp_context7_resolve_library_id, mcp_context7_query_docs, plus mcp_context7_list_prompts, mcp_context7_get_prompt, mcp_context7_list_resources, mcp_context7_read_resource.
  4. When the model invokes any of the four stubs, it gets Method not found and reports the server as dead, even though resolve_library_id / query_docs work fine.

Root cause

tools/mcp_tool.py:_select_utility_schemas (around lines 2614–2641) gates the four utility schemas on:

required_method = _UTILITY_CAPABILITY_METHODS[handler_key]
if not hasattr(server.session, required_method):
    ...
    continue

server.session is an mcp.ClientSession, which always has the four method attributes (list_resources, read_resource, list_prompts, get_prompt) defined on the class — independent of what the remote server actually supports. The check therefore never trips, and all four stubs are always registered.

The InitializeResult returned from await session.initialize() (called at lines 1118, 1219, 1242) is the source of truth — its .capabilities.resources / .capabilities.prompts are non-None only when the server advertises support — but hermes-agent currently discards this object instead of inspecting it.

Expected behavior

Per the MCP spec, capability sub-objects in the initialize response indicate which request types the server actually implements. Utility schemas should be gated on those flags:

Stub schemasRegister only when
list_resources, read_resourceinitialize_result.capabilities.resources is not None
list_prompts, get_promptinitialize_result.capabilities.prompts is not None

Impact

  • Each tools-only MCP server adds 4 dead schemas to the LLM's tool list — context bloat plus wasted tokens.
  • The LLM repeatedly attempts the dead stubs (especially the discovery-style list_* ones) and surfaces "MCP server is broken" reports to the user even when the working tools succeed.
  • Confuses model planning: it thinks the server is half-broken and avoids the two real tools.

Patch

PR will follow this issue. Approach:

  1. Add initialize_result to MCPServerTask.__slots__ and initialise to None in __init__.
  2. In all three await session.initialize() call sites (lines 1118, 1219, 1242), capture the return value into self.initialize_result.
  3. In _select_utility_schemas, inspect server.initialize_result.capabilities.{resources,prompts} and skip the corresponding stubs when they are None. Keep the previous hasattr(server.session, ...) check as a fallback for the rare case where initialize_result is missing (older test fixtures), so behaviour is unchanged for those.

Environment

  • hermes-agent: main (commit at the time of report: 62a5d7207 "feat(plugins): bundle hermes-achievements + scan full session history (#17754)", 2026-04-30)
  • Python 3.13 venv on Debian 13 (ARM64)
  • MCP server tested: @upstash/context7-mcp v2.2.3 (Context7 per serverInfo.name)

extent analysis

TL;DR

The hermes-agent should be modified to only register utility schemas for an MCP server based on the capabilities advertised in the server's initialize response.

Guidance

  • Modify the _select_utility_schemas function to check the initialize_result.capabilities of the MCP server before registering utility schemas.
  • Capture the initialize_result in the MCPServerTask class and store it as an instance variable.
  • Update the await session.initialize() calls to store the result in the initialize_result instance variable.
  • Use the initialize_result.capabilities to determine which utility schemas to register, rather than relying on the presence of methods on the server.session object.

Example

def _select_utility_schemas(self, server):
    if server.initialize_result.capabilities.resources is not None:
        # Register resource-related utility schemas
        pass
    if server.initialize_result.capabilities.prompts is not None:
        # Register prompt-related utility schemas
        pass

Notes

The proposed patch suggests adding a fallback check for the hasattr(server.session, ...) case, which may be necessary for compatibility with older test fixtures.

Recommendation

Apply the proposed patch to modify the hermes-agent to correctly register utility schemas based on the MCP server's advertised capabilities. This should fix the issue of dead schemas being registered and reduce confusion for the model.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Per the MCP spec, capability sub-objects in the initialize response indicate which request types the server actually implements. Utility schemas should be gated on those flags:

Stub schemasRegister only when
list_resources, read_resourceinitialize_result.capabilities.resources is not None
list_prompts, get_promptinitialize_result.capabilities.prompts is not None

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix MCP utility stubs (`list_prompts` / `get_prompt` / `list_resources` / `read_resource`) are registered even when the server doesn't advertise the corresponding capabilities [1 pull requests, 2 comments, 2 participants]