mcp-use 1.2.12__py3-none-any.whl → 1.3.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.

@@ -0,0 +1,291 @@
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
+
16
+ from ..logging import logger
17
+ from ..task_managers import SseConnectionManager
18
+
19
+ # Import E2B SDK components (optional dependency)
20
+ try:
21
+ logger.debug("Attempting to import e2b_code_interpreter...")
22
+ from e2b_code_interpreter import (
23
+ CommandHandle,
24
+ Sandbox,
25
+ )
26
+
27
+ logger.debug("Successfully imported e2b_code_interpreter")
28
+ except ImportError as e:
29
+ logger.debug(f"Failed to import e2b_code_interpreter: {e}")
30
+ CommandHandle = None
31
+ Sandbox = None
32
+
33
+ from ..types.sandbox import SandboxOptions
34
+ from .base import BaseConnector
35
+
36
+
37
+ class SandboxConnector(BaseConnector):
38
+ """Connector for MCP implementations running in a sandbox environment.
39
+
40
+ This connector runs a user-defined stdio command within a sandbox environment,
41
+ currently implemented using E2B, potentially wrapped by a utility like 'supergateway'
42
+ to expose its stdio.
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ command: str,
48
+ args: list[str],
49
+ env: dict[str, str] | None = None,
50
+ e2b_options: SandboxOptions | None = None,
51
+ timeout: float = 5,
52
+ sse_read_timeout: float = 60 * 5,
53
+ ):
54
+ """Initialize a new sandbox connector.
55
+
56
+ Args:
57
+ command: The user's MCP server command to execute in the sandbox.
58
+ args: Command line arguments for the user's MCP server command.
59
+ env: Environment variables for the user's MCP server command.
60
+ e2b_options: Configuration options for the E2B sandbox environment.
61
+ See SandboxOptions for available options and defaults.
62
+ timeout: Timeout for the sandbox process in seconds.
63
+ sse_read_timeout: Timeout for the SSE connection in seconds.
64
+ """
65
+ super().__init__()
66
+ if Sandbox is None:
67
+ raise ImportError(
68
+ "E2B SDK (e2b-code-interpreter) not found. "
69
+ "Please install it with 'pip install mcp-use[e2b]' "
70
+ "(or 'pip install e2b-code-interpreter')."
71
+ )
72
+
73
+ self.user_command = command
74
+ self.user_args = args or []
75
+ self.user_env = env or {}
76
+
77
+ _e2b_options = e2b_options or {}
78
+
79
+ self.api_key = _e2b_options.get("api_key") or os.environ.get("E2B_API_KEY")
80
+ if not self.api_key:
81
+ raise ValueError(
82
+ "E2B API key is required. Provide it via 'e2b_options.api_key' "
83
+ "or the E2B_API_KEY environment variable."
84
+ )
85
+
86
+ self.sandbox_template_id = _e2b_options.get("sandbox_template_id", "base")
87
+ self.supergateway_cmd_parts = _e2b_options.get(
88
+ "supergateway_command", "npx -y supergateway"
89
+ )
90
+
91
+ self.sandbox: Sandbox | None = None
92
+ self.process: CommandHandle | None = None
93
+ self.client: ClientSession | None = None
94
+ self.errlog = sys.stderr
95
+ self.base_url: str | None = None
96
+ self._connected = False
97
+ self._connection_manager: SseConnectionManager | None = None
98
+
99
+ # SSE connection parameters
100
+ self.headers = {}
101
+ self.timeout = timeout
102
+ self.sse_read_timeout = sse_read_timeout
103
+
104
+ self.stdout_lines: list[str] = []
105
+ self.stderr_lines: list[str] = []
106
+ self._server_ready = asyncio.Event()
107
+
108
+ def _handle_stdout(self, data: str) -> None:
109
+ """Handle stdout data from the sandbox process."""
110
+ self.stdout_lines.append(data)
111
+ logger.debug(f"[SANDBOX STDOUT] {data}", end="", flush=True)
112
+
113
+ def _handle_stderr(self, data: str) -> None:
114
+ """Handle stderr data from the sandbox process."""
115
+ self.stderr_lines.append(data)
116
+ logger.debug(f"[SANDBOX STDERR] {data}", file=self.errlog, end="", flush=True)
117
+
118
+ async def wait_for_server_response(self, base_url: str, timeout: int = 30) -> bool:
119
+ """Wait for the server to respond to HTTP requests.
120
+ Args:
121
+ base_url: The base URL to check for server readiness
122
+ timeout: Maximum time to wait in seconds
123
+ Returns:
124
+ True if server is responding, raises TimeoutError otherwise
125
+ """
126
+ logger.info(f"Waiting for server at {base_url} to respond...")
127
+ sys.stdout.flush()
128
+
129
+ start_time = time.time()
130
+ ping_url = f"{base_url}/sse"
131
+
132
+ # Try to connect to the server
133
+ while time.time() - start_time < timeout:
134
+ try:
135
+ async with aiohttp.ClientSession() as session:
136
+ try:
137
+ # First try the endpoint
138
+ async with session.get(ping_url, timeout=2) as response:
139
+ if response.status == 200:
140
+ elapsed = time.time() - start_time
141
+ logger.info(
142
+ f"Server is ready! "
143
+ f"SSE endpoint responded with 200 after {elapsed:.1f}s"
144
+ )
145
+ return True
146
+ except Exception:
147
+ # If sse endpoint doesn't work, try the base URL
148
+ async with session.get(base_url, timeout=2) as response:
149
+ if response.status < 500: # Accept any non-server error
150
+ elapsed = time.time() - start_time
151
+ logger.info(
152
+ f"Server is ready! Base URL responded with "
153
+ f"{response.status} after {elapsed:.1f}s"
154
+ )
155
+ return True
156
+ except Exception:
157
+ # Wait a bit before trying again
158
+ await asyncio.sleep(0.5)
159
+ continue
160
+
161
+ # If we get here, the request failed
162
+ await asyncio.sleep(0.5)
163
+
164
+ # Log status every 5 seconds
165
+ elapsed = time.time() - start_time
166
+ if int(elapsed) % 5 == 0:
167
+ logger.info(f"Still waiting for server to respond... ({elapsed:.1f}s elapsed)")
168
+ sys.stdout.flush()
169
+
170
+ # If we get here, we timed out
171
+ raise TimeoutError(f"Timeout waiting for server to respond (waited {timeout} seconds)")
172
+
173
+ async def connect(self):
174
+ """Connect to the sandbox and start the MCP server."""
175
+
176
+ if self._connected:
177
+ logger.debug("Already connected to MCP implementation")
178
+ return
179
+
180
+ logger.debug("Connecting to MCP implementation in sandbox")
181
+
182
+ try:
183
+ # Create and start the sandbox
184
+ self.sandbox = Sandbox(
185
+ template=self.sandbox_template_id,
186
+ api_key=self.api_key,
187
+ )
188
+
189
+ # Get the host for the sandbox
190
+ host = self.sandbox.get_host(3000)
191
+ self.base_url = f"https://{host}".rstrip("/")
192
+
193
+ # Append command with args
194
+ command = f"{self.user_command} {' '.join(self.user_args)}"
195
+
196
+ # Construct the full command with supergateway
197
+ full_command = f'{self.supergateway_cmd_parts} \
198
+ --base-url {self.base_url} \
199
+ --port 3000 \
200
+ --cors \
201
+ --stdio "{command}"'
202
+
203
+ logger.debug(f"Full command: {full_command}")
204
+
205
+ # Start the process in the sandbox with our stdout/stderr handlers
206
+ self.process: CommandHandle = self.sandbox.commands.run(
207
+ full_command,
208
+ envs=self.user_env,
209
+ timeout=1000 * 60 * 10, # 10 minutes timeout
210
+ background=True,
211
+ on_stdout=self._handle_stdout,
212
+ on_stderr=self._handle_stderr,
213
+ )
214
+
215
+ # Wait for the server to be ready
216
+ await self.wait_for_server_response(self.base_url, timeout=30)
217
+ logger.debug("Initializing connection manager...")
218
+
219
+ # Create the SSE connection URL
220
+ sse_url = f"{self.base_url}/sse"
221
+
222
+ # Create and start the connection manager
223
+ self._connection_manager = SseConnectionManager(
224
+ sse_url, self.headers, self.timeout, self.sse_read_timeout
225
+ )
226
+ read_stream, write_stream = await self._connection_manager.start()
227
+
228
+ # Create the client session
229
+ self.client = ClientSession(read_stream, write_stream, sampling_callback=None)
230
+ await self.client.__aenter__()
231
+
232
+ # Mark as connected
233
+ self._connected = True
234
+ logger.debug(
235
+ f"Successfully connected to MCP implementation via HTTP/SSE: {self.base_url}"
236
+ )
237
+
238
+ except Exception as e:
239
+ logger.error(f"Failed to connect to MCP implementation: {e}")
240
+
241
+ # Clean up any resources if connection failed
242
+ await self._cleanup_resources()
243
+
244
+ raise e
245
+
246
+ async def _cleanup_resources(self) -> None:
247
+ """Clean up all resources associated with this connector, including the sandbox.
248
+ This method extends the base implementation to also terminate the sandbox instance
249
+ and clean up any processes running in the sandbox.
250
+ """
251
+ logger.debug("Cleaning up sandbox resources")
252
+
253
+ # Terminate any running process
254
+ if self.process:
255
+ try:
256
+ logger.debug("Terminating sandbox process")
257
+ self.process.kill()
258
+ except Exception as e:
259
+ logger.warning(f"Error terminating sandbox process: {e}")
260
+ finally:
261
+ self.process = None
262
+
263
+ # Close the sandbox
264
+ if self.sandbox:
265
+ try:
266
+ logger.debug("Closing sandbox instance")
267
+ self.sandbox.kill()
268
+ logger.debug("Sandbox instance closed successfully")
269
+ except Exception as e:
270
+ logger.warning(f"Error closing sandbox: {e}")
271
+ finally:
272
+ self.sandbox = None
273
+
274
+ # Then call the parent method to clean up the rest
275
+ await super()._cleanup_resources()
276
+
277
+ # Clear any collected output
278
+ self.stdout_lines = []
279
+ self.stderr_lines = []
280
+ self.base_url = None
281
+
282
+ async def disconnect(self) -> None:
283
+ """Close the connection to the MCP implementation."""
284
+ if not self._connected:
285
+ logger.debug("Not connected to MCP implementation")
286
+ return
287
+
288
+ logger.debug("Disconnecting from MCP implementation")
289
+ await self._cleanup_resources()
290
+ self._connected = False
291
+ logger.debug("Disconnected from MCP implementation")
@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+
3
+
4
+ def is_stdio_server(server_config: dict[str, Any]) -> bool:
5
+ """Check if the server configuration is for a stdio server.
6
+
7
+ Args:
8
+ server_config: The server configuration section
9
+
10
+ Returns:
11
+ True if the server is a stdio server, False otherwise
12
+ """
13
+ return "command" in server_config and "args" in server_config
mcp_use/session.py CHANGED
@@ -7,6 +7,8 @@ which handles authentication, initialization, and tool discovery.
7
7
 
8
8
  from typing import Any
9
9
 
10
+ from mcp.types import Tool
11
+
10
12
  from .connectors.base import BaseConnector
11
13
 
12
14
 
@@ -30,7 +32,7 @@ class MCPSession:
30
32
  """
