mcp-use 1.3.7__py3-none-any.whl → 1.3.9__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/__init__.py +0 -1
- mcp_use/adapters/base.py +3 -3
- mcp_use/adapters/langchain_adapter.py +5 -4
- mcp_use/agents/__init__.py +2 -2
- mcp_use/agents/mcpagent.py +257 -31
- mcp_use/agents/prompts/templates.py +20 -22
- mcp_use/agents/remote.py +296 -0
- mcp_use/client.py +20 -3
- mcp_use/config.py +12 -8
- mcp_use/connectors/base.py +100 -15
- mcp_use/connectors/http.py +13 -2
- mcp_use/connectors/sandbox.py +12 -6
- mcp_use/connectors/stdio.py +11 -2
- mcp_use/errors/__init__.py +1 -0
- mcp_use/errors/error_formatting.py +29 -0
- mcp_use/managers/__init__.py +3 -5
- mcp_use/managers/server_manager.py +46 -13
- mcp_use/managers/tools/__init__.py +2 -4
- mcp_use/managers/tools/connect_server.py +2 -1
- mcp_use/managers/tools/disconnect_server.py +2 -1
- mcp_use/managers/tools/list_servers_tool.py +2 -0
- mcp_use/session.py +70 -0
- mcp_use/telemetry/telemetry.py +1 -4
- {mcp_use-1.3.7.dist-info → mcp_use-1.3.9.dist-info}/METADATA +80 -57
- mcp_use-1.3.9.dist-info/RECORD +51 -0
- mcp_use/managers/tools/use_tool.py +0 -154
- mcp_use-1.3.7.dist-info/RECORD +0 -49
- {mcp_use-1.3.7.dist-info → mcp_use-1.3.9.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.7.dist-info → mcp_use-1.3.9.dist-info}/licenses/LICENSE +0 -0
mcp_use/agents/remote.py
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Remote agent implementation for executing agents via API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, TypeVar
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
from langchain.schema import BaseMessage
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from ..logging import logger
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T", bound=BaseModel)
|
|
16
|
+
|
|
17
|
+
# API endpoint constants
|
|
18
|
+
API_CHATS_ENDPOINT = "/api/v1/chats"
|
|
19
|
+
API_CHAT_EXECUTE_ENDPOINT = "/api/v1/chats/{chat_id}/execute"
|
|
20
|
+
API_CHAT_DELETE_ENDPOINT = "/api/v1/chats/{chat_id}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class RemoteAgent:
|
|
24
|
+
"""Agent that executes remotely via API."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, agent_id: str, api_key: str | None = None, base_url: str = "https://cloud.mcp-use.com"):
|
|
27
|
+
"""Initialize remote agent.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
agent_id: The ID of the remote agent to execute
|
|
31
|
+
api_key: API key for authentication. If None, will check MCP_USE_API_KEY env var
|
|
32
|
+
base_url: Base URL for the remote API
|
|
33
|
+
"""
|
|
34
|
+
self.agent_id = agent_id
|
|
35
|
+
self.base_url = base_url
|
|
36
|
+
self._chat_id = None # Persistent chat session
|
|
37
|
+
|
|
38
|
+
# Handle API key validation
|
|
39
|
+
if api_key is None:
|
|
40
|
+
api_key = os.getenv("MCP_USE_API_KEY")
|
|
41
|
+
if not api_key:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
"API key is required for remote execution. "
|
|
44
|
+
"Please provide it as a parameter or set the MCP_USE_API_KEY environment variable. "
|
|
45
|
+
"You can get an API key from https://cloud.mcp-use.com"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.api_key = api_key
|
|
49
|
+
# Configure client with reasonable timeouts for agent execution
|
|
50
|
+
self._client = httpx.AsyncClient(
|
|
51
|
+
timeout=httpx.Timeout(
|
|
52
|
+
connect=10.0, # 10 seconds to establish connection
|
|
53
|
+
read=300.0, # 5 minutes to read response (agents can take time)
|
|
54
|
+
write=10.0, # 10 seconds to send request
|
|
55
|
+
pool=10.0, # 10 seconds to get connection from pool
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _pydantic_to_json_schema(self, model_class: type[T]) -> dict[str, Any]:
|
|
60
|
+
"""Convert a Pydantic model to JSON schema for API transmission.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
model_class: The Pydantic model class to convert
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
JSON schema representation of the model
|
|
67
|
+
"""
|
|
68
|
+
return model_class.model_json_schema()
|
|
69
|
+
|
|
70
|
+
def _parse_structured_response(self, response_data: Any, output_schema: type[T]) -> T:
|
|
71
|
+
"""Parse the API response into the structured output format.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
response_data: Raw response data from the API
|
|
75
|
+
output_schema: The Pydantic model to parse into
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Parsed structured output
|
|
79
|
+
"""
|
|
80
|
+
# Handle different response formats
|
|
81
|
+
if isinstance(response_data, dict):
|
|
82
|
+
if "result" in response_data:
|
|
83
|
+
outer_result = response_data["result"]
|
|
84
|
+
# Check if this is a nested result structure (agent execution response)
|
|
85
|
+
if isinstance(outer_result, dict) and "result" in outer_result:
|
|
86
|
+
# Extract the actual structured output from the nested result
|
|
87
|
+
result_data = outer_result["result"]
|
|
88
|
+
else:
|
|
89
|
+
# Use the outer result directly
|
|
90
|
+
result_data = outer_result
|
|
91
|
+
else:
|
|
92
|
+
result_data = response_data
|
|
93
|
+
elif isinstance(response_data, str):
|
|
94
|
+
try:
|
|
95
|
+
result_data = json.loads(response_data)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
# If it's not valid JSON, try to create the model from the string content
|
|
98
|
+
result_data = {"content": response_data}
|
|
99
|
+
else:
|
|
100
|
+
result_data = response_data
|
|
101
|
+
|
|
102
|
+
# Parse into the Pydantic model
|
|
103
|
+
try:
|
|
104
|
+
return output_schema.model_validate(result_data)
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f"Failed to parse structured output: {e}")
|
|
107
|
+
# Fallback: try to parse it as raw content if the model has a content field
|
|
108
|
+
if hasattr(output_schema, "model_fields") and "content" in output_schema.model_fields:
|
|
109
|
+
return output_schema.model_validate({"content": str(result_data)})
|
|
110
|
+
raise
|
|
111
|
+
|
|
112
|
+
async def _create_chat_session(self, query: str) -> str:
|
|
113
|
+
"""Create a persistent chat session for the agent.
|
|
114
|
+
Args:
|
|
115
|
+
query: The initial query (not used in title anymore)
|
|
116
|
+
Returns:
|
|
117
|
+
The chat ID of the created session
|
|
118
|
+
Raises:
|
|
119
|
+
RuntimeError: If chat creation fails
|
|
120
|
+
"""
|
|
121
|
+
chat_payload = {
|
|
122
|
+
"title": f"Remote Agent Session - {self.agent_id}",
|
|
123
|
+
"agent_id": self.agent_id,
|
|
124
|
+
"type": "agent_execution",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
|
|
128
|
+
chat_url = f"{self.base_url}{API_CHATS_ENDPOINT}"
|
|
129
|
+
|
|
130
|
+
logger.info(f"📝 Creating chat session for agent {self.agent_id}")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
chat_response = await self._client.post(chat_url, json=chat_payload, headers=headers)
|
|
134
|
+
chat_response.raise_for_status()
|
|
135
|
+
|
|
136
|
+
chat_data = chat_response.json()
|
|
137
|
+
chat_id = chat_data["id"]
|
|
138
|
+
logger.info(f"✅ Chat session created: {chat_id}")
|
|
139
|
+
return chat_id
|
|
140
|
+
|
|
141
|
+
except httpx.HTTPStatusError as e:
|
|
142
|
+
status_code = e.response.status_code
|
|
143
|
+
response_text = e.response.text
|
|
144
|
+
|
|
145
|
+
if status_code == 404:
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
|
|
148
|
+
"Please verify the agent ID and ensure it exists in your account."
|
|
149
|
+
) from e
|
|
150
|
+
else:
|
|
151
|
+
raise RuntimeError(f"Failed to create chat session: {status_code} - {response_text}") from e
|
|
152
|
+
except Exception as e:
|
|
153
|
+
raise RuntimeError(f"Failed to create chat session: {str(e)}") from e
|
|
154
|
+
|
|
155
|
+
async def run(
|
|
156
|
+
self,
|
|
157
|
+
query: str,
|
|
158
|
+
max_steps: int | None = None,
|
|
159
|
+
manage_connector: bool = True,
|
|
160
|
+
external_history: list[BaseMessage] | None = None,
|
|
161
|
+
output_schema: type[T] | None = None,
|
|
162
|
+
) -> str | T:
|
|
163
|
+
"""Run a query on the remote agent.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
query: The query to execute
|
|
167
|
+
max_steps: Maximum number of steps (default: 10)
|
|
168
|
+
manage_connector: Ignored for remote execution
|
|
169
|
+
external_history: Ignored for remote execution (not supported yet)
|
|
170
|
+
output_schema: Optional Pydantic model for structured output
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The result from the remote agent execution (string or structured output)
|
|
174
|
+
"""
|
|
175
|
+
if external_history is not None:
|
|
176
|
+
logger.warning("External history is not yet supported for remote execution")
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
logger.info(f"🌐 Executing query on remote agent {self.agent_id}")
|
|
180
|
+
|
|
181
|
+
# Step 1: Create a chat session for this agent (only if we don't have one)
|
|
182
|
+
if self._chat_id is None:
|
|
183
|
+
self._chat_id = await self._create_chat_session(query)
|
|
184
|
+
|
|
185
|
+
chat_id = self._chat_id
|
|
186
|
+
|
|
187
|
+
# Step 2: Execute the agent within the chat context
|
|
188
|
+
execution_payload = {"query": query, "max_steps": max_steps or 10}
|
|
189
|
+
|
|
190
|
+
# Add structured output schema if provided
|
|
191
|
+
if output_schema is not None:
|
|
192
|
+
execution_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
|
|
193
|
+
logger.info(f"🔧 Using structured output with schema: {output_schema.__name__}")
|
|
194
|
+
|
|
195
|
+
headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
|
|
196
|
+
execution_url = f"{self.base_url}{API_CHAT_EXECUTE_ENDPOINT.format(chat_id=chat_id)}"
|
|
197
|
+
logger.info(f"🚀 Executing agent in chat {chat_id}")
|
|
198
|
+
|
|
199
|
+
response = await self._client.post(execution_url, json=execution_payload, headers=headers)
|
|
200
|
+
response.raise_for_status()
|
|
201
|
+
|
|
202
|
+
result = response.json()
|
|
203
|
+
logger.info(f"🔧 Response: {result}")
|
|
204
|
+
logger.info("✅ Remote execution completed successfully")
|
|
205
|
+
|
|
206
|
+
# Check for error responses (even with 200 status)
|
|
207
|
+
if isinstance(result, dict):
|
|
208
|
+
# Check for actual error conditions (not just presence of error field)
|
|
209
|
+
if result.get("status") == "error" or (result.get("error") is not None):
|
|
210
|
+
error_msg = result.get("error", str(result))
|
|
211
|
+
logger.error(f"❌ Remote agent execution failed: {error_msg}")
|
|
212
|
+
raise RuntimeError(f"Remote agent execution failed: {error_msg}")
|
|
213
|
+
|
|
214
|
+
# Check if the response indicates agent initialization failure
|
|
215
|
+
if "failed to initialize" in str(result):
|
|
216
|
+
logger.error(f"❌ Agent initialization failed: {result}")
|
|
217
|
+
raise RuntimeError(
|
|
218
|
+
f"Agent initialization failed on remote server. "
|
|
219
|
+
f"This usually indicates:\n"
|
|
220
|
+
f"• Invalid agent configuration (LLM model, system prompt)\n"
|
|
221
|
+
f"• Missing or invalid MCP server configurations\n"
|
|
222
|
+
f"• Network connectivity issues with MCP servers\n"
|
|
223
|
+
f"• Missing environment variables or credentials\n"
|
|
224
|
+
f"Raw error: {result}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Handle structured output
|
|
228
|
+
if output_schema is not None:
|
|
229
|
+
return self._parse_structured_response(result, output_schema)
|
|
230
|
+
|
|
231
|
+
# Regular string output
|
|
232
|
+
if isinstance(result, dict) and "result" in result:
|
|
233
|
+
return result["result"]
|
|
234
|
+
elif isinstance(result, str):
|
|
235
|
+
return result
|
|
236
|
+
else:
|
|
237
|
+
return str(result)
|
|
238
|
+
|
|
239
|
+
except httpx.HTTPStatusError as e:
|
|
240
|
+
status_code = e.response.status_code
|
|
241
|
+
response_text = e.response.text
|
|
242
|
+
|
|
243
|
+
# Provide specific error messages based on status code
|
|
244
|
+
if status_code == 401:
|
|
245
|
+
logger.error(f"❌ Authentication failed: {response_text}")
|
|
246
|
+
raise RuntimeError(
|
|
247
|
+
"Authentication failed: Invalid or missing API key. "
|
|
248
|
+
"Please check your API key and ensure the MCP_USE_API_KEY environment variable is set correctly."
|
|
249
|
+
) from e
|
|
250
|
+
elif status_code == 403:
|
|
251
|
+
logger.error(f"❌ Access forbidden: {response_text}")
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
f"Access denied: You don't have permission to execute agent '{self.agent_id}'. "
|
|
254
|
+
"Check if the agent exists and you have the necessary permissions."
|
|
255
|
+
) from e
|
|
256
|
+
elif status_code == 404:
|
|
257
|
+
logger.error(f"❌ Agent not found: {response_text}")
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
|
|
260
|
+
"Please verify the agent ID and ensure it exists in your account."
|
|
261
|
+
) from e
|
|
262
|
+
elif status_code == 422:
|
|
263
|
+
logger.error(f"❌ Validation error: {response_text}")
|
|
264
|
+
raise RuntimeError(
|
|
265
|
+
f"Request validation failed: {response_text}. "
|
|
266
|
+
"Please check your query parameters and output schema format."
|
|
267
|
+
) from e
|
|
268
|
+
elif status_code == 500:
|
|
269
|
+
logger.error(f"❌ Server error: {response_text}")
|
|
270
|
+
raise RuntimeError(
|
|
271
|
+
"Internal server error occurred during agent execution. "
|
|
272
|
+
"Please try again later or contact support if the issue persists."
|
|
273
|
+
) from e
|
|
274
|
+
else:
|
|
275
|
+
logger.error(f"❌ Remote execution failed with status {status_code}: {response_text}")
|
|
276
|
+
raise RuntimeError(f"Remote agent execution failed: {status_code} - {response_text}") from e
|
|
277
|
+
except httpx.TimeoutException as e:
|
|
278
|
+
logger.error(f"❌ Remote execution timed out: {e}")
|
|
279
|
+
raise RuntimeError(
|
|
280
|
+
"Remote agent execution timed out. The server may be overloaded or the query is taking too long to "
|
|
281
|
+
"process. Try again or use a simpler query."
|
|
282
|
+
) from e
|
|
283
|
+
except httpx.ConnectError as e:
|
|
284
|
+
logger.error(f"❌ Remote execution connection error: {e}")
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
f"Remote agent connection failed: Cannot connect to {self.base_url}. "
|
|
287
|
+
f"Check if the server is running and the URL is correct."
|
|
288
|
+
) from e
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(f"❌ Remote execution error: {e}")
|
|
291
|
+
raise RuntimeError(f"Remote agent execution failed: {str(e)}") from e
|
|
292
|
+
|
|
293
|
+
async def close(self) -> None:
|
|
294
|
+
"""Close the HTTP client."""
|
|
295
|
+
await self._client.aclose()
|
|
296
|
+
logger.info("🔌 Remote agent client closed")
|
mcp_use/client.py
CHANGED
|
@@ -9,7 +9,7 @@ import json
|
|
|
9
9
|
import warnings
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
|
-
from mcp.client.session import ElicitationFnT, SamplingFnT
|
|
12
|
+
from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
13
13
|
|
|
14
14
|
from mcp_use.types.sandbox import SandboxOptions
|
|
15
15
|
|
|
@@ -28,10 +28,13 @@ class MCPClient:
|
|
|
28
28
|
def __init__(
|
|
29
29
|
self,
|
|
30
30
|
config: str | dict[str, Any] | None = None,
|
|
31
|
+
allowed_servers: list[str] | None = None,
|
|
31
32
|
sandbox: bool = False,
|
|
32
33
|
sandbox_options: SandboxOptions | None = None,
|
|
33
34
|
sampling_callback: SamplingFnT | None = None,
|
|
34
35
|
elicitation_callback: ElicitationFnT | None = None,
|
|
36
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
37
|
+
logging_callback: LoggingFnT | None = None,
|
|
35
38
|
) -> None:
|
|
36
39
|
"""Initialize a new MCP client.
|
|
37
40
|
|
|
@@ -43,12 +46,15 @@ class MCPClient:
|
|
|
43
46
|
sampling_callback: Optional sampling callback function.
|
|
44
47
|
"""
|
|
45
48
|
self.config: dict[str, Any] = {}
|
|
49
|
+
self.allowed_servers: list[str] = allowed_servers
|
|
46
50
|
self.sandbox = sandbox
|
|
47
51
|
self.sandbox_options = sandbox_options
|
|
48
52
|
self.sessions: dict[str, MCPSession] = {}
|
|
49
53
|
self.active_sessions: list[str] = []
|
|
50
54
|
self.sampling_callback = sampling_callback
|
|
51
55
|
self.elicitation_callback = elicitation_callback
|
|
56
|
+
self.message_handler = message_handler
|
|
57
|
+
self.logging_callback = logging_callback
|
|
52
58
|
# Load configuration if provided
|
|
53
59
|
if config is not None:
|
|
54
60
|
if isinstance(config, str):
|
|
@@ -64,6 +70,8 @@ class MCPClient:
|
|
|
64
70
|
sandbox_options: SandboxOptions | None = None,
|
|
65
71
|
sampling_callback: SamplingFnT | None = None,
|
|
66
72
|
elicitation_callback: ElicitationFnT | None = None,
|
|
73
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
74
|
+
logging_callback: LoggingFnT | None = None,
|
|
67
75
|
) -> "MCPClient":
|
|
68
76
|
"""Create a MCPClient from a dictionary.
|
|
69
77
|
|
|
@@ -80,6 +88,8 @@ class MCPClient:
|
|
|
80
88
|
sandbox_options=sandbox_options,
|
|
81
89
|
sampling_callback=sampling_callback,
|
|
82
90
|
elicitation_callback=elicitation_callback,
|
|
91
|
+
message_handler=message_handler,
|
|
92
|
+
logging_callback=logging_callback,
|
|
83
93
|
)
|
|
84
94
|
|
|
85
95
|
@classmethod
|
|
@@ -90,6 +100,8 @@ class MCPClient:
|
|
|
90
100
|
sandbox_options: SandboxOptions | None = None,
|
|
91
101
|
sampling_callback: SamplingFnT | None = None,
|
|
92
102
|
elicitation_callback: ElicitationFnT | None = None,
|
|
103
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
104
|
+
logging_callback: LoggingFnT | None = None,
|
|
93
105
|
) -> "MCPClient":
|
|
94
106
|
"""Create a MCPClient from a configuration file.
|
|
95
107
|
|
|
@@ -106,6 +118,8 @@ class MCPClient:
|
|
|
106
118
|
sandbox_options=sandbox_options,
|
|
107
119
|
sampling_callback=sampling_callback,
|
|
108
120
|
elicitation_callback=elicitation_callback,
|
|
121
|
+
message_handler=message_handler,
|
|
122
|
+
logging_callback=logging_callback,
|
|
109
123
|
)
|
|
110
124
|
|
|
111
125
|
def add_server(
|
|
@@ -185,6 +199,8 @@ class MCPClient:
|
|
|
185
199
|
sandbox_options=self.sandbox_options,
|
|
186
200
|
sampling_callback=self.sampling_callback,
|
|
187
201
|
elicitation_callback=self.elicitation_callback,
|
|
202
|
+
message_handler=self.message_handler,
|
|
203
|
+
logging_callback=self.logging_callback,
|
|
188
204
|
)
|
|
189
205
|
|
|
190
206
|
# Create the session
|
|
@@ -220,9 +236,10 @@ class MCPClient:
|
|
|
220
236
|
warnings.warn("No MCP servers defined in config", UserWarning, stacklevel=2)
|
|
221
237
|
return {}
|
|
222
238
|
|
|
223
|
-
# Create sessions for all servers
|
|
239
|
+
# Create sessions only for allowed servers if applicable else create for all servers
|
|
224
240
|
for name in servers:
|
|
225
|
-
|
|
241
|
+
if self.allowed_servers is None or name in self.allowed_servers:
|
|
242
|
+
await self.create_session(name, auto_initialize)
|
|
226
243
|
|
|
227
244
|
return self.sessions
|
|
228
245
|
|
mcp_use/config.py
CHANGED
|
@@ -7,17 +7,11 @@ This module provides functionality to load MCP configuration from JSON files.
|
|
|
7
7
|
import json
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
from mcp.client.session import ElicitationFnT, SamplingFnT
|
|
10
|
+
from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
11
11
|
|
|
12
12
|
from mcp_use.types.sandbox import SandboxOptions
|
|
13
13
|
|
|
14
|
-
from .connectors import
|
|
15
|
-
BaseConnector,
|
|
16
|
-
HttpConnector,
|
|
17
|
-
SandboxConnector,
|
|
18
|
-
StdioConnector,
|
|
19
|
-
WebSocketConnector,
|
|
20
|
-
)
|
|
14
|
+
from .connectors import BaseConnector, HttpConnector, SandboxConnector, StdioConnector, WebSocketConnector
|
|
21
15
|
from .connectors.utils import is_stdio_server
|
|
22
16
|
|
|
23
17
|
|
|
@@ -40,6 +34,8 @@ def create_connector_from_config(
|
|
|
40
34
|
sandbox_options: SandboxOptions | None = None,
|
|
41
35
|
sampling_callback: SamplingFnT | None = None,
|
|
42
36
|
elicitation_callback: ElicitationFnT | None = None,
|
|
37
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
38
|
+
logging_callback: LoggingFnT | None = None,
|
|
43
39
|
) -> BaseConnector:
|
|
44
40
|
"""Create a connector based on server configuration.
|
|
45
41
|
This function can be called with just the server_config parameter:
|
|
@@ -61,6 +57,8 @@ def create_connector_from_config(
|
|
|
61
57
|
env=server_config.get("env", None),
|
|
62
58
|
sampling_callback=sampling_callback,
|
|
63
59
|
elicitation_callback=elicitation_callback,
|
|
60
|
+
message_handler=message_handler,
|
|
61
|
+
logging_callback=logging_callback,
|
|
64
62
|
)
|
|
65
63
|
|
|
66
64
|
# Sandboxed connector
|
|
@@ -72,6 +70,8 @@ def create_connector_from_config(
|
|
|
72
70
|
e2b_options=sandbox_options,
|
|
73
71
|
sampling_callback=sampling_callback,
|
|
74
72
|
elicitation_callback=elicitation_callback,
|
|
73
|
+
message_handler=message_handler,
|
|
74
|
+
logging_callback=logging_callback,
|
|
75
75
|
)
|
|
76
76
|
|
|
77
77
|
# HTTP connector
|
|
@@ -80,8 +80,12 @@ def create_connector_from_config(
|
|
|
80
80
|
base_url=server_config["url"],
|
|
81
81
|
headers=server_config.get("headers", None),
|
|
82
82
|
auth_token=server_config.get("auth_token", None),
|
|
83
|
+
timeout=server_config.get("timeout", 5),
|
|
84
|
+
sse_read_timeout=server_config.get("sse_read_timeout", 60 * 5),
|
|
83
85
|
sampling_callback=sampling_callback,
|
|
84
86
|
elicitation_callback=elicitation_callback,
|
|
87
|
+
message_handler=message_handler,
|
|
88
|
+
logging_callback=logging_callback,
|
|
85
89
|
)
|
|
86
90
|
|
|
87
91
|
# WebSocket connector
|
mcp_use/connectors/base.py
CHANGED
|
@@ -5,14 +5,27 @@ This module provides the base connector interface that all MCP connectors
|
|
|
5
5
|
must implement.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import warnings
|
|
8
9
|
from abc import ABC, abstractmethod
|
|
9
10
|
from datetime import timedelta
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
from mcp import ClientSession, Implementation
|
|
13
|
-
from mcp.client.session import ElicitationFnT, SamplingFnT
|
|
14
|
+
from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
14
15
|
from mcp.shared.exceptions import McpError
|
|
15
|
-
from mcp.types import
|
|
16
|
+
from mcp.types import (
|
|
17
|
+
CallToolResult,
|
|
18
|
+
GetPromptResult,
|
|
19
|
+
Prompt,
|
|
20
|
+
PromptListChangedNotification,
|
|
21
|
+
ReadResourceResult,
|
|
22
|
+
Resource,
|
|
23
|
+
ResourceListChangedNotification,
|
|
24
|
+
ServerCapabilities,
|
|
25
|
+
ServerNotification,
|
|
26
|
+
Tool,
|
|
27
|
+
ToolListChangedNotification,
|
|
28
|
+
)
|
|
16
29
|
from pydantic import AnyUrl
|
|
17
30
|
|
|
18
31
|
import mcp_use
|
|
@@ -31,6 +44,8 @@ class BaseConnector(ABC):
|
|
|
31
44
|
self,
|
|
32
45
|
sampling_callback: SamplingFnT | None = None,
|
|
33
46
|
elicitation_callback: ElicitationFnT | None = None,
|
|
47
|
+
message_handler: MessageHandlerFnT | None = None,
|
|
48
|
+
logging_callback: LoggingFnT | None = None,
|
|
34
49
|
):
|
|
35
50
|
"""Initialize base connector with common attributes."""
|
|
36
51
|
self.client_session: ClientSession | None = None
|
|
@@ -43,6 +58,9 @@ class BaseConnector(ABC):
|
|
|
43
58
|
self.auto_reconnect = True # Whether to automatically reconnect on connection loss (not configurable for now)
|
|
44
59
|
self.sampling_callback = sampling_callback
|
|
45
60
|
self.elicitation_callback = elicitation_callback
|
|
61
|
+
self.message_handler = message_handler
|
|
62
|
+
self.logging_callback = logging_callback
|
|
63
|
+
self.capabilities: ServerCapabilities | None = None
|
|
46
64
|
|
|
47
65
|
@property
|
|
48
66
|
def client_info(self) -> Implementation:
|
|
@@ -53,6 +71,20 @@ class BaseConnector(ABC):
|
|
|
53
71
|
url="https://github.com/mcp-use/mcp-use",
|
|
54
72
|
)
|
|
55
73
|
|
|
74
|
+
async def _internal_message_handler(self, message: Any) -> None:
|
|
75
|
+
"""Wrap the user-provided message handler."""
|
|
76
|
+
if isinstance(message, ServerNotification):
|
|
77
|
+
if isinstance(message.root, ToolListChangedNotification):
|
|
78
|
+
logger.debug("Received tool list changed notification")
|
|
79
|
+
elif isinstance(message.root, ResourceListChangedNotification):
|
|
80
|
+
logger.debug("Received resource list changed notification")
|
|
81
|
+
elif isinstance(message.root, PromptListChangedNotification):
|
|
82
|
+
logger.debug("Received prompt list changed notification")
|
|
83
|
+
|
|
84
|
+
# Call the user's handler
|
|
85
|
+
if self.message_handler:
|
|
86
|
+
await self.message_handler(message)
|
|
87
|
+
|
|
56
88
|
@abstractmethod
|
|
57
89
|
async def connect(self) -> None:
|
|
58
90
|
"""Establish a connection to the MCP implementation."""
|
|
@@ -125,37 +157,37 @@ class BaseConnector(ABC):
|
|
|
125
157
|
result = await self.client_session.initialize()
|
|
126
158
|
self._initialized = True # Mark as initialized
|
|
127
159
|
|
|
128
|
-
|
|
160
|
+
self.capabilities = result.capabilities
|
|
129
161
|
|
|
130
|
-
if
|
|
162
|
+
if self.capabilities.tools:
|
|
131
163
|
# Get available tools directly from client session
|
|
132
164
|
try:
|
|
133
165
|
tools_result = await self.client_session.list_tools()
|
|
134
166
|
self._tools = tools_result.tools if tools_result else []
|
|
135
167
|
except Exception as e:
|
|
136
|
-
logger.error(f"Error listing tools: {e}")
|
|
168
|
+
logger.error(f"Error listing tools for connector {self.public_identifier}: {e}")
|
|
137
169
|
self._tools = []
|
|
138
170
|
else:
|
|
139
171
|
self._tools = []
|
|
140
172
|
|
|
141
|
-
if
|
|
173
|
+
if self.capabilities.resources:
|
|
142
174
|
# Get available resources directly from client session
|
|
143
175
|
try:
|
|
144
176
|
resources_result = await self.client_session.list_resources()
|
|
145
177
|
self._resources = resources_result.resources if resources_result else []
|
|
146
178
|
except Exception as e:
|
|
147
|
-
logger.error(f"Error listing resources: {e}")
|
|
179
|
+
logger.error(f"Error listing resources for connector {self.public_identifier}: {e}")
|
|
148
180
|
self._resources = []
|
|
149
181
|
else:
|
|
150
182
|
self._resources = []
|
|
151
183
|
|
|
152
|
-
if
|
|
184
|
+
if self.capabilities.prompts:
|
|
153
185
|
# Get available prompts directly from client session
|
|
154
186
|
try:
|
|
155
187
|
prompts_result = await self.client_session.list_prompts()
|
|
156
188
|
self._prompts = prompts_result.prompts if prompts_result else []
|
|
157
189
|
except Exception as e:
|
|
158
|
-
logger.error(f"Error listing prompts: {e}")
|
|
190
|
+
logger.error(f"Error listing prompts for connector {self.public_identifier}: {e}")
|
|
159
191
|
self._prompts = []
|
|
160
192
|
else:
|
|
161
193
|
self._prompts = []
|
|
@@ -170,21 +202,57 @@ class BaseConnector(ABC):
|
|
|
170
202
|
|
|
171
203
|
@property
|
|
172
204
|
def tools(self) -> list[Tool]:
|
|
173
|
-
"""Get the list of available tools.
|
|
205
|
+
"""Get the list of available tools.
|
|
206
|
+
|
|
207
|
+
.. deprecated::
|
|
208
|
+
This property is deprecated because it may return stale data when the server
|
|
209
|
+
sends list change notifications. Use `await list_tools()` instead to ensure
|
|
210
|
+
you always get the latest data.
|
|
211
|
+
"""
|
|
212
|
+
warnings.warn(
|
|
213
|
+
"The 'tools' property is deprecated and may return stale data. "
|
|
214
|
+
"Use 'await list_tools()' instead to ensure fresh data.",
|
|
215
|
+
DeprecationWarning,
|
|
216
|
+
stacklevel=2,
|
|
217
|
+
)
|
|
174
218
|
if self._tools is None:
|
|
175
219
|
raise RuntimeError("MCP client is not initialized")
|
|
176
220
|
return self._tools
|
|
177
221
|
|
|
178
222
|
@property
|
|
179
223
|
def resources(self) -> list[Resource]:
|
|
180
|
-
"""Get the list of available resources.
|
|
224
|
+
"""Get the list of available resources.
|
|
225
|
+
|
|
226
|
+
.. deprecated::
|
|
227
|
+
This property is deprecated because it may return stale data when the server
|
|
228
|
+
sends list change notifications. Use `await list_resources()` instead to ensure
|
|
229
|
+
you always get the latest data.
|
|
230
|
+
"""
|
|
231
|
+
warnings.warn(
|
|
232
|
+
"The 'resources' property is deprecated and may return stale data. "
|
|
233
|
+
"Use 'await list_resources()' instead to ensure fresh data.",
|
|
234
|
+
DeprecationWarning,
|
|
235
|
+
stacklevel=2,
|
|
236
|
+
)
|
|
181
237
|
if self._resources is None:
|
|
182
238
|
raise RuntimeError("MCP client is not initialized")
|
|
183
239
|
return self._resources
|
|
184
240
|
|
|
185
241
|
@property
|
|
186
242
|
def prompts(self) -> list[Prompt]:
|
|
187
|
-
"""Get the list of available prompts.
|
|
243
|
+
"""Get the list of available prompts.
|
|
244
|
+
|
|
245
|
+
.. deprecated::
|
|
246
|
+
This property is deprecated because it may return stale data when the server
|
|
247
|
+
sends list change notifications. Use `await list_prompts()' instead to ensure
|
|
248
|
+
you always get the latest data.
|
|
249
|
+
"""
|
|
250
|
+
warnings.warn(
|
|
251
|
+
"The 'prompts' property is deprecated and may return stale data. "
|
|
252
|
+
"Use 'await list_prompts()' instead to ensure fresh data.",
|
|
253
|
+
DeprecationWarning,
|
|
254
|
+
stacklevel=2,
|
|
255
|
+
)
|
|
188
256
|
if self._prompts is None:
|
|
189
257
|
raise RuntimeError("MCP client is not initialized")
|
|
190
258
|
return self._prompts
|
|
@@ -303,28 +371,39 @@ class BaseConnector(ABC):
|
|
|
303
371
|
async def list_tools(self) -> list[Tool]:
|
|
304
372
|
"""List all available tools from the MCP implementation."""
|
|
305
373
|
|
|
374
|
+
if self.capabilities and not self.capabilities.tools:
|
|
375
|
+
logger.debug(f"Server {self.public_identifier} does not support tools")
|
|
376
|
+
return []
|
|
377
|
+
|
|
306
378
|
# Ensure we're connected
|
|
307
379
|
await self._ensure_connected()
|
|
308
380
|
|
|
309
381
|
logger.debug("Listing tools")
|
|
310
382
|
try:
|
|
311
383
|
result = await self.client_session.list_tools()
|
|
384
|
+
self._tools = result.tools
|
|
312
385
|
return result.tools
|
|
313
386
|
except McpError as e:
|
|
314
|
-
logger.error(f"Error listing tools: {e}")
|
|
387
|
+
logger.error(f"Error listing tools for connector {self.public_identifier}: {e}")
|
|
315
388
|
return []
|
|
316
389
|
|
|
317
390
|
async def list_resources(self) -> list[Resource]:
|
|
318
391
|
"""List all available resources from the MCP implementation."""
|
|
392
|
+
|
|
393
|
+
if self.capabilities and not self.capabilities.resources:
|
|
394
|
+
logger.debug(f"Server {self.public_identifier} does not support resources")
|
|
395
|
+
return []
|
|
396
|
+
|
|
319
397
|
# Ensure we're connected
|
|
320
398
|
await self._ensure_connected()
|
|
321
399
|
|
|
322
400
|
logger.debug("Listing resources")
|
|
323
401
|
try:
|
|
324
402
|
result = await self.client_session.list_resources()
|
|
403
|
+
self._resources = result.resources
|
|
325
404
|
return result.resources
|
|
326
405
|
except McpError as e:
|
|
327
|
-
logger.
|
|
406
|
+
logger.warning(f"Error listing resources for connector {self.public_identifier}: {e}")
|
|
328
407
|
return []
|
|
329
408
|
|
|
330
409
|
async def read_resource(self, uri: AnyUrl) -> ReadResourceResult:
|
|
@@ -337,14 +416,20 @@ class BaseConnector(ABC):
|
|
|
337
416
|
|
|
338
417
|
async def list_prompts(self) -> list[Prompt]:
|
|
339
418
|
"""List all available prompts from the MCP implementation."""
|
|
419
|
+
|
|
420
|
+
if self.capabilities and not self.capabilities.prompts:
|
|
421
|
+
logger.debug(f"Server {self.public_identifier} does not support prompts")
|
|
422
|
+
return []
|
|
423
|
+
|
|
340
424
|
await self._ensure_connected()
|
|
341
425
|
|
|
342
426
|
logger.debug("Listing prompts")
|
|
343
427
|
try:
|
|
344
428
|
result = await self.client_session.list_prompts()
|
|
429
|
+
self._prompts = result.prompts
|
|
345
430
|
return result.prompts
|
|
346
431
|
except McpError as e:
|
|
347
|
-
logger.error(f"Error listing prompts: {e}")
|
|
432
|
+
logger.error(f"Error listing prompts for connector {self.public_identifier}: {e}")
|
|
348
433
|
return []
|
|
349
434
|
|
|
350
435
|
async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult:
|