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

@@ -5,13 +5,12 @@ This module provides a connector for communicating with MCP implementations
5
5
  through the standard input/output streams.
6
6
  """
7
7
 
8
- from typing import Any
8
+ import sys
9
9
 
10
10
  from mcp import ClientSession, StdioServerParameters
11
- from mcp.client.stdio import stdio_client
12
- from mcp.types import Tool
13
11
 
14
12
  from ..logging import logger
13
+ from ..task_managers import StdioConnectionManager
15
14
  from .base import BaseConnector
16
15
 
17
16
 
@@ -19,7 +18,8 @@ class StdioConnector(BaseConnector):
19
18
  """Connector for MCP implementations using stdio transport.
20
19
 
21
20
  This connector uses the stdio transport to communicate with MCP implementations
22
- that are executed as child processes.
21
+ that are executed as child processes. It uses a connection manager to handle
22
+ the proper lifecycle management of the stdio client.
23
23
  """
24
24
 
25
25
  def __init__(
@@ -27,6 +27,7 @@ class StdioConnector(BaseConnector):
27
27
  command: str = "npx",
28
28
  args: list[str] | None = None,
29
29
  env: dict[str, str] | None = None,
30
+ errlog=sys.stderr,
30
31
  ):
31
32
  """Initialize a new stdio connector.
32
33
 
@@ -34,91 +35,44 @@ class StdioConnector(BaseConnector):
34
35
  command: The command to execute.
35
36
  args: Optional command line arguments.
36
37
  env: Optional environment variables.
