mcp-use 1.3.10__py3-none-any.whl → 1.3.12__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/adapters/langchain_adapter.py +9 -52
- mcp_use/agents/mcpagent.py +88 -37
- mcp_use/agents/prompts/templates.py +1 -10
- mcp_use/agents/remote.py +154 -128
- mcp_use/auth/__init__.py +6 -0
- mcp_use/auth/bearer.py +17 -0
- mcp_use/auth/oauth.py +625 -0
- mcp_use/auth/oauth_callback.py +214 -0
- mcp_use/client.py +25 -1
- mcp_use/config.py +7 -2
- mcp_use/connectors/base.py +25 -12
- mcp_use/connectors/http.py +135 -27
- mcp_use/connectors/sandbox.py +12 -3
- mcp_use/connectors/stdio.py +11 -3
- mcp_use/connectors/websocket.py +15 -6
- mcp_use/exceptions.py +31 -0
- mcp_use/middleware/__init__.py +50 -0
- mcp_use/middleware/logging.py +31 -0
- mcp_use/middleware/metrics.py +314 -0
- mcp_use/middleware/middleware.py +262 -0
- mcp_use/task_managers/base.py +13 -23
- mcp_use/task_managers/sse.py +5 -0
- mcp_use/task_managers/streamable_http.py +5 -0
- {mcp_use-1.3.10.dist-info → mcp_use-1.3.12.dist-info}/METADATA +21 -25
- {mcp_use-1.3.10.dist-info → mcp_use-1.3.12.dist-info}/RECORD +28 -19
- {mcp_use-1.3.10.dist-info → mcp_use-1.3.12.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.10.dist-info → mcp_use-1.3.12.dist-info}/entry_points.txt +0 -0
- {mcp_use-1.3.10.dist-info → mcp_use-1.3.12.dist-info}/licenses/LICENSE +0 -0
mcp_use/agents/remote.py
CHANGED
|
@@ -4,6 +4,7 @@ Remote agent implementation for executing agents via API.
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
7
8
|
from typing import Any, TypeVar
|
|
8
9
|
from uuid import UUID
|
|
9
10
|
|
|
@@ -17,7 +18,7 @@ T = TypeVar("T", bound=BaseModel)
|
|
|
17
18
|
|
|
18
19
|
# API endpoint constants
|
|
19
20
|
API_CHATS_ENDPOINT = "/api/v1/chats/get-or-create"
|
|
20
|
-
|
|
21
|
+
API_CHAT_STREAM_ENDPOINT = "/api/v1/chats/{chat_id}/stream"
|
|
21
22
|
API_CHAT_DELETE_ENDPOINT = "/api/v1/chats/{chat_id}"
|
|
22
23
|
|
|
23
24
|
UUID_ERROR_MESSAGE = """A UUID is a 36 character string of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \n
|
|
@@ -129,12 +130,25 @@ class RemoteAgent:
|
|
|
129
130
|
|
|
130
131
|
# Parse into the Pydantic model
|
|
131
132
|
try:
|
|
133
|
+
logger.info(f"🔍 Attempting to validate result_data against {output_schema.__name__}")
|
|
134
|
+
logger.info(f"🔍 Result data type: {type(result_data)}")
|
|
135
|
+
logger.info(f"🔍 Result data: {result_data}")
|
|
132
136
|
return output_schema.model_validate(result_data)
|
|
133
137
|
except Exception as e:
|
|
134
|
-
logger.warning(f"Failed to parse structured output: {e}")
|
|
138
|
+
logger.warning(f"❌ Failed to parse structured output: {e}")
|
|
139
|
+
logger.warning(f"🔍 Validation error details: {type(e).__name__}: {str(e)}")
|
|
140
|
+
logger.warning(f"🔍 Result data that failed validation: {result_data}")
|
|
141
|
+
|
|
135
142
|
# Fallback: try to parse it as raw content if the model has a content field
|
|
136
143
|
if hasattr(output_schema, "model_fields") and "content" in output_schema.model_fields:
|
|
137
|
-
|
|
144
|
+
logger.info("🔄 Attempting fallback with content field")
|
|
145
|
+
try:
|
|
146
|
+
fallback_result = output_schema.model_validate({"content": str(result_data)})
|
|
147
|
+
logger.info("✅ Fallback parsing succeeded")
|
|
148
|
+
return fallback_result
|
|
149
|
+
except Exception as fallback_e:
|
|
150
|
+
logger.error(f"❌ Fallback parsing also failed: {fallback_e}")
|
|
151
|
+
raise
|
|
138
152
|
raise
|
|
139
153
|
|
|
140
154
|
async def _upsert_chat_session(self) -> str:
|
|
@@ -153,7 +167,7 @@ class RemoteAgent:
|
|
|
153
167
|
headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
|
|
154
168
|
chat_url = f"{self.base_url}{API_CHATS_ENDPOINT}"
|
|
155
169
|
|
|
156
|
-
logger.info(f"📝 Upserting chat session for agent {self.agent_id}")
|
|
170
|
+
logger.info(f"📝 [{self.chat_id}] Upserting chat session for agent {self.agent_id}")
|
|
157
171
|
|
|
158
172
|
try:
|
|
159
173
|
chat_response = await self._client.post(chat_url, json=chat_payload, headers=headers)
|
|
@@ -162,9 +176,9 @@ class RemoteAgent:
|
|
|
162
176
|
chat_data = chat_response.json()
|
|
163
177
|
chat_id = chat_data["id"]
|
|
164
178
|
if chat_response.status_code == 201:
|
|
165
|
-
logger.info(f"✅ New chat session created
|
|
179
|
+
logger.info(f"✅ [{self.chat_id}] New chat session created")
|
|
166
180
|
else:
|
|
167
|
-
logger.info(f"✅ Resumed chat session
|
|
181
|
+
logger.info(f"✅ [{self.chat_id}] Resumed chat session")
|
|
168
182
|
|
|
169
183
|
return chat_id
|
|
170
184
|
|
|
@@ -182,144 +196,156 @@ class RemoteAgent:
|
|
|
182
196
|
except Exception as e:
|
|
183
197
|
raise RuntimeError(f"Failed to create chat session: {str(e)}") from e
|
|
184
198
|
|
|
185
|
-
async def
|
|
199
|
+
async def stream(
|
|
186
200
|
self,
|
|
187
201
|
query: str,
|
|
188
202
|
max_steps: int | None = None,
|
|
189
203
|
external_history: list[BaseMessage] | None = None,
|
|
190
204
|
output_schema: type[T] | None = None,
|
|
191
|
-
) -> str
|
|
192
|
-
"""
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
query: The query to execute
|
|
196
|
-
max_steps: Maximum number of steps (default: 10)
|
|
197
|
-
external_history: External history (not supported yet for remote execution)
|
|
198
|
-
output_schema: Optional Pydantic model for structured output
|
|
199
|
-
|
|
200
|
-
Returns:
|
|
201
|
-
The result from the remote agent execution (string or structured output)
|
|
202
|
-
"""
|
|
205
|
+
) -> AsyncGenerator[str, None]:
|
|
206
|
+
"""Stream the execution of a query on the remote agent using HTTP streaming."""
|
|
203
207
|
if external_history is not None:
|
|
204
208
|
logger.warning("External history is not yet supported for remote execution")
|
|
205
209
|
|
|
206
|
-
|
|
207
|
-
logger.info(f"
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# This happens once per agent instance.
|
|
211
|
-
if not self._session_established:
|
|
212
|
-
logger.info(f"🔧 Establishing chat session for agent {self.agent_id}")
|
|
213
|
-
self.chat_id = await self._upsert_chat_session()
|
|
214
|
-
self._session_established = True
|
|
215
|
-
|
|
216
|
-
chat_id = self.chat_id
|
|
217
|
-
|
|
218
|
-
# Step 2: Execute the agent within the chat context
|
|
219
|
-
execution_payload = {"query": query, "max_steps": max_steps or 10}
|
|
220
|
-
|
|
221
|
-
# Add structured output schema if provided
|
|
222
|
-
if output_schema is not None:
|
|
223
|
-
execution_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
|
|
224
|
-
logger.info(f"🔧 Using structured output with schema: {output_schema.__name__}")
|
|
225
|
-
|
|
226
|
-
headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
|
|
227
|
-
execution_url = f"{self.base_url}{API_CHAT_EXECUTE_ENDPOINT.format(chat_id=chat_id)}"
|
|
228
|
-
logger.info(f"🚀 Executing agent in chat {chat_id}")
|
|
229
|
-
|
|
230
|
-
response = await self._client.post(execution_url, json=execution_payload, headers=headers)
|
|
231
|
-
response.raise_for_status()
|
|
232
|
-
|
|
233
|
-
result = response.json()
|
|
234
|
-
logger.info(f"🔧 Response: {result}")
|
|
235
|
-
logger.info("✅ Remote execution completed successfully")
|
|
236
|
-
|
|
237
|
-
# Check for error responses (even with 200 status)
|
|
238
|
-
if isinstance(result, dict):
|
|
239
|
-
# Check for actual error conditions (not just presence of error field)
|
|
240
|
-
if result.get("status") == "error" or (result.get("error") is not None):
|
|
241
|
-
error_msg = result.get("error", str(result))
|
|
242
|
-
logger.error(f"❌ Remote agent execution failed: {error_msg}")
|
|
243
|
-
raise RuntimeError(f"Remote agent execution failed: {error_msg}")
|
|
244
|
-
|
|
245
|
-
# Check if the response indicates agent initialization failure
|
|
246
|
-
if "failed to initialize" in str(result):
|
|
247
|
-
logger.error(f"❌ Agent initialization failed: {result}")
|
|
248
|
-
raise RuntimeError(
|
|
249
|
-
f"Agent initialization failed on remote server. "
|
|
250
|
-
f"This usually indicates:\n"
|
|
251
|
-
f"• Invalid agent configuration (LLM model, system prompt)\n"
|
|
252
|
-
f"• Missing or invalid MCP server configurations\n"
|
|
253
|
-
f"• Network connectivity issues with MCP servers\n"
|
|
254
|
-
f"• Missing environment variables or credentials\n"
|
|
255
|
-
f"Raw error: {result}"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
# Handle structured output
|
|
259
|
-
if output_schema is not None:
|
|
260
|
-
return self._parse_structured_response(result, output_schema)
|
|
210
|
+
if not self._session_established:
|
|
211
|
+
logger.info(f"🔧 [{self.chat_id}] Establishing chat session for agent {self.agent_id}")
|
|
212
|
+
self.chat_id = await self._upsert_chat_session()
|
|
213
|
+
self._session_established = True
|
|
261
214
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
215
|
+
chat_id = self.chat_id
|
|
216
|
+
stream_url = f"{self.base_url}{API_CHAT_STREAM_ENDPOINT.format(chat_id=chat_id)}"
|
|
217
|
+
|
|
218
|
+
# Prepare the request payload
|
|
219
|
+
request_payload = {"messages": [{"role": "user", "content": query}], "max_steps": max_steps or 30}
|
|
220
|
+
if output_schema is not None:
|
|
221
|
+
request_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
|
|
222
|
+
|
|
223
|
+
headers = {"Content-Type": "application/json", "x-api-key": self.api_key, "Accept": "text/event-stream"}
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
logger.info(f"🌐 [{self.chat_id}] Connecting to HTTP stream for agent {self.agent_id}")
|
|
227
|
+
|
|
228
|
+
async with self._client.stream("POST", stream_url, headers=headers, json=request_payload) as response:
|
|
229
|
+
logger.info(f"✅ [{self.chat_id}] HTTP stream connection established")
|
|
230
|
+
|
|
231
|
+
if response.status_code != 200:
|
|
232
|
+
error_text = await response.aread()
|
|
233
|
+
raise RuntimeError(f"Failed to stream from remote agent: {error_text.decode()}")
|
|
234
|
+
|
|
235
|
+
# Read the streaming response line by line
|
|
236
|
+
try:
|
|
237
|
+
async for line in response.aiter_lines():
|
|
238
|
+
if line:
|
|
239
|
+
yield line
|
|
240
|
+
except UnicodeDecodeError as e:
|
|
241
|
+
logger.error(f"❌ [{self.chat_id}] UTF-8 decoding error at position {e.start}: {e.reason}")
|
|
242
|
+
logger.error(f"❌ [{self.chat_id}] Error occurred while reading stream for agent {self.agent_id}")
|
|
243
|
+
# Try to read raw bytes and decode with error handling
|
|
244
|
+
logger.info(f"🔄 [{self.chat_id}] Attempting to read raw bytes with error handling...")
|
|
245
|
+
logger.info(f"✅ [{self.chat_id}] Agent execution stream completed")
|
|
269
246
|
|
|
270
247
|
except httpx.HTTPStatusError as e:
|
|
271
248
|
status_code = e.response.status_code
|
|
272
249
|
response_text = e.response.text
|
|
273
250
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
logger.error(f"❌ Authentication failed: {response_text}")
|
|
277
|
-
raise RuntimeError(
|
|
278
|
-
"Authentication failed: Invalid or missing API key. "
|
|
279
|
-
"Please check your API key and ensure the MCP_USE_API_KEY environment variable is set correctly."
|
|
280
|
-
) from e
|
|
281
|
-
elif status_code == 403:
|
|
282
|
-
logger.error(f"❌ Access forbidden: {response_text}")
|
|
283
|
-
raise RuntimeError(
|
|
284
|
-
f"Access denied: You don't have permission to execute agent '{self.agent_id}'. "
|
|
285
|
-
"Check if the agent exists and you have the necessary permissions."
|
|
286
|
-
) from e
|
|
287
|
-
elif status_code == 404:
|
|
288
|
-
logger.error(f"❌ Agent not found: {response_text}")
|
|
289
|
-
raise RuntimeError(
|
|
290
|
-
f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
|
|
291
|
-
"Please verify the agent ID and ensure it exists in your account."
|
|
292
|
-
) from e
|
|
293
|
-
elif status_code == 422:
|
|
294
|
-
logger.error(f"❌ Validation error: {response_text}")
|
|
295
|
-
raise RuntimeError(
|
|
296
|
-
f"Request validation failed: {response_text}. "
|
|
297
|
-
"Please check your query parameters and output schema format."
|
|
298
|
-
) from e
|
|
299
|
-
elif status_code == 500:
|
|
300
|
-
logger.error(f"❌ Server error: {response_text}")
|
|
301
|
-
raise RuntimeError(
|
|
302
|
-
"Internal server error occurred during agent execution. "
|
|
303
|
-
"Please try again later or contact support if the issue persists."
|
|
304
|
-
) from e
|
|
251
|
+
if status_code == 404:
|
|
252
|
+
raise RuntimeError(f"Chat or agent not found: {response_text}") from e
|
|
305
253
|
else:
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
254
|
+
raise RuntimeError(f"Failed to stream from remote agent: {status_code} - {response_text}") from e
|
|
255
|
+
except Exception as e:
|
|
256
|
+
logger.error(f"❌ [{self.chat_id}] An error occurred during HTTP streaming: {e}")
|
|
257
|
+
raise RuntimeError(f"Failed to stream from remote agent: {str(e)}") from e
|
|
258
|
+
|
|
259
|
+
async def run(
|
|
260
|
+
self,
|
|
261
|
+
query: str,
|
|
262
|
+
max_steps: int | None = None,
|
|
263
|
+
external_history: list[BaseMessage] | None = None,
|
|
264
|
+
output_schema: type[T] | None = None,
|
|
265
|
+
) -> str | T:
|
|
266
|
+
"""
|
|
267
|
+
Executes the agent and returns the final result.
|
|
268
|
+
This method uses HTTP streaming to avoid timeouts for long-running tasks.
|
|
269
|
+
It consumes the entire stream and returns only the final result.
|
|
270
|
+
"""
|
|
271
|
+
final_result = None
|
|
272
|
+
steps_taken = 0
|
|
273
|
+
finished = False
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Consume the ENTIRE stream to ensure proper execution
|
|
277
|
+
async for event in self.stream(query, max_steps, external_history, output_schema):
|
|
278
|
+
logger.debug(f"[{self.chat_id}] Processing stream event: {event}...")
|
|
279
|
+
|
|
280
|
+
# Parse AI SDK format events to extract final result
|
|
281
|
+
# The events follow the AI SDK streaming protocol
|
|
282
|
+
if event.startswith("0:"): # Text event
|
|
283
|
+
try:
|
|
284
|
+
text_data = json.loads(event[2:]) # Remove "0:" prefix
|
|
285
|
+
if final_result is None:
|
|
286
|
+
final_result = ""
|
|
287
|
+
final_result += text_data
|
|
288
|
+
result_preview = final_result[:200] if len(final_result) > 200 else final_result
|
|
289
|
+
logger.debug(f"Accumulated text result: {result_preview}...")
|
|
290
|
+
except json.JSONDecodeError:
|
|
291
|
+
logger.warning(f"Failed to parse text event: {event[:100]}")
|
|
292
|
+
continue
|
|
293
|
+
|
|
294
|
+
elif event.startswith("9:"): # Tool call event
|
|
295
|
+
steps_taken += 1
|
|
296
|
+
logger.debug(f"Tool call executed, total steps: {steps_taken}")
|
|
297
|
+
|
|
298
|
+
elif event.startswith("d:"): # Finish event
|
|
299
|
+
logger.debug("Received finish event, marking as finished")
|
|
300
|
+
finished = True
|
|
301
|
+
# Continue consuming to ensure stream cleanup
|
|
302
|
+
|
|
303
|
+
elif event.startswith("3:"): # Error event
|
|
304
|
+
try:
|
|
305
|
+
error_data = json.loads(event[2:])
|
|
306
|
+
error_msg = error_data if isinstance(error_data, str) else json.dumps(error_data)
|
|
307
|
+
raise RuntimeError(f"Agent execution failed: {error_msg}")
|
|
308
|
+
except json.JSONDecodeError as e:
|
|
309
|
+
raise RuntimeError("Agent execution failed with unknown error") from e
|
|
310
|
+
|
|
311
|
+
# Log completion of stream consumption
|
|
312
|
+
logger.info(f"Stream consumption complete. Finished: {finished}, Steps taken: {steps_taken}")
|
|
313
|
+
|
|
314
|
+
if final_result is None:
|
|
315
|
+
logger.warning(f"No final result captured from stream (structured output: {output_schema is not None})")
|
|
316
|
+
final_result = "" # Return empty string instead of error message
|
|
317
|
+
|
|
318
|
+
# For structured output, try to parse the result
|
|
319
|
+
if output_schema:
|
|
320
|
+
logger.info(f"🔍 Attempting structured output parsing for schema: {output_schema.__name__}")
|
|
321
|
+
logger.info(f"🔍 Raw final result type: {type(final_result)}")
|
|
322
|
+
logger.info(f"🔍 Raw final result length: {len(str(final_result)) if final_result else 0}")
|
|
323
|
+
logger.info(f"🔍 Raw final result preview: {str(final_result)[:500] if final_result else 'None'}...")
|
|
324
|
+
|
|
325
|
+
if isinstance(final_result, str) and final_result:
|
|
326
|
+
try:
|
|
327
|
+
# Try to parse as JSON first
|
|
328
|
+
parsed_result = json.loads(final_result)
|
|
329
|
+
logger.info("✅ Successfully parsed structured result as JSON")
|
|
330
|
+
return self._parse_structured_response(parsed_result, output_schema)
|
|
331
|
+
except json.JSONDecodeError as e:
|
|
332
|
+
logger.warning(f"❌ Could not parse result as JSON: {e}")
|
|
333
|
+
logger.warning(f"🔍 Raw string content: {final_result[:1000]}...")
|
|
334
|
+
# Try to parse directly
|
|
335
|
+
return self._parse_structured_response({"content": final_result}, output_schema)
|
|
336
|
+
else:
|
|
337
|
+
logger.warning(f"❌ Final result is empty or not string: {final_result}")
|
|
338
|
+
# Try to parse the result directly
|
|
339
|
+
return self._parse_structured_response(final_result, output_schema)
|
|
340
|
+
|
|
341
|
+
# Regular string output
|
|
342
|
+
return final_result if isinstance(final_result, str) else str(final_result)
|
|
343
|
+
|
|
344
|
+
except RuntimeError:
|
|
345
|
+
raise
|
|
320
346
|
except Exception as e:
|
|
321
|
-
logger.error(f"
|
|
322
|
-
raise RuntimeError(f"
|
|
347
|
+
logger.error(f"Error executing agent: {e}")
|
|
348
|
+
raise RuntimeError(f"Failed to execute agent: {str(e)}") from e
|
|
323
349
|
|
|
324
350
|
async def close(self) -> None:
|
|
325
351
|
"""Close the HTTP client."""
|
mcp_use/auth/__init__.py
ADDED
mcp_use/auth/bearer.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Bearer token authentication support."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Generator
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
from pydantic import BaseModel, SecretStr
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BearerAuth(httpx.Auth, BaseModel):
|
|
10
|
+
"""Bearer token authentication for HTTP requests."""
|
|
11
|
+
|
|
12
|
+
token: SecretStr
|
|
13
|
+
|
|
14
|
+
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
|
|
15
|
+
"""Apply bearer token authentication to the request."""
|
|
16
|
+
request.headers["Authorization"] = f"Bearer {self.token.get_secret_value()}"
|
|
17
|
+
yield request
|