mcp-use 1.3.9__py3-none-any.whl → 1.3.11__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 CHANGED
@@ -7,12 +7,16 @@ to MCP tools through existing LangChain adapters.
7
7
 
8
8
  from importlib.metadata import version
9
9
 
10
- from . import observability
10
+ # Import logging FIRST to ensure it's configured before other modules
11
+ # This MUST happen before importing observability to ensure loggers are configured
12
+ from .logging import MCP_USE_DEBUG, Logger, logger # isort: skip
13
+
14
+ # Now import other modules - observability must come after logging
15
+ from . import observability # noqa: E402
11
16
  from .agents.mcpagent import MCPAgent
12
17
  from .client import MCPClient
13
18
  from .config import load_config_file
14
19
  from .connectors import BaseConnector, HttpConnector, StdioConnector, WebSocketConnector
15
- from .logging import MCP_USE_DEBUG, Logger, logger
16
20
  from .session import MCPSession
17
21
 
18
22
  __version__ = version("mcp-use")
@@ -39,7 +39,7 @@ class LangChainAdapter(BaseAdapter):
39
39
  self._connector_tool_map: dict[BaseConnector, list[BaseTool]] = {}
40
40
 
41
41
  def fix_schema(self, schema: dict) -> dict:
42
- """Convert JSON Schema 'type': ['string', 'null'] to 'anyOf' format.
42
+ """Convert JSON Schema 'type': ['string', 'null'] to 'anyOf' format and fix enum handling.
43
43
 
44
44
  Args:
45
45
  schema: The JSON schema to fix.
@@ -51,6 +51,11 @@ class LangChainAdapter(BaseAdapter):
51
51
  if "type" in schema and isinstance(schema["type"], list):
52
52
  schema["anyOf"] = [{"type": t} for t in schema["type"]]
53
53
  del schema["type"] # Remove 'type' and standardize to 'anyOf'
54
+
55
+ # Fix enum handling - ensure enum fields are properly typed as strings
56
+ if "enum" in schema and "type" not in schema:
57
+ schema["type"] = "string"
58
+
54
59
  for key, value in schema.items():
55
60
  schema[key] = self.fix_schema(value) # Apply recursively
56
61
  return schema
@@ -71,11 +76,8 @@ class LangChainAdapter(BaseAdapter):
71
76
  if tool_result.isError:
72
77
  raise ToolException(f"Tool execution failed: {tool_result.content}")
73
78
 
74
- if not tool_result.content:
75
- raise ToolException("Tool execution returned no content")
76
-
77
79
  decoded_result = ""
78
- for item in tool_result.content:
80
+ for item in tool_result.content or []:
79
81
  match item.type:
80
82
  case "text":
81
83
  item: TextContent
@@ -30,7 +30,11 @@ from mcp_use.telemetry.utils import extract_model_info
30
30
 
31
31
  from ..adapters.langchain_adapter import LangChainAdapter
32
32
  from ..logging import logger
33
+ from ..managers.base import BaseServerManager
33
34
  from ..managers.server_manager import ServerManager
35
+
36
+ # Import observability manager
37
+ from ..observability import ObservabilityManager
34
38
  from .prompts.system_prompt_builder import create_system_message
35
39
  from .prompts.templates import DEFAULT_SYSTEM_PROMPT_TEMPLATE, SERVER_MANAGER_SYSTEM_PROMPT_TEMPLATE
36
40
  from .remote import RemoteAgent
@@ -62,10 +66,15 @@ class MCPAgent:
62
66
  disallowed_tools: list[str] | None = None,
63
67
  tools_used_names: list[str] | None = None,
64
68
  use_server_manager: bool = False,
69
+ server_manager: BaseServerManager | None = None,
65
70
  verbose: bool = False,
66
71
  agent_id: str | None = None,
67
72
  api_key: str | None = None,
68
73
  base_url: str = "https://cloud.mcp-use.com",
74
+ callbacks: list | None = None,
75
+ chat_id: str | None = None,
76
+ retry_on_error: bool = True,
77
+ max_retries_per_step: int = 2,
69
78
  ):
