hud-python 0.2.3__py3-none-any.whl → 0.2.5__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 hud-python might be problematic. Click here for more details.

Files changed (50) hide show
  1. hud/__init__.py +22 -2
  2. hud/adapters/claude/adapter.py +9 -2
  3. hud/adapters/claude/tests/__init__.py +1 -0
  4. hud/adapters/claude/tests/test_adapter.py +519 -0
  5. hud/adapters/common/types.py +5 -1
  6. hud/adapters/operator/adapter.py +4 -0
  7. hud/adapters/operator/tests/__init__.py +1 -0
  8. hud/adapters/operator/tests/test_adapter.py +370 -0
  9. hud/agent/__init__.py +4 -0
  10. hud/agent/base.py +18 -2
  11. hud/agent/claude.py +20 -17
  12. hud/agent/claude_plays_pokemon.py +282 -0
  13. hud/agent/langchain.py +12 -7
  14. hud/agent/misc/__init__.py +3 -0
  15. hud/agent/misc/response_agent.py +80 -0
  16. hud/agent/operator.py +27 -19
  17. hud/agent/tests/__init__.py +1 -0
  18. hud/agent/tests/test_base.py +202 -0
  19. hud/env/docker_client.py +28 -18
  20. hud/env/environment.py +33 -17
  21. hud/env/local_docker_client.py +83 -42
  22. hud/env/remote_client.py +1 -3
  23. hud/env/remote_docker_client.py +72 -15
  24. hud/exceptions.py +12 -0
  25. hud/gym.py +71 -53
  26. hud/job.py +52 -7
  27. hud/settings.py +6 -0
  28. hud/task.py +45 -33
  29. hud/taskset.py +44 -4
  30. hud/telemetry/__init__.py +21 -0
  31. hud/telemetry/_trace.py +173 -0
  32. hud/telemetry/context.py +193 -0
  33. hud/telemetry/exporter.py +417 -0
  34. hud/telemetry/instrumentation/__init__.py +3 -0
  35. hud/telemetry/instrumentation/mcp.py +498 -0
  36. hud/telemetry/instrumentation/registry.py +59 -0
  37. hud/telemetry/mcp_models.py +331 -0
  38. hud/telemetry/tests/__init__.py +1 -0
  39. hud/telemetry/tests/test_context.py +203 -0
  40. hud/telemetry/tests/test_trace.py +270 -0
  41. hud/types.py +10 -26
  42. hud/utils/common.py +22 -2
  43. hud/utils/misc.py +53 -0
  44. hud/utils/tests/test_version.py +1 -1
  45. hud/version.py +7 -0
  46. {hud_python-0.2.3.dist-info → hud_python-0.2.5.dist-info}/METADATA +90 -22
  47. hud_python-0.2.5.dist-info/RECORD +84 -0
  48. hud_python-0.2.3.dist-info/RECORD +0 -62
  49. {hud_python-0.2.3.dist-info → hud_python-0.2.5.dist-info}/WHEEL +0 -0
  50. {hud_python-0.2.3.dist-info → hud_python-0.2.5.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,498 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from contextlib import asynccontextmanager
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ if TYPE_CHECKING:
9
+ from collections.abc import AsyncGenerator, Awaitable, Callable
10
+
11
+ # from dataclasses import dataclass # Likely unused now
12
+ from mcp.shared.message import SessionMessage # type: ignore
13
+
14
+ # MCP type imports for type checking and attribute access safety
15
+ from mcp.types import ( # type: ignore
16
+ JSONRPCError,
17
+ JSONRPCNotification,
18
+ JSONRPCRequest,
19
+ JSONRPCResponse,
20
+ )
21
+ from wrapt import register_post_import_hook, wrap_function_wrapper
22
+
23
+ from hud.telemetry.context import (
24
+ buffer_mcp_call,
25
+ create_notification_record,
26
+ create_request_record,
27
+ create_response_record,
28
+ get_current_task_run_id,
29
+ )
30
+ from hud.telemetry.mcp_models import DirectionType, MCPCallType, MCPManualTestCall, StatusType
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ # Ensure no OTel imports remain
35
+ # from opentelemetry import context as otel_context, propagate # Should be removed
36
+
37
+
38
+ class MCPInstrumentor:
39
+ """
40
+ Instrumentor for MCP calls.
41
+ """
42
+
43
+ def __init__(self) -> None:
44
+ self._installed = False
45
+
46
+ def install(self) -> None:
47
+ """Install instrumentation for MCP"""
48
+ logger.debug("MCPInstrumentor: install() called")
49
+ if self._installed:
50
+ logger.debug("MCP instrumentation already installed")
51
+ return
52
+
53
+ # Updated hooks: removed _transport_wrapper and _base_session_init_wrapper related hooks
54
+ # if they become no-ops. For now, let's assume _transport_wrapper might still be used,
55
+ # but simplified. _base_session_init_wrapper hook is removed.
56
+ hooks = [
57
+ ("mcp.client.sse", "sse_client", self._transport_wrapper),
58
+ ("mcp.server.sse", "SseServerTransport.connect_sse", self._transport_wrapper),
59
+ ("mcp.client.stdio", "stdio_client", self._transport_wrapper),
60
+ ("mcp.server.stdio", "stdio_server", self._transport_wrapper),
61
+ ("mcp.shared.session", "BaseSession._receive_loop", self._receive_loop_wrapper),
62
+ ]
63
+
64
+ def create_hook(module: str, func_name: str, wrapper: Any) -> Any:
65
+ return lambda _: wrap_function_wrapper(module, func_name, wrapper)
66
+
67
+ for module, func_name, wrapper in hooks:
68
+ logger.debug(
69
+ "MCPInstrumentor: Registering post-import hook for %s.%s", module, func_name
70
+ )
71
+ register_post_import_hook(
72
+ create_hook(module, func_name, wrapper),
73
+ module,
74
+ )
75
+
76
+ # Removed hook for BaseSession.__init__ (it was for _base_session_init_wrapper)
77
+
78
+ logger.debug(
79
+ "MCPInstrumentor: Attempting immediate instrumentation of already imported modules."
80
+ )
81
+ for module, func_name, wrapper in hooks:
82
+ try:
83
+ mod = __import__(module, fromlist=[func_name.split(".")[0]])
84
+ target_obj = mod
85
+ parts = func_name.split(".")
86
+ for i, part in enumerate(parts):
87
+ if hasattr(target_obj, part):
88
+ if i == len(parts) - 1:
89
+ target_obj = getattr(target_obj, part)
90
+ else:
91
+ target_obj = getattr(target_obj, part)
92
+ else:
93
+ target_obj = None
94
+ break
95
+
96
+ if target_obj and callable(target_obj):
97
+ logger.debug("MCPInstrumentor: Wrapping %s.%s", module, func_name)
98
+ wrap_function_wrapper(module, func_name, wrapper)
99
+ else:
100
+ logger.debug(
101
+ "Function %s not found in %s during immediate instrumentation attempt",
102
+ func_name,
103
+ module,
104
+ )
105
+ except ImportError:
106
+ logger.debug("Module %s not imported yet, post-import hook will cover it", module)
107
+ except Exception as e:
108
+ logger.warning("Failed to immediately wrap %s.%s: %s", module, func_name, e)
109
+
110
+ try:
111
+ # Import only for testing availability, don't store reference
112
+ __import__("mcp.shared.session")
113
+ wrap_function_wrapper(
114
+ "mcp.shared.session",
115
+ "BaseSession.send_request",
116
+ self._send_request_telemetry_wrapper,
117
+ )
118
+ wrap_function_wrapper(
119
+ "mcp.shared.session",
120
+ "BaseSession.send_notification",
121
+ self._send_notification_telemetry_wrapper,
122
+ )
123
+ logger.debug("Successfully wrapped BaseSession methods for telemetry")
124
+ except ImportError:
125
+ logger.debug("mcp.shared.session not imported yet, post-import hook will cover it")
126
+ except Exception as e:
127
+ logger.warning("Failed to wrap BaseSession methods: %s", e)
128
+
129
+ self._installed = True
130
+ logger.debug("MCP instrumentation installed (simplified)")
131
+
132
+ @asynccontextmanager
133
+ async def _transport_wrapper(
134
+ self, wrapped: Callable[..., Any], instance: Any, args: Any, kwargs: Any
135
+ ) -> AsyncGenerator[Any, None]:
136
+ """Wrap transport functions. Simplified: passes through original streams."""
137
+ original_func_name = f"{wrapped.__module__}.{wrapped.__name__}"
138
+ logger.debug(
139
+ "MCPInstrumentor: _transport_wrapper called for %s (passthrough)", original_func_name
140
+ )
141
+
142
+ # No OTel context or HUD Task ID manipulation here anymore for transport layer itself.
143
+ # Higher level wrappers (_send_request, _receive_loop) will handle HUD Task ID.
144
+
145
+ async with wrapped(*args, **kwargs) as (read_stream, write_stream):
146
+ logger.debug("_transport_wrapper: Yielding original streams for %s", original_func_name)
147
+ yield read_stream, write_stream # Pass original streams directly
148
+ logger.debug(
149
+ "_transport_wrapper: Exited original stream context for %s", original_func_name
150
+ )
151
+
152
+ async def _receive_loop_wrapper(
153
+ self,
154
+ wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
155
+ instance: Any,
156
+ args: Any,
157
+ kwargs: Any,
158
+ ) -> Any:
159
+ """
160
+ Wrap _receive_loop to capture responses and handled incoming messages.
161
+ """
162
+ logger.debug("MCPInstrumentor: _receive_loop_wrapper called")
163
+
164
+ orig_handle_incoming = instance._handle_incoming
165
+
166
+ async def handle_incoming_with_telemetry(req_or_msg: Any) -> Any:
167
+ start_time = time.time()
168
+ hud_task_run_id = get_current_task_run_id()
169
+ method_name = "unknown_method"
170
+ message_id = None
171
+ is_response_or_error_flag = False
172
+ call_type_override = MCPCallType.HANDLE_INCOMING
173
+
174
+ actual_message_root = None
175
+ # SessionMessage
176
+ if hasattr(req_or_msg, "message") and hasattr(req_or_msg.message, "root"):
177
+ actual_message_root = req_or_msg.message.root
178
+ elif hasattr(req_or_msg, "root"): # e.g. RequestResponder
179
+ actual_message_root = req_or_msg.root
180
+ elif isinstance(
181
+ req_or_msg, JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError
182
+ ):
183
+ actual_message_root = req_or_msg
184
+
185
+ if actual_message_root:
186
+ if isinstance(actual_message_root, JSONRPCRequest):
187
+ method_name = actual_message_root.method
188
+ message_id = actual_message_root.id
189
+ # call_type_override can remain HANDLE_INCOMING or be more specific if desired
190
+ elif isinstance(actual_message_root, JSONRPCNotification):
191
+ method_name = actual_message_root.method
192
+ message_id = None # Notifications don't have IDs
193
+ # call_type_override can remain HANDLE_INCOMING
194
+ elif isinstance(actual_message_root, JSONRPCResponse | JSONRPCError):
195
+ # This case implies _handle_incoming is processing a response/error directly
196
+ # (e.g. an error encountered while trying to send/route a response previously)
197
+ message_id = actual_message_root.id
198
+ method_name = f"internal_response_handling_for_id_{message_id}"
199
+ is_response_or_error_flag = True
200
+ # Treat as receiving a response internally
201
+ call_type_override = MCPCallType.RECEIVE_RESPONSE
202
+ else:
203
+ # Fallback for other types, if any, that might appear here
204
+ if hasattr(actual_message_root, "method"):
205
+ method_name = actual_message_root.method
206
+ if hasattr(actual_message_root, "id"):
207
+ message_id = actual_message_root.id
208
+
209
+ record_data_base = {
210
+ "method": method_name,
211
+ "direction": DirectionType.RECEIVED,
212
+ "call_type": call_type_override,
213
+ "message_id": message_id,
214
+ "is_response_or_error": is_response_or_error_flag, # For MCPRequestCall model
215
+ }
216
+
217
+ try:
218
+ if hud_task_run_id:
219
+ create_request_record(
220
+ **record_data_base,
221
+ status=StatusType.STARTED,
222
+ start_time=start_time,
223
+ # request_data might be populated if needed for HANDLE_INCOMING
224
+ )
225
+
226
+ result = await orig_handle_incoming(req_or_msg)
227
+
228
+ if hud_task_run_id:
229
+ create_request_record(
230
+ **record_data_base,
231
+ status=StatusType.COMPLETED,
232
+ start_time=start_time,
233
+ end_time=time.time(),
234
+ duration=time.time() - start_time,
235
+ # result_data might be populated if needed
236
+ )
237
+ return result
238
+ except Exception as e:
239
+ if hud_task_run_id:
240
+ create_request_record(
241
+ **record_data_base,
242
+ status=StatusType.ERROR,
243
+ start_time=start_time,
244
+ end_time=time.time(),
245
+ duration=time.time() - start_time,
246
+ error=str(e),
247
+ error_type=type(e).__name__,
248
+ )
249
+ raise
250
+
251
+ instance._handle_incoming = handle_incoming_with_telemetry
252
+
253
+ try:
254
+ logger.debug("MCPInstrumentor: Entering instrumented _receive_loop section")
255
+ # The original logic for distinguishing request/notification from response for
256
+ # telemetry:
257
+ # Ensure streams are context managed
258
+ async with instance._read_stream, instance._write_stream:
259
+ async for message in instance._read_stream:
260
+ hud_task_run_id = get_current_task_run_id() # Get ID for each message
261
+
262
+ if isinstance(message, Exception):
263
+ await instance._handle_incoming(message) # Will be wrapped
264
+ continue
265
+
266
+ # Ensure we are dealing with SessionMessage
267
+ if (
268
+ not isinstance(message, SessionMessage)
269
+ or not hasattr(message, "message")
270
+ or not hasattr(message.message, "root")
271
+ ):
272
+ logger.warning(
273
+ "Unexpected message type in _receive_loop: %s", type(message)
274
+ )
275
+ await instance._handle_incoming(
276
+ RuntimeError(f"Unknown message type: {message}")
277
+ )
278
+ continue
279
+
280
+ msg_root = message.message.root
281
+
282
+ if isinstance(msg_root, JSONRPCRequest | JSONRPCNotification):
283
+ # Let the (wrapped) _handle_incoming deal with these and record telemetry
284
+ await instance._handle_incoming(message)
285
+ elif isinstance(msg_root, JSONRPCResponse | JSONRPCError):
286
+ # This is a direct response/error, record it here
287
+ logger.debug(
288
+ "MCPInstrumentor: Capturing direct response/error ID: %s", msg_root.id
289
+ )
290
+ if hud_task_run_id:
291
+ is_error = isinstance(msg_root, JSONRPCError)
292
+ error_message = None
293
+ error_code = None
294
+ if is_error and hasattr(msg_root, "error"):
295
+ error_message = getattr(msg_root.error, "message", None)
296
+ error_code = str(getattr(msg_root.error, "code", None))
297
+
298
+ create_response_record(
299
+ method=f"response_to_id_{msg_root.id}", # Consistent method naming
300
+ related_request_id=msg_root.id,
301
+ response_id=msg_root.id,
302
+ is_error=is_error,
303
+ response_data=msg_root.model_dump(exclude_none=True),
304
+ error=error_message,
305
+ error_type=error_code,
306
+ direction=DirectionType.RECEIVED,
307
+ call_type=MCPCallType.RECEIVE_RESPONSE,
308
+ timestamp=time.time(), # Add timestamp here for accuracy
309
+ )
310
+
311
+ # Original logic to pass response to waiting stream
312
+ stream = instance._response_streams.pop(msg_root.id, None)
313
+ if stream:
314
+ await stream.send(msg_root)
315
+ else:
316
+ # This case should ideally be handled by _handle_incoming if it's an
317
+ # unsolicited response
318
+ logger.warning(
319
+ "Received response/error with unknown request ID %s and no "
320
+ "response stream.",
321
+ msg_root.id,
322
+ )
323
+ # Potentially pass to _handle_incoming if that's desired for unroutable
324
+ # responses
325
+ await instance._handle_incoming(message) # Let wrapped handler decide
326
+ else:
327
+ logger.warning(
328
+ "Unknown message root type in _receive_loop: %s", type(msg_root)
329
+ )
330
+ await instance._handle_incoming(message) # Let wrapped handler decide
331
+
332
+ logger.debug("MCPInstrumentor: Exiting instrumented _receive_loop section")
333
+ except Exception as e:
334
+ logger.error("Error in instrumented receive loop: %s, falling back to original", e)
335
+ instance._handle_incoming = orig_handle_incoming
336
+ # Re-raising to ensure original behavior if full loop fails, or call wrapped directly
337
+ # For simplicity now, we just log and let finally restore.
338
+ raise
339
+ finally:
340
+ instance._handle_incoming = orig_handle_incoming
341
+ logger.debug("MCPInstrumentor: Restored original _handle_incoming")
342
+
343
+ async def _send_request_telemetry_wrapper(
344
+ self,
345
+ wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
346
+ instance: Any,
347
+ args: Any,
348
+ kwargs: Any,
349
+ ) -> Any:
350
+ start_time = time.time()
351
+ hud_task_run_id = get_current_task_run_id()
352
+ method_name = "unknown_method"
353
+ request_obj_data = None
354
+ request_id = None
355
+
356
+ if args and len(args) > 0:
357
+ request_session_msg = args[0] # Assuming SessionMessage
358
+ if hasattr(request_session_msg, "message") and hasattr(
359
+ request_session_msg.message, "root"
360
+ ):
361
+ actual_request = request_session_msg.message.root
362
+ if hasattr(actual_request, "method"):
363
+ method_name = actual_request.method
364
+ if hasattr(actual_request, "id"):
365
+ request_id = actual_request.id
366
+ if hasattr(actual_request, "model_dump"):
367
+ try:
368
+ request_obj_data = actual_request.model_dump(exclude_none=True)
369
+ except Exception as e:
370
+ logger.warning(
371
+ "Could not dump request data for %s: %s",
372
+ method_name,
373
+ e,
374
+ )
375
+
376
+ record_data_base = {
377
+ "method": method_name,
378
+ "direction": DirectionType.SENT,
379
+ "call_type": MCPCallType.SEND_REQUEST, # More specific type
380
+ "request_id": request_id,
381
+ "message_id": request_id,
382
+ "request_data": request_obj_data,
383
+ }
384
+
385
+ try:
386
+ if hud_task_run_id:
387
+ create_request_record(
388
+ **record_data_base, status=StatusType.STARTED, start_time=start_time
389
+ )
390
+
391
+ result = await wrapped(*args, **kwargs) # result here is usually None for send_request
392
+
393
+ if hud_task_run_id:
394
+ # For send_request, the 'result' is the response future, not the response itself.
395
+ # Completion means the request was successfully sent to the transport.
396
+ # The actual response is captured by _receive_loop_wrapper.
397
+ create_request_record(
398
+ **record_data_base,
399
+ status=StatusType.COMPLETED, # Meaning: successfully dispatched
400
+ start_time=start_time,
401
+ end_time=time.time(),
402
+ duration=time.time() - start_time,
403
+ )
404
+ return result
405
+ except Exception as e:
406
+ if hud_task_run_id:
407
+ create_request_record(
408
+ **record_data_base,
409
+ status=StatusType.ERROR,
410
+ start_time=start_time,
411
+ end_time=time.time(),
412
+ duration=time.time() - start_time,
413
+ error=str(e),
414
+ error_type=type(e).__name__,
415
+ )
416
+ raise
417
+
418
+ async def _send_notification_telemetry_wrapper(
419
+ self,
420
+ wrapped: Callable[[Any, Any, Any, Any], Awaitable[Any]],
421
+ instance: Any,
422
+ args: Any,
423
+ kwargs: Any,
424
+ ) -> Any:
425
+ start_time = time.time()
426
+ hud_task_run_id = get_current_task_run_id()
427
+ method_name = "unknown_method"
428
+ notification_obj_data = None
429
+
430
+ if args and len(args) > 0:
431
+ notification_session_msg = args[0] # Assuming SessionMessage
432
+ if hasattr(notification_session_msg, "message") and hasattr(
433
+ notification_session_msg.message, "root"
434
+ ):
435
+ actual_notification = notification_session_msg.message.root
436
+ if hasattr(actual_notification, "method"):
437
+ method_name = actual_notification.method
438
+ if hasattr(actual_notification, "model_dump"):
439
+ try:
440
+ notification_obj_data = actual_notification.model_dump(exclude_none=True)
441
+ except Exception as e:
442
+ logger.warning(
443
+ "Could not dump notification data for %s: %s",
444
+ method_name,
445
+ e,
446
+ )
447
+
448
+ record_data_base = {
449
+ "method": method_name,
450
+ "direction": DirectionType.SENT,
451
+ "call_type": MCPCallType.SEND_NOTIFICATION, # More specific type
452
+ "notification_data": notification_obj_data,
453
+ }
454
+
455
+ try:
456
+ if hud_task_run_id:
457
+ create_notification_record(
458
+ **record_data_base, status=StatusType.STARTED, start_time=start_time
459
+ )
460
+
461
+ result = await wrapped(*args, **kwargs) # Usually None
462
+
463
+ if hud_task_run_id:
464
+ create_notification_record(
465
+ **record_data_base,
466
+ status=StatusType.COMPLETED,
467
+ start_time=start_time,
468
+ end_time=time.time(),
469
+ duration=time.time() - start_time,
470
+ )
471
+ return result
472
+ except Exception as e:
473
+ if hud_task_run_id:
474
+ create_notification_record(
475
+ **record_data_base,
476
+ status=StatusType.ERROR,
477
+ start_time=start_time,
478
+ end_time=time.time(),
479
+ duration=time.time() - start_time,
480
+ error=str(e),
481
+ error_type=type(e).__name__,
482
+ )
483
+ raise
484
+
485
+ def record_manual_test(self, **kwargs: Any) -> bool:
486
+ """Record a manual test telemetry entry"""
487
+ hud_task_run_id = get_current_task_run_id()
488
+ if not hud_task_run_id:
489
+ logger.warning("Manual test not recorded: No active task_run_id")
490
+ return False
491
+ try:
492
+ record = MCPManualTestCall.create(task_run_id=hud_task_run_id, **kwargs)
493
+ buffer_mcp_call(record) # buffer_mcp_call handles Pydantic model directly
494
+ logger.debug("Manual test recorded: %s", record.model_dump(exclude_none=True))
495
+ return True
496
+ except Exception as e:
497
+ logger.warning("Manual test not recorded: %s", e)
498
+ return False
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ class InstrumentorRegistry:
10
+ """Registry for telemetry instrumentors."""
11
+
12
+ def __init__(self) -> None:
13
+ self._instrumentors: dict[str, Any] = {}
14
+
15
+ def register(self, name: str, instrumentor: Any) -> None:
16
+ """Register an instrumentor.
17
+
18
+ Args:
19
+ name: Name of the instrumentor
20
+ instrumentor: The instrumentor instance
21
+ """
22
+ self._instrumentors[name] = instrumentor
23
+ logger.debug("Registered instrumentor: %s", name)
24
+
25
+ def _safe_install(self, name: str, instrumentor: Any) -> tuple[str, Exception | None]:
26
+ """Safely install an instrumentor and return result."""
27
+ try:
28
+ instrumentor.install()
29
+ return name, None
30
+ except Exception as e:
31
+ return name, e
32
+
33
+ def install_all(self) -> None:
34
+ """Install all registered instrumentors."""
35
+ # Use map to apply safe installation to all instrumentors
36
+ installation_results = [
37
+ self._safe_install(name, instrumentor)
38
+ for name, instrumentor in self._instrumentors.items()
39
+ ]
40
+
41
+ # Process results
42
+ for name, error in installation_results:
43
+ if error is None:
44
+ logger.debug("Installed instrumentor: %s", name)
45
+ else:
46
+ logger.warning("Failed to install instrumentor %s: %s", name, error)
47
+
48
+
49
+ # Create a singleton registry
50
+ registry = InstrumentorRegistry()
51
+
52
+ # Try to register MCP instrumentor if available
53
+ try:
54
+ from .mcp import MCPInstrumentor
55
+
56
+ registry.register("mcp", MCPInstrumentor())
57
+ logger.debug("MCP instrumentor registered")
58
+ except Exception as e:
59
+ logger.debug("Could not register MCP instrumentor: %s", e)