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.
- mcp_use/agents/mcpagent.py +117 -22
- mcp_use/client.py +35 -9
- mcp_use/config.py +30 -4
- mcp_use/connectors/__init__.py +12 -5
- mcp_use/connectors/base.py +135 -37
- mcp_use/connectors/http.py +108 -30
- mcp_use/connectors/sandbox.py +296 -0
- mcp_use/connectors/stdio.py +7 -2
- mcp_use/connectors/utils.py +13 -0
- mcp_use/connectors/websocket.py +7 -2
- mcp_use/session.py +1 -4
- mcp_use/task_managers/__init__.py +2 -1
- mcp_use/task_managers/base.py +10 -4
- mcp_use/task_managers/streamable_http.py +81 -0
- mcp_use/task_managers/websocket.py +5 -0
- mcp_use/telemetry/__init__.py +0 -0
- mcp_use/telemetry/events.py +93 -0
- mcp_use/telemetry/posthog.py +214 -0
- mcp_use/telemetry/utils.py +48 -0
- mcp_use/types/sandbox.py +23 -0
- mcp_use/utils.py +27 -0
- {mcp_use-1.2.13.dist-info → mcp_use-1.3.1.dist-info}/METADATA +209 -32
- mcp_use-1.3.1.dist-info/RECORD +46 -0
- mcp_use-1.2.13.dist-info/RECORD +0 -37
- {mcp_use-1.2.13.dist-info → mcp_use-1.3.1.dist-info}/WHEEL +0 -0
- {mcp_use-1.2.13.dist-info → mcp_use-1.3.1.dist-info}/licenses/LICENSE +0 -0
mcp_use/connectors/base.py
CHANGED
|
@@ -24,18 +24,25 @@ class BaseConnector(ABC):
|
|
|
24
24
|
|
|
25
25
|
def __init__(self):
|
|
26
26
|
"""Initialize base connector with common attributes."""
|
|
27
|
-
self.
|
|
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.
|
|
62
|
+
if self.client_session:
|
|
56
63
|
try:
|
|
57
64
|
logger.debug("Closing client session")
|
|
58
|
-
await self.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
if
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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.
|
|
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
|
-
|
|
174
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
196
|
-
|
|
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.
|
|
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
|
-
|
|
211
|
-
|
|
308
|
+
# Ensure we're connected
|
|
309
|
+
await self._ensure_connected()
|
|
212
310
|
|
|
213
311
|
logger.debug(f"Getting prompt: {name}")
|
|
214
|
-
result = await self.
|
|
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
|
-
|
|
220
|
-
|
|
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.
|
|
321
|
+
return await self.client_session.request({"method": method, "params": params or {}})
|
mcp_use/connectors/http.py
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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}
|