mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-15 05:58:10 +00:00
Compare commits
2 Commits
docs/custo
...
rn/tag-too
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8ce37c7c3 | ||
|
|
05c6cd2e44 |
@@ -26,6 +26,8 @@ class CrewAIPlatformActionTool(BaseTool):
|
||||
description: str,
|
||||
action_name: str,
|
||||
action_schema: dict[str, Any],
|
||||
provider: str | None = None,
|
||||
provider_id: str | None = None,
|
||||
):
|
||||
parameters = action_schema.get("function", {}).get("parameters", {})
|
||||
|
||||
@@ -48,6 +50,12 @@ class CrewAIPlatformActionTool(BaseTool):
|
||||
)
|
||||
self.action_name = action_name
|
||||
self.action_schema = action_schema
|
||||
# Private metadata used by enterprise tooling to recover the canonical
|
||||
# tool_id (e.g. "crewai_oauth:google_drive" or "paragon:<uuid>") for
|
||||
# ACP rule evaluation. Set by CrewaiPlatformToolBuilder; remains None
|
||||
# for direct construction.
|
||||
self._provider = provider
|
||||
self._provider_id = provider_id
|
||||
|
||||
def _run(self, **kwargs: Any) -> str:
|
||||
try:
|
||||
|
||||
@@ -75,6 +75,7 @@ class CrewaiPlatformToolBuilder:
|
||||
),
|
||||
"parameters": action.get("parameters", {}),
|
||||
"app": app,
|
||||
"provider": action.get("provider"),
|
||||
}
|
||||
}
|
||||
self._actions_schema[action_name] = action_schema
|
||||
@@ -91,6 +92,8 @@ class CrewaiPlatformToolBuilder:
|
||||
description=description,
|
||||
action_name=action_name,
|
||||
action_schema=action_schema,
|
||||
provider=function_details.get("provider"),
|
||||
provider_id=function_details.get("app"),
|
||||
)
|
||||
|
||||
tools.append(tool)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Tests for the _provider / _provider_id metadata attached to platform tools.
|
||||
|
||||
These attributes are private metadata used by enterprise tooling to recover
|
||||
the canonical tool_id (e.g. ``crewai_oauth:google_drive`` or
|
||||
``paragon:<uuid>``) for ACP rule evaluation.
|
||||
"""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from crewai_tools.tools.crewai_platform_tools import (
|
||||
CrewAIPlatformActionTool,
|
||||
CrewaiPlatformToolBuilder,
|
||||
)
|
||||
|
||||
|
||||
class TestActionToolProviderAttrs:
|
||||
def setup_method(self):
|
||||
self.action_schema = {
|
||||
"function": {
|
||||
"name": "test_action",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
}
|
||||
}
|
||||
|
||||
def test_defaults_to_none_when_not_provided(self):
|
||||
tool = CrewAIPlatformActionTool(
|
||||
description="x",
|
||||
action_name="test_action",
|
||||
action_schema=self.action_schema,
|
||||
)
|
||||
assert tool._provider is None
|
||||
assert tool._provider_id is None
|
||||
|
||||
def test_stores_explicit_values(self):
|
||||
tool = CrewAIPlatformActionTool(
|
||||
description="x",
|
||||
action_name="test_action",
|
||||
action_schema=self.action_schema,
|
||||
provider="crewai_oauth",
|
||||
provider_id="google_drive",
|
||||
)
|
||||
assert tool._provider == "crewai_oauth"
|
||||
assert tool._provider_id == "google_drive"
|
||||
|
||||
|
||||
class TestBuilderProviderThreading:
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"})
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_builder_threads_provider_and_app_into_each_tool(self, mock_get):
|
||||
mock_api_response = {
|
||||
"actions": {
|
||||
"google_drive": [
|
||||
{
|
||||
"name": "create_file",
|
||||
"description": "Create a file",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"provider": "crewai_oauth",
|
||||
}
|
||||
],
|
||||
"1b5f2395-65a5-4da8-9b2f-c10eafc83a0b": [
|
||||
{
|
||||
"name": "send_invoice",
|
||||
"description": "Send an invoice",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
"provider": "paragon",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = mock_api_response
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(
|
||||
apps=["google_drive", "1b5f2395-65a5-4da8-9b2f-c10eafc83a0b"]
|
||||
)
|
||||
tools = builder.tools()
|
||||
|
||||
by_action = {tool.action_name: tool for tool in tools}
|
||||
|
||||
oauth_tool = by_action["create_file"]
|
||||
assert oauth_tool._provider == "crewai_oauth"
|
||||
assert oauth_tool._provider_id == "google_drive"
|
||||
|
||||
paragon_tool = by_action["send_invoice"]
|
||||
assert paragon_tool._provider == "paragon"
|
||||
assert paragon_tool._provider_id == "1b5f2395-65a5-4da8-9b2f-c10eafc83a0b"
|
||||
|
||||
@patch.dict("os.environ", {"CREWAI_PLATFORM_INTEGRATION_TOKEN": "test_token"})
|
||||
@patch(
|
||||
"crewai_tools.tools.crewai_platform_tools.crewai_platform_tool_builder.requests.get"
|
||||
)
|
||||
def test_builder_handles_response_without_provider_field(self, mock_get):
|
||||
# Older crewai-plus versions return actions without a "provider" key.
|
||||
# The builder must remain compatible: provider_id is set, provider is None.
|
||||
mock_api_response = {
|
||||
"actions": {
|
||||
"github": [
|
||||
{
|
||||
"name": "create_issue",
|
||||
"description": "Create issue",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
mock_response = Mock()
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_response.json.return_value = mock_api_response
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
builder = CrewaiPlatformToolBuilder(apps=["github"])
|
||||
tools = builder.tools()
|
||||
|
||||
assert len(tools) == 1
|
||||
assert tools[0]._provider is None
|
||||
assert tools[0]._provider_id == "github"
|
||||
@@ -152,6 +152,8 @@ class MCPToolResolver:
|
||||
|
||||
try:
|
||||
tools, clients = self._resolve_native(mcp_server_config)
|
||||
for tool in tools:
|
||||
tool._amp_slug = slug # type: ignore[attr-defined]
|
||||
resolved_cache[slug] = (tools, clients)
|
||||
all_clients.extend(clients)
|
||||
except Exception as e:
|
||||
|
||||
@@ -59,6 +59,11 @@ class MCPNativeTool(BaseTool):
|
||||
self._client_factory = client_factory
|
||||
self._original_tool_name = original_tool_name or tool_name
|
||||
self._server_name = server_name
|
||||
# Set by MCPToolResolver._resolve_amp when this tool is produced for
|
||||
# an AMP slug; remains None for direct config / external URL refs.
|
||||
# Consumed downstream by enterprise tooling to recover the canonical
|
||||
# tool_id (e.g. "crewai_oauth:<slug>|mcp").
|
||||
self._amp_slug: str | None = None
|
||||
|
||||
@property
|
||||
def original_tool_name(self) -> str:
|
||||
|
||||
@@ -54,6 +54,11 @@ class MCPToolWrapper(BaseTool):
|
||||
self._mcp_server_params = mcp_server_params
|
||||
self._original_tool_name = tool_name
|
||||
self._server_name = server_name
|
||||
# Set by MCPToolResolver._resolve_amp when this wrapper is produced for
|
||||
# an AMP slug; remains None for direct config / external URL refs.
|
||||
# Consumed downstream by enterprise tooling to recover the canonical
|
||||
# tool_id (e.g. "crewai_oauth:<slug>|mcp").
|
||||
self._amp_slug: str | None = None
|
||||
|
||||
@property
|
||||
def mcp_server_params(self) -> dict[str, Any]:
|
||||
|
||||
135
lib/crewai/tests/mcp/test_tool_resolver_amp_slug.py
Normal file
135
lib/crewai/tests/mcp/test_tool_resolver_amp_slug.py
Normal file
@@ -0,0 +1,135 @@
|
||||
"""Tests for the _amp_slug attribute set by MCPToolResolver._resolve_amp.
|
||||
|
||||
The slug is private metadata used downstream by enterprise tooling to recover
|
||||
the canonical tool_id (e.g. ``crewai_oauth:<slug>|mcp``) for ACP rule
|
||||
evaluation.
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.mcp.config import MCPServerHTTP
|
||||
from crewai.mcp.tool_resolver import MCPToolResolver
|
||||
from crewai.tools.mcp_native_tool import MCPNativeTool
|
||||
from crewai.tools.mcp_tool_wrapper import MCPToolWrapper
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent():
|
||||
return Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resolver(agent):
|
||||
return MCPToolResolver(agent=agent, logger=agent._logger)
|
||||
|
||||
|
||||
class TestAmpSlugDefaultsNone:
|
||||
def test_native_tool_default_amp_slug_is_none(self):
|
||||
tool = MCPNativeTool(
|
||||
client_factory=lambda: None,
|
||||
tool_name="search",
|
||||
tool_schema={"description": "Search"},
|
||||
server_name="notion",
|
||||
)
|
||||
assert tool._amp_slug is None
|
||||
|
||||
def test_wrapper_tool_default_amp_slug_is_none(self):
|
||||
tool = MCPToolWrapper(
|
||||
mcp_server_params={"url": "https://mcp.example.com"},
|
||||
tool_name="search",
|
||||
tool_schema={"description": "Search"},
|
||||
server_name="notion",
|
||||
)
|
||||
assert tool._amp_slug is None
|
||||
|
||||
|
||||
class TestAmpSlugSetByResolveAmp:
|
||||
@patch("crewai.mcp.tool_resolver.MCPToolResolver._resolve_native")
|
||||
@patch("crewai.mcp.tool_resolver.MCPToolResolver._fetch_amp_mcp_configs")
|
||||
def test_resolve_amp_tags_each_tool_with_its_slug(
|
||||
self, mock_fetch_configs, mock_resolve_native, resolver
|
||||
):
|
||||
mock_fetch_configs.return_value = {
|
||||
"notion": {"url": "https://mcp.crewai.com/notion"},
|
||||
"github": {"url": "https://mcp.crewai.com/github"},
|
||||
}
|
||||
|
||||
notion_tool = MCPNativeTool(
|
||||
client_factory=lambda: None,
|
||||
tool_name="search",
|
||||
tool_schema={"description": "search Notion"},
|
||||
server_name="notion",
|
||||
)
|
||||
github_tool = MCPNativeTool(
|
||||
client_factory=lambda: None,
|
||||
tool_name="list_repos",
|
||||
tool_schema={"description": "list github repos"},
|
||||
server_name="github",
|
||||
)
|
||||
|
||||
def fake_resolve_native(config):
|
||||
url = config.url if hasattr(config, "url") else config["url"]
|
||||
if "notion" in url:
|
||||
return ([notion_tool], [MagicMock()])
|
||||
return ([github_tool], [MagicMock()])
|
||||
|
||||
mock_resolve_native.side_effect = fake_resolve_native
|
||||
|
||||
tools, _ = resolver._resolve_amp(
|
||||
[("notion", None), ("github", None)]
|
||||
)
|
||||
|
||||
assert {tool._amp_slug for tool in tools} == {"notion", "github"}
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPToolResolver._fetch_amp_mcp_configs")
|
||||
def test_resolve_amp_does_not_tag_when_config_missing(
|
||||
self, mock_fetch_configs, resolver
|
||||
):
|
||||
mock_fetch_configs.return_value = {}
|
||||
|
||||
tools, _ = resolver._resolve_amp([("unknown", None)])
|
||||
|
||||
assert tools == []
|
||||
|
||||
|
||||
class TestAmpSlugUntaggedForOtherPaths:
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_resolve_external_does_not_set_amp_slug(self, mock_client_class, resolver):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(
|
||||
return_value=[{"name": "search", "description": "Search"}]
|
||||
)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
with patch.object(
|
||||
resolver, "_get_mcp_tool_schemas", return_value={"search": {"description": "Search"}}
|
||||
):
|
||||
tools = resolver._resolve_external("https://mcp.example.com/api")
|
||||
|
||||
assert len(tools) == 1
|
||||
assert tools[0]._amp_slug is None
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_resolve_native_does_not_set_amp_slug(self, mock_client_class, resolver):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(
|
||||
return_value=[{"name": "search", "description": "Search"}]
|
||||
)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
config = MCPServerHTTP(url="https://mcp.example.com/api")
|
||||
tools, _ = resolver._resolve_native(config)
|
||||
|
||||
assert all(tool._amp_slug is None for tool in tools)
|
||||
Reference in New Issue
Block a user