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.

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]:
@@ -62,9 +43,9 @@ def create_connector_from_config(server_config: dict[str, Any]) -> BaseConnector
62
43
  # HTTP connector
63
44
  elif "url" in server_config:
64
45
  return HttpConnector(
65
- url=server_config["url"],
46
+ base_url=server_config["url"],
66
47
  headers=server_config.get("headers", None),
67
- auth=server_config.get("auth", None),
48
+ auth_token=server_config.get("auth_token", None),
68
49
  )
69
50
 
70
51
  # WebSocket connector
@@ -72,42 +53,7 @@ def create_connector_from_config(server_config: dict[str, Any]) -> BaseConnector
72
53
  return WebSocketConnector(
73
54
  url=server_config["ws_url"],
74
55
  headers=server_config.get("headers", None),
75
- auth=server_config.get("auth", None),
56
+ auth_token=server_config.get("auth_token", None),
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,11 @@ must implement.
8
8
  from abc import ABC, abstractmethod
9
9
  from typing import Any
10
10
 
11
- from mcp.types import CallToolResult
11
+ from mcp import ClientSession
12
+ from mcp.types import CallToolResult, Tool
12
13
 
13
- from mcp_use.types import Tool
14
+ from ..logging import logger
15
+ from ..task_managers import ConnectionManager
14
16
 
15
17
 
16
18
  class BaseConnector(ABC):
@@ -19,43 +21,119 @@ class BaseConnector(ABC):
19
21
  This class defines the interface that all MCP connectors must implement.
20
22
  """
21
23
 
24
+ def __init__(self):
25
+ """Initialize base connector with common attributes."""
26
+ self.client: ClientSession | None = None
27
+ self._connection_manager: ConnectionManager | None = None
28
+ self._tools: list[Tool] | None = None
29
+ self._connected = False
30
+
22
31
  @abstractmethod
23
32
  async def connect(self) -> None:
24
33
  """Establish a connection to the MCP implementation."""
25
34
  pass
26
35
 
27
- @abstractmethod
28
36
  async def disconnect(self) -> None:
29
37
  """Close the connection to the MCP implementation."""
30
- pass
38
+ if not self._connected:
39
+ logger.debug("Not connected to MCP implementation")
40
+ return
41
+
42
+ logger.info("Disconnecting from MCP implementation")
43
+ await self._cleanup_resources()
44
+ self._connected = False
45
+ logger.info("Disconnected from MCP implementation")
46
+
47
+ async def _cleanup_resources(self) -> None:
48
+ """Clean up all resources associated with this connector."""
49
+ errors = []
50
+
51
+ # First close the client session
52
+ if self.client:
53
+ try:
54
+ logger.debug("Closing client session")
55
+ await self.client.__aexit__(None, None, None)
56
+ except Exception as e:
57
+ error_msg = f"Error closing client session: {e}"
58
+ logger.warning(error_msg)
59
+ errors.append(error_msg)
60
+ finally:
61
+ self.client = None
62
+
63
+ # Then stop the connection manager
64
+ if self._connection_manager:
65
+ try:
66
+ logger.debug("Stopping connection manager")
67
+ await self._connection_manager.stop()
68
+ except Exception as e:
69
+ error_msg = f"Error stopping connection manager: {e}"
70
+ logger.warning(error_msg)
71
+ errors.append(error_msg)
72
+ finally:
73
+ self._connection_manager = None
74
+
75
+ # Reset tools
76
+ self._tools = None
77
+
78
+ if errors:
79
+ logger.warning(f"Encountered {len(errors)} errors during resource cleanup")
31
80
 
32
- @abstractmethod
33
81
  async def initialize(self) -> dict[str, Any]:
34
82
  """Initialize the MCP session and return session information."""
35
- pass
83
+ if not self.client:
84
+ raise RuntimeError("MCP client is not connected")
85
+
86
+ logger.info("Initializing MCP session")
87
+
88
+ # Initialize the session
89
+ result = await self.client.initialize()
90
+
91
+ # Get available tools
92
+ tools_result = await self.client.list_tools()
93
+ self._tools = tools_result.tools
94
+
95
+ logger.info(f"MCP session initialized with {len(self._tools)} tools")
96
+
97
+ return result
36
98
 
37
99
  @property
38
- @abstractmethod
39
100
  def tools(self) -> list[Tool]:
40
101
  """Get the list of available tools."""
41
- pass
102
+ if not self._tools:
103
+ raise RuntimeError("MCP client is not initialized")
104
+ return self._tools
42
105
 
43
- @abstractmethod
44
106
  async def call_tool(self, name: str, arguments: dict[str, Any]) -> CallToolResult:
45
107
  """Call an MCP tool with the given arguments."""
46
- pass
108
+ if not self.client:
109
+ raise RuntimeError("MCP client is not connected")
110
+
111
+ logger.debug(f"Calling tool '{name}' with arguments: {arguments}")
112
+ result = await self.client.call_tool(name, arguments)
113
+ return result
47
114
 
48
- @abstractmethod
49
115
  async def list_resources(self) -> list[dict[str, Any]]:
50
116
  """List all available resources from the MCP implementation."""
51
- pass
117
+ if not self.client:
118
+ raise RuntimeError("MCP client is not connected")
119
+
120
+ logger.debug("Listing resources")
121
+ resources = await self.client.list_resources()
122
+ return resources
52
123
 
53
- @abstractmethod
54
124
  async def read_resource(self, uri: str) -> tuple[bytes, str]:
55
125
  """Read a resource by URI."""
56
- pass
126
+ if not self.client:
127
+ raise RuntimeError("MCP client is not connected")
128
+
129
+ logger.debug(f"Reading resource: {uri}")
130
+ resource = await self.client.read_resource(uri)
131
+ return resource.content, resource.mimeType
57
132
 
58
- @abstractmethod
59
133
  async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
60
134
  """Send a raw request to the MCP implementation."""
61
- pass
135
+ if not self.client:
136
+ raise RuntimeError("MCP client is not connected")
137
+
138
+ logger.debug(f"Sending request: {method} with params: {params}")
139
+ return await self.client.request({"method": method, "params": params or {}})
@@ -2,24 +2,30 @@
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.
5
+ through HTTP APIs with SSE for transport.
6
6
  """
7
7
 
8
- from typing import Any
9
-
10
- import aiohttp
8
+ from mcp import ClientSession
11
9
 
10
+ from ..logging import logger
11
+ from ..task_managers import SseConnectionManager
12
12
  from .base import BaseConnector
13
13
 
14
14
 
15
15
  class HttpConnector(BaseConnector):
16
- """Connector for MCP implementations using HTTP transport.
16
+ """Connector for MCP implementations using HTTP transport with SSE.
17
17
 
18
- This connector uses HTTP requests to communicate with remote MCP implementations.
18
+ This connector uses HTTP/SSE to communicate with remote MCP implementations,
19
+ using a connection manager to handle the proper lifecycle management.
19
20
  """
20
21
 
21
22
  def __init__(
22
- self, base_url: str, auth_token: str | None = None, headers: dict[str, str] | None = None
23
+ self,
24
+ base_url: str,
25
+ auth_token: str | None = None,
26
+ headers: dict[str, str] | None = None,
27
+ timeout: float = 5,
28
+ sse_read_timeout: float = 60 * 5,
23
29
  ):
24
30
  """Initialize a new HTTP connector.
25
31
 
@@ -27,100 +33,50 @@ class HttpConnector(BaseConnector):
27
33
  base_url: The base URL of the MCP HTTP API.
28
34
  auth_token: Optional authentication token.
29
35
  headers: Optional additional headers.
36
+ timeout: Timeout for HTTP operations in seconds.
37
+ sse_read_timeout: Timeout for SSE read operations in seconds.
30
38
  """
39
+ super().__init__()
31
40
  self.base_url = base_url.rstrip("/")
32
41
  self.auth_token = auth_token
33
42
  self.headers = headers or {}
34
43
  if auth_token:
35
44
  self.headers["Authorization"] = f"Bearer {auth_token}"
36
- self.session: aiohttp.ClientSession | None = None
45
+ self.timeout = timeout
46
+ self.sse_read_timeout = sse_read_timeout
37
47
 
38
48
  async def connect(self) -> None:
39
49
  """Establish a connection to the MCP implementation."""
40
- self.session = aiohttp.ClientSession(headers=self.headers)
41
-
42
- async def disconnect(self) -> None:
43
- """Close the connection to the MCP implementation."""
44
- if self.session:
45
- await self.session.close()
46
- self.session = None
47
-
48
- async def _request(self, method: str, endpoint: str, data: dict[str, Any] | None = None) -> Any:
49
- """Send an HTTP request to the MCP API.
50
-
51
- Args:
52
- method: The HTTP method (GET, POST, etc.).
53
- endpoint: The API endpoint path.
54
- data: Optional request data.
55
-
56
- Returns:
57
- The parsed JSON response.
58
- """
59
- if not self.session:
60
- raise RuntimeError("HTTP session is not connected")
61
-
62
- url = f"{self.base_url}/{endpoint.lstrip('/')}"
63
-
64
- if method.upper() == "GET" and data:
65
- # For GET requests, convert data to query parameters
66
- async with self.session.get(url, params=data) as response:
67
- response.raise_for_status()
68
- return await response.json()
69
- else:
70
- # For other methods, send data as JSON body
71
- async with self.session.request(method, url, json=data) as response:
72
- response.raise_for_status()
73
- return await response.json()
74
-
75
- async def initialize(self) -> dict[str, Any]:
76
- """Initialize the MCP session and return session information."""
77
- return await self._request("POST", "initialize")
78
-
79
- async def list_tools(self) -> list[dict[str, Any]]:
80
- """List all available tools from the MCP implementation."""
81
- result = await self._request("GET", "tools")
82
- return result.get("tools", [])
83
-
84
- async def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
85
- """Call an MCP tool with the given arguments."""
86
- return await self._request("POST", f"tools/{name}", arguments)
87
-
88
- async def list_resources(self) -> list[dict[str, Any]]:
89
- """List all available resources from the MCP implementation."""
90
- result = await self._request("GET", "resources")
91
- return result
92
-
93
- async def read_resource(self, uri: str) -> tuple[bytes, str]:
94
- """Read a resource by URI."""
95
- # For resources, we may need to handle binary data
96
- if not self.session:
97
- raise RuntimeError("HTTP session is not connected")
98
-
99
- url = f"{self.base_url}/resources/read"
100
-
101
- async with self.session.get(url, params={"uri": uri}) as response:
102
- response.raise_for_status()
103
-
104
- # Check if this is a JSON response or binary data
105
- content_type = response.headers.get("Content-Type", "")
106
- if "application/json" in content_type:
107
- data = await response.json()
108
- content = data.get("content", b"")
109
- mime_type = data.get("mimeType", "")
110
-
111
- # If content is base64 encoded, decode it
112
- if isinstance(content, str):
113
- import base64
114
-
115
- content = base64.b64decode(content)
116
-
117
- return content, mime_type
118
- else:
119
- # Assume binary response
120
- content = await response.read()
121
- return content, content_type
122
-
123
- async def request(self, method: str, params: dict[str, Any] | None = None) -> Any:
124
- """Send a raw request to the MCP implementation."""
125
- # For custom methods, we'll use the RPC-style endpoint
126
- return await self._request("POST", "rpc", {"method": method, "params": params or {}})
50
+ if self._connected:
51
+ logger.debug("Already connected to MCP implementation")
52
+ return
53
+
54
+ logger.info(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}/sse"
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()
64
+
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.info(
72
+ f"Successfully connected to MCP implementation via HTTP/SSE: {self.base_url}"
73
+ )
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