composio-claude-agent-sdk 0.10.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- composio_claude_agent_sdk/__init__.py +3 -0
- composio_claude_agent_sdk/provider.py +187 -0
- composio_claude_agent_sdk/py.typed +0 -0
- composio_claude_agent_sdk-0.10.5.dist-info/METADATA +117 -0
- composio_claude_agent_sdk-0.10.5.dist-info/RECORD +9 -0
- composio_claude_agent_sdk-0.10.5.dist-info/WHEEL +5 -0
- composio_claude_agent_sdk-0.10.5.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/test_provider.py +313 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Claude Agent SDK Provider for Composio
|
|
3
|
+
|
|
4
|
+
This provider enables integration with Claude Agent SDK,
|
|
5
|
+
allowing Composio tools to be used as MCP tools within Claude agents.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import typing as t
|
|
11
|
+
|
|
12
|
+
from claude_agent_sdk import (
|
|
13
|
+
McpSdkServerConfig,
|
|
14
|
+
SdkMcpTool,
|
|
15
|
+
create_sdk_mcp_server,
|
|
16
|
+
tool as sdk_tool,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
from composio.core.provider import AgenticProvider
|
|
20
|
+
from composio.core.provider.agentic import AgenticProviderExecuteFn
|
|
21
|
+
from composio.types import Tool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ClaudeAgentSDKProvider(
|
|
25
|
+
AgenticProvider[SdkMcpTool, list[SdkMcpTool]],
|
|
26
|
+
name="claude_agent_sdk",
|
|
27
|
+
):
|
|
28
|
+
"""
|
|
29
|
+
Composio provider for Claude Agent SDK.
|
|
30
|
+
|
|
31
|
+
This provider wraps Composio tools as MCP tools that can be used with
|
|
32
|
+
the Claude Agent SDK's `query()` function via the `mcp_servers` option.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from composio import Composio
|
|
38
|
+
from composio_claude_agent_sdk import ClaudeAgentSDKProvider
|
|
39
|
+
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
40
|
+
|
|
41
|
+
composio = Composio(provider=ClaudeAgentSDKProvider())
|
|
42
|
+
|
|
43
|
+
async def main():
|
|
44
|
+
tools = composio.tools.get(user_id="default", toolkits=["gmail"])
|
|
45
|
+
mcp_server = composio.provider.create_mcp_server(tools)
|
|
46
|
+
|
|
47
|
+
async for message in query(
|
|
48
|
+
prompt="Fetch my latest email",
|
|
49
|
+
options=ClaudeAgentOptions(
|
|
50
|
+
mcp_servers={"composio": mcp_server},
|
|
51
|
+
permission_mode="bypassPermissions",
|
|
52
|
+
),
|
|
53
|
+
):
|
|
54
|
+
print(message)
|
|
55
|
+
|
|
56
|
+
asyncio.run(main())
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
server_name: str = "composio",
|
|
63
|
+
server_version: str = "1.0.0",
|
|
64
|
+
) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Initialize the Claude Agent SDK provider.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
server_name: Name for the MCP server (default: "composio")
|
|
70
|
+
server_version: Version for the MCP server (default: "1.0.0")
|
|
71
|
+
"""
|
|
72
|
+
super().__init__()
|
|
73
|
+
self.server_name = server_name
|
|
74
|
+
self.server_version = server_version
|
|
75
|
+
|
|
76
|
+
def wrap_tool(
|
|
77
|
+
self,
|
|
78
|
+
tool: Tool,
|
|
79
|
+
execute_tool: AgenticProviderExecuteFn,
|
|
80
|
+
) -> SdkMcpTool:
|
|
81
|
+
"""
|
|
82
|
+
Wrap a Composio tool as a Claude Agent SDK MCP tool.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
tool: The Composio tool to wrap
|
|
86
|
+
execute_tool: Function to execute the tool
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
A Claude Agent SDK MCP tool definition
|
|
90
|
+
"""
|
|
91
|
+
input_schema = tool.input_parameters or {}
|
|
92
|
+
|
|
93
|
+
@sdk_tool(
|
|
94
|
+
tool.slug,
|
|
95
|
+
tool.description or f"Execute {tool.slug}",
|
|
96
|
+
input_schema,
|
|
97
|
+
)
|
|
98
|
+
async def tool_handler(args: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]:
|
|
99
|
+
"""Execute the Composio tool with the given arguments."""
|
|
100
|
+
try:
|
|
101
|
+
# Run the synchronous execute_tool in a thread
|
|
102
|
+
result = await asyncio.to_thread(
|
|
103
|
+
execute_tool,
|
|
104
|
+
tool.slug,
|
|
105
|
+
args,
|
|
106
|
+
)
|
|
107
|
+
# Format the result for Claude Agent SDK
|
|
108
|
+
result_text = result if isinstance(result, str) else json.dumps(result)
|
|
109
|
+
return {
|
|
110
|
+
"content": [
|
|
111
|
+
{
|
|
112
|
+
"type": "text",
|
|
113
|
+
"text": result_text,
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return {
|
|
119
|
+
"content": [
|
|
120
|
+
{
|
|
121
|
+
"type": "text",
|
|
122
|
+
"text": json.dumps(
|
|
123
|
+
{
|
|
124
|
+
"successful": False,
|
|
125
|
+
"error": str(e),
|
|
126
|
+
"data": None,
|
|
127
|
+
}
|
|
128
|
+
),
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return tool_handler
|
|
134
|
+
|
|
135
|
+
def wrap_tools(
|
|
136
|
+
self,
|
|
137
|
+
tools: t.Sequence[Tool],
|
|
138
|
+
execute_tool: AgenticProviderExecuteFn,
|
|
139
|
+
) -> list[SdkMcpTool]:
|
|
140
|
+
"""
|
|
141
|
+
Wrap multiple Composio tools as Claude Agent SDK MCP tools.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
tools: Sequence of Composio tools to wrap
|
|
145
|
+
execute_tool: Function to execute the tools
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of Claude Agent SDK MCP tool definitions
|
|
149
|
+
"""
|
|
150
|
+
return [self.wrap_tool(tool, execute_tool) for tool in tools]
|
|
151
|
+
|
|
152
|
+
def create_mcp_server(
|
|
153
|
+
self,
|
|
154
|
+
wrapped_tools: list[SdkMcpTool],
|
|
155
|
+
) -> McpSdkServerConfig:
|
|
156
|
+
"""
|
|
157
|
+
Create an MCP server configuration for use with Claude Agent SDK.
|
|
158
|
+
|
|
159
|
+
This is the primary method for integrating Composio tools with Claude agents.
|
|
160
|
+
The returned configuration can be passed directly to the `mcp_servers` option.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
wrapped_tools: List of wrapped Claude Agent SDK MCP tools
|
|
164
|
+
(from composio.tools.get())
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
MCP server configuration for Claude Agent SDK
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
```python
|
|
171
|
+
tools = composio.tools.get(user_id="default", toolkits=["gmail"])
|
|
172
|
+
mcp_server = composio.provider.create_mcp_server(tools)
|
|
173
|
+
|
|
174
|
+
async for message in query(
|
|
175
|
+
prompt="Send an email",
|
|
176
|
+
options=ClaudeAgentOptions(
|
|
177
|
+
mcp_servers={"composio": mcp_server},
|
|
178
|
+
),
|
|
179
|
+
):
|
|
180
|
+
print(message)
|
|
181
|
+
```
|
|
182
|
+
"""
|
|
183
|
+
return create_sdk_mcp_server(
|
|
184
|
+
name=self.server_name,
|
|
185
|
+
version=self.server_version,
|
|
186
|
+
tools=wrapped_tools,
|
|
187
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: composio-claude-agent-sdk
|
|
3
|
+
Version: 0.10.5
|
|
4
|
+
Summary: Use Composio to get an array of tools with Claude Code Agents SDK.
|
|
5
|
+
Home-page: https://github.com/ComposioHQ/composio
|
|
6
|
+
Author: Composio
|
|
7
|
+
Author-email: Composio <tech@composio.dev>
|
|
8
|
+
Project-URL: Homepage, https://github.com/ComposioHQ/composio
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10,<4
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: claude-agent-sdk>=0.1.0
|
|
15
|
+
Requires-Dist: composio
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-python
|
|
19
|
+
|
|
20
|
+
# Composio Claude Code Agents Provider
|
|
21
|
+
|
|
22
|
+
Use Composio tools with the Claude Code Agents SDK.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Prerequisites
|
|
27
|
+
|
|
28
|
+
1. **Claude Code CLI**: The Claude Agent SDK requires Claude Code to be installed:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# macOS/Linux/WSL
|
|
32
|
+
curl -fsSL https://claude.ai/install.sh | bash
|
|
33
|
+
|
|
34
|
+
# or via Homebrew
|
|
35
|
+
brew install --cask claude-code
|
|
36
|
+
|
|
37
|
+
# or via npm
|
|
38
|
+
npm install -g @anthropic-ai/claude-code
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
2. **Anthropic API Key**: Set your API key as an environment variable:
|
|
42
|
+
```bash
|
|
43
|
+
export ANTHROPIC_API_KEY="your-api-key"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Install the package
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install composio-claude-agent-sdk
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
from composio import Composio
|
|
57
|
+
from composio_claude_agent_sdk import ClaudeAgentSDKProvider
|
|
58
|
+
from claude_agent_sdk import query, ClaudeAgentOptions
|
|
59
|
+
|
|
60
|
+
# Initialize Composio with the Claude Code Agents provider
|
|
61
|
+
composio = Composio(provider=ClaudeAgentSDKProvider())
|
|
62
|
+
|
|
63
|
+
async def main():
|
|
64
|
+
# Get tools from Composio
|
|
65
|
+
tools = composio.tools.get(
|
|
66
|
+
user_id="default",
|
|
67
|
+
toolkits=["gmail"],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Create an MCP server configuration with the tools
|
|
71
|
+
mcp_server = composio.provider.create_mcp_server(tools)
|
|
72
|
+
|
|
73
|
+
# Run a Claude agent with access to Composio tools
|
|
74
|
+
async for message in query(
|
|
75
|
+
prompt="Fetch my latest email from Gmail",
|
|
76
|
+
options=ClaudeAgentOptions(
|
|
77
|
+
mcp_servers={"composio": mcp_server},
|
|
78
|
+
permission_mode="bypassPermissions",
|
|
79
|
+
),
|
|
80
|
+
):
|
|
81
|
+
if message.type == "assistant":
|
|
82
|
+
print(message.message)
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## API Reference
|
|
88
|
+
|
|
89
|
+
### ClaudeCodeAgentsProvider
|
|
90
|
+
|
|
91
|
+
The main provider class for integrating Composio tools with Claude Code Agents SDK.
|
|
92
|
+
|
|
93
|
+
#### Constructor Options
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
ClaudeCodeAgentsProvider(
|
|
97
|
+
server_name: str = "composio", # Name for the MCP server
|
|
98
|
+
server_version: str = "1.0.0", # Version for the MCP server
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
#### Methods
|
|
103
|
+
|
|
104
|
+
- `wrap_tool(tool, execute_tool)` - Wraps a single Composio tool as a Claude Agent SDK MCP tool
|
|
105
|
+
- `wrap_tools(tools, execute_tool)` - Wraps multiple Composio tools
|
|
106
|
+
- `create_mcp_server(wrapped_tools)` - Creates an MCP server configuration from wrapped tools
|
|
107
|
+
|
|
108
|
+
## Environment Variables
|
|
109
|
+
|
|
110
|
+
- `COMPOSIO_API_KEY` - Your Composio API key (get one at https://app.composio.dev)
|
|
111
|
+
- `ANTHROPIC_API_KEY` - Your Anthropic API key (get one at https://console.anthropic.com)
|
|
112
|
+
|
|
113
|
+
## Links
|
|
114
|
+
|
|
115
|
+
- [Composio Documentation](https://docs.composio.dev)
|
|
116
|
+
- [Claude Agent SDK Documentation](https://platform.claude.com/docs/en/agent-sdk/python)
|
|
117
|
+
- [GitHub Repository](https://github.com/ComposioHQ/composio)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
composio_claude_agent_sdk/__init__.py,sha256=5jU62AJuNHgNf4b8skPmOBqU53hs-zThc8lhUZtCQzs,109
|
|
2
|
+
composio_claude_agent_sdk/provider.py,sha256=hLYnVVkfILqjVoxvThmVoNABpdjMgWJUz00dtbYbUkY,5741
|
|
3
|
+
composio_claude_agent_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
tests/test_provider.py,sha256=TJXJ9XJhuSR_8GVtKFRuUeRc6Tc6dQJjA799yx5nPPM,10694
|
|
6
|
+
composio_claude_agent_sdk-0.10.5.dist-info/METADATA,sha256=tb5f4nnBQuBHef5gkghotsdJgI1aAPxIz1oUvwxQU54,3262
|
|
7
|
+
composio_claude_agent_sdk-0.10.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
composio_claude_agent_sdk-0.10.5.dist-info/top_level.txt,sha256=J2UB7SfZ_56iY7K_cRyIH1pqTYttMFKrj2AfN9qih48,32
|
|
9
|
+
composio_claude_agent_sdk-0.10.5.dist-info/RECORD,,
|
tests/__init__.py
ADDED
|
File without changes
|
tests/test_provider.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for Claude Code Agents Provider
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from unittest.mock import MagicMock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from composio_claude_agent_sdk.provider import ClaudeAgentSDKProvider
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture
|
|
14
|
+
def provider():
|
|
15
|
+
"""Create a ClaudeAgentSDKProvider instance."""
|
|
16
|
+
return ClaudeAgentSDKProvider()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def custom_provider():
|
|
21
|
+
"""Create a ClaudeAgentSDKProvider with custom options."""
|
|
22
|
+
return ClaudeAgentSDKProvider(
|
|
23
|
+
server_name="custom-server",
|
|
24
|
+
server_version="2.0.0",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture
|
|
29
|
+
def mock_tool():
|
|
30
|
+
"""Create a mock Composio tool."""
|
|
31
|
+
return MagicMock(
|
|
32
|
+
slug="GMAIL_SEND_EMAIL",
|
|
33
|
+
name="Gmail Send Email",
|
|
34
|
+
description="Send an email via Gmail",
|
|
35
|
+
input_parameters={
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"to": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Recipient email address",
|
|
41
|
+
},
|
|
42
|
+
"subject": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "Email subject",
|
|
45
|
+
},
|
|
46
|
+
"body": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "Email body content",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
"required": ["to", "subject", "body"],
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture
|
|
57
|
+
def mock_execute_tool():
|
|
58
|
+
"""Create a mock execute_tool function."""
|
|
59
|
+
return MagicMock(
|
|
60
|
+
return_value={
|
|
61
|
+
"data": {"result": "success"},
|
|
62
|
+
"error": None,
|
|
63
|
+
"successful": True,
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestClaudeAgentSDKProviderInit:
|
|
69
|
+
"""Tests for provider initialization."""
|
|
70
|
+
|
|
71
|
+
def test_default_options(self, provider):
|
|
72
|
+
"""Test provider initializes with default options."""
|
|
73
|
+
assert provider.server_name == "composio"
|
|
74
|
+
assert provider.server_version == "1.0.0"
|
|
75
|
+
|
|
76
|
+
def test_custom_options(self, custom_provider):
|
|
77
|
+
"""Test provider initializes with custom options."""
|
|
78
|
+
assert custom_provider.server_name == "custom-server"
|
|
79
|
+
assert custom_provider.server_version == "2.0.0"
|
|
80
|
+
|
|
81
|
+
def test_name_property(self, provider):
|
|
82
|
+
"""Test provider has correct name."""
|
|
83
|
+
assert provider.name == "claude_agent_sdk"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestJsonSchemaConversion:
|
|
87
|
+
"""Tests for JSON Schema conversion."""
|
|
88
|
+
|
|
89
|
+
def test_simple_schema_passthrough(self, provider):
|
|
90
|
+
"""Test that JSON Schema is passed through unchanged."""
|
|
91
|
+
schema = {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"name": {"type": "string"},
|
|
95
|
+
"count": {"type": "integer"},
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
result = provider._json_schema_to_simple_schema(schema)
|
|
99
|
+
assert result == schema
|
|
100
|
+
|
|
101
|
+
def test_empty_schema(self, provider):
|
|
102
|
+
"""Test handling of empty schema."""
|
|
103
|
+
result = provider._json_schema_to_simple_schema({})
|
|
104
|
+
assert result == {}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class TestWrapTool:
|
|
108
|
+
"""Tests for wrap_tool method."""
|
|
109
|
+
|
|
110
|
+
def test_wrap_tool_returns_sdk_mcp_tool(
|
|
111
|
+
self, provider, mock_tool, mock_execute_tool
|
|
112
|
+
):
|
|
113
|
+
"""Test that wrap_tool returns an SdkMcpTool."""
|
|
114
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
115
|
+
mock_sdk_tool.return_value = lambda fn: fn
|
|
116
|
+
provider.wrap_tool(mock_tool, mock_execute_tool)
|
|
117
|
+
|
|
118
|
+
# Verify sdk_tool was called with correct arguments
|
|
119
|
+
mock_sdk_tool.assert_called_once_with(
|
|
120
|
+
mock_tool.slug,
|
|
121
|
+
mock_tool.description,
|
|
122
|
+
mock_tool.input_parameters,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def test_wrap_tool_without_description(self, provider, mock_execute_tool):
|
|
126
|
+
"""Test wrapping a tool without description."""
|
|
127
|
+
tool_without_desc = MagicMock(
|
|
128
|
+
slug="TEST_TOOL",
|
|
129
|
+
description=None,
|
|
130
|
+
input_parameters={"type": "object", "properties": {}},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
134
|
+
mock_sdk_tool.return_value = lambda fn: fn
|
|
135
|
+
provider.wrap_tool(tool_without_desc, mock_execute_tool)
|
|
136
|
+
|
|
137
|
+
# Should use default description
|
|
138
|
+
mock_sdk_tool.assert_called_once()
|
|
139
|
+
call_args = mock_sdk_tool.call_args[0]
|
|
140
|
+
assert call_args[1] == "Execute TEST_TOOL"
|
|
141
|
+
|
|
142
|
+
def test_wrap_tool_without_input_parameters(self, provider, mock_execute_tool):
|
|
143
|
+
"""Test wrapping a tool without input parameters."""
|
|
144
|
+
tool_without_params = MagicMock(
|
|
145
|
+
slug="TEST_TOOL",
|
|
146
|
+
description="Test tool",
|
|
147
|
+
input_parameters=None,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
151
|
+
mock_sdk_tool.return_value = lambda fn: fn
|
|
152
|
+
provider.wrap_tool(tool_without_params, mock_execute_tool)
|
|
153
|
+
|
|
154
|
+
# Should use empty schema
|
|
155
|
+
mock_sdk_tool.assert_called_once()
|
|
156
|
+
call_args = mock_sdk_tool.call_args[0]
|
|
157
|
+
assert call_args[2] == {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestWrapTools:
|
|
161
|
+
"""Tests for wrap_tools method."""
|
|
162
|
+
|
|
163
|
+
def test_wrap_multiple_tools(self, provider, mock_tool, mock_execute_tool):
|
|
164
|
+
"""Test wrapping multiple tools."""
|
|
165
|
+
another_tool = MagicMock(
|
|
166
|
+
slug="SLACK_POST_MESSAGE",
|
|
167
|
+
description="Post a message to Slack",
|
|
168
|
+
input_parameters={"type": "object", "properties": {}},
|
|
169
|
+
)
|
|
170
|
+
tools = [mock_tool, another_tool]
|
|
171
|
+
|
|
172
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
173
|
+
mock_sdk_tool.return_value = lambda fn: fn
|
|
174
|
+
wrapped = provider.wrap_tools(tools, mock_execute_tool)
|
|
175
|
+
|
|
176
|
+
assert len(wrapped) == 2
|
|
177
|
+
assert mock_sdk_tool.call_count == 2
|
|
178
|
+
|
|
179
|
+
def test_wrap_empty_tools_list(self, provider, mock_execute_tool):
|
|
180
|
+
"""Test wrapping an empty tools list."""
|
|
181
|
+
wrapped = provider.wrap_tools([], mock_execute_tool)
|
|
182
|
+
assert wrapped == []
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class TestCreateMcpServer:
|
|
186
|
+
"""Tests for create_mcp_server method."""
|
|
187
|
+
|
|
188
|
+
def test_create_mcp_server_with_default_options(self, provider):
|
|
189
|
+
"""Test creating MCP server with default options."""
|
|
190
|
+
mock_tools = [MagicMock(), MagicMock()]
|
|
191
|
+
|
|
192
|
+
with patch(
|
|
193
|
+
"composio_claude_agent_sdk.provider.create_sdk_mcp_server"
|
|
194
|
+
) as mock_create:
|
|
195
|
+
mock_create.return_value = {"type": "sdk", "name": "composio"}
|
|
196
|
+
provider.create_mcp_server(mock_tools)
|
|
197
|
+
|
|
198
|
+
mock_create.assert_called_once_with(
|
|
199
|
+
name="composio",
|
|
200
|
+
version="1.0.0",
|
|
201
|
+
tools=mock_tools,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def test_create_mcp_server_with_custom_options(self, custom_provider):
|
|
205
|
+
"""Test creating MCP server with custom options."""
|
|
206
|
+
mock_tools = [MagicMock()]
|
|
207
|
+
|
|
208
|
+
with patch(
|
|
209
|
+
"composio_claude_agent_sdk.provider.create_sdk_mcp_server"
|
|
210
|
+
) as mock_create:
|
|
211
|
+
mock_create.return_value = {"type": "sdk", "name": "custom-server"}
|
|
212
|
+
custom_provider.create_mcp_server(mock_tools)
|
|
213
|
+
|
|
214
|
+
mock_create.assert_called_once_with(
|
|
215
|
+
name="custom-server",
|
|
216
|
+
version="2.0.0",
|
|
217
|
+
tools=mock_tools,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def test_create_mcp_server_with_empty_tools(self, provider):
|
|
221
|
+
"""Test creating MCP server with empty tools list."""
|
|
222
|
+
with patch(
|
|
223
|
+
"composio_claude_agent_sdk.provider.create_sdk_mcp_server"
|
|
224
|
+
) as mock_create:
|
|
225
|
+
mock_create.return_value = {"type": "sdk", "name": "composio"}
|
|
226
|
+
provider.create_mcp_server([])
|
|
227
|
+
|
|
228
|
+
mock_create.assert_called_once_with(
|
|
229
|
+
name="composio",
|
|
230
|
+
version="1.0.0",
|
|
231
|
+
tools=[],
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TestToolHandlerExecution:
|
|
236
|
+
"""Tests for tool handler execution."""
|
|
237
|
+
|
|
238
|
+
@pytest.mark.asyncio
|
|
239
|
+
async def test_tool_handler_success(self, provider, mock_tool):
|
|
240
|
+
"""Test successful tool execution."""
|
|
241
|
+
mock_execute = MagicMock(return_value={"data": "success", "successful": True})
|
|
242
|
+
|
|
243
|
+
# Create a real wrapped tool to test the handler
|
|
244
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
245
|
+
# Capture the decorated function
|
|
246
|
+
captured_handler = None
|
|
247
|
+
|
|
248
|
+
def capture_decorator(name, desc, schema):
|
|
249
|
+
def decorator(fn):
|
|
250
|
+
nonlocal captured_handler
|
|
251
|
+
captured_handler = fn
|
|
252
|
+
return fn
|
|
253
|
+
|
|
254
|
+
return decorator
|
|
255
|
+
|
|
256
|
+
mock_sdk_tool.side_effect = capture_decorator
|
|
257
|
+
provider.wrap_tool(mock_tool, mock_execute)
|
|
258
|
+
|
|
259
|
+
# Execute the captured handler
|
|
260
|
+
result = await captured_handler({"to": "test@example.com"})
|
|
261
|
+
|
|
262
|
+
assert result["content"][0]["type"] == "text"
|
|
263
|
+
assert "success" in result["content"][0]["text"]
|
|
264
|
+
|
|
265
|
+
@pytest.mark.asyncio
|
|
266
|
+
async def test_tool_handler_error(self, provider, mock_tool):
|
|
267
|
+
"""Test tool execution with error."""
|
|
268
|
+
mock_execute = MagicMock(side_effect=Exception("Test error"))
|
|
269
|
+
|
|
270
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
271
|
+
captured_handler = None
|
|
272
|
+
|
|
273
|
+
def capture_decorator(name, desc, schema):
|
|
274
|
+
def decorator(fn):
|
|
275
|
+
nonlocal captured_handler
|
|
276
|
+
captured_handler = fn
|
|
277
|
+
return fn
|
|
278
|
+
|
|
279
|
+
return decorator
|
|
280
|
+
|
|
281
|
+
mock_sdk_tool.side_effect = capture_decorator
|
|
282
|
+
provider.wrap_tool(mock_tool, mock_execute)
|
|
283
|
+
|
|
284
|
+
result = await captured_handler({"to": "test@example.com"})
|
|
285
|
+
|
|
286
|
+
assert result["content"][0]["type"] == "text"
|
|
287
|
+
response = json.loads(result["content"][0]["text"])
|
|
288
|
+
assert response["successful"] is False
|
|
289
|
+
assert "Test error" in response["error"]
|
|
290
|
+
|
|
291
|
+
@pytest.mark.asyncio
|
|
292
|
+
async def test_tool_handler_string_result(self, provider, mock_tool):
|
|
293
|
+
"""Test tool execution returning string result."""
|
|
294
|
+
mock_execute = MagicMock(return_value="Simple string result")
|
|
295
|
+
|
|
296
|
+
with patch("composio_claude_agent_sdk.provider.sdk_tool") as mock_sdk_tool:
|
|
297
|
+
captured_handler = None
|
|
298
|
+
|
|
299
|
+
def capture_decorator(name, desc, schema):
|
|
300
|
+
def decorator(fn):
|
|
301
|
+
nonlocal captured_handler
|
|
302
|
+
captured_handler = fn
|
|
303
|
+
return fn
|
|
304
|
+
|
|
305
|
+
return decorator
|
|
306
|
+
|
|
307
|
+
mock_sdk_tool.side_effect = capture_decorator
|
|
308
|
+
provider.wrap_tool(mock_tool, mock_execute)
|
|
309
|
+
|
|
310
|
+
result = await captured_handler({})
|
|
311
|
+
|
|
312
|
+
assert result["content"][0]["type"] == "text"
|
|
313
|
+
assert result["content"][0]["text"] == "Simple string result"
|