sandboxy 0.0.1__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.
- sandboxy/__init__.py +3 -0
- sandboxy/agents/__init__.py +21 -0
- sandboxy/agents/base.py +66 -0
- sandboxy/agents/llm_prompt.py +308 -0
- sandboxy/agents/loader.py +222 -0
- sandboxy/api/__init__.py +5 -0
- sandboxy/api/app.py +76 -0
- sandboxy/api/routes/__init__.py +1 -0
- sandboxy/api/routes/agents.py +92 -0
- sandboxy/api/routes/local.py +1388 -0
- sandboxy/api/routes/tools.py +106 -0
- sandboxy/cli/__init__.py +1 -0
- sandboxy/cli/main.py +1196 -0
- sandboxy/cli/type_detector.py +48 -0
- sandboxy/config.py +49 -0
- sandboxy/core/__init__.py +1 -0
- sandboxy/core/async_runner.py +824 -0
- sandboxy/core/mdl_parser.py +441 -0
- sandboxy/core/runner.py +599 -0
- sandboxy/core/safe_eval.py +165 -0
- sandboxy/core/state.py +234 -0
- sandboxy/datasets/__init__.py +20 -0
- sandboxy/datasets/loader.py +193 -0
- sandboxy/datasets/runner.py +442 -0
- sandboxy/errors.py +166 -0
- sandboxy/local/context.py +235 -0
- sandboxy/local/results.py +173 -0
- sandboxy/logging.py +31 -0
- sandboxy/mcp/__init__.py +25 -0
- sandboxy/mcp/client.py +360 -0
- sandboxy/mcp/wrapper.py +99 -0
- sandboxy/providers/__init__.py +34 -0
- sandboxy/providers/anthropic_provider.py +271 -0
- sandboxy/providers/base.py +123 -0
- sandboxy/providers/http_client.py +101 -0
- sandboxy/providers/openai_provider.py +282 -0
- sandboxy/providers/openrouter.py +958 -0
- sandboxy/providers/registry.py +199 -0
- sandboxy/scenarios/__init__.py +11 -0
- sandboxy/scenarios/comparison.py +491 -0
- sandboxy/scenarios/loader.py +262 -0
- sandboxy/scenarios/runner.py +468 -0
- sandboxy/scenarios/unified.py +1434 -0
- sandboxy/session/__init__.py +21 -0
- sandboxy/session/manager.py +278 -0
- sandboxy/tools/__init__.py +34 -0
- sandboxy/tools/base.py +127 -0
- sandboxy/tools/loader.py +270 -0
- sandboxy/tools/yaml_tools.py +708 -0
- sandboxy/ui/__init__.py +27 -0
- sandboxy/ui/dist/assets/index-CgAkYWrJ.css +1 -0
- sandboxy/ui/dist/assets/index-D4zoGFcr.js +347 -0
- sandboxy/ui/dist/index.html +14 -0
- sandboxy/utils/__init__.py +3 -0
- sandboxy/utils/time.py +20 -0
- sandboxy-0.0.1.dist-info/METADATA +241 -0
- sandboxy-0.0.1.dist-info/RECORD +60 -0
- sandboxy-0.0.1.dist-info/WHEEL +4 -0
- sandboxy-0.0.1.dist-info/entry_points.txt +3 -0
- sandboxy-0.0.1.dist-info/licenses/LICENSE +201 -0
sandboxy/mcp/client.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""MCP client manager - handles connections to MCP servers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from sandboxy.mcp.wrapper import McpToolWrapper
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class McpConnection:
|
|
19
|
+
"""An active connection to an MCP server."""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
session: Any # ClientSession
|
|
23
|
+
tools: dict[str, McpToolWrapper] = field(default_factory=dict)
|
|
24
|
+
transport: str = "stdio" # "stdio", "sse", or "streamable_http"
|
|
25
|
+
_read_stream: Any = None
|
|
26
|
+
_write_stream: Any = None
|
|
27
|
+
_context: Any = None # For HTTP transports
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class McpServerConfig(BaseModel):
|
|
31
|
+
"""Configuration for an MCP server.
|
|
32
|
+
|
|
33
|
+
Supports two modes:
|
|
34
|
+
- Local (stdio): Set `command` and optionally `args`/`env`
|
|
35
|
+
- Remote (HTTP): Set `url` and optionally `headers`
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
name: str
|
|
39
|
+
|
|
40
|
+
# Local server (stdio transport)
|
|
41
|
+
command: str | None = None
|
|
42
|
+
args: list[str] = []
|
|
43
|
+
env: dict[str, str] = {}
|
|
44
|
+
|
|
45
|
+
# Remote server (HTTP transport)
|
|
46
|
+
url: str | None = None
|
|
47
|
+
headers: dict[str, str] = {}
|
|
48
|
+
transport: Literal["auto", "stdio", "sse", "streamable_http"] = "auto"
|
|
49
|
+
|
|
50
|
+
def is_http(self) -> bool:
|
|
51
|
+
"""Check if this config uses HTTP transport."""
|
|
52
|
+
if self.transport in ("sse", "streamable_http"):
|
|
53
|
+
return True
|
|
54
|
+
if self.transport == "stdio":
|
|
55
|
+
return False
|
|
56
|
+
# Auto-detect: if url is set, use HTTP
|
|
57
|
+
return self.url is not None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class McpManager:
|
|
61
|
+
"""Manages connections to multiple MCP servers."""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
"""Initialize the MCP manager."""
|
|
65
|
+
self._connections: dict[str, McpConnection] = {}
|
|
66
|
+
|
|
67
|
+
async def connect(self, config: McpServerConfig) -> McpConnection:
|
|
68
|
+
"""Connect to an MCP server.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config: Server configuration
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Active connection with discovered tools
|
|
75
|
+
"""
|
|
76
|
+
if config.is_http():
|
|
77
|
+
return await self._connect_http(config)
|
|
78
|
+
return await self._connect_stdio(config)
|
|
79
|
+
|
|
80
|
+
async def _connect_stdio(self, config: McpServerConfig) -> McpConnection:
|
|
81
|
+
"""Connect to a local MCP server via stdio."""
|
|
82
|
+
from mcp import ClientSession, StdioServerParameters
|
|
83
|
+
from mcp.client.stdio import stdio_client
|
|
84
|
+
|
|
85
|
+
if not config.command:
|
|
86
|
+
raise ValueError(f"Server '{config.name}' requires 'command' for stdio transport")
|
|
87
|
+
|
|
88
|
+
# Build environment with current env + overrides
|
|
89
|
+
env = os.environ.copy()
|
|
90
|
+
for key, value in config.env.items():
|
|
91
|
+
# Support ${VAR} expansion
|
|
92
|
+
if value.startswith("${") and value.endswith("}"):
|
|
93
|
+
env_key = value[2:-1]
|
|
94
|
+
env[key] = os.environ.get(env_key, "")
|
|
95
|
+
else:
|
|
96
|
+
env[key] = value
|
|
97
|
+
|
|
98
|
+
# Create server parameters
|
|
99
|
+
server_params = StdioServerParameters(
|
|
100
|
+
command=config.command,
|
|
101
|
+
args=config.args,
|
|
102
|
+
env=env,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Connect to server
|
|
106
|
+
read_stream, write_stream = await stdio_client(server_params).__aenter__()
|
|
107
|
+
session = await ClientSession(read_stream, write_stream).__aenter__()
|
|
108
|
+
|
|
109
|
+
# Initialize the session
|
|
110
|
+
await session.initialize()
|
|
111
|
+
|
|
112
|
+
# Discover tools
|
|
113
|
+
tools_result = await session.list_tools()
|
|
114
|
+
tools: dict[str, McpToolWrapper] = {}
|
|
115
|
+
|
|
116
|
+
for tool_info in tools_result.tools:
|
|
117
|
+
wrapper = McpToolWrapper(session, tool_info)
|
|
118
|
+
tools[tool_info.name] = wrapper
|
|
119
|
+
|
|
120
|
+
# Create and store connection
|
|
121
|
+
connection = McpConnection(
|
|
122
|
+
name=config.name,
|
|
123
|
+
session=session,
|
|
124
|
+
tools=tools,
|
|
125
|
+
transport="stdio",
|
|
126
|
+
_read_stream=read_stream,
|
|
127
|
+
_write_stream=write_stream,
|
|
128
|
+
)
|
|
129
|
+
self._connections[config.name] = connection
|
|
130
|
+
|
|
131
|
+
return connection
|
|
132
|
+
|
|
133
|
+
async def _connect_http(self, config: McpServerConfig) -> McpConnection:
|
|
134
|
+
"""Connect to a remote MCP server via HTTP (SSE or Streamable HTTP)."""
|
|
135
|
+
from mcp import ClientSession
|
|
136
|
+
|
|
137
|
+
if not config.url:
|
|
138
|
+
raise ValueError(f"Server '{config.name}' requires 'url' for HTTP transport")
|
|
139
|
+
|
|
140
|
+
# Determine transport type
|
|
141
|
+
transport = config.transport
|
|
142
|
+
if transport == "auto":
|
|
143
|
+
# Auto-detect based on URL path
|
|
144
|
+
if config.url.endswith("/sse"):
|
|
145
|
+
transport = "sse"
|
|
146
|
+
else:
|
|
147
|
+
transport = "streamable_http"
|
|
148
|
+
|
|
149
|
+
# Build headers
|
|
150
|
+
headers = dict(config.headers)
|
|
151
|
+
|
|
152
|
+
if transport == "sse":
|
|
153
|
+
from mcp.client.sse import sse_client
|
|
154
|
+
|
|
155
|
+
# Connect via SSE
|
|
156
|
+
context = sse_client(config.url, headers=headers if headers else None)
|
|
157
|
+
read_stream, write_stream = await context.__aenter__()
|
|
158
|
+
else:
|
|
159
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
160
|
+
|
|
161
|
+
# Connect via Streamable HTTP
|
|
162
|
+
context = streamablehttp_client(config.url, headers=headers if headers else None)
|
|
163
|
+
read_stream, write_stream, _ = await context.__aenter__()
|
|
164
|
+
|
|
165
|
+
session = await ClientSession(read_stream, write_stream).__aenter__()
|
|
166
|
+
|
|
167
|
+
# Initialize the session
|
|
168
|
+
await session.initialize()
|
|
169
|
+
|
|
170
|
+
# Discover tools
|
|
171
|
+
tools_result = await session.list_tools()
|
|
172
|
+
tools: dict[str, McpToolWrapper] = {}
|
|
173
|
+
|
|
174
|
+
for tool_info in tools_result.tools:
|
|
175
|
+
wrapper = McpToolWrapper(session, tool_info)
|
|
176
|
+
tools[tool_info.name] = wrapper
|
|
177
|
+
|
|
178
|
+
# Create and store connection
|
|
179
|
+
connection = McpConnection(
|
|
180
|
+
name=config.name,
|
|
181
|
+
session=session,
|
|
182
|
+
tools=tools,
|
|
183
|
+
transport=transport,
|
|
184
|
+
_read_stream=read_stream,
|
|
185
|
+
_write_stream=write_stream,
|
|
186
|
+
_context=context,
|
|
187
|
+
)
|
|
188
|
+
self._connections[config.name] = connection
|
|
189
|
+
|
|
190
|
+
return connection
|
|
191
|
+
|
|
192
|
+
async def connect_all(
|
|
193
|
+
self,
|
|
194
|
+
configs: list[McpServerConfig],
|
|
195
|
+
) -> dict[str, McpToolWrapper]:
|
|
196
|
+
"""Connect to multiple MCP servers and return all tools.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
configs: List of server configurations
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Dictionary of tool name to wrapper (tools from all servers)
|
|
203
|
+
"""
|
|
204
|
+
all_tools: dict[str, McpToolWrapper] = {}
|
|
205
|
+
|
|
206
|
+
for config in configs:
|
|
207
|
+
try:
|
|
208
|
+
connection = await self.connect(config)
|
|
209
|
+
# Prefix tool names with server name to avoid conflicts
|
|
210
|
+
for tool_name, wrapper in connection.tools.items():
|
|
211
|
+
# Use unprefixed name if unique, otherwise prefix
|
|
212
|
+
if tool_name in all_tools:
|
|
213
|
+
all_tools[f"{config.name}.{tool_name}"] = wrapper
|
|
214
|
+
else:
|
|
215
|
+
all_tools[tool_name] = wrapper
|
|
216
|
+
except Exception as e:
|
|
217
|
+
# Log error but continue with other servers
|
|
218
|
+
logger.warning("Failed to connect to MCP server '%s': %s", config.name, e)
|
|
219
|
+
|
|
220
|
+
return all_tools
|
|
221
|
+
|
|
222
|
+
async def disconnect_all(self) -> None:
|
|
223
|
+
"""Disconnect from all MCP servers."""
|
|
224
|
+
for connection in self._connections.values():
|
|
225
|
+
try:
|
|
226
|
+
if connection.session:
|
|
227
|
+
await connection.session.__aexit__(None, None, None)
|
|
228
|
+
if connection._context:
|
|
229
|
+
await connection._context.__aexit__(None, None, None)
|
|
230
|
+
except Exception:
|
|
231
|
+
logger.debug(
|
|
232
|
+
"Error during disconnect from MCP server '%s' (ignored)",
|
|
233
|
+
connection.name,
|
|
234
|
+
exc_info=True,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self._connections.clear()
|
|
238
|
+
|
|
239
|
+
def get_all_tools(self) -> dict[str, McpToolWrapper]:
|
|
240
|
+
"""Get all tools from all connected servers."""
|
|
241
|
+
all_tools: dict[str, McpToolWrapper] = {}
|
|
242
|
+
for connection in self._connections.values():
|
|
243
|
+
all_tools.update(connection.tools)
|
|
244
|
+
return all_tools
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _extract_tools_info(tools_result: Any) -> list[dict[str, Any]]:
|
|
248
|
+
"""Extract tool information from MCP tools result."""
|
|
249
|
+
tools_info: list[dict[str, Any]] = []
|
|
250
|
+
|
|
251
|
+
for tool in tools_result.tools:
|
|
252
|
+
info: dict[str, Any] = {
|
|
253
|
+
"name": tool.name,
|
|
254
|
+
"description": tool.description or "",
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
# Parse input schema for parameters
|
|
258
|
+
if tool.inputSchema:
|
|
259
|
+
schema = tool.inputSchema
|
|
260
|
+
if isinstance(schema, dict):
|
|
261
|
+
props = schema.get("properties", {})
|
|
262
|
+
required = schema.get("required", [])
|
|
263
|
+
|
|
264
|
+
params = []
|
|
265
|
+
for name, prop in props.items():
|
|
266
|
+
param_info = {
|
|
267
|
+
"name": name,
|
|
268
|
+
"type": prop.get("type", "any"),
|
|
269
|
+
"required": name in required,
|
|
270
|
+
"description": prop.get("description", ""),
|
|
271
|
+
}
|
|
272
|
+
params.append(param_info)
|
|
273
|
+
|
|
274
|
+
info["parameters"] = params
|
|
275
|
+
else:
|
|
276
|
+
info["parameters"] = []
|
|
277
|
+
else:
|
|
278
|
+
info["parameters"] = []
|
|
279
|
+
|
|
280
|
+
tools_info.append(info)
|
|
281
|
+
|
|
282
|
+
return tools_info
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
async def inspect_mcp_server(
|
|
286
|
+
command: str,
|
|
287
|
+
args: list[str] | None = None,
|
|
288
|
+
env: dict[str, str] | None = None,
|
|
289
|
+
) -> list[dict[str, Any]]:
|
|
290
|
+
"""Inspect a local MCP server (stdio) and return its available tools.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
command: Command to start the server
|
|
294
|
+
args: Command arguments
|
|
295
|
+
env: Environment variables
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
List of tool information dictionaries
|
|
299
|
+
"""
|
|
300
|
+
from mcp import ClientSession, StdioServerParameters
|
|
301
|
+
from mcp.client.stdio import stdio_client
|
|
302
|
+
|
|
303
|
+
# Build environment
|
|
304
|
+
full_env = os.environ.copy()
|
|
305
|
+
if env:
|
|
306
|
+
full_env.update(env)
|
|
307
|
+
|
|
308
|
+
server_params = StdioServerParameters(
|
|
309
|
+
command=command,
|
|
310
|
+
args=args or [],
|
|
311
|
+
env=full_env,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
async with stdio_client(server_params) as (read, write):
|
|
315
|
+
async with ClientSession(read, write) as session:
|
|
316
|
+
await session.initialize()
|
|
317
|
+
tools_result = await session.list_tools()
|
|
318
|
+
return _extract_tools_info(tools_result)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def inspect_mcp_server_http(
|
|
322
|
+
url: str,
|
|
323
|
+
headers: dict[str, str] | None = None,
|
|
324
|
+
transport: Literal["auto", "sse", "streamable_http"] = "auto",
|
|
325
|
+
) -> list[dict[str, Any]]:
|
|
326
|
+
"""Inspect a remote MCP server (HTTP) and return its available tools.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
url: Server URL (e.g., "https://example.com/mcp" or "https://example.com/sse")
|
|
330
|
+
headers: Optional HTTP headers (e.g., for authentication)
|
|
331
|
+
transport: Transport type ("auto", "sse", or "streamable_http")
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
List of tool information dictionaries
|
|
335
|
+
"""
|
|
336
|
+
from mcp import ClientSession
|
|
337
|
+
|
|
338
|
+
# Auto-detect transport based on URL
|
|
339
|
+
if transport == "auto":
|
|
340
|
+
if url.endswith("/sse"):
|
|
341
|
+
transport = "sse"
|
|
342
|
+
else:
|
|
343
|
+
transport = "streamable_http"
|
|
344
|
+
|
|
345
|
+
if transport == "sse":
|
|
346
|
+
from mcp.client.sse import sse_client
|
|
347
|
+
|
|
348
|
+
async with sse_client(url, headers=headers) as (read, write):
|
|
349
|
+
async with ClientSession(read, write) as session:
|
|
350
|
+
await session.initialize()
|
|
351
|
+
tools_result = await session.list_tools()
|
|
352
|
+
return _extract_tools_info(tools_result)
|
|
353
|
+
else:
|
|
354
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
355
|
+
|
|
356
|
+
async with streamablehttp_client(url, headers=headers) as (read, write, _):
|
|
357
|
+
async with ClientSession(read, write) as session:
|
|
358
|
+
await session.initialize()
|
|
359
|
+
tools_result = await session.list_tools()
|
|
360
|
+
return _extract_tools_info(tools_result)
|
sandboxy/mcp/wrapper.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""MCP tool wrapper - adapts MCP tools to Sandboxy's Tool protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from sandboxy.tools.base import ToolResult
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from mcp import ClientSession
|
|
11
|
+
from mcp.types import Tool as McpTool
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class McpToolWrapper:
|
|
15
|
+
"""Wraps an MCP tool to match Sandboxy's Tool protocol.
|
|
16
|
+
|
|
17
|
+
This allows MCP tools to be used seamlessly alongside YAML mock tools
|
|
18
|
+
in scenarios.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, session: ClientSession, tool_info: McpTool) -> None:
|
|
22
|
+
"""Initialize the wrapper.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session: Active MCP client session
|
|
26
|
+
tool_info: Tool information from MCP server
|
|
27
|
+
"""
|
|
28
|
+
self.session = session
|
|
29
|
+
self.name = tool_info.name
|
|
30
|
+
self.description = tool_info.description or ""
|
|
31
|
+
self._input_schema = tool_info.inputSchema
|
|
32
|
+
|
|
33
|
+
async def invoke_async(
|
|
34
|
+
self,
|
|
35
|
+
action: str,
|
|
36
|
+
args: dict[str, Any],
|
|
37
|
+
env_state: dict[str, Any],
|
|
38
|
+
) -> ToolResult:
|
|
39
|
+
"""Invoke the MCP tool asynchronously.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
action: Action name (ignored for MCP tools - they're single-action)
|
|
43
|
+
args: Arguments to pass to the tool
|
|
44
|
+
env_state: Environment state (not used by MCP tools)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
ToolResult with success/error and data
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
result = await self.session.call_tool(self.name, args)
|
|
51
|
+
|
|
52
|
+
# Extract content from MCP result
|
|
53
|
+
# MCP tools return a list of content blocks
|
|
54
|
+
if result.content:
|
|
55
|
+
# Combine text content
|
|
56
|
+
text_parts = []
|
|
57
|
+
for block in result.content:
|
|
58
|
+
if hasattr(block, "text"):
|
|
59
|
+
text_parts.append(block.text)
|
|
60
|
+
elif hasattr(block, "data"):
|
|
61
|
+
text_parts.append(str(block.data))
|
|
62
|
+
|
|
63
|
+
data = "\n".join(text_parts) if text_parts else result.content
|
|
64
|
+
|
|
65
|
+
if result.isError:
|
|
66
|
+
return ToolResult(success=False, error=str(data))
|
|
67
|
+
|
|
68
|
+
return ToolResult(success=True, data=data)
|
|
69
|
+
|
|
70
|
+
return ToolResult(success=True, data=None)
|
|
71
|
+
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return ToolResult(success=False, error=str(e))
|
|
74
|
+
|
|
75
|
+
def invoke(
|
|
76
|
+
self,
|
|
77
|
+
action: str,
|
|
78
|
+
args: dict[str, Any],
|
|
79
|
+
env_state: dict[str, Any],
|
|
80
|
+
) -> ToolResult:
|
|
81
|
+
"""Synchronous invoke - raises error, use invoke_async instead."""
|
|
82
|
+
raise RuntimeError("MCP tools must be invoked asynchronously. Use invoke_async() instead.")
|
|
83
|
+
|
|
84
|
+
def get_actions(self) -> list[dict[str, Any]]:
|
|
85
|
+
"""Get available actions with their schemas.
|
|
86
|
+
|
|
87
|
+
MCP tools are single-action, so we return a single "call" action.
|
|
88
|
+
"""
|
|
89
|
+
return [
|
|
90
|
+
{
|
|
91
|
+
"name": "call",
|
|
92
|
+
"description": self.description,
|
|
93
|
+
"parameters": self._input_schema
|
|
94
|
+
or {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"properties": {},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Multi-model provider abstraction layer.
|
|
2
|
+
|
|
3
|
+
Supports multiple LLM providers through a unified interface:
|
|
4
|
+
- OpenRouter (400+ models via single API)
|
|
5
|
+
- OpenAI (direct)
|
|
6
|
+
- Anthropic (direct)
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from sandboxy.providers import get_provider, ProviderRegistry
|
|
10
|
+
|
|
11
|
+
# Get provider for a specific model
|
|
12
|
+
provider = get_provider("openai/gpt-4o")
|
|
13
|
+
response = await provider.complete("openai/gpt-4o", messages)
|
|
14
|
+
|
|
15
|
+
# Or use the registry
|
|
16
|
+
registry = ProviderRegistry()
|
|
17
|
+
provider = registry.get_provider_for_model("anthropic/claude-3-opus")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from sandboxy.providers.base import (
|
|
21
|
+
BaseProvider,
|
|
22
|
+
ModelResponse,
|
|
23
|
+
ProviderError,
|
|
24
|
+
)
|
|
25
|
+
from sandboxy.providers.registry import ProviderRegistry, get_provider, get_registry
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"BaseProvider",
|
|
29
|
+
"ModelResponse",
|
|
30
|
+
"ProviderError",
|
|
31
|
+
"ProviderRegistry",
|
|
32
|
+
"get_provider",
|
|
33
|
+
"get_registry",
|
|
34
|
+
]
|