70
79
  """Initialize a new MCPAgent instance.
71
80
 
@@ -84,10 +93,13 @@ class MCPAgent:
84
93
  agent_id: Remote agent ID for remote execution. If provided, creates a remote agent.
85
94
  api_key: API key for remote execution. If None, checks MCP_USE_API_KEY env var.
86
95
  base_url: Base URL for remote API calls.
96
+ callbacks: List of LangChain callbacks to use. If None and Langfuse is configured, uses langfuse_handler.
97
+ retry_on_error: Whether to retry tool calls that fail due to validation errors.
98
+ max_retries_per_step: Maximum number of retries for validation errors per step.
87
99
  """
88
100
  # Handle remote execution
89
101
  if agent_id is not None:
90
- self._remote_agent = RemoteAgent(agent_id=agent_id, api_key=api_key, base_url=base_url)
102
+ self._remote_agent = RemoteAgent(agent_id=agent_id, api_key=api_key, base_url=base_url, chat_id=chat_id)
91
103
  self._is_remote = True
92
104
  return
93
105
 
@@ -109,13 +121,20 @@ class MCPAgent:
109
121
  self.disallowed_tools = disallowed_tools or []
110
122
  self.tools_used_names = tools_used_names or []
111
123
  self.use_server_manager = use_server_manager
124
+ self.server_manager = server_manager
112
125
  self.verbose = verbose
126
+ self.retry_on_error = retry_on_error
127
+ self.max_retries_per_step = max_retries_per_step
113
128
  # System prompt configuration
114
129
  self.system_prompt = system_prompt # User-provided full prompt override
115
130
  # User can provide a template override, otherwise use the imported default
116
131
  self.system_prompt_template_override = system_prompt_template
117
132
  self.additional_instructions = additional_instructions
118
133
 
134
+ # Set up observability callbacks using the ObservabilityManager
135
+ self.observability_manager = ObservabilityManager(custom_callbacks=callbacks)
136
+ self.callbacks = self.observability_manager.get_callbacks()
137
+
119
138
  # Either client or connector must be provided
120
139
  if not client and len(self.connectors) == 0:
121
140
  raise ValueError("Either client or connector must be provided")
@@ -126,9 +145,7 @@ class MCPAgent:
126
145
  # Initialize telemetry
127
146
  self.telemetry = Telemetry()
128
147
 
129
- # Initialize server manager if requested
130
- self.server_manager = None
131
- if self.use_server_manager:
148
+ if self.use_server_manager and self.server_manager is None:
132
149
  if not self.client:
133
150
  raise ValueError("Client must be provided when using server manager")
134
151
  self.server_manager = ServerManager(self.client, self.adapter)
@@ -246,9 +263,15 @@ class MCPAgent:
246
263
  # Use the standard create_tool_calling_agent
247
264
  agent = create_tool_calling_agent(llm=self.llm, tools=self._tools, prompt=prompt)
248
265
 
249
- # Use the standard AgentExecutor
250
- executor = AgentExecutor(agent=agent, tools=self._tools, max_iterations=self.max_steps, verbose=self.verbose)
251
- logger.debug(f"Created agent executor with max_iterations={self.max_steps}")
266
+ # Use the standard AgentExecutor with callbacks
267
+ executor = AgentExecutor(
268
+ agent=agent,
269
+ tools=self._tools,
270
+ max_iterations=self.max_steps,
271
+ verbose=self.verbose,
272
+ callbacks=self.callbacks,
273
+ )
274
+ logger.debug(f"Created agent executor with max_iterations={self.max_steps} and {len(self.callbacks)} callbacks")
252
275
  return executor
253
276
 
254
277
  def get_conversation_history(self) -> list[BaseMessage]:
@@ -469,6 +492,26 @@ class MCPAgent:
469
492
 
470
493
  logger.info(f"🏁 Starting agent execution with max_steps={steps}")
471
494
 
495
+ # Track whether agent finished successfully vs reached max iterations
496
+ agent_finished_successfully = False
497
+ result = None
498
+
499
+ # Create a run manager with our callbacks if we have any - ONCE for the entire execution
500
+ run_manager = None
501
+ if self.callbacks:
502
+ # Create an async callback manager with our callbacks
503
+ from langchain_core.callbacks.manager import AsyncCallbackManager
504
+
505
+ callback_manager = AsyncCallbackManager.configure(
506
+ inheritable_callbacks=self.callbacks,
507
+ local_callbacks=self.callbacks,
508
+ )
509
+ # Create a run manager for this chain execution
510
+ run_manager = await callback_manager.on_chain_start(
511
+ {"name": "MCPAgent (mcp-use)"},
512
+ inputs,
513
+ )
514
+
472
515
  for step_num in range(steps):
