mcp-use 1.3.0__py3-none-any.whl → 1.3.2__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.

Files changed (38) hide show
  1. mcp_use/__init__.py +2 -0
  2. mcp_use/adapters/base.py +2 -6
  3. mcp_use/adapters/langchain_adapter.py +4 -11
  4. mcp_use/agents/base.py +1 -3
  5. mcp_use/agents/mcpagent.py +121 -45
  6. mcp_use/agents/prompts/system_prompt_builder.py +1 -3
  7. mcp_use/client.py +26 -11
  8. mcp_use/config.py +9 -9
  9. mcp_use/connectors/base.py +136 -32
  10. mcp_use/connectors/http.py +100 -30
  11. mcp_use/connectors/sandbox.py +11 -16
  12. mcp_use/connectors/stdio.py +8 -5
  13. mcp_use/connectors/websocket.py +8 -5
  14. mcp_use/logging.py +1 -1
  15. mcp_use/managers/server_manager.py +5 -16
  16. mcp_use/managers/tools/disconnect_server.py +1 -3
  17. mcp_use/managers/tools/get_active_server.py +1 -4
  18. mcp_use/managers/tools/search_tools.py +29 -36
  19. mcp_use/managers/tools/use_tool.py +5 -18
  20. mcp_use/observability/__init__.py +8 -0
  21. mcp_use/observability/laminar.py +21 -0
  22. mcp_use/observability/langfuse.py +35 -0
  23. mcp_use/session.py +1 -4
  24. mcp_use/task_managers/__init__.py +2 -1
  25. mcp_use/task_managers/base.py +10 -4
  26. mcp_use/task_managers/streamable_http.py +81 -0
  27. mcp_use/task_managers/websocket.py +5 -0
  28. mcp_use/telemetry/__init__.py +0 -0
  29. mcp_use/telemetry/events.py +93 -0
  30. mcp_use/telemetry/telemetry.py +306 -0
  31. mcp_use/telemetry/utils.py +48 -0
  32. mcp_use/utils.py +27 -0
  33. {mcp_use-1.3.0.dist-info → mcp_use-1.3.2.dist-info}/METADATA +82 -26
  34. mcp_use-1.3.2.dist-info/RECORD +49 -0
  35. mcp_use/types/clientoptions.py +0 -23
  36. mcp_use-1.3.0.dist-info/RECORD +0 -41
  37. {mcp_use-1.3.0.dist-info → mcp_use-1.3.2.dist-info}/WHEEL +0 -0
  38. {mcp_use-1.3.0.dist-info → mcp_use-1.3.2.dist-info}/licenses/LICENSE +0 -0
@@ -48,36 +48,12 @@ class SearchToolsTool(MCPServerTool):
48
48
  results = await self._search_tool.search_tools(
49
49
  query, top_k=top_k, active_server=self.server_manager.active_server
50
50
  )
51
- return self.format_search_results(results)
51
+ return results
52
52
 
53
53
  def _run(self, query: str, top_k: int = 100) -> str:
54
54
  """Synchronous version that raises a NotImplementedError - use _arun instead."""
55
55
  raise NotImplementedError("SearchToolsTool requires async execution. Use _arun instead.")
56
56
 
57
- def format_search_results(self, results: list[tuple[BaseTool, str, float]]) -> str:
58
- """Format search results in a consistent format."""
59
-
60
- # Only show top_k results
61
- results = results
62
-
63
- formatted_output = "Search results\n\n"
64
-
65
- for i, (tool, server_name, score) in enumerate(results):
66
- # Format score as percentage
67
- if i < 5:
68
- score_pct = f"{score * 100:.1f}%"
69
- logger.info(f"{i}: {tool.name} ({score_pct} match)")
70
- formatted_output += f"[{i + 1}] Tool: {tool.name} ({score_pct} match)\n"
71
- formatted_output += f" Server: {server_name}\n"
72
- formatted_output += f" Description: {tool.description}\n\n"
73
-
74
- # Add footer with information about how to use the results
75
- formatted_output += (
76
- "\nTo use a tool, connect to the appropriate server first, then invoke the tool."
77
- )
78
-
79
- return formatted_output
80
-
81
57
 