38
+ errlog: Stream to write error output to.
37
39
  """
40
+ super().__init__()
38
41
  self.command = command
39
- self.args = args
42
+ self.args = args or [] # Ensure args is never None
40
43
  self.env = env
41
- self.client: ClientSession | None = None
42
- self._stdio_ctx = None
43
- self._tools: list[Tool] | None = None
44
+ self.errlog = errlog
44
45
 
45
46
  async def connect(self) -> None:
46
47
  """Establish a connection to the MCP implementation."""
47
- server_params = StdioServerParameters(command=self.command, args=self.args, env=self.env)
48
- self._stdio_ctx = stdio_client(server_params)
49
- read_stream, write_stream = await self._stdio_ctx.__aenter__()
50
- self.client = ClientSession(read_stream, write_stream, sampling_callback=None)
51
- await self.client.__aenter__()
52
-
53
- async def disconnect(self) -> None:
54
- """Close the connection to the MCP implementation."""
48
+ if self._connected:
49
+ logger.debug("Already connected to MCP implementation")
50
+ return
51
+
52
+ logger.info(f"Connecting to MCP implementation: {self.command}")
55
53
  try:
56
- if self.client:
57
- await self.client.__aexit__(None, None, None)
58
- self.client = None
59
- if self._stdio_ctx:
60
- await self._stdio_ctx.__aexit__(None, None, None)
61
- self._stdio_ctx = None
54
+ # Create server parameters
55
+ server_params = StdioServerParameters(
56
+ command=self.command, args=self.args, env=self.env
57
+ )
58
+
59
+ # Create and start the connection manager
60
+ self._connection_manager = StdioConnectionManager(server_params, self.errlog)
61
+ read_stream, write_stream = await self._connection_manager.start()
62
+
63
+ # Create the client session
64
+ self.client = ClientSession(read_stream, write_stream, sampling_callback=None)
65
+ await self.client.__aenter__()
66
+
67
+ # Mark as connected
68
+ self._connected = True
69
+ logger.info(f"Successfully connected to MCP implementation: {self.command}")
70
+
62
71
  except Exception as e:
63
- logger.warning(f"Warning: Error during stdio connector disconnect: {e}")
64
- finally:
65
- # Always clean up references even if there were errors
66
- self.client = None
67
- self._stdio_ctx = None
68
- self._tools = None
69
-
70
- async def __aenter__(self) -> "StdioConnector":
71
- """Enter the async context manager."""
72
- await self.connect()
73
- return self
74
-
75
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
76
- """Exit the async context manager."""
77
- await self.disconnect()
78
-
79
- async def initialize(self) -> dict[str, Any]:
80
- """Initialize the MCP session and return session information."""
81
- if not self.client:
82
- raise RuntimeError("MCP client is not connected")
83
-
84
- # Initialize the session
85
- result = await self.client.initialize()
86
-
87
- # Get available tools
88
- tools_result = await self.client.list_tools()
89
- self._tools = tools_result.tools
90
-
91
- return result
92
-
93
- @property
94
- def tools(self) -> list[Tool]:
95
- """Get the list of available tools."""
96
- if not self._tools:
97
- raise RuntimeError("MCP client is not initialized")
98
- return self._tools
99
-
100
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
101
- """Call an MCP tool with the given arguments."""
102
- if not self.client:
103
- raise RuntimeError("MCP client is not connected")
104
- return await self.client.call_tool(name, arguments)
105
-
106
- async def list_resources(self) -> list[dict[str, Any]]:
107
- """List all available resources from the MCP implementation."""
108
- if not self.client:
109
- raise RuntimeError("MCP client is not connected")
110
- resources = await self.client.list_resources()
111
- return resources
112
-
113
- async def read_resource(self, uri: str) -> tuple[bytes, str]:
114
- """Read a resource by URI."""
115
- if not self.client:
116
- raise RuntimeError("MCP client is not connected")
117
- resource = await self.client.read_resource(uri)
118
- return resource.content, resource.mimeType
119
-
120
- async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
121
- """Send a raw request to the MCP implementation."""
122
- if not self.client:
123
- raise RuntimeError("MCP client is not connected")
124
- return await self.client.request({"method": method, "params": params or {}})
72
+ logger.error(f"Failed to connect to MCP implementation: {e}")
73
+
74
+ # Clean up any resources if connection failed
75
+ await self._cleanup_resources()
76
+
77
+ # Re-raise the original exception
78
+ raise
@@ -10,20 +10,26 @@ import json
10
10
  import uuid
11
11
  from typing import Any
12
12
 
13
- import websockets
13
+ from mcp.types import Tool
14
14
  from websockets.client import WebSocketClientProtocol
15
15
 
16
+ from ..logging import logger
17
+ from ..task_managers import ConnectionManager, WebSocketConnectionManager
16
18
  from .base import BaseConnector
17
19
 
18
20
 
19
21
  class WebSocketConnector(BaseConnector):
20
22
  """Connector for MCP implementations using WebSocket transport.
21
23
 
22
- This connector uses WebSockets to communicate with remote MCP implementations.
24
+ This connector uses WebSockets to communicate with remote MCP implementations,
25
+ using a connection manager to handle the proper lifecycle management.
23
26
  """
24
27
 
25
28
  def __init__(
26
- self, url: str, auth_token: str | None = None, headers: dict[str, str] | None = None
29
+ self,
30
+ url: str,
31
+ auth_token: str | None = None,
32
+ headers: dict[str, str] | None = None,
27
33
  ):
28
34
  """Initialize a new WebSocket connector.
29
35
 
@@ -37,15 +43,43 @@ class WebSocketConnector(BaseConnector):
37
43
  self.headers = headers or {}
38
44
  if auth_token:
39
45
  self.headers["Authorization"] = f"Bearer {auth_token}"
46
+
40
47
  self.ws: WebSocketClientProtocol | None = None
48
+ self._connection_manager: ConnectionManager | None = None
49
+ self._receiver_task: asyncio.Task | None = None
41
50
  self.pending_requests: dict[str, asyncio.Future] = {}
51
+ self._tools: list[Tool] | None = None
52
+ self._connected = False
42
53
 
43
54
  async def connect(self) -> None:
44
55
  """Establish a connection to the MCP implementation."""
