diff --git a/lib/crewai-tools/src/crewai_tools/adapters/mcp_adapter.py b/lib/crewai-tools/src/crewai_tools/adapters/mcp_adapter.py index edfb222a3..e2424aa72 100644 --- a/lib/crewai-tools/src/crewai_tools/adapters/mcp_adapter.py +++ b/lib/crewai-tools/src/crewai_tools/adapters/mcp_adapter.py @@ -2,24 +2,30 @@ from __future__ import annotations +from collections.abc import Callable import logging from typing import TYPE_CHECKING, Any from crewai.tools import BaseTool +from crewai.utilities.pydantic_schema_utils import ( + create_model_from_schema, + generate_model_description, +) +from crewai.utilities.string_utils import sanitize_tool_name from crewai_tools.adapters.tool_collection import ToolCollection -logger = logging.getLogger(__name__) - if TYPE_CHECKING: from mcp import StdioServerParameters - from mcpadapt.core import MCPAdapt - from mcpadapt.crewai_adapter import CrewAIAdapter + from mcp.types import CallToolResult, Tool + from mcpadapt.core import MCPAdapt # type: ignore[import-not-found] + from mcpadapt.crewai_adapter import CrewAIAdapter # type: ignore[import-not-found] try: from mcp import StdioServerParameters + from mcp.types import CallToolResult, Tool from mcpadapt.core import MCPAdapt from mcpadapt.crewai_adapter import CrewAIAdapter @@ -28,15 +34,39 @@ except ImportError: MCP_AVAILABLE = False +logger = logging.getLogger(__name__) + + +class CrewAIToolAdapter(CrewAIAdapter): # type: ignore[misc,no-any-unimported] + """Adapter that normalizes JSON Schema before processing.""" + + def adapt( + self, + func: Callable[[dict[str, Any] | None], CallToolResult], + mcp_tool: Tool, + ) -> BaseTool: + """Adapt a MCP tool to a CrewAI tool. + + Args: + func: The function to call when the tool is invoked. + mcp_tool: The MCP tool definition to adapt. + + Returns: + A CrewAI BaseTool instance. + """ + mcp_tool.name = sanitize_tool_name(mcp_tool.name) + model = create_model_from_schema(mcp_tool.inputSchema) + normalized = generate_model_description(model) + mcp_tool.inputSchema = normalized["json_schema"]["schema"] + return super().adapt(func, mcp_tool) # type: ignore[no-any-return] + + class MCPServerAdapter: """Manages the lifecycle of an MCP server and make its tools available to CrewAI. Note: tools can only be accessed after the server has been started with the `start()` method. - Attributes: - tools: The CrewAI tools available from the MCP server. - Usage: # context manager + stdio with MCPServerAdapter(...) as tools: @@ -89,7 +119,9 @@ class MCPServerAdapter: super().__init__() self._adapter = None self._tools = None - self._tool_names = list(tool_names) if tool_names else None + self._tool_names = ( + [sanitize_tool_name(name) for name in tool_names] if tool_names else None + ) if not MCP_AVAILABLE: import click @@ -112,7 +144,7 @@ class MCPServerAdapter: try: self._serverparams = serverparams self._adapter = MCPAdapt( - self._serverparams, CrewAIAdapter(), connect_timeout + self._serverparams, CrewAIToolAdapter(), connect_timeout ) self.start() @@ -124,13 +156,13 @@ class MCPServerAdapter: logger.error(f"Error during stop cleanup: {stop_e}") raise RuntimeError(f"Failed to initialize MCP Adapter: {e}") from e - def start(self): + def start(self) -> None: """Start the MCP server and initialize the tools.""" - self._tools = self._adapter.__enter__() + self._tools = self._adapter.__enter__() # type: ignore[union-attr] - def stop(self): + def stop(self) -> None: """Stop the MCP server.""" - self._adapter.__exit__(None, None, None) + self._adapter.__exit__(None, None, None) # type: ignore[union-attr] @property def tools(self) -> ToolCollection[BaseTool]: @@ -152,12 +184,19 @@ class MCPServerAdapter: return tools_collection.filter_by_names(self._tool_names) return tools_collection - def __enter__(self): - """Enter the context manager. Note that `__init__()` already starts the MCP server. - So tools should already be available. + def __enter__(self) -> ToolCollection[BaseTool]: + """Enter the context manager. + + Note that `__init__()` already starts the MCP server, + so tools should already be available. """ return self.tools - def __exit__(self, exc_type, exc_value, traceback): + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: Any, + ) -> None: """Exit the context manager.""" - return self._adapter.__exit__(exc_type, exc_value, traceback) + self._adapter.__exit__(exc_type, exc_value, traceback) # type: ignore[union-attr] diff --git a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py index 2b50caea8..a462c8ea2 100644 --- a/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py +++ b/lib/crewai/src/crewai/utilities/pydantic_schema_utils.py @@ -22,6 +22,7 @@ import logging from typing import TYPE_CHECKING, Annotated, Any, Literal, Union import uuid +import jsonref # type: ignore[import-untyped] from pydantic import ( UUID1, UUID3, @@ -400,6 +401,8 @@ def create_model_from_schema( # type: ignore[no-any-unimported] >>> person.name 'John' """ + json_schema = dict(jsonref.replace_refs(json_schema, proxies=False)) + effective_root = root_schema or json_schema json_schema = force_additional_properties_false(json_schema)