fix: resolve complex schema $ref pointers in mcp tools

This commit is contained in:
Greyson LaLonde
2026-02-03 12:48:06 -05:00
parent d3f424fd8f
commit 608b860484
2 changed files with 60 additions and 18 deletions

View File

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

View File

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