45
- self.ws = await websockets.connect(self.url, extra_headers=self.headers)
56
+ if self._connected:
57
+ logger.debug("Already connected to MCP implementation")
58
+ return
59
+
60
+ logger.info(f"Connecting to MCP implementation via WebSocket: {self.url}")
61
+ try:
62
+ # Create and start the connection manager
63
+ self._connection_manager = WebSocketConnectionManager(self.url, self.headers)
64
+ self.ws = await self._connection_manager.start()
46
65
 
47
- # Start the message receiver task
48
- self._receiver_task = asyncio.create_task(self._receive_messages())
66
+ # Start the message receiver task
67
+ self._receiver_task = asyncio.create_task(
68
+ self._receive_messages(), name="websocket_receiver_task"
69
+ )
70
+
71
+ # Mark as connected
72
+ self._connected = True
73
+ logger.info(f"Successfully connected to MCP implementation via WebSocket: {self.url}")
74
+
75
+ except Exception as e:
76
+ logger.error(f"Failed to connect to MCP implementation via WebSocket: {e}")
77
+
78
+ # Clean up any resources if connection failed
79
+ await self._cleanup_resources()
80
+
81
+ # Re-raise the original exception
82
+ raise
49
83
 
50
84
  async def _receive_messages(self) -> None:
51
85
  """Continuously receive and process messages from the WebSocket."""
@@ -65,7 +99,12 @@ class WebSocketConnector(BaseConnector):
65
99
  future.set_result(data["result"])
66
100
  elif "error" in data:
67
101
  future.set_exception(Exception(data["error"]))
102
+
103
+ logger.debug(f"Received response for request {request_id}")
104
+ else:
105
+ logger.debug(f"Received message: {data}")
68
106
  except Exception as e:
107
+ logger.error(f"Error in WebSocket message receiver: {e}")
69
108
  # If the websocket connection was closed or errored,
70
109
  # reject all pending requests
71
110
  for future in self.pending_requests.values():
@@ -74,22 +113,63 @@ class WebSocketConnector(BaseConnector):
74
113
 
75
114
  async def disconnect(self) -> None:
76
115
  """Close the connection to the MCP implementation."""
77
- if self._receiver_task:
78
- self._receiver_task.cancel()
79
- try:
80
- await self._receiver_task
81
- except asyncio.CancelledError:
82
- pass
116
+ if not self._connected:
117
+ logger.debug("Not connected to MCP implementation")
118
+ return
119
+
120
+ logger.info("Disconnecting from MCP implementation")
121
+ await self._cleanup_resources()
122
+ self._connected = False
123
+ logger.info("Disconnected from MCP implementation")
83
124
 
84
- if self.ws:
85
- await self.ws.close()
86
- self.ws = None
125
+ async def _cleanup_resources(self) -> None:
126
+ """Clean up all resources associated with this connector."""
127
+ errors = []
128
+
129
+ # First cancel the receiver task
130
+ if self._receiver_task and not self._receiver_task.done():
131
+ try:
132
+ logger.debug("Cancelling WebSocket receiver task")
133
+ self._receiver_task.cancel()
134
+ try:
135
+ await self._receiver_task
136
+ except asyncio.CancelledError:
137
+ logger.debug("WebSocket receiver task cancelled successfully")
138
+ except Exception as e:
139
+ logger.warning(f"Error during WebSocket receiver task cancellation: {e}")
140
+ except Exception as e:
141
+ error_msg = f"Error cancelling WebSocket receiver task: {e}"
142
+ logger.warning(error_msg)
143
+ errors.append(error_msg)
144
+ finally:
145
+ self._receiver_task = None
87
146
 
88
147
  # Reject any pending requests
