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.

@@ -24,18 +24,25 @@ class BaseConnector(ABC):
24
24
 
25
25
  def __init__(self):
26
26
  """Initialize base connector with common attributes."""
27
- self.client: ClientSession | None = None
27
+ self.client_session: ClientSession | None = None
28
28
  self._connection_manager: ConnectionManager | None = None
29
29
  self._tools: list[Tool] | None = None
30
30
  self._resources: list[Resource] | None = None
31
31
  self._prompts: list[Prompt] | None = None
32
32
  self._connected = False
33
+ self.auto_reconnect = True # Whether to automatically reconnect on connection loss (not configurable for now), may be made configurable through the connector_config
33
34
 
34
35
  @abstractmethod
35
36
  async def connect(self) -> None:
36
37
  """Establish a connection to the MCP implementation."""
37
38
  pass
38
39
 
40
+ @property
41
+ @abstractmethod
42
+ def public_identifier(self) -> str:
43
+ """Get the identifier for the connector."""
44
+ pass
45
+
39
46
  async def disconnect(self) -> None:
40
47
  """Close the connection to the MCP implementation."""
41
48
  if not self._connected:
@@ -52,16 +59,16 @@ class BaseConnector(ABC):
52
59
  errors = []
53
60
 
54
61
  # First close the client session
55
- if self.client:
62
+ if self.client_session:
56
63
  try:
57
64
  logger.debug("Closing client session")
58
- await self.client.__aexit__(None, None, None)
65
+ await self.client_session.__aexit__(None, None, None)
59
66
  except Exception as e:
60
67
  error_msg = f"Error closing client session: {e}"
61
68
  logger.warning(error_msg)
62
69
  errors.append(error_msg)
63
70
  finally:
64
- self.client = None
71
+ self.client_session = None
65
72
 
66
73
  # Then stop the connection manager
67
74
  if self._connection_manager:
@@ -85,13 +92,13 @@ class BaseConnector(ABC):
85
92
 
86
93
  async def initialize(self) -> dict[str, Any]:
87
94
  """Initialize the MCP session and return session information."""
88
- if not self.client:
95
+ if not self.client_session:
89
96
  raise RuntimeError("MCP client is not connected")
90
97
 
91
98
  logger.debug("Initializing MCP session")
92
99
 
93
100
  # Initialize the session
94
- result = await self.client.initialize()
101
+ result = await self.client_session.initialize()
95
102
 
96
103
  server_capabilities = result.capabilities
97
104
 
@@ -116,11 +123,7 @@ class BaseConnector(ABC):
116
123
  else:
117
124
  self._prompts = []
118
125
 
119
- logger.debug(
120
- f"MCP session initialized with {len(self._tools)} tools, "
121
- f"{len(self._resources)} resources, "
122
- f"and {len(self._prompts)} prompts"
123
- )
126
+ logger.debug(f"MCP session initialized with {len(self._tools)} tools, " f"{len(self._resources)} resources, " f"and {len(self._prompts)} prompts")
124
127
 
125
128
  return result
126
129
 
@@ -145,24 +148,121 @@ class BaseConnector(ABC):
145
148
  raise RuntimeError("MCP client is not initialized")
146
149
  return self._prompts
147
150
 
148
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> CallToolResult:
149
- """Call an MCP tool with the given arguments."""
150
- if not self.client:
151
+ @property
152
+ def is_connected(self) -> bool:
153
+ """Check if the connector is actually connected and the connection is alive.
154
+
155
+ This property checks not only the connected flag but also verifies that
156
+ the underlying connection manager and streams are still active.
157
+
158
+ Returns:
159
+ True if the connector is connected and the connection is alive, False otherwise.
160
+ """
161
+
162
+ # Check if we have a client session
163
+ if not self.client_session:
164
+ # Update the connected flag since we don't have a client session
165
+ self._connected = False
166
+ return False
167
+
168
+ # First check the basic connected flag
169
+ if not self._connected:
170
+ return False
171
+
172
+ # Check if we have a connection manager and if its task is still running
173
+ if self._connection_manager:
174
+ try:
175
+ # Check if the connection manager task is done (indicates disconnection)
176
+ if hasattr(self._connection_manager, "_task") and self._connection_manager._task:
177
+ if self._connection_manager._task.done():
178
+ logger.debug("Connection manager task is done, marking as disconnected")
179
+ self._connected = False
180
+ return False
181
+
182
+ # For HTTP-based connectors, also check if streams are still open
183
+ # Use the get_streams method to get the current connection
184
+ streams = self._connection_manager.get_streams()
185
+ if streams:
186
+ # Connection should be a tuple of (read_stream, write_stream)
187
+ if isinstance(streams, tuple) and len(streams) == 2:
188
+ read_stream, write_stream = streams
189
+ # Check if streams are closed using getattr with default value
190
+ if getattr(read_stream, "_closed", False):
191
+ logger.debug("Read stream is closed, marking as disconnected")
192
+ self._connected = False
193
+ return False
194
+ if getattr(write_stream, "_closed", False):
195
+ logger.debug("Write stream is closed, marking as disconnected")
196
+ self._connected = False
197
+ return False
198
+
199
+ except Exception as e:
200
+ # If we can't check the connection state, assume disconnected for safety
201
+ logger.debug(f"Error checking connection state: {e}, marking as disconnected")
202
+ self._connected = False
203
+ return False
204
+
205
+ return True
206
+
207
+ async def _ensure_connected(self) -> None:
208
+ """Ensure the connector is connected, reconnecting if necessary.
209
+
210
+ Raises:
211
+ RuntimeError: If connection cannot be established and auto_reconnect is False.
212
+ """
213
+ if not self.client_session:
151
214
  raise RuntimeError("MCP client is not connected")
