|
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, |
@@ -541,35 +562,13 @@ async def _tool_handler_wrapper( |
541 | 562 | result_data_key, |
542 | 563 | ) = _prepare_handler_data("tool", original_args, original_kwargs, params=params) |
543 | 564 |
|
544 | | - scopes = _get_active_http_scopes(ctx=ctx) |
545 | | - |
546 | | - isolation_scope_context: "ContextManager[Any]" |
547 | | - current_scope_context: "ContextManager[Any]" |
548 | | - |
549 | | - if scopes is None: |
550 | | - isolation_scope_context = nullcontext() |
551 | | - current_scope_context = nullcontext() |
552 | | - else: |
553 | | - isolation_scope, current_scope = scopes |
554 | | - |
555 | | - isolation_scope_context = ( |
556 | | - nullcontext() |
557 | | - if isolation_scope is None |
558 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
559 | | - ) |
560 | | - current_scope_context = ( |
561 | | - nullcontext() |
562 | | - if current_scope is None |
563 | | - else sentry_sdk.scope.use_scope(current_scope) |
564 | | - ) |
565 | | - |
566 | 565 | # Get request ID, session ID, and transport from context |
567 | 566 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
568 | 567 |
|
569 | 568 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
570 | 569 |
|
571 | 570 | # Start span and execute |
572 | | - with isolation_scope_context, current_scope_context: |
| 571 | + with _with_active_http_scopes(ctx=ctx): |
573 | 572 | span_mgr: "Union[Span, StreamedSpan]" |
574 | 573 | if span_streaming: |
575 | 574 | span_mgr = sentry_sdk.traces.start_span( |
@@ -659,35 +658,13 @@ async def _prompt_handler_wrapper( |
659 | 658 | result_data_key, |
660 | 659 | ) = _prepare_handler_data("prompt", original_args, original_kwargs, params=params) |
661 | 660 |
|
662 | | - scopes = _get_active_http_scopes(ctx=ctx) |
663 | | - |
664 | | - isolation_scope_context: "ContextManager[Any]" |
665 | | - current_scope_context: "ContextManager[Any]" |
666 | | - |
667 | | - if scopes is None: |
668 | | - isolation_scope_context = nullcontext() |
669 | | - current_scope_context = nullcontext() |
670 | | - else: |
671 | | - isolation_scope, current_scope = scopes |
672 | | - |
673 | | - isolation_scope_context = ( |
674 | | - nullcontext() |
675 | | - if isolation_scope is None |
676 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
677 | | - ) |
678 | | - current_scope_context = ( |
679 | | - nullcontext() |
680 | | - if current_scope is None |
681 | | - else sentry_sdk.scope.use_scope(current_scope) |
682 | | - ) |
683 | | - |
684 | 661 | # Get request ID, session ID, and transport from context |
685 | 662 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
686 | 663 |
|
687 | 664 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
688 | 665 |
|
689 | 666 | # Start span and execute |
690 | | - with isolation_scope_context, current_scope_context: |
| 667 | + with _with_active_http_scopes(ctx=ctx): |
691 | 668 | span_mgr: "Union[Span, StreamedSpan]" |
692 | 669 | if span_streaming: |
693 | 670 | span_mgr = sentry_sdk.traces.start_span( |
@@ -775,35 +752,13 @@ async def _resource_handler_wrapper( |
775 | 752 | result_data_key, |
776 | 753 | ) = _prepare_handler_data("resource", original_args, original_kwargs, params=params) |
777 | 754 |
|
778 | | - scopes = _get_active_http_scopes(ctx=ctx) |
779 | | - |
780 | | - isolation_scope_context: "ContextManager[Any]" |
781 | | - current_scope_context: "ContextManager[Any]" |
782 | | - |
783 | | - if scopes is None: |
784 | | - isolation_scope_context = nullcontext() |
785 | | - current_scope_context = nullcontext() |
786 | | - else: |
787 | | - isolation_scope, current_scope = scopes |
788 | | - |
789 | | - isolation_scope_context = ( |
790 | | - nullcontext() |
791 | | - if isolation_scope is None |
792 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
793 | | - ) |
794 | | - current_scope_context = ( |
795 | | - nullcontext() |
796 | | - if current_scope is None |
797 | | - else sentry_sdk.scope.use_scope(current_scope) |
798 | | - ) |
799 | | - |
800 | 755 | # Get request ID, session ID, and transport from context |
801 | 756 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
802 | 757 |
|
803 | 758 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
804 | 759 |
|
805 | 760 | # Start span and execute |
806 | | - with isolation_scope_context, current_scope_context: |
| 761 | + with _with_active_http_scopes(ctx=ctx): |
807 | 762 | span_mgr: "Union[Span, StreamedSpan]" |
808 | 763 | if span_streaming: |
809 | 764 | span_mgr = sentry_sdk.traces.start_span( |
|
0 commit comments