473
516
  steps_taken = step_num + 1
474
517
  # --- Check for tool updates if using server manager ---
@@ -498,20 +541,52 @@ class MCPAgent:
498
541
 
499
542
  # --- Plan and execute the next step ---
500
543
  try:
501
- # Use the internal _atake_next_step which handles planning and execution
502
- # This requires providing the necessary context like maps and intermediate steps
503
- next_step_output = await self._agent_executor._atake_next_step(
504
- name_to_tool_map=name_to_tool_map,
505
- color_mapping=color_mapping,
506
- inputs=inputs,
507
- intermediate_steps=intermediate_steps,
508
- run_manager=None,
509
- )
544
+ retry_count = 0
545
+ next_step_output = None
546
+
547
+ while retry_count <= self.max_retries_per_step:
548
+ try:
549
+ # Use the internal _atake_next_step which handles planning and execution
550
+ # This requires providing the necessary context like maps and intermediate steps
551
+ next_step_output = await self._agent_executor._atake_next_step(
552
+ name_to_tool_map=name_to_tool_map,
553
+ color_mapping=color_mapping,
554
+ inputs=inputs,
555
+ intermediate_steps=intermediate_steps,
556
+ run_manager=run_manager,
557
+ )
558
+
559
+ # If we get here, the step succeeded, break out of retry loop
560
+ break
561
+
562
+ except Exception as e:
563
+ if not self.retry_on_error or retry_count >= self.max_retries_per_step:
564
+ logger.error(f"❌ Validation error during step {step_num + 1}: {e}")
565
+ result = f"Agent stopped due to a validation error: {str(e)}"
566
+ success = False
567
+ yield result
568
+ return
569
+
570
+ retry_count += 1
571
+ logger.warning(
572
+ f"⚠️ Validation error, retrying ({retry_count}/{self.max_retries_per_step}): {e}"
573
+ )
574
+
575
+ # Create concise feedback for the LLM about the validation error
576
+ error_message = f"Error: {str(e)}"
577
+ inputs["input"] = error_message
578
+
579
+ # Continue to next iteration of retry loop
580
+ continue
510
581
 
511
582
  # Process the output
512
583
  if isinstance(next_step_output, AgentFinish):
513
584
  logger.info(f"✅ Agent finished at step {step_num + 1}")
585
+ agent_finished_successfully = True
514
586
  result = next_step_output.return_values.get("output", "No output generated")
587
+ # End the chain if we have a run manager
588
+ if run_manager:
589
+ await run_manager.on_chain_end({"output": result})
515
590
 
516
591
  # If structured output is requested, attempt to create it
517
592
  if output_schema and structured_llm:
@@ -563,6 +638,12 @@ class MCPAgent:
563
638
  for agent_step in next_step_output:
564
639
  yield agent_step
565
640
  action, observation = agent_step
641
+ reasoning = getattr(action, "log", "")
642
+ if reasoning:
643
+ reasoning_str = reasoning.replace("\n", " ")
644
+ if len(reasoning_str) > 300:
645
+ reasoning_str = reasoning_str[:297] + "..."
646
+ logger.info(f"💭 Reasoning: {reasoning_str}")
566
647
  tool_name = action.tool
567
648
  self.tools_used_names.append(tool_name)
568
649
  tool_input_str = str(action.tool_input)
@@ -583,25 +664,39 @@ class MCPAgent:
583
664
  tool_return = self._agent_executor._get_tool_return(last_step)
584
665
  if tool_return is not None:
585
666
  logger.info(f"🏆 Tool returned directly at step {step_num + 1}")
667
+ agent_finished_successfully = True
586
668
  result = tool_return.return_values.get("output", "No output generated")
587
669
  break
588
670
 
589
671
  except OutputParserException as e:
590
672
  logger.error(f"❌ Output parsing error during step {step_num + 1}: {e}")
