|
8 | 8 | """ |
9 | 9 |
|
10 | 10 | import inspect |
| 11 | +from collections.abc import Iterator |
| 12 | +from contextlib import contextmanager |
11 | 13 | from functools import wraps |
12 | 14 | from typing import TYPE_CHECKING |
13 | 15 |
|
|
53 | 55 |
|
54 | 56 |
|
55 | 57 | if TYPE_CHECKING: |
56 | | - from typing import Any, Awaitable, Callable, ContextManager, Optional, Tuple, Union |
| 58 | + from typing import Any, Awaitable, Callable, Optional, Union |
57 | 59 |
|
58 | 60 | from mcp_types import ( |
59 | 61 | CallToolResult, |
@@ -93,29 +95,48 @@ def setup_once() -> None: |
93 | 95 | _patch_fastmcp() |
94 | 96 |
|
95 | 97 |
|
96 | | -def _get_active_http_scopes( |
97 | | - ctx: "Optional[Any]" = None, |
98 | | -) -> "Optional[Tuple[Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]]]": |
| 98 | +@contextmanager |
| 99 | +def _with_active_http_scopes( |
| 100 | + ctx: "Any" = None, |
| 101 | +) -> "Iterator[None]": |
| 102 | + """ |
| 103 | + Use isolation and current scopes that were stored before the in-memory MCP request queue. |
| 104 | + This ensures that MCP spans are nested under the HTTP server span when using the Streamable HTTP transport. |
| 105 | + """ |
99 | 106 | if MCP_PACKAGE_VERSION and MCP_PACKAGE_VERSION < (2, 0, 0): |
100 | 107 | if ctx is None: |
101 | 108 | try: |
102 | 109 | ctx = request_ctx.get() |
103 | 110 | except LookupError: |
104 | | - return None |
| 111 | + yield None |
| 112 | + return |
105 | 113 |
|
106 | 114 | if ( |
107 | 115 | ctx is None |
108 | 116 | or not hasattr(ctx, "request") |
109 | 117 | or ctx.request is None |
110 | 118 | or "state" not in ctx.request.scope |
111 | 119 | ): |
112 | | - return None |
| 120 | + yield |
| 121 | + return |
113 | 122 |
|
114 | | - return ( |
115 | | - ctx.request.scope["state"].get("sentry_sdk.isolation_scope"), |
116 | | - ctx.request.scope["state"].get("sentry_sdk.current_scope"), |
| 123 | + isolation_scope = ctx.request.scope["state"].get("sentry_sdk.isolation_scope") |
| 124 | + current_scope = ctx.request.scope["state"].get("sentry_sdk.current_scope") |
| 125 | + |
| 126 | + isolation_scope_context = ( |
| 127 | + nullcontext() |
| 128 | + if isolation_scope is None |
| 129 | + else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
| 130 | + ) |
| 131 | + current_scope_context = ( |
| 132 | + nullcontext() |
| 133 | + if current_scope is None |
| 134 | + else sentry_sdk.scope.use_scope(current_scope) |
117 | 135 | ) |
118 | 136 |
|
| 137 | + with isolation_scope_context, current_scope_context: |
| 138 | + yield |
| 139 | + |
119 | 140 |
|
120 | 141 | def _get_request_context_data( |
121 | 142 | ctx: "Optional[Any]" = None, |
@@ -375,35 +396,13 @@ async def _tool_handler_wrapper( |
375 | 396 | "tool", original_args, original_kwargs |
376 | 397 | ) |
377 | 398 |
|
378 | | - scopes = _get_active_http_scopes(ctx=ctx) |
379 | | - |
380 | | - isolation_scope_context: "ContextManager[Any]" |
381 | | - current_scope_context: "ContextManager[Any]" |
382 | | - |
383 | | - if scopes is None: |
384 | | - isolation_scope_context = nullcontext() |
385 | | - current_scope_context = nullcontext() |
386 | | - else: |
387 | | - isolation_scope, current_scope = scopes |
388 | | - |
389 | | - isolation_scope_context = ( |
390 | | - nullcontext() |
391 | | - if isolation_scope is None |
392 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
393 | | - ) |
394 | | - current_scope_context = ( |
395 | | - nullcontext() |
396 | | - if current_scope is None |
397 | | - else sentry_sdk.scope.use_scope(current_scope) |
398 | | - ) |
399 | | - |
400 | 399 | # Get request ID, session ID, and transport from context |
401 | 400 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
402 | 401 |
|
403 | 402 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
404 | 403 |
|
405 | 404 | # Start span and execute |
406 | | - with isolation_scope_context, current_scope_context: |
| 405 | + with _with_active_http_scopes(ctx=ctx): |
407 | 406 | span_mgr: "Union[Span, StreamedSpan]" |
408 | 407 | if span_streaming: |
409 | 408 | span_mgr = sentry_sdk.traces.start_span( |
@@ -515,35 +514,13 @@ async def _prompt_handler_wrapper( |
515 | 514 | "prompt", original_args, original_kwargs |
516 | 515 | ) |
517 | 516 |
|
518 | | - scopes = _get_active_http_scopes(ctx=ctx) |
519 | | - |
520 | | - isolation_scope_context: "ContextManager[Any]" |
521 | | - current_scope_context: "ContextManager[Any]" |
522 | | - |
523 | | - if scopes is None: |
524 | | - isolation_scope_context = nullcontext() |
525 | | - current_scope_context = nullcontext() |
526 | | - else: |
527 | | - isolation_scope, current_scope = scopes |
528 | | - |
529 | | - isolation_scope_context = ( |
530 | | - nullcontext() |
531 | | - if isolation_scope is None |
532 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
533 | | - ) |
534 | | - current_scope_context = ( |
535 | | - nullcontext() |
536 | | - if current_scope is None |
537 | | - else sentry_sdk.scope.use_scope(current_scope) |
538 | | - ) |
539 | | - |
540 | 517 | # Get request ID, session ID, and transport from context |
541 | 518 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
542 | 519 |
|
543 | 520 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
544 | 521 |
|
545 | 522 | # Start span and execute |
546 | | - with isolation_scope_context, current_scope_context: |
| 523 | + with _with_active_http_scopes(ctx=ctx): |
547 | 524 | span_mgr: "Union[Span, StreamedSpan]" |
548 | 525 | if span_streaming: |
549 | 526 | span_mgr = sentry_sdk.traces.start_span( |
@@ -707,35 +684,13 @@ async def _resource_handler_wrapper( |
707 | 684 | "resource", original_args, original_kwargs |
708 | 685 | ) |
709 | 686 |
|
710 | | - scopes = _get_active_http_scopes(ctx=ctx) |
711 | | - |
712 | | - isolation_scope_context: "ContextManager[Any]" |
713 | | - current_scope_context: "ContextManager[Any]" |
714 | | - |
715 | | - if scopes is None: |
716 | | - isolation_scope_context = nullcontext() |
717 | | - current_scope_context = nullcontext() |
718 | | - else: |
719 | | - isolation_scope, current_scope = scopes |
720 | | - |
721 | | - isolation_scope_context = ( |
722 | | - nullcontext() |
723 | | - if isolation_scope is None |
724 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
725 | | - ) |
726 | | - current_scope_context = ( |
727 | | - nullcontext() |
728 | | - if current_scope is None |
729 | | - else sentry_sdk.scope.use_scope(current_scope) |
730 | | - ) |
731 | | - |
732 | 687 | # Get request ID, session ID, and transport from context |
733 | 688 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
734 | 689 |
|
735 | 690 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
736 | 691 |
|
737 | 692 | # Start span and execute |
738 | | - with isolation_scope_context, current_scope_context: |
| 693 | + with _with_active_http_scopes(ctx=ctx): |
739 | 694 | span_mgr: "Union[Span, StreamedSpan]" |
740 | 695 | if span_streaming: |
741 | 696 | span_mgr = sentry_sdk.traces.start_span( |
@@ -826,7 +781,7 @@ async def wrapper(*args: "Any") -> "Any": |
826 | 781 | return await _tool_handler_wrapper(func, args, force_await=False) |
827 | 782 |
|
828 | 783 | # Then register it with the original MCP decorator |
829 | | - return original_call_tool(self)(wrapper) |
| 784 | + return original_call_tool(self, **kwargs)(wrapper) |
830 | 785 |
|
831 | 786 | return instrumented_decorator |
832 | 787 |
|
|
0 commit comments