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

Potentially problematic release.


This version of mcp-use might be problematic. Click here for more details.

@@ -0,0 +1,296 @@
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 'sandbox_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")
292
+
293
+ @property
294
+ def public_identifier(self) -> str:
295
+ """Get the identifier for the connector."""
296
+ return {"type": "sandbox", "command": self.user_command, "args": self.user_args}
@@ -61,8 +61,8 @@ class StdioConnector(BaseConnector):
61
61
  read_stream, write_stream = await self._connection_manager.start()
62
62
 
63
63
  # Create the client session
64
- self.client = ClientSession(read_stream, write_stream, sampling_callback=None)
65
- await self.client.__aenter__()
64
+ self.client_session = ClientSession(read_stream, write_stream, sampling_callback=None)
65
+ await self.client_session.__aenter__()
66
66
 
67
67
  # Mark as connected
68
68
  self._connected = True
@@ -76,3 +76,8 @@ class StdioConnector(BaseConnector):
76
76
 
77
77
  # Re-raise the original exception
78
78
  raise
79
+
80
+ @property
81
+ def public_identifier(self) -> str:
82
+ """Get the identifier for the connector."""
83
+ return {"type": "stdio", "command&args": f"{self.command} {' '.join(self.args)}"}
@@ -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
@@ -11,7 +11,7 @@ import uuid
11
11
  from typing import Any
12
12
 
13
13
  from mcp.types import Tool
14
- from websockets.client import WebSocketClientProtocol
14
+ from websockets import ClientConnection
15
15
 
16
16
  from ..logging import logger
17
17
  from ..task_managers import ConnectionManager, WebSocketConnectionManager
@@ -44,7 +44,7 @@ class WebSocketConnector(BaseConnector):
44
44
  if auth_token:
45
45
  self.headers["Authorization"] = f"Bearer {auth_token}"
46
46
 
47
- self.ws: WebSocketClientProtocol | None = None
47
+ self.ws: ClientConnection | None = None
48
48
  self._connection_manager: ConnectionManager | None = None
49
49
  self._receiver_task: asyncio.Task | None = None
50
50
  self.pending_requests: dict[str, asyncio.Future] = {}
@@ -243,3 +243,8 @@ class WebSocketConnector(BaseConnector):
243
243
  """Send a raw request to the MCP implementation."""
244
244
  logger.debug(f"Sending request: {method} with params: {params}")
245
245
  return await self._send_request(method, params)
246
+
247
+ @property
248
+ def public_identifier(self) -> str:
249
+ """Get the identifier for the connector."""
250
+ return {"type": "websocket", "url": self.url}
mcp_use/session.py CHANGED
@@ -7,8 +7,6 @@ which handles authentication, initialization, and tool discovery.
7
7
 
8
8
  from typing import Any
9
9
 
10
- from mcp.types import Tool
11
-
12
10
  from .connectors.base import BaseConnector
13
11
 
14
12
 
@@ -32,7 +30,6 @@ class MCPSession:
32
30
  """
33
31
  self.connector = connector
34
32
  self.session_info: dict[str, Any] | None = None
35
- self.tools: list[Tool] = []
36
33
  self.auto_connect = auto_connect
37
34
 
38
35
  async def __aenter__(self) -> "MCPSession":
@@ -84,4 +81,4 @@ class MCPSession:
84
81
  Returns:
85
82
  True if the connector is connected, False otherwise.
86
83
  """
87
- return hasattr(self.connector, "client") and self.connector.client is not None
84
+ return self.connector.is_connected
@@ -8,12 +8,13 @@ through different transport mechanisms.
8
8
  from .base import ConnectionManager
9
9
  from .sse import SseConnectionManager
10
10
  from .stdio import StdioConnectionManager
11
+ from .streamable_http import StreamableHttpConnectionManager
11
12
  from .websocket import WebSocketConnectionManager
12
13
 
13
14
  __all__ = [
14
15
  "ConnectionManager",
15
- "HttpConnectionManager",
16
16
  "StdioConnectionManager",
17
17
  "WebSocketConnectionManager",
18
18
  "SseConnectionManager",
19
+ "StreamableHttpConnectionManager",
19
20
  ]
@@ -70,9 +70,7 @@ class ConnectionManager(Generic[T], ABC):
70
70
  self._exception = None
71
71
 