31
33
  self.connector = connector
32
34
  self.session_info: dict[str, Any] | None = None
33
- self.tools: list[dict[str, Any]] = []
35
+ self.tools: list[Tool] = []
34
36
  self.auto_connect = auto_connect
35
37
 
36
38
  async def __aenter__(self) -> "MCPSession":
@@ -73,9 +75,6 @@ class MCPSession:
73
75
  # Initialize the session
74
76
  self.session_info = await self.connector.initialize()
75
77
 
76
- # Discover available tools
77
- await self.discover_tools()
78
-
79
78
  return self.session_info
80
79
 
81
80
  @property
@@ -86,28 +85,3 @@ class MCPSession:
86
85
  True if the connector is connected, False otherwise.
87
86
  """
88
87
  return hasattr(self.connector, "client") and self.connector.client is not None
89
-
90
- async def discover_tools(self) -> list[dict[str, Any]]:
91
- """Discover available tools from the MCP implementation.
92
-
93
- Returns:
94
- The list of available tools in MCP format.
95
- """
96
- self.tools = self.connector.tools
97
- return self.tools
98
-
99
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
100
- """Call an MCP tool with the given arguments.
101
-
102
- Args:
103
- name: The name of the tool to call.
104
- arguments: The arguments to pass to the tool.
105
-
106
- Returns:
107
- The result of the tool call.
108
- """
109
- # Make sure we're connected
110
- if not self.is_connected and self.auto_connect:
111
- await self.connect()
112
-
113
- return await self.connector.call_tool(name, arguments)
@@ -46,14 +46,12 @@ class ConnectionManager(Generic[T], ABC):
46
46
  pass
47
47
 
48
48
  @abstractmethod
49
- async def _close_connection(self, connection: T) -> None:
49
+ async def _close_connection(self) -> None:
50
50
  """Close the connection.