82
58
  class ToolSearchEngine:
83
59
  """
@@ -115,13 +91,18 @@ class ToolSearchEngine:
115
91
 
116
92
  try:
117
93
  from fastembed import TextEmbedding # optional dependency install with [search]
118
- except ImportError:
94
+ except ImportError as exc:
119
95
  logger.error(
120
96
  "The 'fastembed' library is not installed. "
121
97
  "To use the search functionality, please install it by running: "
122
98
  "pip install mcp-use[search]"
123
99
  )
124
- return False
100
+ raise ImportError(
101
+ "The 'fastembed' library is not installed. "
102
+ "To use the server_manager functionality, please install it by running: "
103
+ "pip install mcp-use[search] "
104
+ "or disable the server_manager by setting use_server_manager=False in the MCPAgent constructor."
105
+ ) from exc
125
106
 
126
107
  try:
127
108
  self.model = TextEmbedding(model_name="BAAI/bge-small-en-v1.5")
@@ -282,11 +263,7 @@ class ToolSearchEngine:
282
263
  )
283
264
 
284
265
  # If the server manager has an active server but it wasn't provided, use it
285
- if (
286
- active_server is None
287
- and self.server_manager
288
- and hasattr(self.server_manager, "active_server")
289
- ):
266
+ if active_server is None and self.server_manager and hasattr(self.server_manager, "active_server"):
290
267
  active_server = self.server_manager.active_server
291
268
 
292
269
  results = self.search(query, top_k=top_k)
@@ -303,11 +280,27 @@ class ToolSearchEngine:
303
280
  marked_results = []
304
281
  for tool, server_name, score in results:
305
282
  # If this is the active server, add "(ACTIVE)" marker
306
- display_server = (
307
- f"{server_name} (ACTIVE)" if server_name == active_server else server_name
308
- )
283
+ display_server = f"{server_name} (ACTIVE)" if server_name == active_server else server_name
309
284
  marked_results.append((tool, display_server, score))
310
285
  results = marked_results
311
286
 
312
287
  # Format and return the results
313
- return results
288
+ return self._format_search_results(results)
289
+
290
+ def _format_search_results(self, results: list[tuple[BaseTool, str, float]]) -> str:
291
+ """Format search results in a consistent format."""
292
+ formatted_output = "Search results\n\n"
293
+
294
+ for i, (tool, server_name, score) in enumerate(results):
295
+ # Format score as percentage
296
+ score_pct = f"{score * 100:.1f}%"
297
+ if i < 5:
298
+ logger.info(f"{i}: {tool.name} ({score_pct} match)")
299
+ formatted_output += f"[{i + 1}] Tool: {tool.name} ({score_pct} match)\n"
300
+ formatted_output += f" Server: {server_name}\n"
301
+ formatted_output += f" Description: {tool.description}\n\n"
302
+
303
+ # Add footer with information about how to use the results
304
+ formatted_output += "\nTo use a tool, connect to the appropriate server first, then invoke the tool."
305
+
306
+ return formatted_output
@@ -30,9 +30,7 @@ class UseToolFromServerTool(MCPServerTool):
30
30
  )
31
31
  args_schema: ClassVar[type[BaseModel]] = UseToolInput
32
32
 
33
- async def _arun(
34
- self, server_name: str, tool_name: str, tool_input: dict[str, Any] | str
35
- ) -> str:
33
+ async def _arun(self, server_name: str, tool_name: str, tool_input: dict[str, Any] | str) -> str:
36
34
  """Execute a tool from a specific server."""
37
35
  # Check if server exists
38
36
  servers = self.server_manager.client.get_server_names()
@@ -78,20 +76,14 @@ class UseToolFromServerTool(MCPServerTool):
78
76
 
79
77
  if not target_tool:
80
78
  tool_names = [t.name for t in server_tools]
81
- return (
82
- f"Tool '{tool_name}' not found on server '{server_name}'. "
83
- f"Available tools: {', '.join(tool_names)}"
84
- )
79
+ return f"Tool '{tool_name}' not found on server '{server_name}'. Available tools: {', '.join(tool_names)}"
85
80
 
86
81
  # Execute the tool with the provided input
87
82
  try:
88
83
  # Parse the input based on target tool's schema
89
84
  structured_input = self._parse_tool_input(target_tool, tool_input)
90
85
  if structured_input is None:
91
- return (
92
- f"Could not parse input for tool '{tool_name}'."
93
- " Please check the input format and try again."
94
- )
86
+ return f"Could not parse input for tool '{tool_name}'. Please check the input format and try again."
95
87
 
96
88
  # Store the previous active server
97
89
  previous_active = self.server_manager.active_server
@@ -100,10 +92,7 @@ class UseToolFromServerTool(MCPServerTool):
100
92
  self.server_manager.active_server = server_name
101
93
 
102
94
  # Execute the tool
103
- logger.info(
104
- f"Executing tool '{tool_name}' on server '{server_name}'"
105
- "with input: {structured_input}"
106
- )
95
+ logger.info(f"Executing tool '{tool_name}' on server '{server_name}'with input: {{structured_input}}")
107
96
  result = await target_tool._arun(**structured_input)
108
97
 
109
98
  # Restore the previous active server
@@ -162,6 +151,4 @@ class UseToolFromServerTool(MCPServerTool):
162
151
 
163
152
  def _run(self, server_name: str, tool_name: str, tool_input: dict[str, Any] | str) -> str:
164
153
  """Synchronous version that raises a NotImplementedError."""
165
- raise NotImplementedError(
166
- "UseToolFromServerTool requires async execution. Use _arun instead."
167
- )
154
+ raise NotImplementedError("UseToolFromServerTool requires async execution. Use _arun instead.")
@@ -0,0 +1,8 @@
1
+ from dotenv import load_dotenv
2
+
3
+ # Load environment variables once for all observability modules
4
+ load_dotenv()
5
+
6
+ from . import laminar, langfuse # noqa
7
+
8
+ __all__ = ["laminar", "langfuse"]
@@ -0,0 +1,21 @@
1
+ import logging
2
+ import os
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+ # Check if Laminar is disabled via environment variable
7
+ _laminar_disabled = os.getenv("MCP_USE_LAMINAR", "").lower() == "false"
8
+
9
+ # Only initialize if not disabled and API key is present
10
+ if _laminar_disabled:
11
+ logger.debug("Laminar tracing disabled via MCP_USE_LAMINAR environment variable")
12
+ elif not os.getenv("LAMINAR_PROJECT_API_KEY"):
13
+ logger.debug("Laminar API key not found - tracing disabled. Set LAMINAR_PROJECT_API_KEY to enable")
14
+ else:
15
+ try:
16
+ from lmnr import Laminar
17
+
18
+ Laminar.initialize(project_api_key=os.getenv("LAMINAR_PROJECT_API_KEY"))
19
+ logger.debug("Laminar observability initialized successfully")
20
+ except ImportError:
21
+ logger.debug("Laminar package not installed - tracing disabled. Install with: pip install lmnr")
@@ -0,0 +1,35 @@
1
+ import logging
2
+ import os
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+ # Check if Langfuse is disabled via environment variable
7
+ _langfuse_disabled = os.getenv("MCP_USE_LANGFUSE", "").lower() == "false"
8
+
9
+ # Only initialize if not disabled and required keys are present
10
+ if _langfuse_disabled:
11
+ logger.debug("Langfuse tracing disabled via MCP_USE_LANGFUSE environment variable")
12
+ langfuse = None
13
+ langfuse_handler = None
14
+ elif not os.getenv("LANGFUSE_PUBLIC_KEY") or not os.getenv("LANGFUSE_SECRET_KEY"):
15
+ logger.debug(
16
+ "Langfuse API keys not found - tracing disabled. Set LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY to enable"
17
+ )
18
+ langfuse = None
19
+ langfuse_handler = None
20
+ else:
21
+ try:
22
+ from langfuse import Langfuse
23
+ from langfuse.langchain import CallbackHandler
24
+
25
+ langfuse = Langfuse(
26
+ public_key=os.getenv("LANGFUSE_PUBLIC_KEY"),
27
+ secret_key=os.getenv("LANGFUSE_SECRET_KEY"),
28
+ host=os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com"),
29
+ )
30
+ langfuse_handler = CallbackHandler()
31
+ logger.debug("Langfuse observability initialized successfully")
32
+ except ImportError:
33
+ logger.debug("Langfuse package not installed - tracing disabled. Install with: pip install langfuse")
34
+ langfuse = None
35
+ langfuse_handler = None
mcp_use/session.py CHANGED
@@ -7,8 +7,6 @@ which handles authentication, initialization, and tool discovery.
7
7
 
8
8
  from typing import Any
9
9
 
10
- from mcp.types import Tool
11
-
12
10
  from .connectors.base import BaseConnector
13
11
 
14
12
 
@@ -32,7 +30,6 @@ class MCPSession:
32
30
  """
