mcp-use 1.3.12__py3-none-any.whl → 1.4.0__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.

Files changed (108) hide show
  1. mcp_use/__init__.py +1 -1
  2. mcp_use/adapters/.deprecated +0 -0
  3. mcp_use/adapters/__init__.py +18 -7
  4. mcp_use/adapters/base.py +12 -185
  5. mcp_use/adapters/langchain_adapter.py +12 -219
  6. mcp_use/agents/adapters/__init__.py +17 -0
  7. mcp_use/agents/adapters/anthropic.py +93 -0
  8. mcp_use/agents/adapters/base.py +316 -0
  9. mcp_use/agents/adapters/google.py +103 -0
  10. mcp_use/agents/adapters/langchain_adapter.py +212 -0
  11. mcp_use/agents/adapters/openai.py +111 -0
  12. mcp_use/agents/base.py +1 -1
  13. mcp_use/agents/managers/__init__.py +19 -0
  14. mcp_use/agents/managers/base.py +36 -0
  15. mcp_use/agents/managers/server_manager.py +131 -0
  16. mcp_use/agents/managers/tools/__init__.py +15 -0
  17. mcp_use/agents/managers/tools/base_tool.py +19 -0
  18. mcp_use/agents/managers/tools/connect_server.py +69 -0
  19. mcp_use/agents/managers/tools/disconnect_server.py +43 -0
  20. mcp_use/agents/managers/tools/get_active_server.py +29 -0
  21. mcp_use/agents/managers/tools/list_servers_tool.py +53 -0
  22. mcp_use/agents/managers/tools/search_tools.py +328 -0
  23. mcp_use/agents/mcpagent.py +386 -485
  24. mcp_use/agents/prompts/system_prompt_builder.py +1 -1
  25. mcp_use/agents/remote.py +15 -2
  26. mcp_use/auth/.deprecated +0 -0
  27. mcp_use/auth/__init__.py +19 -4
  28. mcp_use/auth/bearer.py +11 -12
  29. mcp_use/auth/oauth.py +11 -620
  30. mcp_use/auth/oauth_callback.py +16 -207
  31. mcp_use/client/__init__.py +1 -0
  32. mcp_use/client/auth/__init__.py +6 -0
  33. mcp_use/client/auth/bearer.py +23 -0
  34. mcp_use/client/auth/oauth.py +629 -0
  35. mcp_use/client/auth/oauth_callback.py +215 -0
  36. mcp_use/client/client.py +356 -0
  37. mcp_use/client/config.py +106 -0
  38. mcp_use/client/connectors/__init__.py +20 -0
  39. mcp_use/client/connectors/base.py +470 -0
  40. mcp_use/client/connectors/http.py +304 -0
  41. mcp_use/client/connectors/sandbox.py +332 -0
  42. mcp_use/client/connectors/stdio.py +109 -0
  43. mcp_use/client/connectors/utils.py +13 -0
  44. mcp_use/client/connectors/websocket.py +257 -0
  45. mcp_use/client/exceptions.py +31 -0
  46. mcp_use/client/middleware/__init__.py +50 -0
  47. mcp_use/client/middleware/logging.py +31 -0
  48. mcp_use/client/middleware/metrics.py +314 -0
  49. mcp_use/client/middleware/middleware.py +266 -0
  50. mcp_use/client/session.py +162 -0
  51. mcp_use/client/task_managers/__init__.py +20 -0
  52. mcp_use/client/task_managers/base.py +145 -0
  53. mcp_use/client/task_managers/sse.py +84 -0
  54. mcp_use/client/task_managers/stdio.py +69 -0
  55. mcp_use/client/task_managers/streamable_http.py +86 -0
  56. mcp_use/client/task_managers/websocket.py +68 -0
  57. mcp_use/client.py +12 -344
  58. mcp_use/config.py +20 -97
  59. mcp_use/connectors/.deprecated +0 -0
  60. mcp_use/connectors/__init__.py +46 -20
  61. mcp_use/connectors/base.py +12 -455
  62. mcp_use/connectors/http.py +13 -300
  63. mcp_use/connectors/sandbox.py +13 -306
  64. mcp_use/connectors/stdio.py +13 -104
  65. mcp_use/connectors/utils.py +15 -8
  66. mcp_use/connectors/websocket.py +13 -252
  67. mcp_use/exceptions.py +33 -18
  68. mcp_use/logging.py +1 -1
  69. mcp_use/managers/.deprecated +0 -0
  70. mcp_use/managers/__init__.py +56 -17
  71. mcp_use/managers/base.py +13 -31
  72. mcp_use/managers/server_manager.py +13 -119
  73. mcp_use/managers/tools/__init__.py +45 -15
  74. mcp_use/managers/tools/base_tool.py +5 -16
  75. mcp_use/managers/tools/connect_server.py +5 -67
  76. mcp_use/managers/tools/disconnect_server.py +5 -41
  77. mcp_use/managers/tools/get_active_server.py +5 -26
  78. mcp_use/managers/tools/list_servers_tool.py +5 -51
  79. mcp_use/managers/tools/search_tools.py +17 -321
  80. mcp_use/middleware/.deprecated +0 -0
  81. mcp_use/middleware/__init__.py +89 -50
  82. mcp_use/middleware/logging.py +14 -26
  83. mcp_use/middleware/metrics.py +30 -303
  84. mcp_use/middleware/middleware.py +39 -246
  85. mcp_use/session.py +13 -149
  86. mcp_use/task_managers/.deprecated +0 -0
  87. mcp_use/task_managers/__init__.py +48 -20
  88. mcp_use/task_managers/base.py +13 -140
  89. mcp_use/task_managers/sse.py +13 -79
  90. mcp_use/task_managers/stdio.py +13 -64
  91. mcp_use/task_managers/streamable_http.py +15 -81
  92. mcp_use/task_managers/websocket.py +13 -63
  93. mcp_use/telemetry/events.py +58 -0
  94. mcp_use/telemetry/telemetry.py +71 -1
  95. mcp_use/telemetry/utils.py +1 -1
  96. mcp_use/types/.deprecated +0 -0
  97. mcp_use/types/sandbox.py +13 -18
  98. {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/METADATA +68 -43
  99. mcp_use-1.4.0.dist-info/RECORD +111 -0
  100. mcp_use/cli.py +0 -581
  101. mcp_use-1.3.12.dist-info/RECORD +0 -64
  102. mcp_use-1.3.12.dist-info/licenses/LICENSE +0 -21
  103. /mcp_use/{observability → agents/observability}/__init__.py +0 -0
  104. /mcp_use/{observability → agents/observability}/callbacks_manager.py +0 -0
  105. /mcp_use/{observability → agents/observability}/laminar.py +0 -0
  106. /mcp_use/{observability → agents/observability}/langfuse.py +0 -0
  107. {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.dist-info}/WHEEL +0 -0
  108. {mcp_use-1.3.12.dist-info → mcp_use-1.4.0.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()