51
51
 
52
52
  This method should be implemented by subclasses to close
53
53
  the specific type of connection.
54
54
 
55
- Args:
56
- connection: The connection to close.
57
55
  """
58
56
  pass
59
57
 
@@ -139,10 +137,10 @@ class ConnectionManager(Generic[T], ABC):
139
137
  self._ready_event.set()
140
138
 
141
139
  finally:
142
- # Close the connection if it was established
140
+ # Close the connection if it was establishedSUPABASE_URL
143
141
  if self._connection is not None:
144
142
  try:
145
- await self._close_connection(self._connection)
143
+ await self._close_connection()
146
144
  except Exception as e:
147
145
  logger.warning(f"Error closing connection in {self.__class__.__name__}: {e}")
148
146
  self._connection = None
@@ -66,12 +66,9 @@ class SseConnectionManager(ConnectionManager[tuple[Any, Any]]):
66
66
  # Return the streams
67
67
  return (read_stream, write_stream)
68
68
 
69
- async def _close_connection(self, connection: tuple[Any, Any]) -> None:
70
- """Close the SSE connection.
69
+ async def _close_connection(self) -> None:
70
+ """Close the SSE connection."""
71
71
 
72
- Args:
73
- connection: The connection to close (ignored, we use the context manager)
74
- """
75
72
  if self._sse_ctx:
76
73
  # Exit the context manager
77
74
  try:
@@ -57,12 +57,8 @@ class StdioConnectionManager(ConnectionManager[tuple[Any, Any]]):
57
57
  # Return the streams
58
58
  return (read_stream, write_stream)
59
59
 
60
- async def _close_connection(self, connection: tuple[Any, Any]) -> None:
61
- """Close the stdio connection.
62
-
63
- Args:
64
- connection: The connection to close (ignored, we use the context manager)
65
- """
60
+ async def _close_connection(self) -> None:
61
+ """Close the stdio connection."""
66
62
  if self._stdio_ctx:
67
63
  # Exit the context manager
68
64
  try:
@@ -4,14 +4,15 @@ WebSocket connection management for MCP implementations.
4
4
  This module provides a connection manager for WebSocket-based MCP connections.
5
5
  """