33
31
  self.connector = connector
34
32
  self.session_info: dict[str, Any] | None = None
35
- self.tools: list[Tool] = []
36
33
  self.auto_connect = auto_connect
37
34
 
38
35
  async def __aenter__(self) -> "MCPSession":
@@ -84,4 +81,4 @@ class MCPSession:
84
81
  Returns:
85
82
  True if the connector is connected, False otherwise.
86
83
  """
87
- return hasattr(self.connector, "client") and self.connector.client is not None
84
+ return self.connector.is_connected
@@ -8,12 +8,13 @@ through different transport mechanisms.
8
8
  from .base import ConnectionManager
9
9
  from .sse import SseConnectionManager
10
10
  from .stdio import StdioConnectionManager
11
+ from .streamable_http import StreamableHttpConnectionManager
11
12
  from .websocket import WebSocketConnectionManager
12
13
 
13
14
  __all__ = [
14
15
  "ConnectionManager",
15
- "HttpConnectionManager",
16
16
  "StdioConnectionManager",
17
17
  "WebSocketConnectionManager",
18
18
  "SseConnectionManager",
19
+ "StreamableHttpConnectionManager",
19
20
  ]
@@ -70,9 +70,7 @@ class ConnectionManager(Generic[T], ABC):
70
70
  self._exception = None
71
71
 
72
72
  # Create a task to establish and maintain the connection
73
- self._task = asyncio.create_task(
74
- self._connection_task(), name=f"{self.__class__.__name__}_task"
75
- )
73
+ self._task = asyncio.create_task(self._connection_task(), name=f"{self.__class__.__name__}_task")
76
74
 
77
75
  # Wait for the connection to be ready or fail
78
76
  await self._ready_event.wait()
@@ -105,6 +103,14 @@ class ConnectionManager(Generic[T], ABC):
105
103
  await self._done_event.wait()
106
104
  logger.debug(f"{self.__class__.__name__} task completed")
107
105
 
106
+ def get_streams(self) -> T | None:
107
+ """Get the current connection streams.
108
+
109
+ Returns:
110
+ The current connection (typically a tuple of read_stream, write_stream) or None if not connected.
111
+ """
112
+ return self._connection
113
+
108
114
  async def _connection_task(self) -> None:
109
115
  """Run the connection task.