152
215
 
216
+ if not self.is_connected:
217
+ if self.auto_reconnect:
218
+ logger.debug("Connection lost, attempting to reconnect...")
219
+ try:
220
+ await self.connect()
221
+ logger.debug("Reconnection successful")
222
+ except Exception as e:
223
+ raise RuntimeError(f"Failed to reconnect to MCP server: {e}") from e
224
+ else:
225
+ raise RuntimeError("Connection to MCP server has been lost. " "Auto-reconnection is disabled. Please reconnect manually.")
226
+
227
+ async def call_tool(self, name: str, arguments: dict[str, Any]) -> CallToolResult:
228
+ """Call an MCP tool with automatic reconnection handling.
229
+
230
+ Args:
231
+ name: The name of the tool to call.
232
+ arguments: The arguments to pass to the tool.
233
+
234
+ Returns:
235
+ The result of the tool call.
236
+
237
+ Raises:
238
+ RuntimeError: If the connection is lost and cannot be reestablished.
239
+ """
240
+
241
+ # Ensure we're connected
242
+ await self._ensure_connected()
243
+
153
244
  logger.debug(f"Calling tool '{name}' with arguments: {arguments}")
154
- result = await self.client.call_tool(name, arguments)
155
- logger.debug(f"Tool '{name}' called with result: {result}")
156
- return result
245
+ try:
246
+ result = await self.client_session.call_tool(name, arguments)
247
+ logger.debug(f"Tool '{name}' called with result: {result}")
248
+ return result
249
+ except Exception as e:
250
+ # Check if the error might be due to connection loss
251
+ if not self.is_connected:
252
+ raise RuntimeError(f"Tool call '{name}' failed due to connection loss: {e}") from e
253
+ else:
254
+ # Re-raise the original error if it's not connection-related
255
+ raise
157
256
 
158
257
  async def list_tools(self) -> list[Tool]:
159
258
  """List all available tools from the MCP implementation."""
160
- if not self.client:
161
- raise RuntimeError("MCP client is not connected")
259
+
260
+ # Ensure we're connected
261
+ await self._ensure_connected()
162
262
 
163
263
  logger.debug("Listing tools")
164
264
  try:
165
- result = await self.client.list_tools()
265
+ result = await self.client_session.list_tools()
166
266
  return result.tools
167
267
  except McpError as e:
168
268
  logger.error(f"Error listing tools: {e}")
@@ -170,12 +270,12 @@ class BaseConnector(ABC):
170
270
 
171
271
  async def list_resources(self) -> list[Resource]:
172
272
  """List all available resources from the MCP implementation."""
173
- if not self.client:
174
- raise RuntimeError("MCP client is not connected")
273
+ # Ensure we're connected
274
+ await self._ensure_connected()
175
275
 
176
276
  logger.debug("Listing resources")
177
277
  try:
178
- result = await self.client.list_resources()
278
+ result = await self.client_session.list_resources()
179
279
  return result.resources
180
280
  except McpError as e:
181
281
  logger.error(f"Error listing resources: {e}")
@@ -183,41 +283,39 @@ class BaseConnector(ABC):
183
283
 
184
284
  async def read_resource(self, uri: str) -> ReadResourceResult:
185
285
  """Read a resource by URI."""
186
- if not self.client:
286
+ if not self.client_session:
187
287
  raise RuntimeError("MCP client is not connected")
188
288
 
189
289
  logger.debug(f"Reading resource: {uri}")
190
- result = await self.client.read_resource(uri)
290
+ result = await self.client_session.read_resource(uri)
191
291
  return result
192
292
 
193
293
  async def list_prompts(self) -> list[Prompt]:
194
294
  """List all available prompts from the MCP implementation."""
195
- if not self.client:
196
- raise RuntimeError("MCP client is not connected")
295
+ # Ensure we're connected
296
+ await self._ensure_connected()
197
297
 
198
298
  logger.debug("Listing prompts")
199
299
  try:
200
- result = await self.client.list_prompts()
300
+ result = await self.client_session.list_prompts()
201
301
  return result.prompts
202
302
  except McpError as e:
203
303
  logger.error(f"Error listing prompts: {e}")
204
304
  return []
205
305
 
206
- async def get_prompt(
207
- self, name: str, arguments: dict[str, Any] | None = None
208
- ) -> GetPromptResult:
306
+ async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult:
209
307
  """Get a prompt by name."""
210
- if not self.client:
211
- raise RuntimeError("MCP client is not connected")
308
+ # Ensure we're connected
309
+ await self._ensure_connected()
212
310
 
213
311
  logger.debug(f"Getting prompt: {name}")
214
- result = await self.client.get_prompt(name, arguments)
312
+ result = await self.client_session.get_prompt(name, arguments)
215
313
  return result
216
314
 
217
315
  async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
218
316
  """Send a raw request to the MCP implementation."""
219
- if not self.client:
220
- raise RuntimeError("MCP client is not connected")
317
+ # Ensure we're connected
318
+ await self._ensure_connected()
221
319
 
222
320
  logger.debug(f"Sending request: {method} with params: {params}")
223
- return await self.client.request({"method": method, "params": params or {}})
321
+ return await self.client_session.request({"method": method, "params": params or {}})
@@ -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}