6
6
 
7
- import websockets
8
- from websockets.client import ClientConnection
7
+ from typing import Any
8
+
9
+ from mcp.client.websocket import websocket_client
9
10
 
10
11
  from ..logging import logger
11
12
  from .base import ConnectionManager
12
13
 
13
14
 
14
- class WebSocketConnectionManager(ConnectionManager[ClientConnection]):
15
+ class WebSocketConnectionManager(ConnectionManager[tuple[Any, Any]]):
15
16
  """Connection manager for WebSocket-based MCP connections.
16
17
 
17
18
  This class handles the lifecycle of WebSocket connections, ensuring proper
@@ -21,19 +22,16 @@ class WebSocketConnectionManager(ConnectionManager[ClientConnection]):
21
22
  def __init__(
22
23
  self,
23
24
  url: str,
24
- headers: dict[str, str] | None = None,
25
25
  ):
26
26
  """Initialize a new WebSocket connection manager.
27
27
 
28
28
  Args:
29
29
  url: The WebSocket URL to connect to
30
- headers: Optional headers to include in the WebSocket connection
31
30
  """
32
31
  super().__init__()
33
32
  self.url = url
34
- self.headers = headers or {}
35
33
 
36
- async def _establish_connection(self) -> ClientConnection:
34
+ async def _establish_connection(self) -> tuple[Any, Any]:
37
35
  """Establish a WebSocket connection.
