mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-22 19:06:25 +00:00
feat(mcp): carry the AMP slug on tools resolved from a slug reference (#7029)
An MCP tool's name is derived from the server URL, so nothing on the resolved tool records which reference it was requested by. `MCPNativeTool` now keeps that reference as `server_reference` when `_resolve_amp` built it, leaving servers requested by URL untouched. Hooks can then attribute a tool call to the server the user actually selected.
This commit is contained in:
@@ -150,7 +150,9 @@ class MCPToolResolver:
|
||||
mcp_server_config = self._build_mcp_config_from_dict(config_dict)
|
||||
|
||||
try:
|
||||
tools, clients = self._resolve_native(mcp_server_config)
|
||||
tools, clients = self._resolve_native(
|
||||
mcp_server_config, server_reference=slug
|
||||
)
|
||||
resolved_cache[slug] = (tools, clients)
|
||||
all_clients.extend(clients)
|
||||
except Exception as e:
|
||||
@@ -311,7 +313,7 @@ class MCPToolResolver:
|
||||
return transport, server_name
|
||||
|
||||
def _resolve_native(
|
||||
self, mcp_config: MCPServerConfig
|
||||
self, mcp_config: MCPServerConfig, server_reference: str | None = None
|
||||
) -> tuple[list[BaseTool], list[Any]]:
|
||||
"""Resolve an ``MCPServerConfig`` into tools.
|
||||
|
||||
@@ -320,6 +322,10 @@ class MCPToolResolver:
|
||||
A ``client_factory`` closure is passed to each ``MCPNativeTool`` so
|
||||
every call -- even concurrent calls to the *same* tool -- gets its
|
||||
own ``MCPClient`` + transport with no shared mutable state.
|
||||
|
||||
*server_reference* is the AMP slug the server was requested by, when
|
||||
there is one; the tool name is derived from the URL and cannot be
|
||||
traced back to it.
|
||||
"""
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.mcp_native_tool import MCPNativeTool
|
||||
@@ -449,6 +455,7 @@ class MCPToolResolver:
|
||||
tool_schema=tool_schema,
|
||||
server_name=server_name,
|
||||
original_tool_name=original_tool_name,
|
||||
server_reference=server_reference,
|
||||
)
|
||||
tools.append(native_tool)
|
||||
except Exception as e:
|
||||
|
||||
@@ -31,6 +31,7 @@ class MCPNativeTool(BaseTool):
|
||||
tool_schema: dict[str, Any],
|
||||
server_name: str,
|
||||
original_tool_name: str | None = None,
|
||||
server_reference: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize native MCP tool.
|
||||
|
||||
@@ -40,6 +41,8 @@ class MCPNativeTool(BaseTool):
|
||||
tool_schema: Schema information for the tool.
|
||||
server_name: Name of the MCP server for prefixing.
|
||||
original_tool_name: Original name of the tool on the MCP server.
|
||||
server_reference: Identifier the server was requested by, when it
|
||||
was not requested by URL -- currently the AMP slug.
|
||||
"""
|
||||
prefixed_name = f"{server_name}_{tool_name}"
|
||||
|
||||
@@ -60,12 +63,18 @@ class MCPNativeTool(BaseTool):
|
||||
self._client_factory = client_factory
|
||||
self._original_tool_name = original_tool_name or tool_name
|
||||
self._server_name = server_name
|
||||
self._server_reference = server_reference
|
||||
|
||||
@property
|
||||
def original_tool_name(self) -> str:
|
||||
"""Get the original tool name."""
|
||||
return self._original_tool_name
|
||||
|
||||
@property
|
||||
def server_reference(self) -> str | None:
|
||||
"""Get the identifier the server was requested by, if any."""
|
||||
return self._server_reference
|
||||
|
||||
@property
|
||||
def server_name(self) -> str:
|
||||
"""Get the server name."""
|
||||
|
||||
@@ -268,6 +268,51 @@ class TestGetMCPToolsAmpIntegration:
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "mcp_notion_so_sse_search"
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
@patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
|
||||
def test_tools_carry_the_slug_they_were_requested_by(
|
||||
self, mock_fetch, mock_client_class, agent, mock_tool_definitions
|
||||
):
|
||||
mock_fetch.return_value = {
|
||||
"notion": {
|
||||
"type": "sse",
|
||||
"url": "https://mcp.notion.so/sse",
|
||||
"headers": {"Authorization": "Bearer token"},
|
||||
},
|
||||
}
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
tools = agent.get_mcp_tools(["notion"])
|
||||
|
||||
# The name is derived from the URL, so the slug is only recoverable here.
|
||||
assert {tool.name for tool in tools} == {
|
||||
"mcp_notion_so_sse_search",
|
||||
"mcp_notion_so_sse_create_page",
|
||||
}
|
||||
assert all(tool.server_reference == "notion" for tool in tools)
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_tools_from_a_url_have_no_slug(
|
||||
self, mock_client_class, agent, mock_tool_definitions
|
||||
):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
tools = agent.get_mcp_tools([MCPServerSSE(url="https://mcp.notion.so/sse")])
|
||||
|
||||
assert tools
|
||||
assert all(tool.server_reference is None for tool in tools)
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
@patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
|
||||
def test_tool_filter_with_hyphenated_hash_syntax(
|
||||
|
||||
Reference in New Issue
Block a user