feat(mcp): adopt mcp__server__tool naming convention

Port from anomalyco/opencode#33533. Native MCP tools now register as
mcp__<server>__<tool> (double-underscore delimiter) instead of
mcp_<server>_<tool>, aligning with the convention used by Claude Code,
Codex, and OpenCode.

The double-underscore delimiter disambiguates the server/tool boundary
even when either component contains underscores (the single-underscore
form was ambiguous, which is why is_mcp_tool_parallel_safe already had to
track provenance in a side-map). It also unifies native registration with
the Anthropic-OAuth wire form (_MCP_TOOL_PREFIX = 'mcp__'), so the
single->double promotion that path performed is now a no-op for native
tools while still handling legacy replayed names.

- tools/mcp_tool.py: add MCP_TOOL_NAME_PREFIX + mcp_prefixed_tool_name()
  helper; route _convert_mcp_schema, utility schemas, refresh stale-set,
  and the parallel-safe prefix gate through it
- agent/transports/codex_event_projector.py: mirror convention in the
  deterministic call_id input for MCP server-executed tool calls
- tests: update produced-name assertions to the new convention
This commit is contained in:
teknium1 2026-06-25 17:08:44 -07:00 committed by Teknium
parent 5986cdd380
commit e01f58ff1f
5 changed files with 171 additions and 152 deletions

View file

@ -1681,8 +1681,7 @@ class MCPServerTask:
# notifications. Tools absent from the fresh list are no longer
# callable, so remove only those stale registry entries first.
stale_tool_names = old_tool_names - {
f"mcp_{sanitize_mcp_name_component(self.name)}_"
f"{sanitize_mcp_name_component(tool.name)}"
mcp_prefixed_tool_name(self.name, tool.name)
for tool in new_mcp_tools
}
for tool_name in stale_tool_names:
@ -3995,6 +3994,27 @@ def sanitize_mcp_name_component(value: str) -> str:
return re.sub(r"[^A-Za-z0-9_]", "_", str(value or ""))
# Native MCP tool-name prefix. Hermes uses the ``mcp__<server>__<tool>``
# convention shared by Claude Code, Codex, and OpenCode (anomalyco/opencode
# #33533). The double-underscore delimiter disambiguates the server/tool
# boundary even when either component contains underscores, and matches the
# naming models are trained on. It also aligns native registration with the
# Anthropic-OAuth wire form (``_MCP_TOOL_PREFIX`` in anthropic_adapter.py),
# removing the single->double rewrite that path previously had to perform.
MCP_TOOL_NAME_PREFIX = "mcp__"
_MCP_NAME_DELIM = "__"
def mcp_prefixed_tool_name(server_name: str, tool_name: str) -> str:
"""Build the registry/wire name for an MCP tool.
Produces ``mcp__<sanitizedServer>__<sanitizedTool>``.
"""
safe_server = sanitize_mcp_name_component(server_name)
safe_tool = sanitize_mcp_name_component(tool_name)
return f"{MCP_TOOL_NAME_PREFIX}{safe_server}{_MCP_NAME_DELIM}{safe_tool}"
def _convert_mcp_schema(server_name: str, mcp_tool) -> dict:
"""Convert an MCP tool listing to the Hermes registry schema format.
@ -4006,9 +4026,7 @@ def _convert_mcp_schema(server_name: str, mcp_tool) -> dict:
Returns:
A dict suitable for ``registry.register(schema=...)``.
"""
safe_tool_name = sanitize_mcp_name_component(mcp_tool.name)
safe_server_name = sanitize_mcp_name_component(server_name)
prefixed_name = f"mcp_{safe_server_name}_{safe_tool_name}"
prefixed_name = mcp_prefixed_tool_name(server_name, mcp_tool.name)
return {
"name": prefixed_name,
"description": mcp_tool.description or f"MCP tool {mcp_tool.name} from {server_name}",
@ -4022,11 +4040,10 @@ def _build_utility_schemas(server_name: str) -> List[dict]:
Returns a list of (schema, handler_factory_name) tuples encoded as dicts
with keys: schema, handler_key.
"""
safe_name = sanitize_mcp_name_component(server_name)
return [
{
"schema": {
"name": f"mcp_{safe_name}_list_resources",
"name": mcp_prefixed_tool_name(server_name, "list_resources"),
"description": f"List available resources from MCP server '{server_name}'",
"parameters": {
"type": "object",
@ -4037,7 +4054,7 @@ def _build_utility_schemas(server_name: str) -> List[dict]:
},
{
"schema": {
"name": f"mcp_{safe_name}_read_resource",
"name": mcp_prefixed_tool_name(server_name, "read_resource"),
"description": f"Read a resource by URI from MCP server '{server_name}'",
"parameters": {
"type": "object",
@ -4054,7 +4071,7 @@ def _build_utility_schemas(server_name: str) -> List[dict]:
},
{
"schema": {
"name": f"mcp_{safe_name}_list_prompts",
"name": mcp_prefixed_tool_name(server_name, "list_prompts"),
"description": f"List available prompts from MCP server '{server_name}'",
"parameters": {
"type": "object",
@ -4065,7 +4082,7 @@ def _build_utility_schemas(server_name: str) -> List[dict]:
},
{
"schema": {
"name": f"mcp_{safe_name}_get_prompt",
"name": mcp_prefixed_tool_name(server_name, "get_prompt"),
"description": f"Get a prompt by name from MCP server '{server_name}'",
"parameters": {
"type": "object",
@ -4525,15 +4542,15 @@ def discover_mcp_tools() -> List[str]:
def is_mcp_tool_parallel_safe(tool_name: str) -> bool:
"""Check if an MCP tool belongs to a server that supports parallel tool calls.
MCP tool names follow the pattern ``mcp_{server}_{tool}``, but that string
shape is ambiguous when server names contain underscores. Use the exact
server provenance captured at registration time rather than prefix
MCP tool names follow the pattern ``mcp__{server}__{tool}``, but that
string shape is ambiguous when server names contain underscores. Use the
exact server provenance captured at registration time rather than prefix
matching, then check whether that server's config includes
``supports_parallel_tool_calls: true``.
Returns False for non-MCP tools or tools from servers without the flag.
"""
if not tool_name.startswith("mcp_"):
if not tool_name.startswith(MCP_TOOL_NAME_PREFIX):
return False
with _lock:
server_name = _mcp_tool_server_names.get(tool_name)