38
36
 
39
37
  Returns:
@@ -43,21 +41,23 @@ class WebSocketConnectionManager(ConnectionManager[ClientConnection]):
43
41
  Exception: If connection cannot be established
44
42
  """
45
43
  logger.debug(f"Connecting to WebSocket: {self.url}")
46
- try:
47
- ws = await websockets.connect(self.url, extra_headers=self.headers)
48
- return ws
49
- except Exception as e:
50
- logger.error(f"Failed to connect to WebSocket: {e}")
51
- raise
52
-
53
- async def _close_connection(self, connection: ClientConnection) -> None:
54
- """Close the WebSocket connection.
55
-
56
- Args:
57
- connection: The WebSocket connection to close
58
- """
59
- try:
60
- logger.debug("Closing WebSocket connection")
61
- await connection.close()
62
- except Exception as e:
63
- logger.warning(f"Error closing WebSocket connection: {e}")
44
+ # Create the context manager
45
+ self._ws_ctx = websocket_client(self.url)
46
+
47
+ # Enter the context manager
48
+ read_stream, write_stream = await self._ws_ctx.__aenter__()
49
+
50
+ # Return the streams
51
+ return (read_stream, write_stream)
52
+
53
+ async def _close_connection(self) -> None:
54
+ """Close the WebSocket connection."""
55
+ if self._ws_ctx:
56
+ # Exit the context manager
57
+ try:
58
+ logger.debug("Closing WebSocket connection")
59
+ await self._ws_ctx.__aexit__(None, None, None)
60
+ except Exception as e:
61
+ logger.warning(f"Error closing WebSocket connection: {e}")
62
+ finally:
63
+ self._ws_ctx = None
@@ -0,0 +1,23 @@
1
+ """
2
+ Options for MCP client configuration.
3
+
4
+ This module provides data classes and type definitions for configuring the MCP client.
5
+ """
6
+
7
+ from typing import NotRequired, TypedDict
8
+
9
+ from .sandbox import SandboxOptions
10
+
11
+
12
+ class ClientOptions(TypedDict):
13
+ """Options for configuring the MCP client.
14
+
15
+ This class encapsulates all configuration options for the MCPClient,
16
+ making it easier to extend the API without breaking backward compatibility.
17
+ """
18
+
19
+ is_sandboxed: NotRequired[bool]
20
+ """Whether to use sandboxed execution mode for running MCP servers."""
21
+
22
+ sandbox_options: NotRequired[SandboxOptions]
23
+ """Options for sandbox configuration when is_sandboxed=True."""
@@ -0,0 +1,23 @@
1
+ """Type definitions for sandbox-related configurations."""
2
+
3
+ from typing import NotRequired, TypedDict
4
+
5
+
6
+ class SandboxOptions(TypedDict):
7
+ """Configuration options for sandbox execution.
8
+
9
+ This type defines the configuration options available when running
10
+ MCP servers in a sandboxed environment (e.g., using E2B).
11
+ """
12
+
13
+ api_key: str
14
+ """Direct API key for sandbox provider (e.g., E2B API key).
15
+ If not provided, will use E2B_API_KEY environment variable."""
16
+
17
+ sandbox_template_id: NotRequired[str]
18
+ """Template ID for the sandbox environment.
19
+ Default: 'base'"""
20
+
21
+ supergateway_command: NotRequired[str]
22
+ """Command to run supergateway.
23
+ Default: 'npx -y supergateway'"""