mcp-use 1.3.11__py3-none-any.whl → 1.3.13__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.
Potentially problematic release.
This version of mcp-use might be problematic. Click here for more details.
- mcp_use/__init__.py +1 -1
- mcp_use/adapters/.deprecated +0 -0
- mcp_use/adapters/__init__.py +18 -7
- mcp_use/adapters/base.py +12 -185
- mcp_use/adapters/langchain_adapter.py +12 -264
- mcp_use/agents/adapters/__init__.py +10 -0
- mcp_use/agents/adapters/base.py +193 -0
- mcp_use/agents/adapters/langchain_adapter.py +228 -0
- mcp_use/agents/base.py +1 -1
- mcp_use/agents/managers/__init__.py +19 -0
- mcp_use/agents/managers/base.py +36 -0
- mcp_use/agents/managers/server_manager.py +131 -0
- mcp_use/agents/managers/tools/__init__.py +15 -0
- mcp_use/agents/managers/tools/base_tool.py +19 -0
- mcp_use/agents/managers/tools/connect_server.py +69 -0
- mcp_use/agents/managers/tools/disconnect_server.py +43 -0
- mcp_use/agents/managers/tools/get_active_server.py +29 -0
- mcp_use/agents/managers/tools/list_servers_tool.py +53 -0
- mcp_use/agents/managers/tools/search_tools.py +328 -0
- mcp_use/agents/mcpagent.py +88 -47
- mcp_use/agents/remote.py +168 -129
- mcp_use/auth/.deprecated +0 -0
- mcp_use/auth/__init__.py +19 -4
- mcp_use/auth/bearer.py +11 -12
- mcp_use/auth/oauth.py +11 -620
- mcp_use/auth/oauth_callback.py +16 -207
- mcp_use/client/__init__.py +1 -0
- mcp_use/client/auth/__init__.py +6 -0
- mcp_use/client/auth/bearer.py +23 -0
- mcp_use/client/auth/oauth.py +629 -0
- mcp_use/client/auth/oauth_callback.py +214 -0
- mcp_use/client/client.py +356 -0
- mcp_use/client/config.py +106 -0
- mcp_use/client/connectors/__init__.py +20 -0
- mcp_use/client/connectors/base.py +470 -0
- mcp_use/client/connectors/http.py +304 -0
- mcp_use/client/connectors/sandbox.py +332 -0
- mcp_use/client/connectors/stdio.py +109 -0
- mcp_use/client/connectors/utils.py +13 -0
- mcp_use/client/connectors/websocket.py +257 -0
- mcp_use/client/exceptions.py +31 -0
- mcp_use/client/middleware/__init__.py +50 -0
- mcp_use/client/middleware/logging.py +31 -0
- mcp_use/client/middleware/metrics.py +314 -0
- mcp_use/client/middleware/middleware.py +266 -0
- mcp_use/client/session.py +162 -0
- mcp_use/client/task_managers/__init__.py +20 -0
- mcp_use/client/task_managers/base.py +145 -0
- mcp_use/client/task_managers/sse.py +84 -0
- mcp_use/client/task_managers/stdio.py +69 -0
- mcp_use/client/task_managers/streamable_http.py +86 -0
- mcp_use/client/task_managers/websocket.py +68 -0
- mcp_use/client.py +12 -320
- mcp_use/config.py +20 -92
- mcp_use/connectors/.deprecated +0 -0
- mcp_use/connectors/__init__.py +46 -20
- mcp_use/connectors/base.py +12 -447
- mcp_use/connectors/http.py +13 -288
- mcp_use/connectors/sandbox.py +13 -297
- mcp_use/connectors/stdio.py +13 -96
- mcp_use/connectors/utils.py +15 -8
- mcp_use/connectors/websocket.py +13 -252
- mcp_use/exceptions.py +33 -18
- mcp_use/managers/.deprecated +0 -0
- mcp_use/managers/__init__.py +56 -17
- mcp_use/managers/base.py +13 -31
- mcp_use/managers/server_manager.py +13 -119
- mcp_use/managers/tools/__init__.py +45 -15
- mcp_use/managers/tools/base_tool.py +5 -16
- mcp_use/managers/tools/connect_server.py +5 -67
- mcp_use/managers/tools/disconnect_server.py +5 -41
- mcp_use/managers/tools/get_active_server.py +5 -26
- mcp_use/managers/tools/list_servers_tool.py +5 -51
- mcp_use/managers/tools/search_tools.py +17 -321
- mcp_use/middleware/.deprecated +0 -0
- mcp_use/middleware/__init__.py +89 -0
- mcp_use/middleware/logging.py +19 -0
- mcp_use/middleware/metrics.py +41 -0
- mcp_use/middleware/middleware.py +55 -0
- mcp_use/session.py +13 -149
- mcp_use/task_managers/.deprecated +0 -0
- mcp_use/task_managers/__init__.py +48 -20
- mcp_use/task_managers/base.py +13 -140
- mcp_use/task_managers/sse.py +13 -79
- mcp_use/task_managers/stdio.py +13 -64
- mcp_use/task_managers/streamable_http.py +15 -81
- mcp_use/task_managers/websocket.py +13 -63
- mcp_use/telemetry/events.py +58 -0
- mcp_use/telemetry/telemetry.py +71 -1
- mcp_use/types/.deprecated +0 -0
- mcp_use/types/sandbox.py +13 -18
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.13.dist-info}/METADATA +66 -40
- mcp_use-1.3.13.dist-info/RECORD +109 -0
- mcp_use-1.3.11.dist-info/RECORD +0 -60
- mcp_use-1.3.11.dist-info/licenses/LICENSE +0 -21
- /mcp_use/{observability → agents/observability}/__init__.py +0 -0
- /mcp_use/{observability → agents/observability}/callbacks_manager.py +0 -0
- /mcp_use/{observability → agents/observability}/laminar.py +0 -0
- /mcp_use/{observability → agents/observability}/langfuse.py +0 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.13.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.11.dist-info → mcp_use-1.3.13.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP connector for MCP implementations.
|
|
3
|
+
|
|
4
|
+
This module provides a connector for communicating with MCP implementations
|
|
5
|
+
through HTTP APIs with SSE or Streamable HTTP for transport.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from mcp import ClientSession
|
|
12
|
+
from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
13
|
+
from mcp.shared.exceptions import McpError
|
|
14
|
+
|
|
15
|
+
from mcp_use.client.auth.oauth import BearerAuth, OAuth, OAuthClientProvider
|
|
16
|
+
from mcp_use.client.exceptions import OAuthAuthenticationError, OAuthDiscoveryError
|
|
17
|
+
from mcp_use.client.middleware import CallbackClientSession, Middleware
|
|
18
|
+
from mcp_use.client.task_managers import SseConnectionManager, StreamableHttpConnectionManager
|
|
19
|
+
from mcp_use.logging import logger
|
|
20
|
+
|
|
21
|
+
from .base import BaseConnector
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HttpConnector(BaseConnector):
|
|
25
|
+
"""Connector for MCP implementations using HTTP transport with SSE or streamable HTTP.
|
|
26
|
+
|
|
27
|
+
This connector uses HTTP/SSE or streamable HTTP to communicate with remote MCP implementations,
|
|
28
|
+
using a connection manager to handle the proper lifecycle management.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
base_url: str,
|
|
34
|
+
headers: dict[str, str] | None = None,
|
|
35
|
+
timeout: float = 5,
|
|
36
|
+
sse_read_timeout: float = 60 * 5,
|
|
37
|
+
auth: str | dict[str, Any] | httpx.Auth | None = None,
|
|
38
|
+
sampling_callback: SamplingFnT | None = None,
|
|
39
|
+
elicitation_callback: ElicitationFnT | None = None,
|
|
40
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
41
|
+
logging_callback: LoggingFnT | None = None,
|
|
42
|
+
middleware: list[Middleware] | None = None,
|
|
43
|
+
):
|
|
44
|
+
"""Initialize a new HTTP connector.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
base_url: The base URL of the MCP HTTP API.
|
|
48
|
+
headers: Optional additional headers.
|
|
49
|
+
timeout: Timeout for HTTP operations in seconds.
|
|
50
|
+
sse_read_timeout: Timeout for SSE read operations in seconds.
|
|
51
|
+
auth: Authentication method - can be:
|
|
52
|
+
- A string token: Use Bearer token authentication
|
|
53
|
+
- A dict with OAuth config: {"client_id": "...", "client_secret": "...", "scope": "..."}
|
|
54
|
+
- An httpx.Auth object: Use custom authentication
|
|
55
|
+
sampling_callback: Optional sampling callback.
|
|
56
|
+
elicitation_callback: Optional elicitation callback.
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(
|
|
59
|
+
sampling_callback=sampling_callback,
|
|
60
|
+
elicitation_callback=elicitation_callback,
|
|
61
|
+
message_handler=message_handler,
|
|
62
|
+
logging_callback=logging_callback,
|
|
63
|
+
middleware=middleware,
|
|
64
|
+
)
|
|
65
|
+
self.base_url = base_url.rstrip("/")
|
|
66
|
+
self.headers = headers or {}
|
|
67
|
+
self.timeout = timeout
|
|
68
|
+
self.sse_read_timeout = sse_read_timeout
|
|
69
|
+
self._auth: httpx.Auth | None = None
|
|
70
|
+
self._oauth: OAuth | None = None
|
|
71
|
+
|
|
72
|
+
# Handle authentication
|
|
73
|
+
if auth is not None:
|
|
74
|
+
self._set_auth(auth)
|
|
75
|
+
|
|
76
|
+
def _set_auth(self, auth: str | dict[str, Any] | httpx.Auth) -> None:
|
|
77
|
+
"""Set authentication method.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
auth: Authentication method - can be:
|
|
81
|
+
- A string token: Use Bearer token authentication
|
|
82
|
+
- A dict with OAuth config: {"client_id": "...", "client_secret": "...", "scope": "..."}
|
|
83
|
+
- An httpx.Auth object: Use custom authentication
|
|
84
|
+
"""
|
|
85
|
+
if isinstance(auth, str):
|
|
86
|
+
# Treat as bearer token
|
|
87
|
+
self._auth = BearerAuth(token=auth)
|
|
88
|
+
self.headers["Authorization"] = f"Bearer {auth}"
|
|
89
|
+
elif isinstance(auth, dict):
|
|
90
|
+
# Check if this is an OAuth provider configuration
|
|
91
|
+
if "oauth_provider" in auth:
|
|
92
|
+
oauth_provider = auth["oauth_provider"]
|
|
93
|
+
if isinstance(oauth_provider, dict):
|
|
94
|
+
oauth_provider = OAuthClientProvider(**oauth_provider)
|
|
95
|
+
self._oauth = OAuth(
|
|
96
|
+
self.base_url,
|
|
97
|
+
scope=auth.get("scope"),
|
|
98
|
+
client_id=auth.get("client_id"),
|
|
99
|
+
client_secret=auth.get("client_secret"),
|
|
100
|
+
callback_port=auth.get("callback_port"),
|
|
101
|
+
oauth_provider=oauth_provider,
|
|
102
|
+
)
|
|
103
|
+
self._oauth_config = auth
|
|
104
|
+
else:
|
|
105
|
+
self._oauth = OAuth(
|
|
106
|
+
self.base_url,
|
|
107
|
+
scope=auth.get("scope"),
|
|
108
|
+
client_id=auth.get("client_id"),
|
|
109
|
+
client_secret=auth.get("client_secret"),
|
|
110
|
+
callback_port=auth.get("callback_port"),
|
|
111
|
+
)
|
|
112
|
+
self._oauth_config = auth
|
|
113
|
+
elif isinstance(auth, httpx.Auth):
|
|
114
|
+
self._auth = auth
|
|
115
|
+
else:
|
|
116
|
+
raise ValueError(f"Invalid auth type: {type(auth)}")
|
|
117
|
+
|
|
118
|
+
async def connect(self) -> None:
|
|
119
|
+
"""Establish a connection to the MCP implementation."""
|
|
120
|
+
if self._connected:
|
|
121
|
+
logger.debug("Already connected to MCP implementation")
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Handle OAuth if needed
|
|
125
|
+
if self._oauth:
|
|
126
|
+
try:
|
|
127
|
+
# Create a temporary client for OAuth metadata discovery
|
|
128
|
+
async with httpx.AsyncClient() as client:
|
|
129
|
+
bearer_auth = await self._oauth.initialize(client)
|
|
130
|
+
if not bearer_auth:
|
|
131
|
+
# Need to perform OAuth flow
|
|
132
|
+
logger.info("OAuth authentication required")
|
|
133
|
+
bearer_auth = await self._oauth.authenticate()
|
|
134
|
+
|
|
135
|
+
# Update auth and headers
|
|
136
|
+
self._auth = bearer_auth
|
|
137
|
+
self.headers["Authorization"] = f"Bearer {bearer_auth.token.get_secret_value()}"
|
|
138
|
+
except OAuthDiscoveryError:
|
|
139
|
+
# OAuth discovery failed - it means server doesn't support OAuth default urls
|
|
140
|
+
logger.debug("OAuth discovery failed, continuing without initialization.")
|
|
141
|
+
self._oauth = None
|
|
142
|
+
self._auth = None
|
|
143
|
+
except OAuthAuthenticationError as e:
|
|
144
|
+
logger.error(f"OAuth initialization failed: {e}")
|
|
145
|
+
raise
|
|
146
|
+
|
|
147
|
+
# Try streamable HTTP first (new transport), fall back to SSE (old transport)
|
|
148
|
+
# This implements backwards compatibility per MCP specification
|
|
149
|
+
self.transport_type = None
|
|
150
|
+
connection_manager = None
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
# First, try the new streamable HTTP transport
|
|
154
|
+
logger.debug(f"Attempting streamable HTTP connection to: {self.base_url}")
|
|
155
|
+
connection_manager = StreamableHttpConnectionManager(
|
|
156
|
+
self.base_url, self.headers, self.timeout, self.sse_read_timeout, auth=self._auth
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# Test if this is a streamable HTTP server by attempting initialization
|
|
160
|
+
read_stream, write_stream = await connection_manager.start()
|
|
161
|
+
|
|
162
|
+
# Test if this actually works by trying to create a client session and initialize it
|
|
163
|
+
raw_test_client = ClientSession(
|
|
164
|
+
read_stream,
|
|
165
|
+
write_stream,
|
|
166
|
+
sampling_callback=self.sampling_callback,
|
|
167
|
+
elicitation_callback=self.elicitation_callback,
|
|
168
|
+
message_handler=self._internal_message_handler,
|
|
169
|
+
logging_callback=self.logging_callback,
|
|
170
|
+
client_info=self.client_info,
|
|
171
|
+
)
|
|
172
|
+
await raw_test_client.__aenter__()
|
|
173
|
+
|
|
174
|
+
# Wrap test client with middleware temporarily for testing
|
|
175
|
+
test_client = CallbackClientSession(raw_test_client, self.public_identifier, self.middleware_manager)
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
# Try to initialize - this is where streamable HTTP vs SSE difference should show up
|
|
179
|
+
result = await test_client.initialize()
|
|
180
|
+
logger.debug(f"Streamable HTTP initialization result: {result}")
|
|
181
|
+
|
|
182
|
+
# If we get here, streamable HTTP works
|
|
183
|
+
self.client_session = test_client
|
|
184
|
+
self.transport_type = "streamable HTTP"
|
|
185
|
+
self._initialized = True # Mark as initialized since we just called initialize()
|
|
186
|
+
|
|
187
|
+
# Populate tools, resources, and prompts since we've initialized
|
|
188
|
+
server_capabilities = result.capabilities
|
|
189
|
+
|
|
190
|
+
if server_capabilities.tools:
|
|
191
|
+
# Get available tools directly from client session
|
|
192
|
+
tools_result = await self.client_session.list_tools()
|
|
193
|
+
self._tools = tools_result.tools if tools_result else []
|
|
194
|
+
else:
|
|
195
|
+
self._tools = []
|
|
196
|
+
|
|
197
|
+
if server_capabilities.resources:
|
|
198
|
+
# Get available resources directly from client session
|
|
199
|
+
resources_result = await self.client_session.list_resources()
|
|
200
|
+
self._resources = resources_result.resources if resources_result else []
|
|
201
|
+
else:
|
|
202
|
+
self._resources = []
|
|
203
|
+
|
|
204
|
+
if server_capabilities.prompts:
|
|
205
|
+
# Get available prompts directly from client session
|
|
206
|
+
prompts_result = await self.client_session.list_prompts()
|
|
207
|
+
self._prompts = prompts_result.prompts if prompts_result else []
|
|
208
|
+
else:
|
|
209
|
+
self._prompts = []
|
|
210
|
+
|
|
211
|
+
# Only McpError is raised from client's initialization because
|
|
212
|
+
# exceptions are handled internally.
|
|
213
|
+
except McpError as mcp_error:
|
|
214
|
+
logger.error("MCP protocol error during initialization: %s", mcp_error.error)
|
|
215
|
+
# Clean up the test client
|
|
216
|
+
try:
|
|
217
|
+
await raw_test_client.__aexit__(None, None, None)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
raise mcp_error
|
|
221
|
+
|
|
222
|
+
except Exception as init_error:
|
|
223
|
+
# This catches non-McpError exceptions, like a direct httpx timeout
|
|
224
|
+
# but in the most cases this won't happen. It's for safety.
|
|
225
|
+
try:
|
|
226
|
+
await raw_test_client.__aexit__(None, None, None)
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
raise init_error
|
|
230
|
+
|
|
231
|
+
# Exception from the inner try is propagated here and in
|
|
232
|
+
# the most cases is an McpError, so checking instances is useless
|
|
233
|
+
except Exception as streamable_error:
|
|
234
|
+
logger.debug(f"Streamable HTTP failed: {streamable_error}")
|
|
235
|
+
|
|
236
|
+
# Clean up the failed streamable HTTP connection manager
|
|
237
|
+
if connection_manager:
|
|
238
|
+
try:
|
|
239
|
+
await connection_manager.close()
|
|
240
|
+
except Exception:
|
|
241
|
+
pass
|
|
242
|
+
|
|
243
|
+
# It doesn't make sense to check error types. Because client
|
|
244
|
+
# always return a McpError, if he can't reach the server
|
|
245
|
+
# because it's offline, or if it has an auth problem.
|
|
246
|
+
should_fallback = True
|
|
247
|
+
|
|
248
|
+
if should_fallback:
|
|
249
|
+
try:
|
|
250
|
+
# Fall back to the old SSE transport
|
|
251
|
+
logger.debug(f"Attempting SSE fallback connection to: {self.base_url}")
|
|
252
|
+
connection_manager = SseConnectionManager(
|
|
253
|
+
self.base_url, self.headers, self.timeout, self.sse_read_timeout, auth=self._auth
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
read_stream, write_stream = await connection_manager.start()
|
|
257
|
+
|
|
258
|
+
# Create the client session for SSE
|
|
259
|
+
raw_client_session = ClientSession(
|
|
260
|
+
read_stream,
|
|
261
|
+
write_stream,
|
|
262
|
+
sampling_callback=self.sampling_callback,
|
|
263
|
+
elicitation_callback=self.elicitation_callback,
|
|
264
|
+
message_handler=self._internal_message_handler,
|
|
265
|
+
logging_callback=self.logging_callback,
|
|
266
|
+
client_info=self.client_info,
|
|
267
|
+
)
|
|
268
|
+
await raw_client_session.__aenter__()
|
|
269
|
+
|
|
270
|
+
# Wrap with middleware
|
|
271
|
+
self.client_session = CallbackClientSession(
|
|
272
|
+
raw_client_session, self.public_identifier, self.middleware_manager
|
|
273
|
+
)
|
|
274
|
+
self.transport_type = "SSE"
|
|
275
|
+
|
|
276
|
+
except* Exception as sse_error:
|
|
277
|
+
# Get the exception from the ExceptionGroup, and here we will get the correct type.
|
|
278
|
+
sse_error = sse_error.exceptions[0]
|
|
279
|
+
if isinstance(sse_error, httpx.HTTPStatusError) and sse_error.response.status_code in [
|
|
280
|
+
401,
|
|
281
|
+
403,
|
|
282
|
+
407,
|
|
283
|
+
]:
|
|
284
|
+
raise OAuthAuthenticationError(
|
|
285
|
+
f"Server requires authentication (HTTP {sse_error.response.status_code}) "
|
|
286
|
+
"but auth failed. Please provide auth configuration manually."
|
|
287
|
+
) from sse_error
|
|
288
|
+
logger.error(
|
|
289
|
+
f"Both transport methods failed. Streamable HTTP: {streamable_error}, SSE: {sse_error}"
|
|
290
|
+
)
|
|
291
|
+
raise sse_error
|
|
292
|
+
else:
|
|
293
|
+
raise streamable_error
|
|
294
|
+
|
|
295
|
+
# Store the successful connection manager and mark as connected
|
|
296
|
+
self._connection_manager = connection_manager
|
|
297
|
+
self._connected = True
|
|
298
|
+
logger.debug(f"Successfully connected to MCP implementation via {self.transport_type}: {self.base_url}")
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def public_identifier(self) -> str:
|
|
302
|
+
"""Get the identifier for the connector."""
|
|
303
|
+
transport_type = getattr(self, "transport_type", "http")
|
|
304
|
+
return f"{transport_type}:{self.base_url}"
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sandbox connector for MCP implementations.
|
|
3
|
+
|
|
4
|
+
This module provides a connector for communicating with MCP implementations
|
|
5
|
+
that are executed inside a sandbox environment (currently using E2B).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
import aiohttp
|
|
14
|
+
from mcp import ClientSession
|
|
15
|
+
from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
16
|
+
|
|
17
|
+
from mcp_use.client.middleware import CallbackClientSession, Middleware
|
|
18
|
+
from mcp_use.client.task_managers import SseConnectionManager
|
|
19
|
+
from mcp_use.logging import logger
|
|
20
|
+
|
|
21
|
+
# Import E2B SDK components (optional dependency)
|
|
22
|
+
try:
|
|
23
|
+
logger.debug("Attempting to import e2b_code_interpreter...")
|
|
24
|
+
from e2b_code_interpreter import CommandHandle, Sandbox
|
|
25
|
+
|
|
26
|
+
logger.debug("Successfully imported e2b_code_interpreter")
|
|
27
|
+
except ImportError as e:
|
|
28
|
+
logger.debug(f"Failed to import e2b_code_interpreter: {e}")
|
|
29
|
+
CommandHandle = None
|
|
30
|
+
Sandbox = None
|
|
31
|
+
|
|
32
|
+
from typing import NotRequired, TypedDict
|
|
33
|
+
|
|
34
|
+
from .base import BaseConnector
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SandboxOptions(TypedDict):
|
|
38
|
+
"""Configuration options for sandbox execution.
|
|
39
|
+
|
|
40
|
+
This type defines the configuration options available when running
|
|
41
|
+
MCP servers in a sandboxed environment (e.g., using E2B).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
api_key: str
|
|
45
|
+
"""Direct API key for sandbox provider (e.g., E2B API key).
|
|
46
|
+
If not provided, will use E2B_API_KEY environment variable."""
|
|
47
|
+
|
|
48
|
+
sandbox_template_id: NotRequired[str]
|
|
49
|
+
"""Template ID for the sandbox environment.
|
|
50
|
+
Default: 'base'"""
|
|
51
|
+
|
|
52
|
+
supergateway_command: NotRequired[str]
|
|
53
|
+
"""Command to run supergateway.
|
|
54
|
+
Default: 'npx -y supergateway'"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SandboxConnector(BaseConnector):
|
|
58
|
+
"""Connector for MCP implementations running in a sandbox environment.
|
|
59
|
+
|
|
60
|
+
This connector runs a user-defined stdio command within a sandbox environment,
|
|
61
|
+
currently implemented using E2B, potentially wrapped by a utility like 'supergateway'
|
|
62
|
+
to expose its stdio.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
command: str,
|
|
68
|
+
args: list[str],
|
|
69
|
+
env: dict[str, str] | None = None,
|
|
70
|
+
e2b_options: SandboxOptions | None = None,
|
|
71
|
+
timeout: float = 5,
|
|
72
|
+
sse_read_timeout: float = 60 * 5,
|
|
73
|
+
sampling_callback: SamplingFnT | None = None,
|
|
74
|
+
elicitation_callback: ElicitationFnT | None = None,
|
|
75
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
76
|
+
logging_callback: LoggingFnT | None = None,
|
|
77
|
+
middleware: list[Middleware] | None = None,
|
|
78
|
+
):
|
|
79
|
+
"""Initialize a new sandbox connector.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
command: The user's MCP server command to execute in the sandbox.
|
|
83
|
+
args: Command line arguments for the user's MCP server command.
|
|
84
|
+
env: Environment variables for the user's MCP server command.
|
|
85
|
+
e2b_options: Configuration options for the E2B sandbox environment.
|
|
86
|
+
See SandboxOptions for available options and defaults.
|
|
87
|
+
timeout: Timeout for the sandbox process in seconds.
|
|
88
|
+
sse_read_timeout: Timeout for the SSE connection in seconds.
|
|
89
|
+
sampling_callback: Optional sampling callback.
|
|
90
|
+
elicitation_callback: Optional elicitation callback.
|
|
91
|
+
"""
|
|
92
|
+
super().__init__(
|
|
93
|
+
sampling_callback=sampling_callback,
|
|
94
|
+
elicitation_callback=elicitation_callback,
|
|
95
|
+
message_handler=message_handler,
|
|
96
|
+
logging_callback=logging_callback,
|
|
97
|
+
middleware=middleware,
|
|
98
|
+
)
|
|
99
|
+
if Sandbox is None:
|
|
100
|
+
raise ImportError(
|
|
101
|
+
"E2B SDK (e2b-code-interpreter) not found. Please install it with "
|
|
102
|
+
"'pip install mcp-use[e2b]' (or 'pip install e2b-code-interpreter')."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.user_command = command
|
|
106
|
+
self.user_args = args or []
|
|
107
|
+
self.user_env = env or {}
|
|
108
|
+
|
|
109
|
+
_e2b_options = e2b_options or {}
|
|
110
|
+
|
|
111
|
+
self.api_key = _e2b_options.get("api_key") or os.environ.get("E2B_API_KEY")
|
|
112
|
+
if not self.api_key:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
"E2B API key is required. Provide it via 'sandbox_options.api_key'"
|
|
115
|
+
" or the E2B_API_KEY environment variable."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
self.sandbox_template_id = _e2b_options.get("sandbox_template_id", "base")
|
|
119
|
+
self.supergateway_cmd_parts = _e2b_options.get("supergateway_command", "npx -y supergateway")
|
|
120
|
+
|
|
121
|
+
self.sandbox: Sandbox | None = None
|
|
122
|
+
self.process: CommandHandle | None = None
|
|
123
|
+
self.client_session: ClientSession | None = None
|
|
124
|
+
self.errlog = sys.stderr
|
|
125
|
+
self.base_url: str | None = None
|
|
126
|
+
self._connected = False
|
|
127
|
+
self._connection_manager: SseConnectionManager | None = None
|
|
128
|
+
|
|
129
|
+
# SSE connection parameters
|
|
130
|
+
self.headers = {}
|
|
131
|
+
self.timeout = timeout
|
|
132
|
+
self.sse_read_timeout = sse_read_timeout
|
|
133
|
+
|
|
134
|
+
self.stdout_lines: list[str] = []
|
|
135
|
+
self.stderr_lines: list[str] = []
|
|
136
|
+
self._server_ready = asyncio.Event()
|
|
137
|
+
|
|
138
|
+
def _handle_stdout(self, data: str) -> None:
|
|
139
|
+
"""Handle stdout data from the sandbox process."""
|
|
140
|
+
self.stdout_lines.append(data)
|
|
141
|
+
logger.debug(f"[SANDBOX STDOUT] {data}", end="", flush=True)
|
|
142
|
+
|
|
143
|
+
def _handle_stderr(self, data: str) -> None:
|
|
144
|
+
"""Handle stderr data from the sandbox process."""
|
|
145
|
+
self.stderr_lines.append(data)
|
|
146
|
+
logger.debug(f"[SANDBOX STDERR] {data}", file=self.errlog, end="", flush=True)
|
|
147
|
+
|
|
148
|
+
async def wait_for_server_response(self, base_url: str, timeout: int = 30) -> bool:
|
|
149
|
+
"""Wait for the server to respond to HTTP requests.
|
|
150
|
+
Args:
|
|
151
|
+
base_url: The base URL to check for server readiness
|
|
152
|
+
timeout: Maximum time to wait in seconds
|
|
153
|
+
Returns:
|
|
154
|
+
True if server is responding, raises TimeoutError otherwise
|
|
155
|
+
"""
|
|
156
|
+
logger.info(f"Waiting for server at {base_url} to respond...")
|
|
157
|
+
sys.stdout.flush()
|
|
158
|
+
|
|
159
|
+
start_time = time.time()
|
|
160
|
+
ping_url = f"{base_url}/sse"
|
|
161
|
+
|
|
162
|
+
# Try to connect to the server
|
|
163
|
+
while time.time() - start_time < timeout:
|
|
164
|
+
try:
|
|
165
|
+
async with aiohttp.ClientSession() as session:
|
|
166
|
+
try:
|
|
167
|
+
# First try the endpoint
|
|
168
|
+
async with session.get(ping_url, timeout=2) as response:
|
|
169
|
+
if response.status == 200:
|
|
170
|
+
elapsed = time.time() - start_time
|
|
171
|
+
logger.info(f"Server is ready! SSE endpoint responded with 200 after {elapsed:.1f}s")
|
|
172
|
+
return True
|
|
173
|
+
except Exception:
|
|
174
|
+
# If sse endpoint doesn't work, try the base URL
|
|
175
|
+
async with session.get(base_url, timeout=2) as response:
|
|
176
|
+
if response.status < 500: # Accept any non-server error
|
|
177
|
+
elapsed = time.time() - start_time
|
|
178
|
+
logger.info(
|
|
179
|
+
f"Server is ready! Base URL responded with {response.status} after {elapsed:.1f}s"
|
|
180
|
+
)
|
|
181
|
+
return True
|
|
182
|
+
except Exception:
|
|
183
|
+
# Wait a bit before trying again
|
|
184
|
+
await asyncio.sleep(0.5)
|
|
185
|
+
continue
|
|
186
|
+
|
|
187
|
+
# If we get here, the request failed
|
|
188
|
+
await asyncio.sleep(0.5)
|
|
189
|
+
|
|
190
|
+
# Log status every 5 seconds
|
|
191
|
+
elapsed = time.time() - start_time
|
|
192
|
+
if int(elapsed) % 5 == 0:
|
|
193
|
+
logger.info(f"Still waiting for server to respond... ({elapsed:.1f}s elapsed)")
|
|
194
|
+
sys.stdout.flush()
|
|
195
|
+
|
|
196
|
+
# If we get here, we timed out
|
|
197
|
+
raise TimeoutError(f"Timeout waiting for server to respond (waited {timeout} seconds)")
|
|
198
|
+
|
|
199
|
+
async def connect(self):
|
|
200
|
+
"""Connect to the sandbox and start the MCP server."""
|
|
201
|
+
|
|
202
|
+
if self._connected:
|
|
203
|
+
logger.debug("Already connected to MCP implementation")
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
logger.debug("Connecting to MCP implementation in sandbox")
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
# Create and start the sandbox
|
|
210
|
+
self.sandbox = Sandbox(
|
|
211
|
+
template=self.sandbox_template_id,
|
|
212
|
+
api_key=self.api_key,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Get the host for the sandbox
|
|
216
|
+
host = self.sandbox.get_host(3000)
|
|
217
|
+
self.base_url = f"https://{host}".rstrip("/")
|
|
218
|
+
|
|
219
|
+
# Append command with args
|
|
220
|
+
command = f"{self.user_command} {' '.join(self.user_args)}"
|
|
221
|
+
|
|
222
|
+
# Construct the full command with supergateway
|
|
223
|
+
full_command = f'{self.supergateway_cmd_parts} \
|
|
224
|
+
--base-url {self.base_url} \
|
|
225
|
+
--port 3000 \
|
|
226
|
+
--cors \
|
|
227
|
+
--stdio "{command}"'
|
|
228
|
+
|
|
229
|
+
logger.debug(f"Full command: {full_command}")
|
|
230
|
+
|
|
231
|
+
# Start the process in the sandbox with our stdout/stderr handlers
|
|
232
|
+
self.process: CommandHandle = self.sandbox.commands.run(
|
|
233
|
+
full_command,
|
|
234
|
+
envs=self.user_env,
|
|
235
|
+
timeout=1000 * 60 * 10, # 10 minutes timeout
|
|
236
|
+
background=True,
|
|
237
|
+
on_stdout=self._handle_stdout,
|
|
238
|
+
on_stderr=self._handle_stderr,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Wait for the server to be ready
|
|
242
|
+
await self.wait_for_server_response(self.base_url, timeout=30)
|
|
243
|
+
logger.debug("Initializing connection manager...")
|
|
244
|
+
|
|
245
|
+
# Create the SSE connection URL
|
|
246
|
+
sse_url = f"{self.base_url}/sse"
|
|
247
|
+
|
|
248
|
+
# Create and start the connection manager
|
|
249
|
+
self._connection_manager = SseConnectionManager(sse_url, self.headers, self.timeout, self.sse_read_timeout)
|
|
250
|
+
read_stream, write_stream = await self._connection_manager.start()
|
|
251
|
+
|
|
252
|
+
# Create the client session
|
|
253
|
+
raw_client_session = ClientSession(
|
|
254
|
+
read_stream,
|
|
255
|
+
write_stream,
|
|
256
|
+
sampling_callback=self.sampling_callback,
|
|
257
|
+
elicitation_callback=self.elicitation_callback,
|
|
258
|
+
message_handler=self._internal_message_handler,
|
|
259
|
+
logging_callback=self.logging_callback,
|
|
260
|
+
client_info=self.client_info,
|
|
261
|
+
)
|
|
262
|
+
await raw_client_session.__aenter__()
|
|
263
|
+
|
|
264
|
+
# Wrap with middleware
|
|
265
|
+
self.client_session = CallbackClientSession(
|
|
266
|
+
raw_client_session, self.public_identifier, self.middleware_manager
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
# Mark as connected
|
|
270
|
+
self._connected = True
|
|
271
|
+
logger.debug(f"Successfully connected to MCP implementation via HTTP/SSE: {self.base_url}")
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.error(f"Failed to connect to MCP implementation: {e}")
|
|
275
|
+
|
|
276
|
+
# Clean up any resources if connection failed
|
|
277
|
+
await self._cleanup_resources()
|
|
278
|
+
|
|
279
|
+
raise e
|
|
280
|
+
|
|
281
|
+
async def _cleanup_resources(self) -> None:
|
|
282
|
+
"""Clean up all resources associated with this connector, including the sandbox.
|
|
283
|
+
This method extends the base implementation to also terminate the sandbox instance
|
|
284
|
+
and clean up any processes running in the sandbox.
|
|
285
|
+
"""
|
|
286
|
+
logger.debug("Cleaning up sandbox resources")
|
|
287
|
+
|
|
288
|
+
# Terminate any running process
|
|
289
|
+
if self.process:
|
|
290
|
+
try:
|
|
291
|
+
logger.debug("Terminating sandbox process")
|
|
292
|
+
self.process.kill()
|
|
293
|
+
except Exception as e:
|
|
294
|
+
logger.warning(f"Error terminating sandbox process: {e}")
|
|
295
|
+
finally:
|
|
296
|
+
self.process = None
|
|
297
|
+
|
|
298
|
+
# Close the sandbox
|
|
299
|
+
if self.sandbox:
|
|
300
|
+
try:
|
|
301
|
+
logger.debug("Closing sandbox instance")
|
|
302
|
+
self.sandbox.kill()
|
|
303
|
+
logger.debug("Sandbox instance closed successfully")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.warning(f"Error closing sandbox: {e}")
|
|
306
|
+
finally:
|
|
307
|
+
self.sandbox = None
|
|
308
|
+
|
|
309
|
+
# Then call the parent method to clean up the rest
|
|
310
|
+
await super()._cleanup_resources()
|
|
311
|
+
|
|
312
|
+
# Clear any collected output
|
|
313
|
+
self.stdout_lines = []
|
|
314
|
+
self.stderr_lines = []
|
|
315
|
+
self.base_url = None
|
|
316
|
+
|
|
317
|
+
async def disconnect(self) -> None:
|
|
318
|
+
"""Close the connection to the MCP implementation."""
|
|
319
|
+
if not self._connected:
|
|
320
|
+
logger.debug("Not connected to MCP implementation")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
logger.debug("Disconnecting from MCP implementation")
|
|
324
|
+
await self._cleanup_resources()
|
|
325
|
+
self._connected = False
|
|
326
|
+
logger.debug("Disconnected from MCP implementation")
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def public_identifier(self) -> str:
|
|
330
|
+
"""Get the identifier for the connector."""
|
|
331
|
+
args_str = " ".join(self.user_args) if self.user_args else ""
|
|
332
|
+
return f"sandbox:{self.user_command} {args_str}".strip()
|