Skip to content

feat(acp): register client-provided MCP servers as agent tools#4496

Closed
Jckwind wants to merge 3 commits into
NousResearch:mainfrom
TheProxyCompany:feat/acp-mcp-server-registration
Closed

feat(acp): register client-provided MCP servers as agent tools#4496
Jckwind wants to merge 3 commits into
NousResearch:mainfrom
TheProxyCompany:feat/acp-mcp-server-registration

Conversation

@Jckwind

@Jckwind Jckwind commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Summary

ACP clients pass MCP server definitions (stdio and HTTP/SSE) in session/new, load_session, resume_session, and fork_session. Currently these are accepted but silently dropped — the agent never connects to them, so client-provided tools are unavailable.

This wires the mcp_servers parameter into the existing MCP registration pipeline so client-provided servers are connected, their tools discovered, and the agent's tool surface refreshed before the first prompt.

Partial progress on #569 (ACP server mode).

How it works

  1. ACP client sends mcpServers in session/new (typed as McpServerStdio / McpServerHttp / McpServerSse)
  2. _register_session_mcp_servers converts them to Hermes MCP config dicts and calls register_mcp_servers() via asyncio.to_thread() (avoids blocking the ACP event loop)
  3. The existing _discover_and_register_server pipeline connects, discovers tools, and injects them into hermes-* umbrella toolsets
  4. Agent's tools, valid_tool_names, and cached system prompt are refreshed so the new tools are immediately available

Idempotent — already-connected servers (by name) are skipped.

Bug fix: tool name sanitization

_convert_mcp_schema, _sync_mcp_toolsets, and _build_utility_schemas each independently sanitized - and . but missed / and other characters. Server names like ai.exa/exa produce tool names containing /, which crashes Anthropic's API (tools.N.custom.name: String should match pattern '^[a-zA-Z0-9_-]{1,128}$').

Extracted sanitize_mcp_name_component() that replaces all non-[A-Za-z0-9_] characters, used in all three locations. Raw server names are preserved as registry keys — sanitization only applies when generating tool name prefixes.

Bug fix: tool results missing from ACP events

The step_callback previously forwarded only tool names (as strings) to build_tool_complete, so ACP tool_call_update events had empty content and rawOutput. Now prev_tools carries dicts with both name and result by pairing each tool_call with its matching tool-role message via tool_call_id.

Changes

File What
tools/mcp_tool.py sanitize_mcp_name_component(), register_mcp_servers() public API
acp_adapter/server.py _register_session_mcp_servers(), wired to all session methods
run_agent.py step_callback now includes tool results

Test results

$ python -m pytest tests/tools/test_mcp_tool.py tests/acp/test_server.py -q
182 passed, 1 warning

Tested on macOS (Apple Silicon), Python 3.11.

Jckwind added 3 commits April 1, 2026 16:35
ACP clients pass MCP server definitions in session/new, load_session,
resume_session, and fork_session. Previously these were accepted but
silently ignored — the agent never connected to them.

This wires the mcp_servers parameter into the existing MCP registration
pipeline (tools/mcp_tool.py) so client-provided servers are connected,
their tools discovered, and the agent's tool surface refreshed before
the first prompt.

Changes:

tools/mcp_tool.py:
- Extract sanitize_mcp_name_component() to replace all non-[A-Za-z0-9_]
  characters (fixes crash when server names contain / or other chars
  that violate provider tool-name validation rules)
- Use it in _convert_mcp_schema, _sync_mcp_toolsets, _build_utility_schemas
- Extract register_mcp_servers(servers: dict) as a public API that takes
  an explicit {name: config} map. discover_mcp_tools() becomes a thin
  wrapper that loads config.yaml and calls register_mcp_servers()

acp_adapter/server.py:
- Add _register_session_mcp_servers() which converts ACP McpServerStdio /
  McpServerHttp / McpServerSse objects to Hermes MCP config dicts,
  registers them via asyncio.to_thread (avoids blocking the ACP event
  loop), then rebuilds agent.tools, valid_tool_names, and invalidates
  the cached system prompt
- Call it from new_session, load_session, resume_session, fork_session

Tested with Eden (theproxycompany.com) as ACP client — 5 MCP servers
(HTTP + stdio) registered successfully, 110 tools available to the agent.
…ate events

The step_callback previously only forwarded tool names as strings,
so build_tool_complete received result=None and ACP tool_call_update
events had empty content/rawOutput. Now prev_tools carries dicts with
both name and result by pairing each tool_call with its matching
tool-role message via tool_call_id.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant