mcp-use 0.1.0__py3-none-any.whl → 1.0.0__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/client.py CHANGED
@@ -9,8 +9,8 @@ import json
9
9
  from typing import Any
10
10
 
11
11
  from .config import create_connector_from_config, load_config_file
12
+ from .logging import logger
12
13
  from .session import MCPSession
13
- from .tools.converter import ModelProvider
14
14
 
15
15
 
16
16
  class MCPClient:
@@ -23,19 +23,16 @@ class MCPClient:
23
23
  def __init__(
24
24
  self,
25
25
  config: str | dict[str, Any] | None = None,
26
- model_provider: str | ModelProvider = "openai",
27
26
  ) -> None:
28
27
  """Initialize a new MCP client.
29
28
 
30
29
  Args:
31
30
  config: Either a dict containing configuration or a path to a JSON config file.
32
31
  If None, an empty configuration is used.
33
- model_provider: The model provider to use for tool conversion.
34
32
  """
35
- self.model_provider = model_provider
36
33
  self.config: dict[str, Any] = {}
37
34
  self.sessions: dict[str, MCPSession] = {}
38
- self.active_session: str | None = None
35
+ self.active_sessions: list[str] = []
39
36
 
40
37
  # Load configuration if provided
41
38
  if config is not None:
@@ -87,9 +84,9 @@ class MCPClient:
87
84
  if "mcpServers" in self.config and name in self.config["mcpServers"]:
88
85
  del self.config["mcpServers"][name]
89
86
 
90
- # If we removed the active session, set active_session to None
91
- if name == self.active_session:
92
- self.active_session = None
87
+ # If we removed an active session, remove it from active_sessions
88
+ if name in self.active_sessions:
89
+ self.active_sessions.remove(name)
93
90
 
94
91
  def get_server_names(self) -> list[str]:
95
92
  """Get the list of configured server names.
@@ -108,17 +105,11 @@ class MCPClient:
108
105
  with open(filepath, "w") as f:
109
106
  json.dump(self.config, f, indent=2)
110
107
 
111
- async def create_session(
112
- self,
113
- server_name: str | None = None,
114
- auto_initialize: bool = True,
115
- ) -> MCPSession:
108
+ async def create_session(self, server_name: str, auto_initialize: bool = True) -> MCPSession:
116
109
  """Create a session for the specified server.
117
110
 
118
111
  Args:
119
112
  server_name: The name of the server to create a session for.
120
- If None, uses the first available server.
121
- auto_initialize: Whether to automatically initialize the session.
122
113
 
123
114
  Returns:
124
115
  The created MCPSession.
@@ -131,10 +122,6 @@ class MCPClient:
131
122
  if not servers:
132
123
  raise ValueError("No MCP servers defined in config")
133
124
 
134
- # If server_name not specified, use the first one
135
- if not server_name:
136
- server_name = next(iter(servers.keys()))
137
-
138
125
  if server_name not in servers:
139
126
  raise ValueError(f"Server '{server_name}' not found in config")
140
127
 
@@ -142,85 +129,123 @@ class MCPClient:
142
129
  connector = create_connector_from_config(server_config)
143
130
 
144
131
  # Create the session
145
- session = MCPSession(connector, self.model_provider)
146
- self.sessions[server_name] = session
147
-
148
- # Make this the active session
149
- self.active_session = server_name
150
-
151
- # Initialize if requested
132
+ session = MCPSession(connector)
152
133
  if auto_initialize:
153
134
  await session.initialize()
135
+ self.sessions[server_name] = session
136
+
137
+ # Add to active sessions
138
+ if server_name not in self.active_sessions:
139
+ self.active_sessions.append(server_name)
154
140
 
155
141
  return session
156
142
 
157
- def get_session(self, server_name: str | None = None) -> MCPSession:
143
+ async def create_all_sessions(
144
+ self,
145
+ auto_initialize: bool = True,
146
+ ) -> dict[str, MCPSession]:
147
+ """Create a session for the specified server.
148
+
149
+ Args:
150
+ auto_initialize: Whether to automatically initialize the session.
151
+
152
+ Returns:
153
+ The created MCPSession. If server_name is None, returns the first created session.
154
+
155
+ Raises:
156
+ ValueError: If no servers are configured or the specified server doesn't exist.
157
+ """
158
+ # Get server config
159
+ servers = self.config.get("mcpServers", {})
160
+ if not servers:
161
+ raise ValueError("No MCP servers defined in config")
162
+
163
+ # Create sessions for all servers
164
+ for name in servers:
165
+ session = await self.create_session(name, auto_initialize)
166
+ if auto_initialize:
167
+ await session.initialize()
168
+
169
+ return self.sessions
170
+
171
+ def get_session(self, server_name: str) -> MCPSession:
158
172
  """Get an existing session.
159
173
 
160
174
  Args:
161
175
  server_name: The name of the server to get the session for.
162
- If None, uses the active session.
176
+ If None, uses the first active session.
163
177
 
164
178
  Returns:
165
179
  The MCPSession for the specified server.
166
180
 
167
181
  Raises:
168
- ValueError: If no active session exists or the specified session doesn't exist.
182
+ ValueError: If no active sessions exist or the specified session doesn't exist.
169
183
  """
170
- if server_name is None:
171
- if self.active_session is None:
172
- raise ValueError("No active session")
173
- server_name = self.active_session
174
-
175
184
  if server_name not in self.sessions:
176
185
  raise ValueError(f"No session exists for server '{server_name}'")
177
186
 
178
187
  return self.sessions[server_name]
179
188
 
180
- async def close_session(self, server_name: str | None = None) -> None:
189
+ def get_all_active_sessions(self) -> dict[str, MCPSession]:
190
+ """Get all active sessions.
191
+
192
+ Returns:
193
+ Dictionary mapping server names to their MCPSession instances.
194
+ """
195
+ return {name: self.sessions[name] for name in self.active_sessions if name in self.sessions}
196
+
197
+ async def close_session(self, server_name: str) -> None:
181
198
  """Close a session.
182
199
 
183
200
  Args:
184
201
  server_name: The name of the server to close the session for.
185
- If None, uses the active session.
202
+ If None, uses the first active session.
186
203
 
187
204
  Raises:
188
- ValueError: If no active session exists or the specified session doesn't exist.
205
+ ValueError: If no active sessions exist or the specified session doesn't exist.
189
206
  """
190
- session = self.get_session(server_name)
191
- await session.disconnect()
192
-
193
- # Remove the session
194
- if server_name is None:
195
- server_name = self.active_session
196
-
197
- if server_name in self.sessions:
207
+ # Check if the session exists
208
+ if server_name not in self.sessions:
209
+ logger.warning(f"No session exists for server '{server_name}', nothing to close")
210
+ return
211
+
212
+ # Get the session
213
+ session = self.sessions[server_name]
214
+
215
+ try:
216
+ # Disconnect from the session
217
+ logger.info(f"Closing session for server '{server_name}'")
218
+ await session.disconnect()
219
+ except Exception as e:
220
+ logger.error(f"Error closing session for server '{server_name}': {e}")
221
+ finally:
222
+ # Remove the session regardless of whether disconnect succeeded
198
223
  del self.sessions[server_name]
199
224
 
200
- # If we closed the active session, set active_session to None
201
- if server_name == self.active_session:
202
- self.active_session = None
225
+ # Remove from active_sessions
226
+ if server_name in self.active_sessions:
227
+ self.active_sessions.remove(server_name)
203
228
 
204
229
  async def close_all_sessions(self) -> None:
205
- """Close all active sessions."""
206
- for server_name in list(self.sessions.keys()):
207
- await self.close_session(server_name)
208
-
209
- async def __aenter__(self) -> "MCPClient":
210
- """Enter the async context manager.
211
-
212
- Creates a session for the first available server if no sessions exist.
213
-
214
- Returns:
215
- The client instance.
216
- """
217
- if not self.sessions and self.config.get("mcpServers"):
218
- await self.create_session()
219
- return self
220
-
221
- async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
222
- """Exit the async context manager.
230
+ """Close all active sessions.
223
231
 
224
- Closes all active sessions.
232
+ This method ensures all sessions are closed even if some fail.
225
233
  """
226
- await self.close_all_sessions()
234
+ # Get a list of all session names first to avoid modification during iteration
235
+ server_names = list(self.sessions.keys())
236
+ errors = []
237
+
238
+ for server_name in server_names:
239
+ try:
240
+ logger.info(f"Closing session for server '{server_name}'")
241
+ await self.close_session(server_name)
242
+ except Exception as e:
243
+ error_msg = f"Failed to close session for server '{server_name}': {e}"
244
+ logger.error(error_msg)
245
+ errors.append(error_msg)
246
+
247
+ # Log summary if there were errors
248
+ if errors:
249
+ logger.error(f"Encountered {len(errors)} errors while closing sessions")
250
+ else:
251
+ logger.info("All sessions closed successfully")
mcp_use/config.py CHANGED
@@ -5,28 +5,9 @@ This module provides functionality to load MCP configuration from JSON files.
5
5
  """
6
6
 
7
7
  import json
8
- import os
9
- import subprocess
10
8
  from typing import Any
11
9
 
12
10
  from .connectors import BaseConnector, HttpConnector, StdioConnector, WebSocketConnector
13
- from .session import MCPSession
14
- from .tools.converter import ModelProvider
15
-
16
-
17
- def _execute_command(command: str, args: list[str], env: dict[str, str] | None = None) -> None:
18
- """Execute a command with given arguments and environment variables.
19
-
20
- Args:
21
- command: The command to execute
22
- args: List of command arguments
23
- env: Optional environment variables to set
24
- """
25
- full_env = os.environ.copy()
26
- if env:
27
- full_env.update(env)
28
-
29
- subprocess.Popen([command] + args, env=full_env)
30
11
 
31
12
 
32
13
  def load_config_file(filepath: str) -> dict[str, Any]:
@@ -76,38 +57,3 @@ def create_connector_from_config(server_config: dict[str, Any]) -> BaseConnector
76
57
  )
77
58
 
78
59
  raise ValueError("Cannot determine connector type from config")
79
-
80
-
81
- def create_session_from_config(
82
- filepath: str,
83
- server_name: str | None = None,
84
- model_provider: str | ModelProvider = "openai",
85
- ) -> MCPSession:
86
- """Create an MCPSession from a configuration file.
87
-
88
- Args:
89
- filepath: Path to the configuration file
90
- server_name: Name of the server to use from config, uses first if None
91
- model_provider: Model provider to use for tool conversion
92
-
93
- Returns:
94
- Configured MCPSession instance
95
- """
96
- config = load_config_file(filepath)
97
-
98
- # Get server config
99
- servers = config.get("mcpServers", {})
100
- if not servers:
101
- raise ValueError("No MCP servers defined in config")
102
-
103
- # If server_name not specified, use the first one
104
- if not server_name:
105
- server_name = next(iter(servers.keys()))
106
-
107
- if server_name not in servers:
108
- raise ValueError(f"Server '{server_name}' not found in config")
109
-
110
- server_config = servers[server_name]
111
- connector = create_connector_from_config(server_config)
112
-
113
- return MCPSession(connector, model_provider)
@@ -8,9 +8,7 @@ must implement.
8
8
  from abc import ABC, abstractmethod
9
9
  from typing import Any
10
10
 
11
- from mcp.types import CallToolResult
12
-
13
- from mcp_use.types import Tool
11
+ from mcp.types import CallToolResult, Tool
14
12
 
15
13
 
16
14
  class BaseConnector(ABC):
@@ -8,18 +8,25 @@ through HTTP APIs.
8
8
  from typing import Any
9
9
 
10
10
  import aiohttp
11
+ from mcp.types import Tool
11
12
 
13
+ from ..logging import logger
14
+ from ..task_managers import ConnectionManager, HttpConnectionManager
12
15
  from .base import BaseConnector
13
16
 
14
17
 
15
18
  class HttpConnector(BaseConnector):
16
19
  """Connector for MCP implementations using HTTP transport.
17
20
 
18
- This connector uses HTTP requests to communicate with remote MCP implementations.
21
+ This connector uses HTTP requests to communicate with remote MCP implementations,
22
+ using a connection manager to handle the proper lifecycle management.
19
23
  """
20
24
 
21
25
  def __init__(
22
- self, base_url: str, auth_token: str | None = None, headers: dict[str, str] | None = None
26
+ self,
27
+ base_url: str,
28
+ auth_token: str | None = None,
29
+ headers: dict[str, str] | None = None,
23
30
  ):