72
72
  # Create a task to establish and maintain the connection
73
- self._task = asyncio.create_task(
74
- self._connection_task(), name=f"{self.__class__.__name__}_task"
75
- )
73
+ self._task = asyncio.create_task(self._connection_task(), name=f"{self.__class__.__name__}_task")
76
74
 
77
75
  # Wait for the connection to be ready or fail
78
76
  await self._ready_event.wait()
@@ -105,6 +103,14 @@ class ConnectionManager(Generic[T], ABC):
105
103
  await self._done_event.wait()
106
104
  logger.debug(f"{self.__class__.__name__} task completed")
107
105
 
106
+ def get_streams(self) -> T | None:
107
+ """Get the current connection streams.
108
+
109
+ Returns:
110
+ The current connection (typically a tuple of read_stream, write_stream) or None if not connected.
111
+ """
112
+ return self._connection
113
+
108
114
  async def _connection_task(self) -> None:
109
115
  """Run the connection task.
110
116
 
@@ -137,7 +143,7 @@ class ConnectionManager(Generic[T], ABC):
137
143
  self._ready_event.set()
138
144
 
139
145
  finally:
140
- # Close the connection if it was establishedSUPABASE_URL
146
+ # Close the connection if it was established
141
147
  if self._connection is not None:
142
148
  try:
143
149
  await self._close_connection()
@@ -0,0 +1,81 @@
1
+ """
2
+ Streamable HTTP connection management for MCP implementations.
3
+
4
+ This module provides a connection manager for streamable HTTP-based MCP connections
5
+ that ensures proper task isolation and resource cleanup.
6
+ """
7
+
8
+ from datetime import timedelta
9
+ from typing import Any
10
+
11
+ from mcp.client.streamable_http import streamablehttp_client
12
+
13
+ from ..logging import logger
14
+ from .base import ConnectionManager
15
+
16
+
17
+ class StreamableHttpConnectionManager(ConnectionManager[tuple[Any, Any]]):
18
+ """Connection manager for streamable HTTP-based MCP connections.
19
+
20
+ This class handles the proper task isolation for HTTP streaming connections
21
+ to prevent the "cancel scope in different task" error. It runs the http_stream_client
22
+ in a dedicated task and manages its lifecycle.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ url: str,
28
+ headers: dict[str, str] | None = None,
29
+ timeout: float = 5,
30
+ read_timeout: float = 60 * 5,
31
+ ):
32
+ """Initialize a new streamable HTTP connection manager.
33
+
34
+ Args:
35
+ url: The HTTP endpoint URL
36
+ headers: Optional HTTP headers
37
+ timeout: Timeout for HTTP operations in seconds
38
+ read_timeout: Timeout for HTTP read operations in seconds
39
+ """
40
+ super().__init__()
41
+ self.url = url
42
+ self.headers = headers or {}
43
+ self.timeout = timedelta(seconds=timeout)
44
+ self.read_timeout = timedelta(seconds=read_timeout)
45
+ self._http_ctx = None
46
+
47
+ async def _establish_connection(self) -> tuple[Any, Any]:
48
+ """Establish a streamable HTTP connection.
49
+
50
+ Returns:
51
+ A tuple of (read_stream, write_stream)
52
+
53
+ Raises:
54
+ Exception: If connection cannot be established.
55
+ """
56
+ # Create the context manager
57
+ self._http_ctx = streamablehttp_client(
58
+ url=self.url,
59
+ headers=self.headers,
60
+ timeout=self.timeout,
61
+ sse_read_timeout=self.read_timeout,
62
+ )
63
+
64
+ # Enter the context manager. Ignoring the session id callback
65
+ read_stream, write_stream, _ = await self._http_ctx.__aenter__()
66
+
67
+ # Return the streams
68
+ return (read_stream, write_stream)
69
+
70
+ async def _close_connection(self) -> None:
71
+ """Close the streamable HTTP connection."""
72
+
73
+ if self._http_ctx:
74
+ # Exit the context manager
75
+ try:
76
+ await self._http_ctx.__aexit__(None, None, None)
77
+ except Exception as e:
78
+ # Only log if it's not a normal connection termination
79
+ logger.debug(f"Streamable HTTP context cleanup: {e}")
80
+ finally:
81
+ self._http_ctx = None
@@ -22,14 +22,17 @@ class WebSocketConnectionManager(ConnectionManager[tuple[Any, Any]]):
22
22
  def __init__(
23
23
  self,
24
24
  url: str,
25
+ headers: dict[str, str] | None = None,
25
26
  ):
