mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-06 07:29:24 +00:00
fix: ignore extra fields (security_context) in MCP tool schemas
Fixes #4796 CrewAI's tool execution framework injects a security_context parameter into tool arguments via _add_fingerprint_metadata(). MCP tools created via create_model_from_schema() defaulted to ConfigDict(extra='forbid'), causing Pydantic validation to fail with 'Extra inputs are not permitted'. This change passes ConfigDict(extra='ignore') when creating Pydantic models for MCP tool schemas, so that extra fields like security_context are silently dropped during validation instead of raising errors. Changes: - lib/crewai-tools: MCPServerAdapter's CrewAIToolAdapter now creates args models with extra='ignore' - lib/crewai: Native MCP tool resolver also uses extra='ignore' - Added regression tests for both the adapter and schema utils Co-Authored-By: João <joao@crewai.com>
This commit is contained in:
@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
|
||||
from crewai.tools import BaseTool
|
||||
from crewai.utilities.pydantic_schema_utils import create_model_from_schema
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from crewai_tools.adapters.tool_collection import ToolCollection
|
||||
|
||||
@@ -51,7 +51,10 @@ try:
|
||||
"""
|
||||
tool_name = sanitize_tool_name(mcp_tool.name)
|
||||
tool_description = mcp_tool.description or ""
|
||||
args_model = create_model_from_schema(mcp_tool.inputSchema)
|
||||
args_model = create_model_from_schema(
|
||||
mcp_tool.inputSchema,
|
||||
__config__=ConfigDict(extra="ignore"),
|
||||
)
|
||||
|
||||
class CrewAIMCPTool(BaseTool):
|
||||
name: str = tool_name
|
||||
|
||||
@@ -221,6 +221,74 @@ def test_connect_timeout_with_filtered_tools(echo_server_script):
|
||||
assert tools[0].run(text="timeout test") == "Echo: timeout test"
|
||||
|
||||
|
||||
def test_mcp_tool_ignores_security_context(echo_server_script):
|
||||
"""Test that MCP tools ignore extra fields like security_context.
|
||||
|
||||
This is a regression test for https://github.com/crewAIInc/crewAI/issues/4796.
|
||||
CrewAI's tool execution framework injects a `security_context` parameter
|
||||
(containing agent_fingerprint and metadata) into tool calls. MCP tools must
|
||||
ignore this extra field instead of raising a Pydantic validation error.
|
||||
"""
|
||||
serverparams = StdioServerParameters(
|
||||
command="uv", args=["run", "python", "-c", echo_server_script]
|
||||
)
|
||||
with MCPServerAdapter(serverparams) as tools:
|
||||
echo_tool = tools[0]
|
||||
# Simulate what CrewAI's tool_usage._add_fingerprint_metadata does:
|
||||
# it adds security_context to the tool arguments before invocation.
|
||||
result = echo_tool.run(
|
||||
text="hello",
|
||||
security_context={
|
||||
"agent_fingerprint": {
|
||||
"uuid_str": "test-uuid-12345",
|
||||
"created_at": "2026-01-01T00:00:00",
|
||||
},
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
assert result == "Echo: hello"
|
||||
|
||||
|
||||
def test_mcp_tool_ignores_security_context_calc(echo_server_script):
|
||||
"""Test that security_context is ignored for tools with multiple args."""
|
||||
serverparams = StdioServerParameters(
|
||||
command="uv", args=["run", "python", "-c", echo_server_script]
|
||||
)
|
||||
with MCPServerAdapter(serverparams) as tools:
|
||||
calc_tool = tools[1]
|
||||
result = calc_tool.run(
|
||||
a=10,
|
||||
b=20,
|
||||
security_context={
|
||||
"agent_fingerprint": {
|
||||
"uuid_str": "test-uuid-67890",
|
||||
"created_at": "2026-01-01T00:00:00",
|
||||
},
|
||||
"task_fingerprint": {
|
||||
"uuid_str": "task-uuid-11111",
|
||||
"created_at": "2026-01-01T00:00:00",
|
||||
},
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
assert result == "30"
|
||||
|
||||
|
||||
def test_mcp_tool_args_schema_allows_extra_fields(echo_server_script):
|
||||
"""Test that the args_schema on MCP tools is configured to ignore extra fields."""
|
||||
serverparams = StdioServerParameters(
|
||||
command="uv", args=["run", "python", "-c", echo_server_script]
|
||||
)
|
||||
with MCPServerAdapter(serverparams) as tools:
|
||||
echo_tool = tools[0]
|
||||
# Validate that the schema accepts and ignores extra fields
|
||||
schema = echo_tool.args_schema
|
||||
validated = schema.model_validate(
|
||||
{"text": "test", "security_context": {"agent_fingerprint": "abc"}}
|
||||
)
|
||||
assert validated.model_dump() == {"text": "test"}
|
||||
|
||||
|
||||
@patch("crewai_tools.adapters.mcp_adapter.MCPAdapt")
|
||||
def test_connect_timeout_passed_to_mcpadapt(mock_mcpadapt):
|
||||
mock_adapter_instance = MagicMock()
|
||||
|
||||
@@ -582,6 +582,8 @@ class MCPToolResolver:
|
||||
@staticmethod
|
||||
def _json_schema_to_pydantic(tool_name: str, json_schema: dict[str, Any]) -> type:
|
||||
"""Convert JSON Schema to a Pydantic model for tool arguments."""
|
||||
from pydantic import ConfigDict
|
||||
|
||||
from crewai.utilities.pydantic_schema_utils import create_model_from_schema
|
||||
|
||||
model_name = f"{tool_name.replace('-', '_').replace(' ', '_')}Schema"
|
||||
@@ -589,4 +591,5 @@ class MCPToolResolver:
|
||||
json_schema,
|
||||
model_name=model_name,
|
||||
enrich_descriptions=True,
|
||||
__config__=ConfigDict(extra="ignore"),
|
||||
)
|
||||
|
||||
@@ -882,3 +882,86 @@ class TestEndToEndMCPSchema:
|
||||
)
|
||||
assert obj.filters.date_from == datetime.date(2025, 1, 1)
|
||||
assert obj.filters.categories == ["news", "tech"]
|
||||
|
||||
|
||||
class TestExtraFieldsConfig:
|
||||
"""Tests for extra fields handling with ConfigDict.
|
||||
|
||||
Regression tests for https://github.com/crewAIInc/crewAI/issues/4796.
|
||||
MCP tools need to ignore extra fields like security_context injected
|
||||
by CrewAI's tool execution framework.
|
||||
"""
|
||||
|
||||
SIMPLE_SCHEMA: dict = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string"},
|
||||
"top_k": {
|
||||
"anyOf": [{"type": "integer"}, {"type": "null"}],
|
||||
"default": None,
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
}
|
||||
|
||||
def test_default_config_forbids_extra(self) -> None:
|
||||
"""By default, create_model_from_schema forbids extra fields."""
|
||||
Model = create_model_from_schema(self.SIMPLE_SCHEMA)
|
||||
with pytest.raises(Exception):
|
||||
Model(query="test", security_context={"agent_fingerprint": "abc"})
|
||||
|
||||
def test_ignore_config_allows_extra(self) -> None:
|
||||
"""With extra='ignore', extra fields are silently dropped."""
|
||||
from pydantic import ConfigDict
|
||||
|
||||
Model = create_model_from_schema(
|
||||
self.SIMPLE_SCHEMA,
|
||||
__config__=ConfigDict(extra="ignore"),
|
||||
)
|
||||
obj = Model(
|
||||
query="test",
|
||||
security_context={
|
||||
"agent_fingerprint": {
|
||||
"uuid_str": "test-uuid",
|
||||
"created_at": "2026-01-01T00:00:00",
|
||||
},
|
||||
"metadata": {},
|
||||
},
|
||||
)
|
||||
assert obj.query == "test"
|
||||
# security_context should not appear in the dumped model
|
||||
assert "security_context" not in obj.model_dump()
|
||||
|
||||
def test_ignore_config_preserves_validation(self) -> None:
|
||||
"""Extra='ignore' still validates declared fields correctly."""
|
||||
from pydantic import ConfigDict
|
||||
|
||||
Model = create_model_from_schema(
|
||||
self.SIMPLE_SCHEMA,
|
||||
__config__=ConfigDict(extra="ignore"),
|
||||
)
|
||||
# Valid input works
|
||||
obj = Model(query="test", top_k=5)
|
||||
assert obj.query == "test"
|
||||
assert obj.top_k == 5
|
||||
|
||||
# Missing required field still fails
|
||||
with pytest.raises(Exception):
|
||||
Model(top_k=5)
|
||||
|
||||
def test_ignore_config_with_mcp_schema(self) -> None:
|
||||
"""Extra='ignore' works with complex MCP-like schemas."""
|
||||
from pydantic import ConfigDict
|
||||
|
||||
Model = create_model_from_schema(
|
||||
TestEndToEndMCPSchema.MCP_SCHEMA,
|
||||
__config__=ConfigDict(extra="ignore"),
|
||||
)
|
||||
obj = Model(
|
||||
query="search term",
|
||||
format="json",
|
||||
filters={"date_from": "2025-01-01"},
|
||||
security_context={"agent_fingerprint": "test"},
|
||||
)
|
||||
assert obj.query == "search term"
|
||||
assert "security_context" not in obj.model_dump()
|
||||
|
||||
Reference in New Issue
Block a user