110
116
 
@@ -137,7 +143,7 @@ class ConnectionManager(Generic[T], ABC):
137
143
  self._ready_event.set()
138
144
 
139
145
  finally:
140
- # Close the connection if it was establishedSUPABASE_URL
146
+ # Close the connection if it was established
141
147
  if self._connection is not None:
142
148
  try:
143
149
  await self._close_connection()
@@ -0,0 +1,81 @@
1
+ """
2
+ Streamable HTTP connection management for MCP implementations.
3
+
4
+ This module provides a connection manager for streamable HTTP-based MCP connections
5
+ that ensures proper task isolation and resource cleanup.
6
+ """
7
+
8
+ from datetime import timedelta
9
+ from typing import Any
10
+
11
+ from mcp.client.streamable_http import streamablehttp_client
12
+
13
+ from ..logging import logger
14
+ from .base import ConnectionManager
15
+
16
+
17
+ class StreamableHttpConnectionManager(ConnectionManager[tuple[Any, Any]]):
18
+ """Connection manager for streamable HTTP-based MCP connections.
19
+
20
+ This class handles the proper task isolation for HTTP streaming connections
21
+ to prevent the "cancel scope in different task" error. It runs the http_stream_client
22
+ in a dedicated task and manages its lifecycle.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ url: str,
28
+ headers: dict[str, str] | None = None,
29
+ timeout: float = 5,
30
+ read_timeout: float = 60 * 5,
31
+ ):
32
+ """Initialize a new streamable HTTP connection manager.
33
+
34
+ Args:
35
+ url: The HTTP endpoint URL
36
+ headers: Optional HTTP headers
37
+ timeout: Timeout for HTTP operations in seconds
38
+ read_timeout: Timeout for HTTP read operations in seconds
39
+ """
40
+ super().__init__()
41
+ self.url = url
42
+ self.headers = headers or {}
43
+ self.timeout = timedelta(seconds=timeout)
44
+ self.read_timeout = timedelta(seconds=read_timeout)
45
+ self._http_ctx = None
46
+
47
+ async def _establish_connection(self) -> tuple[Any, Any]:
48
+ """Establish a streamable HTTP connection.
49
+
50
+ Returns:
51
+ A tuple of (read_stream, write_stream)
52
+
53
+ Raises:
54
+ Exception: If connection cannot be established.
55
+ """
56
+ # Create the context manager
57
+ self._http_ctx = streamablehttp_client(
58
+ url=self.url,
59
+ headers=self.headers,
60
+ timeout=self.timeout,
61
+ sse_read_timeout=self.read_timeout,
62
+ )
63
+
64
+ # Enter the context manager. Ignoring the session id callback
65
+ read_stream, write_stream, _ = await self._http_ctx.__aenter__()
66
+
67
+ # Return the streams
68
+ return (read_stream, write_stream)
69
+
70
+ async def _close_connection(self) -> None:
71
+ """Close the streamable HTTP connection."""
72
+
73
+ if self._http_ctx:
74
+ # Exit the context manager
75
+ try:
76
+ await self._http_ctx.__aexit__(None, None, None)
77
+ except Exception as e:
78
+ # Only log if it's not a normal connection termination
79
+ logger.debug(f"Streamable HTTP context cleanup: {e}")
80
+ finally:
81
+ self._http_ctx = None
@@ -22,14 +22,17 @@ class WebSocketConnectionManager(ConnectionManager[tuple[Any, Any]]):
22
22
  def __init__(
23
23
  self,
24
24
  url: str,
25
+ headers: dict[str, str] | None = None,
25
26
  ):
26
27
  """Initialize a new WebSocket connection manager.
