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.

@@ -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
- except Exception as init_error:
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
- # Check if this is a 4xx error that indicates we should try SSE fallback
147
- should_fallback = False
148
- if isinstance(streamable_error, httpx.HTTPStatusError):
149
- if streamable_error.response.status_code in [404, 405]:
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
  )
@@ -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
- if auth_token:
45
- self.headers["Authorization"] = f"Bearer {auth_token}"
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
- # Update log level for existing loggers
122
+ # Determine the target level
118
123
  if debug_level == 2:
119
- for logger in cls._loggers.values():
120
- logger.setLevel(logging.DEBUG)
121
- langchain_set_debug(True)
124
+ target_level = logging.DEBUG
125
+ langchain_set_debug(True)
122
126
  elif debug_level == 1:
123
- for logger in cls._loggers.values():
124
- logger.setLevel(logging.INFO)
125
- langchain_set_debug(False)
127
+ target_level = logging.INFO
128
+ langchain_set_debug(False)
126
129
  else:
127
- # Reset to default (WARNING)
128
- for logger in cls._loggers.values():
129
- logger.setLevel(logging.WARNING)
130
- langchain_set_debug(False)
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":
@@ -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)
@@ -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}")