24
31
  """Initialize a new HTTP connector.
25
32
 
@@ -33,17 +40,70 @@ class HttpConnector(BaseConnector):
33
40
  self.headers = headers or {}
34
41
  if auth_token:
35
42
  self.headers["Authorization"] = f"Bearer {auth_token}"
43
+
36
44
  self.session: aiohttp.ClientSession | None = None
45
+ self._connection_manager: ConnectionManager | None = None
46
+ self._tools: list[Tool] | None = None
47
+ self._connected = False
37
48
 
38
49
  async def connect(self) -> None:
39
50
  """Establish a connection to the MCP implementation."""
40
- self.session = aiohttp.ClientSession(headers=self.headers)
51
+ if self._connected:
52
+ logger.debug("Already connected to MCP implementation")
53
+ return
54
+
55
+ logger.info(f"Connecting to MCP implementation via HTTP: {self.base_url}")
56
+ try:
57
+ # Create and start the connection manager
58
+ self._connection_manager = HttpConnectionManager(self.base_url, self.headers)
59
+ self.session = await self._connection_manager.start()
60
+
61
+ # Mark as connected
62
+ self._connected = True
63
+ logger.info(f"Successfully connected to MCP implementation via HTTP: {self.base_url}")
64
+
65
+ except Exception as e:
66
+ logger.error(f"Failed to connect to MCP implementation via HTTP: {e}")
67
+
68
+ # Clean up any resources if connection failed
69
+ await self._cleanup_resources()
70
+
71
+ # Re-raise the original exception
72
+ raise
41
73
 
42
74
  async def disconnect(self) -> None:
43
75
  """Close the connection to the MCP implementation."""
44
- if self.session:
45
- await self.session.close()
46
- self.session = None
76
+ if not self._connected:
77
+ logger.debug("Not connected to MCP implementation")
78
+ return
79
+
80
+ logger.info("Disconnecting from MCP implementation")
81
+ await self._cleanup_resources()
82
+ self._connected = False
83
+ logger.info("Disconnected from MCP implementation")
84
+
85
+ async def _cleanup_resources(self) -> None:
86
+ """Clean up all resources associated with this connector."""
87
+ errors = []
88
+
89
+ # Stop the connection manager
90
+ if self._connection_manager:
91
+ try:
92
+ logger.debug("Stopping connection manager")
93
+ await self._connection_manager.stop()
94
+ except Exception as e:
95
+ error_msg = f"Error stopping connection manager: {e}"
96
+ logger.warning(error_msg)
97
+ errors.append(error_msg)
98
+ finally:
99
+ self._connection_manager = None
100
+ self.session = None
101
+
102
+ # Reset tools
103
+ self._tools = None
104
+
105
+ if errors:
106
+ logger.warning(f"Encountered {len(errors)} errors during resource cleanup")
47
107
 
48
108
  async def _request(self, method: str, endpoint: str, data: dict[str, Any] | None = None) -> Any:
49
109
  """Send an HTTP request to the MCP API.
@@ -60,6 +120,7 @@ class HttpConnector(BaseConnector):
60
120
  raise RuntimeError("HTTP session is not connected")
61
121
 
62
122
  url = f"{self.base_url}/{endpoint.lstrip('/')}"
123
+ logger.debug(f"Sending {method} request to {url}")
63
124
 
64
125
  if method.upper() == "GET" and data:
65
126
  # For GET requests, convert data to query parameters
@@ -74,24 +135,46 @@ class HttpConnector(BaseConnector):
74
135
 
75
136
  async def initialize(self) -> dict[str, Any]:
76
137
  """Initialize the MCP session and return session information."""
77
- return await self._request("POST", "initialize")
138
+ logger.info("Initializing MCP session")
139
+
140
+ # Initialize the session
141
+ result = await self._request("POST", "initialize")
142
+
143
+ # Get available tools
144
+ tools_result = await self.list_tools()
145
+ self._tools = [Tool(**tool) for tool in tools_result]
146
+
147
+ logger.info(f"MCP session initialized with {len(self._tools)} tools")
148
+ return result
78
149
 
79
150
  async def list_tools(self) -> list[dict[str, Any]]:
80
151
  """List all available tools from the MCP implementation."""
152
+ logger.debug("Listing tools")
81
153
  result = await self._request("GET", "tools")
82
154
  return result.get("tools", [])
83
155
 
156
+ @property
157
+ def tools(self) -> list[Tool]:
158
+ """Get the list of available tools."""
159
+ if not self._tools:
160
+ raise RuntimeError("MCP client is not initialized")
161
+ return self._tools
162
+
84
163
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
85
164
  """Call an MCP tool with the given arguments."""
165
+ logger.debug(f"Calling tool '{name}' with arguments: {arguments}")
86
166
  return await self._request("POST", f"tools/{name}", arguments)
87
167
 
88
168
  async def list_resources(self) -> list[dict[str, Any]]:
89
169
  """List all available resources from the MCP implementation."""
170
+ logger.debug("Listing resources")
90
171
  result = await self._request("GET", "resources")
91
172
  return result
92
173
 
93
174
  async def read_resource(self, uri: str) -> tuple[bytes, str]:
94
175
  """Read a resource by URI."""
176
+ logger.debug(f"Reading resource: {uri}")
177
+
95
178
  # For resources, we may need to handle binary data
96
179
  if not self.session:
97
180
  raise RuntimeError("HTTP session is not connected")
@@ -122,5 +205,6 @@ class HttpConnector(BaseConnector):
122
205
 
123
206
  async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
124
207
  """Send a raw request to the MCP implementation."""
208
+ logger.debug(f"Sending request: {method} with params: {params}")
125
209
  # For custom methods, we'll use the RPC-style endpoint
126
210
  return await self._request("POST", "rpc", {"method": method, "params": params or {}})