Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot]
959534d506 chore: update tool specifications 2026-03-10 10:52:27 +00:00
Devin AI
3148a75684 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>
2026-03-10 10:49:51 +00:00
5 changed files with 159 additions and 63 deletions

View File

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

View File

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

View File

@@ -5664,10 +5664,6 @@
"title": "Bucket Name",
"type": "string"
},
"cluster": {
"description": "An instance of the Couchbase Cluster connected to the desired Couchbase server.",
"title": "Cluster"
},
"collection_name": {
"description": "The name of the Couchbase collection to search",
"title": "Collection Name",
@@ -5716,7 +5712,6 @@
}
},
"required": [
"cluster",
"collection_name",
"scope_name",
"bucket_name",
@@ -14460,13 +14455,9 @@
"properties": {
"config": {
"$ref": "#/$defs/OxylabsAmazonProductScraperConfig"
},
"oxylabs_api": {
"title": "Oxylabs Api"
}
},
"required": [
"oxylabs_api",
"config"
],
"title": "OxylabsAmazonProductScraperTool",
@@ -14689,13 +14680,9 @@
"properties": {
"config": {
"$ref": "#/$defs/OxylabsAmazonSearchScraperConfig"
},
"oxylabs_api": {
"title": "Oxylabs Api"
}
},
"required": [
"oxylabs_api",
"config"
],
"title": "OxylabsAmazonSearchScraperTool",
@@ -14931,13 +14918,9 @@
"properties": {
"config": {
"$ref": "#/$defs/OxylabsGoogleSearchScraperConfig"
},
"oxylabs_api": {
"title": "Oxylabs Api"
}
},
"required": [
"oxylabs_api",
"config"
],
"title": "OxylabsGoogleSearchScraperTool",
@@ -15121,13 +15104,9 @@
"properties": {
"config": {
"$ref": "#/$defs/OxylabsUniversalScraperConfig"
},
"oxylabs_api": {
"title": "Oxylabs Api"
}
},
"required": [
"oxylabs_api",
"config"
],
"title": "OxylabsUniversalScraperTool",
@@ -23229,26 +23208,6 @@
"description": "The Tavily API key. If not provided, it will be loaded from the environment variable TAVILY_API_KEY.",
"title": "Api Key"
},
"async_client": {
"anyOf": [
{},
{
"type": "null"
}
],
"default": null,
"title": "Async Client"
},
"client": {
"anyOf": [
{},
{
"type": "null"
}
],
"default": null,
"title": "Client"
},
"extract_depth": {
"default": "basic",
"description": "The depth of extraction. 'basic' for basic extraction, 'advanced' for advanced extraction.",
@@ -23384,26 +23343,6 @@
"description": "The Tavily API key. If not provided, it will be loaded from the environment variable TAVILY_API_KEY.",
"title": "Api Key"
},
"async_client": {
"anyOf": [
{},
{
"type": "null"
}
],
"default": null,
"title": "Async Client"
},
"client": {
"anyOf": [
{},
{
"type": "null"
}
],
"default": null,
"title": "Client"
},
"days": {
"default": 7,
"description": "The number of days to search back.",

View File

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

View File

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