diff --git a/lib/crewai/src/crewai/mcp/tool_resolver.py b/lib/crewai/src/crewai/mcp/tool_resolver.py index cbfd3e40a..cad33f4c1 100644 --- a/lib/crewai/src/crewai/mcp/tool_resolver.py +++ b/lib/crewai/src/crewai/mcp/tool_resolver.py @@ -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: diff --git a/lib/crewai/src/crewai/tools/mcp_native_tool.py b/lib/crewai/src/crewai/tools/mcp_native_tool.py index f039e7444..d2ffae9f5 100644 --- a/lib/crewai/src/crewai/tools/mcp_native_tool.py +++ b/lib/crewai/src/crewai/tools/mcp_native_tool.py @@ -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.""" diff --git a/lib/crewai/tests/mcp/test_amp_mcp.py b/lib/crewai/tests/mcp/test_amp_mcp.py index 5b86a525d..f3c03cd52 100644 --- a/lib/crewai/tests/mcp/test_amp_mcp.py +++ b/lib/crewai/tests/mcp/test_amp_mcp.py @@ -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(