fix: limit long MCP-derived tool names
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"""MCP client: connects to MCP servers and wraps their tools as native nanobot tools."""
|
"""MCP client: connects to MCP servers and wraps their tools as native nanobot tools."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -122,6 +123,25 @@ def _sanitize_name(name: str) -> str:
|
|||||||
return _SANITIZE_RE.sub("_", re.sub(r"[^a-zA-Z0-9_-]", "_", name))
|
return _SANITIZE_RE.sub("_", re.sub(r"[^a-zA-Z0-9_-]", "_", name))
|
||||||
|
|
||||||
|
|
||||||
|
_MAX_TOOL_NAME_LENGTH = 64
|
||||||
|
_HASH_LENGTH = 8
|
||||||
|
|
||||||
|
|
||||||
|
def _limit_tool_name(name: str, max_length: int = _MAX_TOOL_NAME_LENGTH) -> str:
|
||||||
|
"""Limit a tool name while keeping short names unchanged."""
|
||||||
|
if len(name) <= max_length:
|
||||||
|
return name
|
||||||
|
|
||||||
|
digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:_HASH_LENGTH]
|
||||||
|
prefix_length = max_length - _HASH_LENGTH - 1
|
||||||
|
return f"{name[:prefix_length]}_{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
def _sanitize_mcp_tool_name(name: str) -> str:
|
||||||
|
"""Sanitize and limit an MCP-derived tool name."""
|
||||||
|
return _limit_tool_name(_sanitize_name(name))
|
||||||
|
|
||||||
|
|
||||||
def _is_transient(exc: BaseException) -> bool:
|
def _is_transient(exc: BaseException) -> bool:
|
||||||
"""Check if an exception looks like a transient connection error."""
|
"""Check if an exception looks like a transient connection error."""
|
||||||
return type(exc).__name__ in _TRANSIENT_EXC_NAMES
|
return type(exc).__name__ in _TRANSIENT_EXC_NAMES
|
||||||
@@ -389,7 +409,7 @@ class MCPToolWrapper(_MCPWrapperBase):
|
|||||||
def __init__(self, session, server_name: str, tool_def, tool_timeout: int = 30):
|
def __init__(self, session, server_name: str, tool_def, tool_timeout: int = 30):
|
||||||
self._set_mcp_connection(session, server_name)
|
self._set_mcp_connection(session, server_name)
|
||||||
self._original_name = tool_def.name
|
self._original_name = tool_def.name
|
||||||
self._name = _sanitize_name(f"mcp_{server_name}_{tool_def.name}")
|
self._name = _sanitize_mcp_tool_name(f"mcp_{server_name}_{tool_def.name}")
|
||||||
self._description = tool_def.description or tool_def.name
|
self._description = tool_def.description or tool_def.name
|
||||||
raw_schema = tool_def.inputSchema or {"type": "object", "properties": {}}
|
raw_schema = tool_def.inputSchema or {"type": "object", "properties": {}}
|
||||||
self._parameters = _normalize_schema_for_openai(raw_schema)
|
self._parameters = _normalize_schema_for_openai(raw_schema)
|
||||||
@@ -548,7 +568,7 @@ class MCPResourceWrapper(_MCPWrapperBase):
|
|||||||
def __init__(self, session, server_name: str, resource_def, resource_timeout: int = 30):
|
def __init__(self, session, server_name: str, resource_def, resource_timeout: int = 30):
|
||||||
self._set_mcp_connection(session, server_name)
|
self._set_mcp_connection(session, server_name)
|
||||||
self._uri = resource_def.uri
|
self._uri = resource_def.uri
|
||||||
self._name = _sanitize_name(f"mcp_{server_name}_resource_{resource_def.name}")
|
self._name = _sanitize_mcp_tool_name(f"mcp_{server_name}_resource_{resource_def.name}")
|
||||||
desc = resource_def.description or resource_def.name
|
desc = resource_def.description or resource_def.name
|
||||||
self._description = f"[MCP Resource] {desc}\nURI: {self._uri}"
|
self._description = f"[MCP Resource] {desc}\nURI: {self._uri}"
|
||||||
self._parameters: dict[str, Any] = {
|
self._parameters: dict[str, Any] = {
|
||||||
@@ -649,7 +669,7 @@ class MCPPromptWrapper(_MCPWrapperBase):
|
|||||||
def __init__(self, session, server_name: str, prompt_def, prompt_timeout: int = 30):
|
def __init__(self, session, server_name: str, prompt_def, prompt_timeout: int = 30):
|
||||||
self._set_mcp_connection(session, server_name)
|
self._set_mcp_connection(session, server_name)
|
||||||
self._prompt_name = prompt_def.name
|
self._prompt_name = prompt_def.name
|
||||||
self._name = _sanitize_name(f"mcp_{server_name}_prompt_{prompt_def.name}")
|
self._name = _sanitize_mcp_tool_name(f"mcp_{server_name}_prompt_{prompt_def.name}")
|
||||||
desc = prompt_def.description or prompt_def.name
|
desc = prompt_def.description or prompt_def.name
|
||||||
self._description = (
|
self._description = (
|
||||||
f"[MCP Prompt] {desc}\n"
|
f"[MCP Prompt] {desc}\n"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from nanobot.agent.tools.mcp import (
|
|||||||
MCPResourceWrapper,
|
MCPResourceWrapper,
|
||||||
MCPToolWrapper,
|
MCPToolWrapper,
|
||||||
_normalize_windows_stdio_command,
|
_normalize_windows_stdio_command,
|
||||||
|
_sanitize_mcp_tool_name,
|
||||||
_sanitize_name,
|
_sanitize_name,
|
||||||
connect_mcp_servers,
|
connect_mcp_servers,
|
||||||
)
|
)
|
||||||
@@ -1319,3 +1320,15 @@ async def test_connect_mcp_servers_enabled_tools_matches_sanitized_name(
|
|||||||
)
|
)
|
||||||
def test_redact_url_strips_credentials_and_query(url: str, expected: str) -> None:
|
def test_redact_url_strips_credentials_and_query(url: str, expected: str) -> None:
|
||||||
assert mcp_mod._redact_url(url) == expected
|
assert mcp_mod._redact_url(url) == expected
|
||||||
|
|
||||||
|
def test_mcp_tool_name_keeps_short_name():
|
||||||
|
name = _sanitize_mcp_tool_name("mcp_myserver_resource_myres")
|
||||||
|
assert name == "mcp_myserver_resource_myres"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mcp_tool_name_limits_long_name():
|
||||||
|
long_name = "mcp_" + "a" * 100
|
||||||
|
name = _sanitize_mcp_tool_name(long_name)
|
||||||
|
|
||||||
|
assert len(name) <= 64
|
||||||
|
assert name.startswith("mcp_")
|
||||||
|
|||||||
Reference in New Issue
Block a user