fix(mcp): capability-gate tools/list so prompt-only MCP servers can connect (port opencode#31271)#44550
Merged
Conversation
…onnect Port from anomalyco/opencode#31271: only call tools/list when the server advertises the 'tools' capability in InitializeResult.capabilities. Previously, _discover_tools() unconditionally called session.list_tools() right after initialize. Prompt-only / resource-only servers (which omit the tools capability per the MCP spec) raise McpError(-32601 Method not found), which aborted the connection — burning all 3 initial-connect retries and permanently failing the server even though its prompts and resources were perfectly usable. The 180s keepalive had the same problem: it probed with list_tools(), so even a successfully connected prompt-only server would be torn down on the first keepalive cycle. Changes: - MCPServerTask._advertises_tools(): capability check with a legacy fallback (no captured InitializeResult -> behave as before) - _discover_tools(): skip tools/list for non-tool servers - keepalive: use the universal ping request for non-tool servers - _refresh_tools(): guard against tools/list_changed from non-tool servers E2E verified with a real stdio prompt-only FastMCP-style server: on main it fails all 3 connection attempts with Method-not-found; with this fix it connects, lists prompts, answers ping keepalives, and shuts down cleanly.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
1 |
invalid-assignment |
1 |
First entries
tests/tools/test_mcp_capability_gating.py:16: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/tools/test_mcp_capability_gating.py:126: [invalid-assignment] invalid-assignment: Object of type `def fake_wait(tasks, timeout=None, return_when=None) -> CoroutineType[Any, Any, Unknown]` is not assignable to attribute `wait` of type `def wait[_FT](fs: Iterable[_FT], *, timeout: int | float | None = None, return_when: str = "ALL_COMPLETED") -> CoroutineType[Any, Any, tuple[set[_FT], set[_FT]]]`
✅ Fixed issues: none
Unchanged: 5637 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
Contributor
|
Verified: MCP capability-gating is clean and correct. Checked:
No issues found. LGTM. |
teddyjfpender
added a commit
to teddyjfpender/superforecasting-agent
that referenced
this pull request
Jun 13, 2026
…onnect (#5affecb44) Only call tools/list when the server advertises the 'tools' capability in InitializeResult.capabilities. Previously _discover_tools() unconditionally called session.list_tools() right after initialize; prompt-only / resource-only servers (which omit the tools capability per the MCP spec) raise McpError(-32601 Method not found), aborting the connection and burning all initial-connect retries. The 180s keepalive had the same problem — it probed with list_tools(), so even a successfully connected prompt-only server was torn down on the first keepalive cycle. - MCPServerTask._advertises_tools(): capability check with a legacy fallback (no captured InitializeResult -> behave as before) - _discover_tools(): skip tools/list for non-tool servers - keepalive: use the universal ping request for non-tool servers - _refresh_tools(): guard against tools/list_changed from non-tool servers Upstream test file brought verbatim (tests/tools/test_mcp_capability_gating.py). Ported from upstream NousResearch/hermes-agent (NousResearch#44550, originally anomalyco/opencode#31271). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Prompt-only and resource-only MCP servers can now connect and stay connected — previously they failed permanently because Hermes unconditionally called
tools/listduring discovery and keepalive, which raisesMcpError(-32601 Method not found)on servers that don't implement thetools/*family.Port of anomalyco/opencode#31271 ("respect MCP server capabilities"), adapted to Hermes's
MCPServerTasklifecycle.Root cause
Per the MCP spec,
InitializeResult.capabilities.toolsis non-None iff the server implementstools/*. Hermes already used this gate for the prompts/resources utility tool registration (_select_utility_schemas, #18051) but never for thetools/listcalls themselves:_discover_tools()calledsession.list_tools()unconditionally right afterinitialize()→ the -32601 error aborted the connection, burning all 3 initial-connect retries → server permanently dead.list_tools()→ even a connected prompt-only server got torn down on the first keepalive cycle.Changes
tools/mcp_tool.py: newMCPServerTask._advertises_tools()(capability check, legacy fallback to old behavior when noInitializeResultwas captured);_discover_tools()skipstools/listfor non-tool servers; keepalive uses the universalpingrequest instead for them;_refresh_tools()guards against spurioustools/list_changed.tests/tools/test_mcp_capability_gating.py: 10 tests covering the capability check, discovery gating, refresh gating, and both keepalive probe paths.Validation
_errorsetlist_promptsreturns prompts,pingkeepalive OK, clean shutdownlist_toolsstill used for discovery + keepalive)tests/tools/test_mcp_tool.py+test_mcp_sse_transport.py+test_mcp_reconnect_signal.pyE2E used a real
mcp.server.Serverexposing onlylist_promptsover stdio, driven throughMCPServerTask.run()with an isolatedHERMES_HOME.Architectural notes vs the source PR
OpenCode gates via the TS SDK's
client.getServerCapabilities(); Hermes capturesInitializeResultonMCPServerTask.initialize_result(already present since #18051) and readscapabilities.toolsfrom it. OpenCode returns[]tools for non-tool servers; we do the same and additionally swap the keepalive toping(OpenCode has no equivalent keepalive loop). Their warn-not-error log change maps to the existing -32601 handling discussion in #10292 and is out of scope here.Infographic