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/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
- API_CHAT_EXECUTE_ENDPOINT = "/api/v1/chats/{chat_id}/execute"
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
- return output_schema.model_validate({"content": str(result_data)})
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: {chat_id}")
179
+ logger.info(f"✅ [{self.chat_id}] New chat session created")
166
180
  else:
167
- logger.info(f"✅ Resumed chat session: {chat_id}")
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 run(
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 | T:
192
- """Run a query on the remote agent.
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
- try:
207
- logger.info(f"🌐 Executing query on remote agent {self.agent_id}")
208
-
209
- # Step 1: Ensure chat session exists on the backend by upserting.
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
- # Regular string output
263
- if isinstance(result, dict) and "result" in result:
264
- return result["result"]
265
- elif isinstance(result, str):
266
- return result
267
- else:
268
- return str(result)
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
- # Provide specific error messages based on status code
275
- if status_code == 401:
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
- logger.error(f" Remote execution failed with status {status_code}: {response_text}")
307
- raise RuntimeError(f"Remote agent execution failed: {status_code} - {response_text}") from e
308
- except httpx.TimeoutException as e:
309
- logger.error(f" Remote execution timed out: {e}")
310
- raise RuntimeError(
311
- "Remote agent execution timed out. The server may be overloaded or the query is taking too long to "
312
- "process. Try again or use a simpler query."
313
- ) from e
314
- except httpx.ConnectError as e:
315
- logger.error(f"❌ Remote execution connection error: {e}")
316
- raise RuntimeError(
317
- f"Remote agent connection failed: Cannot connect to {self.base_url}. "
318
- f"Check if the server is running and the URL is correct."
319
- ) from e
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" Remote execution error: {e}")
322
- raise RuntimeError(f"Remote agent execution failed: {str(e)}") from e
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."""
@@ -0,0 +1,6 @@
1
+ """Authentication support for MCP clients."""
2
+
3
+ from .bearer import BearerAuth
4
+ from .oauth import OAuth
5
+
6
+ __all__ = ["BearerAuth", "OAuth"]
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