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:
Lucas Gomide
2026-08-18 16:15:27 -03:00
committed by GitHub
parent 6388421510
commit 2ebb9390fe
3 changed files with 63 additions and 2 deletions

View File

@@ -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:

View File

@@ -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."""

View File

@@ -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(