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