26
27
  """Initialize a new WebSocket connection manager.
27
28
 
28
29
  Args:
29
30
  url: The WebSocket URL to connect to
31
+ headers: Optional HTTP headers
30
32
  """
31
33
  super().__init__()
32
34
  self.url = url
35
+ self.headers = headers or {}
33
36
 
34
37
  async def _establish_connection(self) -> tuple[Any, Any]:
35
38
  """Establish a WebSocket connection.
@@ -42,6 +45,8 @@ class WebSocketConnectionManager(ConnectionManager[tuple[Any, Any]]):
42
45
  """
43
46
  logger.debug(f"Connecting to WebSocket: {self.url}")
44
47
  # Create the context manager
48
+ # Note: The current MCP websocket_client implementation doesn't support headers
49
+ # If headers need to be passed, this would need to be updated when MCP supports it
45
50
  self._ws_ctx = websocket_client(self.url)
46
51
 
47
52
  # Enter the context manager
File without changes
@@ -0,0 +1,93 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+
6
+ class BaseTelemetryEvent(ABC):
7
+ """Base class for all telemetry events"""
8
+
9
+ @property
10
+ @abstractmethod
11
+ def name(self) -> str:
12
+ """Event name for tracking"""
13
+ pass
14
+
15
+ @property
16
+ @abstractmethod
17
+ def properties(self) -> dict[str, Any]:
18
+ """Event properties to send with the event"""
19
+ pass
20
+
21
+
22
+ @dataclass
23
+ class MCPAgentExecutionEvent(BaseTelemetryEvent):
24
+ """Comprehensive event for tracking complete MCP agent execution"""
25
+
26
+ # Execution method and context
27
+ execution_method: str # "run" or "astream"
28
+ query: str # The actual user query
29
+ success: bool
30
+
31
+ # Agent configuration
32
+ model_provider: str
33
+ model_name: str
34
+ server_count: int
35
+ server_identifiers: list[dict[str, str]]
36
+ total_tools_available: int
37
+ tools_available_names: list[str]
38
+ max_steps_configured: int
39
+ memory_enabled: bool
40
+ use_server_manager: bool
41
+
42
+ # Execution PARAMETERS
43
+ max_steps_used: int | None
44
+ manage_connector: bool
45
+ external_history_used: bool
46
+
47
+ # Execution results
48
+ steps_taken: int | None = None
49
+ tools_used_count: int | None = None
50
+ tools_used_names: list[str] | None = None
51
+ response: str | None = None # The actual response
52
+ execution_time_ms: int | None = None
53
+ error_type: str | None = None
54
+
55
+ # Context
56
+ conversation_history_length: int | None = None
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ return "mcp_agent_execution"
61
+
62
+ @property
63
+ def properties(self) -> dict[str, Any]:
64
+ return {
65
+ # Core execution info
66
+ "execution_method": self.execution_method,
67
+ "query": self.query,
68
+ "query_length": len(self.query),
69
+ "success": self.success,
70
+ # Agent configuration
71
+ "model_provider": self.model_provider,
72
+ "model_name": self.model_name,
73
+ "server_count": self.server_count,
74
+ "server_identifiers": self.server_identifiers,
75
+ "total_tools_available": self.total_tools_available,
76
+ "tools_available_names": self.tools_available_names,
77
+ "max_steps_configured": self.max_steps_configured,
78
+ "memory_enabled": self.memory_enabled,
79
+ "use_server_manager": self.use_server_manager,
80
+ # Execution parameters (always include, even if None)
81
+ "max_steps_used": self.max_steps_used,
82
+ "manage_connector": self.manage_connector,
83
+ "external_history_used": self.external_history_used,
84
+ # Execution results (always include, even if None)
85
+ "steps_taken": self.steps_taken,
86
+ "tools_used_count": self.tools_used_count,
87
+ "tools_used_names": self.tools_used_names,
88
+ "response": self.response,
89
+ "response_length": len(self.response) if self.response else None,
90
+ "execution_time_ms": self.execution_time_ms,
91
+ "error_type": self.error_type,
92
+ "conversation_history_length": self.conversation_history_length,
93
+ }