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 +6 -2
- mcp_use/adapters/langchain_adapter.py +7 -5
- mcp_use/agents/mcpagent.py +115 -19
- mcp_use/agents/prompts/templates.py +1 -10
- mcp_use/agents/remote.py +50 -19
- 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/cli.py +581 -0
- mcp_use/client.py +1 -1
- mcp_use/config.py +2 -2
- mcp_use/connectors/base.py +17 -12
- mcp_use/connectors/http.py +117 -21
- mcp_use/connectors/websocket.py +14 -5
- mcp_use/exceptions.py +31 -0
- mcp_use/logging.py +27 -12
- mcp_use/managers/base.py +36 -0
- mcp_use/managers/server_manager.py +2 -1
- mcp_use/observability/__init__.py +2 -1
- mcp_use/observability/callbacks_manager.py +162 -0
- mcp_use/observability/laminar.py +24 -3
- mcp_use/observability/langfuse.py +27 -3
- 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.9.dist-info → mcp_use-1.3.11.dist-info}/METADATA +22 -26
- mcp_use-1.3.11.dist-info/RECORD +60 -0
- mcp_use-1.3.11.dist-info/entry_points.txt +2 -0
- mcp_use-1.3.9.dist-info/RECORD +0 -51
- {mcp_use-1.3.9.dist-info → mcp_use-1.3.11.dist-info}/WHEEL +0 -0
- {mcp_use-1.3.9.dist-info → mcp_use-1.3.11.dist-info}/licenses/LICENSE +0 -0
mcp_use/connectors/http.py
CHANGED
|
@@ -5,10 +5,17 @@ This module provides a connector for communicating with MCP implementations
|
|
|
5
5
|
through HTTP APIs with SSE or Streamable HTTP for transport.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
8
10
|
import httpx
|
|
9
11
|
from mcp import ClientSession
|
|
10
12
|
from mcp.client.session import ElicitationFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT
|
|
13
|
+
from mcp.shared.exceptions import McpError
|
|
14
|
+
|
|
15
|
+
from mcp_use.auth.oauth import OAuthClientProvider
|
|
11
16
|
|
|
17
|
+
from ..auth import BearerAuth, OAuth
|
|
18
|
+
from ..exceptions import OAuthAuthenticationError, OAuthDiscoveryError
|
|
12
19
|
from ..logging import logger
|
|
13
20
|
from ..task_managers import SseConnectionManager, StreamableHttpConnectionManager
|
|
14
21
|
from .base import BaseConnector
|
|
@@ -24,10 +31,10 @@ class HttpConnector(BaseConnector):
|
|
|
24
31
|
def __init__(
|
|
25
32
|
self,
|
|
26
33
|
base_url: str,
|
|
27
|
-
auth_token: str | None = None,
|
|
28
34
|
headers: dict[str, str] | None = None,
|
|
29
35
|
timeout: float = 5,
|
|
30
36
|
sse_read_timeout: float = 60 * 5,
|
|
37
|
+
auth: str | dict[str, Any] | httpx.Auth | None = None,
|
|
31
38
|
sampling_callback: SamplingFnT | None = None,
|
|
32
39
|
elicitation_callback: ElicitationFnT | None = None,
|
|
33
40
|
message_handler: MessageHandlerFnT | None = None,
|
|
@@ -37,10 +44,13 @@ class HttpConnector(BaseConnector):
|
|
|
37
44
|
|
|
38
45
|
Args:
|
|
39
46
|
base_url: The base URL of the MCP HTTP API.
|
|
40
|
-
auth_token: Optional authentication token.
|
|
41
47
|
headers: Optional additional headers.
|
|
42
48
|
timeout: Timeout for HTTP operations in seconds.
|
|
43
49
|
sse_read_timeout: Timeout for SSE read operations in seconds.
|
|
50
|
+
auth: Authentication method - can be:
|
|
51
|
+
- A string token: Use Bearer token authentication
|
|
52
|
+
- A dict with OAuth config: {"client_id": "...", "client_secret": "...", "scope": "..."}
|
|
53
|
+
- An httpx.Auth object: Use custom authentication
|
|
44
54
|
sampling_callback: Optional sampling callback.
|
|
45
55
|
elicitation_callback: Optional elicitation callback.
|
|
46
56
|
"""
|
|
@@ -51,12 +61,57 @@ class HttpConnector(BaseConnector):
|
|
|
51
61
|
logging_callback=logging_callback,
|
|
52
62
|
)
|
|
53
63
|
self.base_url = base_url.rstrip("/")
|
|
54
|
-
self.auth_token = auth_token
|
|
55
64
|
self.headers = headers or {}
|
|
56
|
-
if auth_token:
|
|
57
|
-
self.headers["Authorization"] = f"Bearer {auth_token}"
|
|
58
65
|
self.timeout = timeout
|
|
59
66
|
self.sse_read_timeout = sse_read_timeout
|
|
67
|
+
self._auth: httpx.Auth | None = None
|
|
68
|
+
self._oauth: OAuth | None = None
|
|
69
|
+
|
|
70
|
+
# Handle authentication
|
|
71
|
+
if auth is not None:
|
|
72
|
+
self._set_auth(auth)
|
|
73
|
+
|
|
74
|
+
def _set_auth(self, auth: str | dict[str, Any] | httpx.Auth) -> None:
|
|
75
|
+
"""Set authentication method.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
auth: Authentication method - can be:
|
|
79
|
+
- A string token: Use Bearer token authentication
|
|
80
|
+
- A dict with OAuth config: {"client_id": "...", "client_secret": "...", "scope": "..."}
|
|
81
|
+
- An httpx.Auth object: Use custom authentication
|
|
82
|
+
"""
|
|
83
|
+
if isinstance(auth, str):
|
|
84
|
+
# Treat as bearer token
|
|
85
|
+
self._auth = BearerAuth(token=auth)
|
|
86
|
+
self.headers["Authorization"] = f"Bearer {auth}"
|
|
87
|
+
elif isinstance(auth, dict):
|
|
88
|
+
# Check if this is an OAuth provider configuration
|
|
89
|
+
if "oauth_provider" in auth:
|
|
90
|
+
oauth_provider = auth["oauth_provider"]
|
|
91
|
+
if isinstance(oauth_provider, dict):
|
|
92
|
+
oauth_provider = OAuthClientProvider(**oauth_provider)
|
|
93
|
+
self._oauth = OAuth(
|
|
94
|
+
self.base_url,
|
|
95
|
+
scope=auth.get("scope"),
|
|
96
|
+
client_id=auth.get("client_id"),
|
|
97
|
+
client_secret=auth.get("client_secret"),
|
|
98
|
+
callback_port=auth.get("callback_port"),
|
|
99
|
+
oauth_provider=oauth_provider,
|
|
100
|
+
)
|
|
101
|
+
self._oauth_config = auth
|
|
102
|
+
else:
|
|
103
|
+
self._oauth = OAuth(
|
|
104
|
+
self.base_url,
|
|
105
|
+
scope=auth.get("scope"),
|
|
106
|
+
client_id=auth.get("client_id"),
|
|
107
|
+
client_secret=auth.get("client_secret"),
|
|
108
|
+
callback_port=auth.get("callback_port"),
|
|
109
|
+
)
|
|
110
|
+
self._oauth_config = auth
|
|
111
|
+
elif isinstance(auth, httpx.Auth):
|
|
112
|
+
self._auth = auth
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError(f"Invalid auth type: {type(auth)}")
|
|
60
115
|
|
|
61
116
|
async def connect(self) -> None:
|
|
62
117
|
"""Establish a connection to the MCP implementation."""
|
|
@@ -64,6 +119,29 @@ class HttpConnector(BaseConnector):
|
|
|
64
119
|
logger.debug("Already connected to MCP implementation")
|
|
65
120
|
return
|
|
66
121
|
|
|
122
|
+
# Handle OAuth if needed
|
|
123
|
+
if self._oauth:
|
|
124
|
+
try:
|
|
125
|
+
# Create a temporary client for OAuth metadata discovery
|
|
126
|
+
async with httpx.AsyncClient() as client:
|
|
127
|
+
bearer_auth = await self._oauth.initialize(client)
|
|
128
|
+
if not bearer_auth:
|
|
129
|
+
# Need to perform OAuth flow
|
|
130
|
+
logger.info("OAuth authentication required")
|
|
131
|
+
bearer_auth = await self._oauth.authenticate()
|
|
132
|
+
|
|
133
|
+
# Update auth and headers
|
|
134
|
+
self._auth = bearer_auth
|
|
135
|
+
self.headers["Authorization"] = f"Bearer {bearer_auth.token.get_secret_value()}"
|
|
136
|
+
except OAuthDiscoveryError:
|
|
137
|
+
# OAuth discovery failed - it means server doesn't support OAuth default urls
|
|
138
|
+
logger.debug("OAuth discovery failed, continuing without initialization.")
|
|
139
|
+
self._oauth = None
|
|
140
|
+
self._auth = None
|
|
141
|
+
except OAuthAuthenticationError as e:
|
|
142
|
+
logger.error(f"OAuth initialization failed: {e}")
|
|
143
|
+
raise
|
|
144
|
+
|
|
67
145
|
# Try streamable HTTP first (new transport), fall back to SSE (old transport)
|
|
68
146
|
# This implements backwards compatibility per MCP specification
|
|
69
147
|
self.transport_type = None
|
|
@@ -73,7 +151,7 @@ class HttpConnector(BaseConnector):
|
|
|
73
151
|
# First, try the new streamable HTTP transport
|
|
74
152
|
logger.debug(f"Attempting streamable HTTP connection to: {self.base_url}")
|
|
75
153
|
connection_manager = StreamableHttpConnectionManager(
|
|
76
|
-
self.base_url, self.headers, self.timeout, self.sse_read_timeout
|
|
154
|
+
self.base_url, self.headers, self.timeout, self.sse_read_timeout, auth=self._auth
|
|
77
155
|
)
|
|
78
156
|
|
|
79
157
|
# Test if this is a streamable HTTP server by attempting initialization
|
|
@@ -94,9 +172,9 @@ class HttpConnector(BaseConnector):
|
|
|
94
172
|
try:
|
|
95
173
|
# Try to initialize - this is where streamable HTTP vs SSE difference should show up
|
|
96
174
|
result = await test_client.initialize()
|
|
175
|
+
logger.debug(f"Streamable HTTP initialization result: {result}")
|
|
97
176
|
|
|
98
177
|
# If we get here, streamable HTTP works
|
|
99
|
-
|
|
100
178
|
self.client_session = test_client
|
|
101
179
|
self.transport_type = "streamable HTTP"
|
|
102
180
|
self._initialized = True # Mark as initialized since we just called initialize()
|
|
@@ -125,14 +203,28 @@ class HttpConnector(BaseConnector):
|
|
|
125
203
|
else:
|
|
126
204
|
self._prompts = []
|
|
127
205
|
|
|
128
|
-
|
|
206
|
+
# Only McpError is raised from client's initialization because
|
|
207
|
+
# exceptions are handled internally.
|
|
208
|
+
except McpError as mcp_error:
|
|
209
|
+
logger.error("MCP protocol error during initialization: %s", mcp_error.error)
|
|
129
210
|
# Clean up the test client
|
|
211
|
+
try:
|
|
212
|
+
await test_client.__aexit__(None, None, None)
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
raise mcp_error
|
|
216
|
+
|
|
217
|
+
except Exception as init_error:
|
|
218
|
+
# This catches non-McpError exceptions, like a direct httpx timeout
|
|
219
|
+
# but in the most cases this won't happen. It's for safety.
|
|
130
220
|
try:
|
|
131
221
|
await test_client.__aexit__(None, None, None)
|
|
132
222
|
except Exception:
|
|
133
223
|
pass
|
|
134
224
|
raise init_error
|
|
135
225
|
|
|
226
|
+
# Exception from the inner try is propagated here and in
|
|
227
|
+
# the most cases is an McpError, so checking instances is useless
|
|
136
228
|
except Exception as streamable_error:
|
|
137
229
|
logger.debug(f"Streamable HTTP failed: {streamable_error}")
|
|
138
230
|
|
|
@@ -143,24 +235,17 @@ class HttpConnector(BaseConnector):
|
|
|
143
235
|
except Exception:
|
|
144
236
|
pass
|
|
145
237
|
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
should_fallback = True
|
|
151
|
-
elif "405 Method Not Allowed" in str(streamable_error) or "404 Not Found" in str(streamable_error):
|
|
152
|
-
should_fallback = True
|
|
153
|
-
else:
|
|
154
|
-
# For other errors, still try fallback but they might indicate
|
|
155
|
-
# real connectivity issues
|
|
156
|
-
should_fallback = True
|
|
238
|
+
# It doesn't make sense to check error types. Because client
|
|
239
|
+
# always return a McpError, if he can't reach the server
|
|
240
|
+
# because it's offline, or if it has an auth problem.
|
|
241
|
+
should_fallback = True
|
|
157
242
|
|
|
158
243
|
if should_fallback:
|
|
159
244
|
try:
|
|
160
245
|
# Fall back to the old SSE transport
|
|
161
246
|
logger.debug(f"Attempting SSE fallback connection to: {self.base_url}")
|
|
162
247
|
connection_manager = SseConnectionManager(
|
|
163
|
-
self.base_url, self.headers, self.timeout, self.sse_read_timeout
|
|
248
|
+
self.base_url, self.headers, self.timeout, self.sse_read_timeout, auth=self._auth
|
|
164
249
|
)
|
|
165
250
|
|
|
166
251
|
read_stream, write_stream = await connection_manager.start()
|
|
@@ -178,7 +263,18 @@ class HttpConnector(BaseConnector):
|
|
|
178
263
|
await self.client_session.__aenter__()
|
|
179
264
|
self.transport_type = "SSE"
|
|
180
265
|
|
|
181
|
-
except Exception as sse_error:
|
|
266
|
+
except* Exception as sse_error:
|
|
267
|
+
# Get the exception from the ExceptionGroup, and here we will get the correct type.
|
|
268
|
+
sse_error = sse_error.exceptions[0]
|
|
269
|
+
if isinstance(sse_error, httpx.HTTPStatusError) and sse_error.response.status_code in [
|
|
270
|
+
401,
|
|
271
|
+
403,
|
|
272
|
+
407,
|
|
273
|
+
]:
|
|
274
|
+
raise OAuthAuthenticationError(
|
|
275
|
+
f"Server requires authentication (HTTP {sse_error.response.status_code}) "
|
|
276
|
+
"but auth failed. Please provide auth configuration manually."
|
|
277
|
+
) from sse_error
|
|
182
278
|
logger.error(
|
|
183
279
|
f"Both transport methods failed. Streamable HTTP: {streamable_error}, SSE: {sse_error}"
|
|
184
280
|
)
|
mcp_use/connectors/websocket.py
CHANGED
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import uuid
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
|
+
import httpx
|
|
13
14
|
from mcp.types import Tool
|
|
14
15
|
from websockets import ClientConnection
|
|
15
16
|
|
|
@@ -28,21 +29,29 @@ class WebSocketConnector(BaseConnector):
|
|
|
28
29
|
def __init__(
|
|
29
30
|
self,
|
|
30
31
|
url: str,
|
|
31
|
-
auth_token: str | None = None,
|
|
32
32
|
headers: dict[str, str] | None = None,
|
|
33
|
+
auth: str | dict[str, Any] | httpx.Auth | None = None,
|
|
33
34
|
):
|
|
34
35
|
"""Initialize a new WebSocket connector.
|
|
35
36
|
|
|
36
37
|
Args:
|
|
37
38
|
url: The WebSocket URL to connect to.
|
|
38
|
-
auth_token: Optional authentication token.
|
|
39
39
|
headers: Optional additional headers.
|
|
40
|
+
auth: Authentication method - can be:
|
|
41
|
+
- A string token: Use Bearer token authentication
|
|
42
|
+
- A dict: Not supported for WebSocket (will log warning)
|
|
43
|
+
- An httpx.Auth object: Not supported for WebSocket (will log warning)
|
|
40
44
|
"""
|
|
41
45
|
self.url = url
|
|
42
|
-
self.auth_token = auth_token
|
|
43
46
|
self.headers = headers or {}
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
|
|
48
|
+
# Handle authentication - WebSocket only supports bearer tokens
|
|
49
|
+
# An auth field it's not needed
|
|
50
|
+
if auth is not None:
|
|
51
|
+
if isinstance(auth, str):
|
|
52
|
+
self.headers["Authorization"] = f"Bearer {auth}"
|
|
53
|
+
else:
|
|
54
|
+
logger.warning("WebSocket connector only supports bearer token authentication")
|
|
46
55
|
|
|
47
56
|
self.ws: ClientConnection | None = None
|
|
48
57
|
self._connection_manager: ConnectionManager | None = None
|
mcp_use/exceptions.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""MCP-use exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MCPError(Exception):
|
|
5
|
+
"""Base exception for MCP-use."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OAuthDiscoveryError(MCPError):
|
|
11
|
+
"""OAuth discovery auth metadata error"""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class OAuthAuthenticationError(MCPError):
|
|
17
|
+
"""OAuth authentication-related errors"""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ConnectionError(MCPError):
|
|
23
|
+
"""Connection-related errors."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ConfigurationError(MCPError):
|
|
29
|
+
"""Configuration-related errors."""
|
|
30
|
+
|
|
31
|
+
pass
|
mcp_use/logging.py
CHANGED
|
@@ -80,6 +80,9 @@ class Logger:
|
|
|
80
80
|
|
|
81
81
|
root_logger.setLevel(level)
|
|
82
82
|
|
|
83
|
+
# Set propagate to True to ensure child loggers inherit settings
|
|
84
|
+
root_logger.propagate = True
|
|
85
|
+
|
|
83
86
|
# Clear existing handlers
|
|
84
87
|
for handler in root_logger.handlers[:]:
|
|
85
88
|
root_logger.removeHandler(handler)
|
|
@@ -91,6 +94,7 @@ class Logger:
|
|
|
91
94
|
if log_to_console:
|
|
92
95
|
console_handler = logging.StreamHandler(sys.stdout)
|
|
93
96
|
console_handler.setFormatter(formatter)
|
|
97
|
+
console_handler.setLevel(level) # Ensure handler respects the level
|
|
94
98
|
root_logger.addHandler(console_handler)
|
|
95
99
|
|
|
96
100
|
# Add file handler if requested
|
|
@@ -102,6 +106,7 @@ class Logger:
|
|
|
102
106
|
|
|
103
107
|
file_handler = logging.FileHandler(log_to_file)
|
|
104
108
|
file_handler.setFormatter(formatter)
|
|
109
|
+
file_handler.setLevel(level) # Ensure handler respects the level
|
|
105
110
|
root_logger.addHandler(file_handler)
|
|
106
111
|
|
|
107
112
|
@classmethod
|
|
@@ -114,24 +119,34 @@ class Logger:
|
|
|
114
119
|
global MCP_USE_DEBUG
|
|
115
120
|
MCP_USE_DEBUG = debug_level
|
|
116
121
|
|
|
117
|
-
#
|
|
122
|
+
# Determine the target level
|
|
118
123
|
if debug_level == 2:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
langchain_set_debug(True)
|
|
124
|
+
target_level = logging.DEBUG
|
|
125
|
+
langchain_set_debug(True)
|
|
122
126
|
elif debug_level == 1:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
langchain_set_debug(False)
|
|
127
|
+
target_level = logging.INFO
|
|
128
|
+
langchain_set_debug(False)
|
|
126
129
|
else:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
target_level = logging.WARNING
|
|
131
|
+
langchain_set_debug(False)
|
|
132
|
+
|
|
133
|
+
# Update log level for existing loggers in our registry
|
|
134
|
+
for logger in cls._loggers.values():
|
|
135
|
+
logger.setLevel(target_level)
|
|
136
|
+
# Also update handler levels
|
|
137
|
+
for handler in logger.handlers:
|
|
138
|
+
handler.setLevel(target_level)
|
|
139
|
+
|
|
140
|
+
# Also update all mcp_use child loggers that might exist
|
|
141
|
+
# This ensures loggers created with logging.getLogger() are also updated
|
|
142
|
+
base_logger = logging.getLogger("mcp_use")
|
|
143
|
+
base_logger.setLevel(target_level)
|
|
144
|
+
for handler in base_logger.handlers:
|
|
145
|
+
handler.setLevel(target_level)
|
|
131
146
|
|
|
132
147
|
|
|
133
148
|
# Check environment variable for debug flag
|
|
134
|
-
debug_env = os.environ.get("DEBUG", "").lower()
|
|
149
|
+
debug_env = os.environ.get("MCP_USE_DEBUG", "").lower() or os.environ.get("DEBUG", "").lower()
|
|
135
150
|
if debug_env == "2":
|
|
136
151
|
MCP_USE_DEBUG = 2
|
|
137
152
|
elif debug_env == "1":
|
mcp_use/managers/base.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from langchain_core.tools import BaseTool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseServerManager(ABC):
|
|
7
|
+
"""Abstract base class for server managers.
|
|
8
|
+
|
|
9
|
+
This class defines the interface for server managers that can be used with MCPAgent.
|
|
10
|
+
Custom server managers should inherit from this class and implement the required methods.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def initialize(self) -> None:
|
|
15
|
+
"""Initialize the server manager."""
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def tools(self) -> list[BaseTool]:
|
|
21
|
+
"""Get all server management tools and tools from the active server.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
list of LangChain tools for server management plus tools from active server
|
|
25
|
+
"""
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def has_tool_changes(self, current_tool_names: set[str]) -> bool:
|
|
30
|
+
"""Check if the available tools have changed.
|
|
31
|
+
Args:
|
|
32
|
+
current_tool_names: Set of currently known tool names
|
|
33
|
+
Returns:
|
|
34
|
+
True if tools have changed, False otherwise
|
|
35
|
+
"""
|
|
36
|
+
raise NotImplementedError
|
|
@@ -4,10 +4,11 @@ from mcp_use.client import MCPClient
|
|
|
4
4
|
from mcp_use.logging import logger
|
|
5
5
|
|
|
6
6
|
from ..adapters.base import BaseAdapter
|
|
7
|
+
from .base import BaseServerManager
|
|
7
8
|
from .tools import ConnectServerTool, DisconnectServerTool, GetActiveServerTool, ListServersTool, SearchToolsTool
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
class ServerManager:
|
|
11
|
+
class ServerManager(BaseServerManager):
|
|
11
12
|
"""Manages MCP servers and provides tools for server selection and management.
|
|
12
13
|
|
|
13
14
|
This class allows an agent to discover and select which MCP server to use,
|
|
@@ -4,5 +4,6 @@ from dotenv import load_dotenv
|
|
|
4
4
|
load_dotenv()
|
|
5
5
|
|
|
6
6
|
from . import laminar, langfuse # noqa
|
|
7
|
+
from .callbacks_manager import ObservabilityManager, get_default_manager, create_manager # noqa
|
|
7
8
|
|
|
8
|
-
__all__ = ["laminar", "langfuse"]
|
|
9
|
+
__all__ = ["laminar", "langfuse", "ObservabilityManager", "get_default_manager", "create_manager"]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Observability callbacks manager for MCP-use.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized manager for handling observability callbacks
|
|
5
|
+
from various platforms (Langfuse, Laminar, etc.) in a clean and extensible way.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ObservabilityManager:
|
|
14
|
+
"""
|
|
15
|
+
Manages observability callbacks for MCP agents.
|
|
16
|
+
|
|
17
|
+
This class provides a centralized way to collect and manage callbacks
|
|
18
|
+
from various observability platforms (Langfuse, Laminar, etc.).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, custom_callbacks: list | None = None):
|
|
22
|
+
"""
|
|
23
|
+
Initialize the ObservabilityManager.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
custom_callbacks: Optional list of custom callbacks to use instead of defaults.
|
|
27
|
+
"""
|
|
28
|
+
self.custom_callbacks = custom_callbacks
|
|
29
|
+
self._available_handlers = []
|
|
30
|
+
self._handler_names = []
|
|
31
|
+
self._initialized = False
|
|
32
|
+
|
|
33
|
+
def _collect_available_handlers(self) -> None:
|
|
34
|
+
"""Collect all available observability handlers from configured platforms."""
|
|
35
|
+
if self._initialized:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
# Import handlers lazily to avoid circular imports
|
|
39
|
+
try:
|
|
40
|
+
from .langfuse import langfuse_handler
|
|
41
|
+
|
|
42
|
+
if langfuse_handler is not None:
|
|
43
|
+
self._available_handlers.append(langfuse_handler)
|
|
44
|
+
self._handler_names.append("Langfuse")
|
|
45
|
+
logger.debug("ObservabilityManager: Langfuse handler available")
|
|
46
|
+
except ImportError:
|
|
47
|
+
logger.debug("ObservabilityManager: Langfuse module not available")
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
from .laminar import laminar_initialized
|
|
51
|
+
|
|
52
|
+
if laminar_initialized:
|
|
53
|
+
# Laminar is initialized with automatic instrumentation only
|
|
54
|
+
self._handler_names.append("Laminar (auto-instrumentation)")
|
|
55
|
+
logger.debug("ObservabilityManager: Laminar auto-instrumentation active")
|
|
56
|
+
except ImportError:
|
|
57
|
+
logger.debug("ObservabilityManager: Laminar module not available")
|
|
58
|
+
|
|
59
|
+
# Future: Add more platforms here...
|
|
60
|
+
|
|
61
|
+
self._initialized = True
|
|
62
|
+
|
|
63
|
+
def get_callbacks(self) -> list:
|
|
64
|
+
"""
|
|
65
|
+
Get the list of callbacks to use.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of callbacks - either custom callbacks if provided,
|
|
69
|
+
or all available observability handlers.
|
|
70
|
+
"""
|
|
71
|
+
# If custom callbacks were provided, use those
|
|
72
|
+
if self.custom_callbacks is not None:
|
|
73
|
+
logger.debug(f"ObservabilityManager: Using {len(self.custom_callbacks)} custom callbacks")
|
|
74
|
+
return self.custom_callbacks
|
|
75
|
+
|
|
76
|
+
# Otherwise, collect and return all available handlers
|
|
77
|
+
self._collect_available_handlers()
|
|
78
|
+
|
|
79
|
+
if self._available_handlers:
|
|
80
|
+
logger.debug(f"ObservabilityManager: Using {len(self._available_handlers)} handlers")
|
|
81
|
+
else:
|
|
82
|
+
logger.debug("ObservabilityManager: No callbacks configured")
|
|
83
|
+
|
|
84
|
+
return self._available_handlers
|
|
85
|
+
|
|
86
|
+
def get_handler_names(self) -> list[str]:
|
|
87
|
+
"""
|
|
88
|
+
Get the names of available handlers.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of handler names (e.g., ["Langfuse", "Laminar"])
|
|
92
|
+
"""
|
|
93
|
+
if self.custom_callbacks is not None:
|
|
94
|
+
# For custom callbacks, try to get their class names
|
|
95
|
+
return [type(cb).__name__ for cb in self.custom_callbacks]
|
|
96
|
+
|
|
97
|
+
self._collect_available_handlers()
|
|
98
|
+
return self._handler_names
|
|
99
|
+
|
|
100
|
+
def has_callbacks(self) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Check if any callbacks are available.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
True if callbacks are available, False otherwise.
|
|
106
|
+
"""
|
|
107
|
+
callbacks = self.get_callbacks()
|
|
108
|
+
return len(callbacks) > 0
|
|
109
|
+
|
|
110
|
+
def add_callback(self, callback) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Add a callback to the custom callbacks list.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
callback: The callback to add.
|
|
116
|
+
"""
|
|
117
|
+
if self.custom_callbacks is None:
|
|
118
|
+
self.custom_callbacks = []
|
|
119
|
+
self.custom_callbacks.append(callback)
|
|
120
|
+
logger.debug(f"ObservabilityManager: Added custom callback: {type(callback).__name__}")
|
|
121
|
+
|
|
122
|
+
def clear_callbacks(self) -> None:
|
|
123
|
+
"""Clear all custom callbacks."""
|
|
124
|
+
self.custom_callbacks = []
|
|
125
|
+
logger.debug("ObservabilityManager: Cleared all custom callbacks")
|
|
126
|
+
|
|
127
|
+
def __repr__(self) -> str:
|
|
128
|
+
"""String representation of the ObservabilityManager."""
|
|
129
|
+
handler_names = self.get_handler_names()
|
|
130
|
+
if handler_names:
|
|
131
|
+
return f"ObservabilityManager(handlers={handler_names})"
|
|
132
|
+
return "ObservabilityManager(no handlers)"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Singleton instance for easy access
|
|
136
|
+
_default_manager = None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_default_manager() -> ObservabilityManager:
|
|
140
|
+
"""
|
|
141
|
+
Get the default ObservabilityManager instance.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
The default ObservabilityManager instance (singleton).
|
|
145
|
+
"""
|
|
146
|
+
global _default_manager
|
|
147
|
+
if _default_manager is None:
|
|
148
|
+
_default_manager = ObservabilityManager()
|
|
149
|
+
return _default_manager
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def create_manager(custom_callbacks: list | None = None) -> ObservabilityManager:
|
|
153
|
+
"""
|
|
154
|
+
Create a new ObservabilityManager instance.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
custom_callbacks: Optional list of custom callbacks.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
A new ObservabilityManager instance.
|
|
161
|
+
"""
|
|
162
|
+
return ObservabilityManager(custom_callbacks=custom_callbacks)
|
mcp_use/observability/laminar.py
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Laminar observability integration for MCP-use.
|
|
3
|
+
|
|
4
|
+
This module provides automatic instrumentation for Laminar AI observability platform.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
import logging
|
|
2
8
|
import os
|
|
3
9
|
|
|
@@ -6,6 +12,9 @@ logger = logging.getLogger(__name__)
|
|
|
6
12
|
# Check if Laminar is disabled via environment variable
|
|
7
13
|
_laminar_disabled = os.getenv("MCP_USE_LAMINAR", "").lower() == "false"
|
|
8
14
|
|
|
15
|
+
# Track if Laminar is initialized for other modules to check
|
|
16
|
+
laminar_initialized = False
|
|
17
|
+
|
|
9
18
|
# Only initialize if not disabled and API key is present
|
|
10
19
|
if _laminar_disabled:
|
|
11
20
|
logger.debug("Laminar tracing disabled via MCP_USE_LAMINAR environment variable")
|
|
@@ -13,9 +22,21 @@ elif not os.getenv("LAMINAR_PROJECT_API_KEY"):
|
|
|
13
22
|
logger.debug("Laminar API key not found - tracing disabled. Set LAMINAR_PROJECT_API_KEY to enable")
|
|
14
23
|
else:
|
|
15
24
|
try:
|
|
16
|
-
from lmnr import Laminar
|
|
25
|
+
from lmnr import Instruments, Laminar
|
|
26
|
+
|
|
27
|
+
# Initialize Laminar with LangChain instrumentation
|
|
28
|
+
logger.debug("Laminar: Initializing automatic instrumentation for LangChain")
|
|
29
|
+
|
|
30
|
+
# Initialize with specific instruments
|
|
31
|
+
instruments = {Instruments.LANGCHAIN, Instruments.OPENAI}
|
|
32
|
+
logger.debug(f"Laminar: Enabling instruments: {[i.name for i in instruments]}")
|
|
33
|
+
|
|
34
|
+
Laminar.initialize(project_api_key=os.getenv("LAMINAR_PROJECT_API_KEY"), instruments=instruments)
|
|
35
|
+
|
|
36
|
+
laminar_initialized = True
|
|
37
|
+
logger.debug("Laminar observability initialized successfully with LangChain instrumentation")
|
|
17
38
|
|
|
18
|
-
Laminar.initialize(project_api_key=os.getenv("LAMINAR_PROJECT_API_KEY"))
|
|
19
|
-
logger.debug("Laminar observability initialized successfully")
|
|
20
39
|
except ImportError:
|
|
21
40
|
logger.debug("Laminar package not installed - tracing disabled. Install with: pip install lmnr")
|
|
41
|
+
except Exception as e:
|
|
42
|
+
logger.error(f"Failed to initialize Laminar: {e}")
|