|
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, |
@@ -501,35 +522,13 @@ async def _tool_handler_wrapper( |
501 | 522 | "tool", handler_name |
502 | 523 | ) |
503 | 524 |
|
504 | | - scopes = _get_active_http_scopes(ctx=ctx) |
505 | | - |
506 | | - isolation_scope_context: "ContextManager[Any]" |
507 | | - current_scope_context: "ContextManager[Any]" |
508 | | - |
509 | | - if scopes is None: |
510 | | - isolation_scope_context = nullcontext() |
511 | | - current_scope_context = nullcontext() |
512 | | - else: |
513 | | - isolation_scope, current_scope = scopes |
514 | | - |
515 | | - isolation_scope_context = ( |
516 | | - nullcontext() |
517 | | - if isolation_scope is None |
518 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
519 | | - ) |
520 | | - current_scope_context = ( |
521 | | - nullcontext() |
522 | | - if current_scope is None |
523 | | - else sentry_sdk.scope.use_scope(current_scope) |
524 | | - ) |
525 | | - |
526 | 525 | # Get request ID, session ID, and transport from context |
527 | 526 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
528 | 527 |
|
529 | 528 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
530 | 529 |
|
531 | 530 | # Start span and execute |
532 | | - with isolation_scope_context, current_scope_context: |
| 531 | + with _with_active_http_scopes(ctx=ctx): |
533 | 532 | span_mgr: "Union[Span, StreamedSpan]" |
534 | 533 | if span_streaming: |
535 | 534 | span_mgr = sentry_sdk.traces.start_span( |
@@ -623,35 +622,13 @@ async def _prompt_handler_wrapper( |
623 | 622 | "prompt", handler_name |
624 | 623 | ) |
625 | 624 |
|
626 | | - scopes = _get_active_http_scopes(ctx=ctx) |
627 | | - |
628 | | - isolation_scope_context: "ContextManager[Any]" |
629 | | - current_scope_context: "ContextManager[Any]" |
630 | | - |
631 | | - if scopes is None: |
632 | | - isolation_scope_context = nullcontext() |
633 | | - current_scope_context = nullcontext() |
634 | | - else: |
635 | | - isolation_scope, current_scope = scopes |
636 | | - |
637 | | - isolation_scope_context = ( |
638 | | - nullcontext() |
639 | | - if isolation_scope is None |
640 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
641 | | - ) |
642 | | - current_scope_context = ( |
643 | | - nullcontext() |
644 | | - if current_scope is None |
645 | | - else sentry_sdk.scope.use_scope(current_scope) |
646 | | - ) |
647 | | - |
648 | 625 | # Get request ID, session ID, and transport from context |
649 | 626 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
650 | 627 |
|
651 | 628 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
652 | 629 |
|
653 | 630 | # Start span and execute |
654 | | - with isolation_scope_context, current_scope_context: |
| 631 | + with _with_active_http_scopes(ctx=ctx): |
655 | 632 | span_mgr: "Union[Span, StreamedSpan]" |
656 | 633 | if span_streaming: |
657 | 634 | span_mgr = sentry_sdk.traces.start_span( |
@@ -745,35 +722,13 @@ async def _resource_handler_wrapper( |
745 | 722 | "resource", handler_name |
746 | 723 | ) |
747 | 724 |
|
748 | | - scopes = _get_active_http_scopes(ctx=ctx) |
749 | | - |
750 | | - isolation_scope_context: "ContextManager[Any]" |
751 | | - current_scope_context: "ContextManager[Any]" |
752 | | - |
753 | | - if scopes is None: |
754 | | - isolation_scope_context = nullcontext() |
755 | | - current_scope_context = nullcontext() |
756 | | - else: |
757 | | - isolation_scope, current_scope = scopes |
758 | | - |
759 | | - isolation_scope_context = ( |
760 | | - nullcontext() |
761 | | - if isolation_scope is None |
762 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
763 | | - ) |
764 | | - current_scope_context = ( |
765 | | - nullcontext() |
766 | | - if current_scope is None |
767 | | - else sentry_sdk.scope.use_scope(current_scope) |
768 | | - ) |
769 | | - |
770 | 725 | # Get request ID, session ID, and transport from context |
771 | 726 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
772 | 727 |
|
773 | 728 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
774 | 729 |
|
775 | 730 | # Start span and execute |
776 | | - with isolation_scope_context, current_scope_context: |
| 731 | + with _with_active_http_scopes(ctx=ctx): |
777 | 732 | span_mgr: "Union[Span, StreamedSpan]" |
778 | 733 | if span_streaming: |
779 | 734 | span_mgr = sentry_sdk.traces.start_span( |
|
0 commit comments