Skip to content

Commit 04379e0

Browse files
Fix MCP tool naming for LLM provider compatibility (#3)
This PR updates tool naming in nexus-mcp from service/operation to service_operation to comply with LLM provider requirements (^[a-zA-Z0-9_-]{1,64}$ for Claude Desktop and ^[a-zA-Z0-9_-]{1,128}$ for others). It adds validation, clearer errors, and logging, ensuring that generated tool names are always valid. Parsing logic has been updated to support underscore-separated names while maintaining compatibility with existing Nexus usage. This guarantees full compatibility with OpenAI, Claude/Anthropic, and Groq function calling. --------- Co-authored-by: Roey Berman <[email protected]>
1 parent 50477fd commit 04379e0

File tree

3 files changed

+73
-11
lines changed

3 files changed

+73
-11
lines changed

nexusmcp/inbound_gateway.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,24 +78,27 @@ async def _handle_call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
7878
Handle MCP tool call request by calling the corresponding Nexus Operation in the given endpoint.
7979
8080
Args:
81-
name: Tool name in the format "service/operation"
81+
name: Tool name in the format "service_operation" (LLM-compatible format)
8282
arguments: Dictionary of arguments to pass to the tool
8383
8484
Returns:
8585
Result of the tool call (type depends on the specific tool)
8686
8787
Raises:
88-
ValueError: If the tool name format is invalid (missing '/')
88+
ValueError: If the tool name format is invalid (missing '_' separator)
8989
9090
Example:
9191
result = await gateway._handle_call_tool(
92-
"calculator/add",
92+
"calculator_add",
9393
{"a": 5, "b": 3}
9494
)
9595
"""
96-
service, _, operation = name.partition("/")
96+
# Parse tool name in LLM-compatible format: "service_operation"
97+
service, _, operation = name.partition("_")
98+
9799
if not service or not operation:
98-
raise ValueError(f"Invalid tool name: {name}, must be in the format 'service/operation'")
100+
raise ValueError(f"Invalid tool name: {name}, must be in the format 'service_operation'")
101+
99102
return await self._client.execute_workflow(
100103
ToolCallWorkflow.run,
101104
arg=ToolCallInput(

nexusmcp/service_handler.py

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@
1212
from collections.abc import Callable
1313
from dataclasses import dataclass
1414
from typing import Any
15+
import re
16+
import logging
1517

1618
import mcp.types
1719
import nexusrpc
1820
import pydantic
1921

2022
from .service import MCPService
2123

24+
# Set up logging
25+
logger = logging.getLogger(__name__)
26+
27+
# Compile regex patterns ahead of time for better performance
28+
_TOOL_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9_-]{1,64}$")
29+
_SERVICE_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9-]{1,64}$")
30+
2231

2332
@dataclass
2433
class _Tool:
@@ -50,8 +59,18 @@ def to_mcp_tool(self, service: nexusrpc.ServiceDefinition) -> mcp.types.Tool:
5059
Returns:
5160
An MCP Tool object ready for use by MCP clients
5261
"""
62+
# Generate LLM-compatible tool name using underscores instead of forward slashes
63+
tool_name = f"{service.name}_{self.defn.name}"
64+
65+
# Validate tool name meets LLM provider requirements
66+
if not _is_valid_tool_name(tool_name):
67+
raise ValueError(
68+
f"Generated tool name '{tool_name}' does not meet LLM provider requirements. "
69+
f"Tool names must match pattern ^[a-zA-Z0-9_-]{{1,64}}$ for Claude Desktop or ^[a-zA-Z0-9_-]{{1,128}}$ for other clients."
70+
)
71+
5372
return mcp.types.Tool(
54-
name=f"{service.name}/{self.defn.name}",
73+
name=tool_name,
5574
description=(self.func.__doc__.strip() if self.func.__doc__ is not None else None),
5675
inputSchema=(
5776
self.defn.input_type.model_json_schema()
@@ -119,9 +138,14 @@ def register(self, cls: type) -> type:
119138
service_defn = nexusrpc.get_service_definition(cls)
120139
if service_defn is None:
121140
raise ValueError(f"Class {cls.__name__} is not a Nexus Service")
122-
if "/" in service_defn.name:
141+
# Validate service name contains only characters that will create valid tool names
142+
if not _is_valid_service_name(service_defn.name):
143+
invalid_chars = set(service_defn.name) - set(
144+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-"
145+
)
123146
raise ValueError(
124-
f"Service name {service_defn.name} cannot contain '/' as it is used to separate service and operation names in MCP tools"
147+
f"Service name '{service_defn.name}' contains invalid characters {invalid_chars}. "
148+
f"Only alphanumeric characters and hyphens are allowed for LLM provider compatibility."
125149
)
126150

127151
tools: list[_Tool] = []
@@ -153,7 +177,10 @@ async def list_tools(self, _ctx: nexusrpc.handler.StartOperationContext, _input:
153177
Returns:
154178
List of MCP Tool objects representing all available Operations
155179
"""
156-
return [tool.to_mcp_tool(service.defn) for service in self._tool_services for tool in service.tools]
180+
tools = [tool.to_mcp_tool(service.defn) for service in self._tool_services for tool in service.tools]
181+
if logger.isEnabledFor(logging.DEBUG):
182+
logger.debug(f"MCP.list_tools found {len(tools)} tools: {[tool.name for tool in tools]}")
183+
return tools
157184

158185

159186
ExcludedCallable = Callable[..., Any]
@@ -191,3 +218,35 @@ def internal_operation(self, _ctx: nexusrpc.handler.StartOperationContext, _inpu
191218
"""
192219
setattr(fn, "__nexus_mcp_tool__", False)
193220
return fn
221+
222+
223+
def _is_valid_tool_name(name: str) -> bool:
224+
"""Validate tool name against LLM provider requirements.
225+
226+
Tool names must match the pattern ^[a-zA-Z0-9_-]{1,64}$ for Claude Desktop
227+
or ^[a-zA-Z0-9_-]{1,128}$ for other clients (Goose, OpenAI, etc.).
228+
Validates against the most restrictive (64 chars) for maximum compatibility.
229+
230+
Args:
231+
name: The tool name to validate
232+
233+
Returns:
234+
True if the name is valid, False otherwise
235+
"""
236+
return bool(_TOOL_NAME_PATTERN.match(name))
237+
238+
239+
def _is_valid_service_name(name: str) -> bool:
240+
"""Validate service name for tool naming compatibility.
241+
242+
Service names cannot contain underscores as they are used to
243+
split service names from operation names when creating tool names.
244+
245+
Args:
246+
name: The service name to validate
247+
248+
Returns:
249+
True if the service name is valid, False otherwise
250+
"""
251+
# Service names cannot contain underscores (used as delimiter)
252+
return bool(_SERVICE_NAME_PATTERN.match(name))

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)