591
673
  result = f"Agent stopped due to a parsing error: {str(e)}"
674
+ if run_manager:
675
+ await run_manager.on_chain_error(e)
592
676
  break
593
677
  except Exception as e:
594
678
  logger.error(f"❌ Error during agent execution step {step_num + 1}: {e}")
595
679
  import traceback
596
680
 
597
681
  traceback.print_exc()
682
+ # End the chain with error if we have a run manager
683
+ if run_manager:
684
+ await run_manager.on_chain_error(e)
598
685
  result = f"Agent stopped due to an error: {str(e)}"
599
686
  break
600
687
 
601
688
  # --- Loop finished ---
602
689
  if not result:
603
- logger.warning(f"⚠️ Agent stopped after reaching max iterations ({steps})")
604
- result = f"Agent stopped after reaching the maximum number of steps ({steps})."
690
+ if agent_finished_successfully:
691
+ # Agent finished successfully but returned empty output
692
+ result = "Agent completed the task successfully."
693
+ logger.info("✅ Agent finished successfully with empty output")
694
+ else:
695
+ # Agent actually reached max iterations
696
+ logger.warning(f"⚠️ Agent stopped after reaching max iterations ({steps})")
697
+ result = f"Agent stopped after reaching the maximum number of steps ({steps})."
698
+ if run_manager:
699
+ await run_manager.on_chain_end({"output": result})
605
700
 
606
701
  # If structured output was requested but not achieved, attempt one final time
607
702
  if output_schema and structured_llm and not success:
@@ -738,7 +833,8 @@ class MCPAgent:
738
833
  """
739
834
  # Delegate to remote agent if in remote mode
740
835
  if self._is_remote and self._remote_agent:
741
- return await self._remote_agent.run(query, max_steps, manage_connector, external_history, output_schema)
836
+ result = await self._remote_agent.run(query, max_steps, external_history, output_schema)
837
+ return result
742
838
 
743
839
  success = True
744
840
  start_time = time.time()
@@ -5,16 +5,7 @@ You have access to the following tools:
5
5
 
6
6
  {tool_descriptions}
7
7
 
8
- Use the following format:
9
-
10
- Question: the input question you must answer
11
- Thought: you should always think about what to do
12
- Action: the action to take, should be one of the available tools
13
- Action Input: the input to the action
14
- Observation: the result of the action
15
- ... (this Thought/Action/Action Input/Observation can repeat N times)
16
- Thought: I now know the final answer
17
- Final Answer: the final answer to the original input question"""
8
+ Use these tools to help answer questions and complete tasks as needed."""
18
9
 
19
10
 
20
11
  SERVER_MANAGER_SYSTEM_PROMPT_TEMPLATE = """You are a helpful assistant designed
mcp_use/agents/remote.py CHANGED
@@ -5,6 +5,7 @@ Remote agent implementation for executing agents via API.
5
5
  import json
6
6
  import os
7
7
  from typing import Any, TypeVar
8
+ from uuid import UUID
8
9
 
9
10
  import httpx
10
11
  from langchain.schema import BaseMessage
@@ -15,25 +16,52 @@ from ..logging import logger
15
16
  T = TypeVar("T", bound=BaseModel)
16
17
 
17
18
  # API endpoint constants
18
- API_CHATS_ENDPOINT = "/api/v1/chats"
19
+ API_CHATS_ENDPOINT = "/api/v1/chats/get-or-create"
19
20
  API_CHAT_EXECUTE_ENDPOINT = "/api/v1/chats/{chat_id}/execute"
20
21
  API_CHAT_DELETE_ENDPOINT = "/api/v1/chats/{chat_id}"
21
22
 
23
+ UUID_ERROR_MESSAGE = """A UUID is a 36 character string of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \n
24
+ Example: 123e4567-e89b-12d3-a456-426614174000
25
+ To generate a UUID, you can use the following command:
26
+ import uuid
27
+
28
+ # Generate a random UUID
29
+ my_uuid = uuid.uuid4()
30
+ print(my_uuid)
31
+ """
32
+
22
33
 
23
34
  class RemoteAgent:
24
35
  """Agent that executes remotely via API."""
25
36
 