89
- for future in self.pending_requests.values():
90
- if not future.done():
91
- future.set_exception(ConnectionError("WebSocket disconnected"))
92
- self.pending_requests.clear()
148
+ if self.pending_requests:
149
+ logger.debug(f"Rejecting {len(self.pending_requests)} pending requests")
150
+ for future in self.pending_requests.values():
151
+ if not future.done():
152
+ future.set_exception(ConnectionError("WebSocket disconnected"))
153
+ self.pending_requests.clear()
154
+
155
+ # Then stop the connection manager
156
+ if self._connection_manager:
157
+ try:
158
+ logger.debug("Stopping connection manager")
159
+ await self._connection_manager.stop()
160
+ except Exception as e:
161
+ error_msg = f"Error stopping connection manager: {e}"
162
+ logger.warning(error_msg)
163
+ errors.append(error_msg)
164
+ finally:
165
+ self._connection_manager = None
166
+ self.ws = None
167
+
168
+ # Reset tools
169
+ self._tools = None
170
+
171
+ if errors:
172
+ logger.warning(f"Encountered {len(errors)} errors during resource cleanup")
93
173
 
94
174
  async def _send_request(self, method: str, params: dict[str, Any] | None = None) -> Any:
95
175
  """Send a request and wait for a response."""
@@ -106,37 +186,60 @@ class WebSocketConnector(BaseConnector):
106
186
  # Send the request
107
187
  await self.ws.send(json.dumps({"id": request_id, "method": method, "params": params or {}}))
108
188
 
189
+ logger.debug(f"Sent request {request_id} method: {method}")
190
+
109
191
  # Wait for the response
110
192
  try:
111
193
  return await future
112
194
  except Exception as e:
113
195
  # Remove the request from pending requests
114
196
  self.pending_requests.pop(request_id, None)
115
- raise e
197
+ logger.error(f"Error waiting for response to request {request_id}: {e}")
198
+ raise
116
199
 
117
200
  async def initialize(self) -> dict[str, Any]:
118
201
  """Initialize the MCP session and return session information."""
119
- return await self._send_request("initialize")
202
+ logger.info("Initializing MCP session")
203
+ result = await self._send_request("initialize")
204
+
205
+ # Get available tools
206
+ tools_result = await self.list_tools()
207
+ self._tools = [Tool(**tool) for tool in tools_result]
208
+
209
+ logger.info(f"MCP session initialized with {len(self._tools)} tools")
210
+ return result
120
211
 
121
212
  async def list_tools(self) -> list[dict[str, Any]]:
122
213
  """List all available tools from the MCP implementation."""
214
+ logger.debug("Listing tools")
123
215
  result = await self._send_request("tools/list")
124
216
  return result.get("tools", [])
125
217
 
218
+ @property
219
+ def tools(self) -> list[Tool]:
220
+ """Get the list of available tools."""
221
+ if not self._tools:
222
+ raise RuntimeError("MCP client is not initialized")
223
+ return self._tools
224
+
126
225
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
127
226
  """Call an MCP tool with the given arguments."""
227
+ logger.debug(f"Calling tool '{name}' with arguments: {arguments}")
128
228
  return await self._send_request("tools/call", {"name": name, "arguments": arguments})
129
229
 
130
230
  async def list_resources(self) -> list[dict[str, Any]]:
131
231
  """List all available resources from the MCP implementation."""
232
+ logger.debug("Listing resources")
132
233
  result = await self._send_request("resources/list")
133
234
  return result
134
235
 
135
236
  async def read_resource(self, uri: str) -> tuple[bytes, str]:
136
237
  """Read a resource by URI."""
238
+ logger.debug(f"Reading resource: {uri}")
137
239
  result = await self._send_request("resources/read", {"uri": uri})
138
240
  return result.get("content", b""), result.get("mimeType", "")
139
241
 
140
242
  async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
141
243
  """Send a raw request to the MCP implementation."""
244
+ logger.debug(f"Sending request: {method} with params: {params}")
142
245
  return await self._send_request(method, params)
mcp_use/session.py CHANGED
@@ -8,7 +8,6 @@ which handles authentication, initialization, and tool discovery.
8
8
  from typing import Any
9
9
 
10
10
  from .connectors.base import BaseConnector
11
- from .tools.converter import ModelProvider, ToolConverter
12
11
 
13
12
 
14
13
  class MCPSession:
@@ -21,18 +20,15 @@ class MCPSession:
21
20
  def __init__(
22
21
  self,
23
22
  connector: BaseConnector,
24
- model_provider: str | ModelProvider,
25
23
  auto_connect: bool = True,
26
24
  ) -> None:
