mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-14 06:53:25 +00:00
Compare commits
2 Commits
fix/hierar
...
devin/1773
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
959534d506 | ||
|
|
3148a75684 |
@@ -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()
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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