mcp-use 1.3.0__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.

@@ -2,20 +2,21 @@
2
2
  HTTP connector for MCP implementations.
3
3
 
4
4
  This module provides a connector for communicating with MCP implementations
5
- through HTTP APIs with SSE for transport.
5
+ through HTTP APIs with SSE or Streamable HTTP for transport.
6
6
  """
7
7
 
8
+ import httpx
8
9
  from mcp import ClientSession
9
10
 
10
11
  from ..logging import logger
11
- from ..task_managers import SseConnectionManager
12
+ from ..task_managers import ConnectionManager, SseConnectionManager, StreamableHttpConnectionManager
12
13
  from .base import BaseConnector
13
14
 
14
15
 
15
16
  class HttpConnector(BaseConnector):
16
- """Connector for MCP implementations using HTTP transport with SSE.
17
+ """Connector for MCP implementations using HTTP transport with SSE or streamable HTTP.
17
18
 
18
- This connector uses HTTP/SSE to communicate with remote MCP implementations,
19
+ This connector uses HTTP/SSE or streamable HTTP to communicate with remote MCP implementations,
19
20
  using a connection manager to handle the proper lifecycle management.
20
21
  """
21
22
 
@@ -45,38 +46,115 @@ class HttpConnector(BaseConnector):
45
46
  self.timeout = timeout
46
47
  self.sse_read_timeout = sse_read_timeout
47
48
 
49
+ async def _setup_client(self, connection_manager: ConnectionManager) -> None:
50
+ """Set up the client session with the provided connection manager."""
51
+
52
+ self._connection_manager = connection_manager
53
+ read_stream, write_stream = await self._connection_manager.start()
54
+ self.client_session = ClientSession(read_stream, write_stream, sampling_callback=None)
55
+ await self.client_session.__aenter__()
56
+
48
57
  async def connect(self) -> None:
49
58
  """Establish a connection to the MCP implementation."""
50
59
  if self._connected:
51
60
  logger.debug("Already connected to MCP implementation")
52
61
  return
53
62
 
54
- logger.debug(f"Connecting to MCP implementation via HTTP/SSE: {self.base_url}")
55
- try:
56
- # Create the SSE connection URL
57
- sse_url = f"{self.base_url}"
58
-
59
- # Create and start the connection manager
60
- self._connection_manager = SseConnectionManager(
61
- sse_url, self.headers, self.timeout, self.sse_read_timeout
62
- )
63
- read_stream, write_stream = await self._connection_manager.start()
63
+ # Try streamable HTTP first (new transport), fall back to SSE (old transport)
64
+ # This implements backwards compatibility per MCP specification
65
+ self.transport_type = None
66
+ connection_manager = None
64
67
 
65
- # Create the client session
66
- self.client = ClientSession(read_stream, write_stream, sampling_callback=None)
67
- await self.client.__aenter__()
68
-
69
- # Mark as connected
70
- self._connected = True
71
- logger.debug(
72
- f"Successfully connected to MCP implementation via HTTP/SSE: {self.base_url}"
68
+ try:
69
+ # First, try the new streamable HTTP transport
70
+ logger.debug(f"Attempting streamable HTTP connection to: {self.base_url}")
71
+ connection_manager = StreamableHttpConnectionManager(
72
+ self.base_url, self.headers, self.timeout, self.sse_read_timeout
73
73
  )
74
74
 
75
- except Exception as e:
76
- logger.error(f"Failed to connect to MCP implementation via HTTP/SSE: {e}")
77
-
78
- # Clean up any resources if connection failed
79
- await self._cleanup_resources()
80
-
81
- # Re-raise the original exception
82
- raise
75
+ # Test if this is a streamable HTTP server by attempting initialization
76
+ read_stream, write_stream = await connection_manager.start()
77
+
78
+ # Test if this actually works by trying to create a client session and initialize it
79
+ test_client = ClientSession(read_stream, write_stream, sampling_callback=None)
80
+ await test_client.__aenter__()
81
+
82
+ try:
83
+ # Try to initialize - this is where streamable HTTP vs SSE difference should show up
84
+ await test_client.initialize()
85
+
86
+ # If we get here, streamable HTTP works
87
+
88
+ self.client_session = test_client
89
+ self.transport_type = "streamable HTTP"
90
+
91
+ except Exception as init_error:
92
+ # Clean up the test client
93
+ try:
94
+ await test_client.__aexit__(None, None, None)
95
+ except Exception:
96
+ pass
97
+ raise init_error
98
+
99
+ except Exception as streamable_error:
100
+ logger.debug(f"Streamable HTTP failed: {streamable_error}")
101
+
102
+ # Clean up the failed streamable HTTP connection manager
103
+ if connection_manager:
104
+ try:
105
+ await connection_manager.close()
106
+ except Exception:
107
+ pass
108
+
109
+ # Check if this is a 4xx error that indicates we should try SSE fallback
110
+ should_fallback = False
111
+ if isinstance(streamable_error, httpx.HTTPStatusError):
112
+ if streamable_error.response.status_code in [404, 405]:
113
+ should_fallback = True
114
+ elif "405 Method Not Allowed" in str(streamable_error) or "404 Not Found" in str(
115
+ streamable_error
116
+ ):
117
+ should_fallback = True
118
+ else:
119
+ # For other errors, still try fallback but they might indicate
120
+ # real connectivity issues
121
+ should_fallback = True
122
+
123
+ if should_fallback:
124
+ try:
125
+ # Fall back to the old SSE transport
126
+ logger.debug(f"Attempting SSE fallback connection to: {self.base_url}")
127
+ connection_manager = SseConnectionManager(
128
+ self.base_url, self.headers, self.timeout, self.sse_read_timeout
129
+ )
130
+
131
+ read_stream, write_stream = await connection_manager.start()
132
+
133
+ # Create the client session for SSE
134
+ self.client_session = ClientSession(
135
+ read_stream, write_stream, sampling_callback=None
136
+ )
137
+ await self.client_session.__aenter__()
138
+ self.transport_type = "SSE"
139
+
140
+ except Exception as sse_error:
141
+ logger.error(
142
+ f"Both transport methods failed. Streamable HTTP: {streamable_error}, "
143
+ f"SSE: {sse_error}"
144
+ )
145
+ raise sse_error
146
+ else:
147
+ raise streamable_error
148
+
149
+ # Store the successful connection manager and mark as connected
150
+ self._connection_manager = connection_manager
151
+ self._connected = True
152
+ logger.debug(
153
+ f"Successfully connected to MCP implementation via"
154
+ f" {self.transport_type}: {self.base_url}"
155
+ )
156
+
157
+ @property
158
+ def public_identifier(self) -> str:
159
+ """Get the identifier for the connector."""
160
+ return {"type": self.transport_type, "base_url": self.base_url}
@@ -79,7 +79,7 @@ class SandboxConnector(BaseConnector):
79
79
  self.api_key = _e2b_options.get("api_key") or os.environ.get("E2B_API_KEY")
80
80
  if not self.api_key:
81
81
  raise ValueError(
82
- "E2B API key is required. Provide it via 'e2b_options.api_key' "
82
+ "E2B API key is required. Provide it via 'sandbox_options.api_key' "
83
83
  "or the E2B_API_KEY environment variable."
84
84
  )
85
85
 
@@ -289,3 +289,8 @@ class SandboxConnector(BaseConnector):
289
289
  await self._cleanup_resources()
290
290
  self._connected = False
291
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)}"}
@@ -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
+ }