26
- def __init__(self, agent_id: str, api_key: str | None = None, base_url: str = "https://cloud.mcp-use.com"):
37
+ def __init__(
38
+ self,
39
+ agent_id: str,
40
+ chat_id: str | None = None,
41
+ api_key: str | None = None,
42
+ base_url: str = "https://cloud.mcp-use.com",
43
+ ):
27
44
  """Initialize remote agent.
28
45
 
29
46
  Args:
30
47
  agent_id: The ID of the remote agent to execute
48
+ chat_id: The ID of the chat session to use. If None, a new chat session will be created.
31
49
  api_key: API key for authentication. If None, will check MCP_USE_API_KEY env var
32
50
  base_url: Base URL for the remote API
33
51
  """
52
+
53
+ if chat_id is not None:
54
+ try:
55
+ chat_id = str(UUID(chat_id))
56
+ except ValueError as e:
57
+ raise ValueError(
58
+ f"Invalid chat ID: {chat_id}, make sure to provide a valid UUID.\n{UUID_ERROR_MESSAGE}"
59
+ ) from e
60
+
34
61
  self.agent_id = agent_id
62
+ self.chat_id = chat_id
63
+ self._session_established = False
35
64
  self.base_url = base_url
36
- self._chat_id = None # Persistent chat session
37
65
 
38
66
  # Handle API key validation
39
67
  if api_key is None:
@@ -109,16 +137,14 @@ class RemoteAgent:
109
137
  return output_schema.model_validate({"content": str(result_data)})
110
138
  raise
111
139
 
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)
140
+ async def _upsert_chat_session(self) -> str:
141
+ """Create or resume a persistent chat session for the agent via upsert.
142
+
116
143
  Returns:
117
- The chat ID of the created session
118
- Raises:
119
- RuntimeError: If chat creation fails
144
+ The chat session ID
120
145
  """
121
146
  chat_payload = {
147
+ "id": self.chat_id, # Include chat_id for resuming or None for creating
122
148
  "title": f"Remote Agent Session - {self.agent_id}",
123
149
  "agent_id": self.agent_id,
124
150
  "type": "agent_execution",
@@ -127,7 +153,7 @@ class RemoteAgent:
127
153
  headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
128
154
  chat_url = f"{self.base_url}{API_CHATS_ENDPOINT}"
129
155
 
130
- logger.info(f"📝 Creating chat session for agent {self.agent_id}")
156
+ logger.info(f"📝 Upserting chat session for agent {self.agent_id}")
131
157
 
132
158
  try:
133
159
  chat_response = await self._client.post(chat_url, json=chat_payload, headers=headers)
@@ -135,7 +161,11 @@ class RemoteAgent:
135
161
 
136
162
  chat_data = chat_response.json()
137
163
  chat_id = chat_data["id"]
138
- logger.info(f"✅ Chat session created: {chat_id}")
164
+ if chat_response.status_code == 201:
165
+ logger.info(f"✅ New chat session created: {chat_id}")
166
+ else:
167
+ logger.info(f"✅ Resumed chat session: {chat_id}")
168
+
139
169
  return chat_id
140
170
 
141
171
  except httpx.HTTPStatusError as e:
@@ -156,7 +186,6 @@ class RemoteAgent:
156
186
  self,
157
187
  query: str,
158
188
  max_steps: int | None = None,
159
- manage_connector: bool = True,
160
189
  external_history: list[BaseMessage] | None = None,
161
190
  output_schema: type[T] | None = None,
162
191
  ) -> str | T:
@@ -165,8 +194,7 @@ class RemoteAgent:
165
194
  Args:
166
195
  query: The query to execute
167
196
  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)
197
+ external_history: External history (not supported yet for remote execution)
170
198
  output_schema: Optional Pydantic model for structured output
171
199
 
172
200
  Returns:
@@ -178,11 +206,14 @@ class RemoteAgent:
178
206
  try:
179
207
  logger.info(f"🌐 Executing query on remote agent {self.agent_id}")
180
208
 
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)
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
184
215
 
185
- chat_id = self._chat_id
216
+ chat_id = self.chat_id
186
217
 
187
218
  # Step 2: Execute the agent within the chat context
188
219
  execution_payload = {"query": query, "max_steps": max_steps or 10}
@@ -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