27
28
 
28
29
  Args:
29
30
  url: The WebSocket URL to connect to
31
+ headers: Optional HTTP headers
30
32
  """
31
33
  super().__init__()
32
34
  self.url = url
35
+ self.headers = headers or {}
33
36
 
34
37
  async def _establish_connection(self) -> tuple[Any, Any]:
35
38
  """Establish a WebSocket connection.
@@ -42,6 +45,8 @@ class WebSocketConnectionManager(ConnectionManager[tuple[Any, Any]]):
42
45
  """
43
46
  logger.debug(f"Connecting to WebSocket: {self.url}")
44
47
  # Create the context manager
48
+ # Note: The current MCP websocket_client implementation doesn't support headers
49
+ # If headers need to be passed, this would need to be updated when MCP supports it
45
50
  self._ws_ctx = websocket_client(self.url)
46
51
 
47
52
  # Enter the context manager
File without changes
@@ -0,0 +1,93 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import Any
4
+
5
+
6
+ class BaseTelemetryEvent(ABC):
7
+ """Base class for all telemetry events"""
8
+
9
+ @property
10
+ @abstractmethod
11
+ def name(self) -> str:
12
+ """Event name for tracking"""
13
+ pass
14
+
15
+ @property
16
+ @abstractmethod
17
+ def properties(self) -> dict[str, Any]:
18
+ """Event properties to send with the event"""
19
+ pass
20
+
21
+
22
+ @dataclass
23
+ class MCPAgentExecutionEvent(BaseTelemetryEvent):
24
+ """Comprehensive event for tracking complete MCP agent execution"""
25
+
26
+ # Execution method and context
27
+ execution_method: str # "run" or "astream"
28
+ query: str # The actual user query
29
+ success: bool
30
+
31
+ # Agent configuration
32
+ model_provider: str
33
+ model_name: str
34
+ server_count: int
35
+ server_identifiers: list[dict[str, str]]
36
+ total_tools_available: int
37
+ tools_available_names: list[str]
38
+ max_steps_configured: int
39
+ memory_enabled: bool
40
+ use_server_manager: bool
41
+
42
+ # Execution PARAMETERS
43
+ max_steps_used: int | None
44
+ manage_connector: bool
45
+ external_history_used: bool
46
+
47
+ # Execution results
48
+ steps_taken: int | None = None
49
+ tools_used_count: int | None = None
50
+ tools_used_names: list[str] | None = None
51
+ response: str | None = None # The actual response
52
+ execution_time_ms: int | None = None
53
+ error_type: str | None = None
54
+
55
+ # Context
56
+ conversation_history_length: int | None = None
57
+
58
+ @property
59
+ def name(self) -> str:
60
+ return "mcp_agent_execution"
61
+
62
+ @property
63
+ def properties(self) -> dict[str, Any]:
64
+ return {
65
+ # Core execution info
66
+ "execution_method": self.execution_method,
67
+ "query": self.query,
68
+ "query_length": len(self.query),
69
+ "success": self.success,
70
+ # Agent configuration
71
+ "model_provider": self.model_provider,
72
+ "model_name": self.model_name,
73
+ "server_count": self.server_count,
74
+ "server_identifiers": self.server_identifiers,
75
+ "total_tools_available": self.total_tools_available,
76
+ "tools_available_names": self.tools_available_names,
77
+ "max_steps_configured": self.max_steps_configured,
78
+ "memory_enabled": self.memory_enabled,
79
+ "use_server_manager": self.use_server_manager,
80
+ # Execution parameters (always include, even if None)
81
+ "max_steps_used": self.max_steps_used,
82
+ "manage_connector": self.manage_connector,
83
+ "external_history_used": self.external_history_used,
84
+ # Execution results (always include, even if None)
85
+ "steps_taken": self.steps_taken,
86
+ "tools_used_count": self.tools_used_count,
87
+ "tools_used_names": self.tools_used_names,
88
+ "response": self.response,
89
+ "response_length": len(self.response) if self.response else None,
90
+ "execution_time_ms": self.execution_time_ms,
91
+ "error_type": self.error_type,
92
+ "conversation_history_length": self.conversation_history_length,
93
+ }