27
25
  """Initialize a new MCP session.
28
26
 
29
27
  Args:
30
28
  connector: The connector to use for communicating with the MCP implementation.
31
- model_provider: The model provider to convert tools for.
32
29
  auto_connect: Whether to automatically connect to the MCP implementation.
33
30
  """
34
31
  self.connector = connector
35
- self.tool_converter = ToolConverter(model_provider)
36
32
  self.session_info: dict[str, Any] | None = None
37
33
  self.tools: list[dict[str, Any]] = []
38
34
  self.auto_connect = auto_connect
@@ -100,14 +96,6 @@ class MCPSession:
100
96
  self.tools = self.connector.tools
101
97
  return self.tools
102
98
 
103
- def get_tools_for_llm(self) -> list[dict[str, Any]]:
104
- """Get the tools in the format required by the LLM.
105
-
106
- Returns:
107
- The list of tools in the LLM-specific format.
108
- """
109
- return self.tool_converter.convert_tools(self.tools)
110
-
111
99
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
112
100
  """Call an MCP tool with the given arguments.
113
101
 
@@ -123,46 +111,3 @@ class MCPSession:
123
111
  await self.connect()
124
112
 
125
113
  return await self.connector.call_tool(name, arguments)
126
-
127
- async def process_tool_calls(self, response: dict[str, Any]) -> list[dict[str, Any]]:
128
- """Process tool calls from an LLM response.
129
-
130
- This method parses tool calls from the LLM response, executes them,
131
- and returns the results.
132
-
133
- Args:
134
- response: The response from the LLM.
135
-
136
- Returns:
137
- A list of tool call results, each containing 'name', 'arguments', and 'result' keys.
138
- """
139
- # Parse tool calls from the response
140
- tool_calls = self.tool_converter.parse_tool_calls(response)
141
-
142
- # Execute each tool call
143
- results = []
144
- for tool_call in tool_calls:
145
- name = tool_call["name"]
146
- arguments = tool_call["arguments"]
147
-
148
- try:
149
- result = await self.call_tool(name, arguments)
150
- results.append(
151
- {
152
- "name": name,
153
- "arguments": arguments,
154
- "result": result,
155
- "error": None,
156
- }
157
- )
158
- except Exception as e:
159
- results.append(
160
- {
161
- "name": name,
162
- "arguments": arguments,
163
- "result": None,
164
- "error": str(e),
165
- }
166
- )
167
-
168
- return results
@@ -0,0 +1,19 @@
1
+ """
2
+ Connectors for various MCP transports.
3
+
4
+ This module provides interfaces for connecting to MCP implementations
5
+ through different transport mechanisms.
6
+ """
7
+
8
+ from .base import ConnectionManager
9
+ from .sse import SseConnectionManager
10
+ from .stdio import StdioConnectionManager
11
+ from .websocket import WebSocketConnectionManager
12
+
13
+ __all__ = [
14
+ "ConnectionManager",
15
+ "HttpConnectionManager",
16
+ "StdioConnectionManager",
17
+ "WebSocketConnectionManager",
18
+ "SseConnectionManager",
19
+ ]
@@ -0,0 +1,151 @@
1
+ """
2
+ Connection management for MCP implementations.
3
+
4
+ This module provides an abstract base class for different types of connection
5
+ managers used in MCP connectors.
6
+ """
7
+
8
+ import asyncio
9
+ from abc import ABC, abstractmethod
10
+ from typing import Generic, TypeVar
11
+
12
+ from ..logging import logger
13
+
14
+ # Type variable for connection types
15
+ T = TypeVar("T")
16
+
17
+
18
+ class ConnectionManager(Generic[T], ABC):
19
+ """Abstract base class for connection managers.
20
+
21
+ This class defines the interface for different types of connection managers
22
+ used with MCP connectors.
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize a new connection manager."""
27
+ self._ready_event = asyncio.Event()
28
+ self._done_event = asyncio.Event()
29
+ self._exception: Exception | None = None
30
+ self._connection: T | None = None
31
+ self._task: asyncio.Task | None = None
32
+
33
+ @abstractmethod
34
+ async def _establish_connection(self) -> T:
35
+ """Establish the connection.
36
+
37
+ This method should be implemented by subclasses to establish
38
+ the specific type of connection needed.
39
+
40
+ Returns:
41
+ The established connection.
42
+
43
+ Raises:
44
+ Exception: If connection cannot be established.
45
+ """
46
+ pass
47
+
48
+ @abstractmethod
49
+ async def _close_connection(self, connection: T) -> None:
50
+ """Close the connection.
51
+
52
+ This method should be implemented by subclasses to close
53
+ the specific type of connection.
54
+
55
+ Args:
56
+ connection: The connection to close.
57
+ """
58
+ pass
59
+
60
+ async def start(self) -> T:
61
+ """Start the connection manager and establish a connection.
62
+
63
+ Returns:
64
+ The established connection.
65
+
66
+ Raises:
67
+ Exception: If connection cannot be established.
68
+ """
69
+ # Reset state
70
+ self._ready_event.clear()
71
+ self._done_event.clear()
72
+ self._exception = None
73
+
74
+ # Create a task to establish and maintain the connection
75
+ self._task = asyncio.create_task(
76
+ self._connection_task(), name=f"{self.__class__.__name__}_task"
77
+ )
78
+
79
+ # Wait for the connection to be ready or fail
80
+ await self._ready_event.wait()
81
+
82
+ # If there was an exception, raise it
83
+ if self._exception:
84
+ raise self._exception
85
+
86
+ # Return the connection
87
+ if self._connection is None:
88
+ raise RuntimeError("Connection was not established")
89
+ return self._connection
90
+
91
+ async def stop(self) -> None:
92
+ """Stop the connection manager and close the connection."""
93
+ if self._task and not self._task.done():
94
+ # Cancel the task
95
+ logger.debug(f"Cancelling {self.__class__.__name__} task")
96
+ self._task.cancel()
97
+
98
+ # Wait for it to complete
99
+ try:
100
+ await self._task
101
+ except asyncio.CancelledError:
102
+ logger.debug(f"{self.__class__.__name__} task cancelled successfully")
103
+ except Exception as e:
104
+ logger.warning(f"Error stopping {self.__class__.__name__} task: {e}")
105
+
106
+ # Wait for the connection to be done
107
+ await self._done_event.wait()
108
+ logger.debug(f"{self.__class__.__name__} task completed")
109
+
110
+ async def _connection_task(self) -> None:
111
+ """Run the connection task.
112
+
113
+ This task establishes and maintains the connection until cancelled.
114
+ """
115
+ logger.debug(f"Starting {self.__class__.__name__} task")
116
+ try:
117
+ # Establish the connection
118
+ self._connection = await self._establish_connection()
119
+ logger.debug(f"{self.__class__.__name__} connected successfully")
120
+
121
+ # Signal that the connection is ready
122
+ self._ready_event.set()
123
+
124
+ # Wait indefinitely until cancelled
125
+ try:
126
+ # This keeps the connection open until cancelled
127
+ await asyncio.Event().wait()
128
+ except asyncio.CancelledError:
129
+ # Expected when stopping
130
+ logger.debug(f"{self.__class__.__name__} task received cancellation")
131
+ pass
132
+
133
+ except Exception as e:
134
+ # Store the exception
135
+ self._exception = e
136
+ logger.error(f"Error in {self.__class__.__name__} task: {e}")
137
+
138
+ # Signal that the connection is ready (with error)
139
+ self._ready_event.set()
140
+
141
+ finally:
142
+ # Close the connection if it was established
143
+ if self._connection is not None:
144
+ try:
145
+ await self._close_connection(self._connection)
146
+ except Exception as e:
147
+ logger.warning(f"Error closing connection in {self.__class__.__name__}: {e}")
148
+ self._connection = None
149
+
150
+ # Signal that the connection is done
151
+ self._done_event.set()