|
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, |
@@ -470,35 +491,13 @@ async def _tool_handler_wrapper( |
470 | 491 | "tool", original_args, original_kwargs |
471 | 492 | ) |
472 | 493 |
|
473 | | - scopes = _get_active_http_scopes(ctx=ctx) |
474 | | - |
475 | | - isolation_scope_context: "ContextManager[Any]" |
476 | | - current_scope_context: "ContextManager[Any]" |
477 | | - |
478 | | - if scopes is None: |
479 | | - isolation_scope_context = nullcontext() |
480 | | - current_scope_context = nullcontext() |
481 | | - else: |
482 | | - isolation_scope, current_scope = scopes |
483 | | - |
484 | | - isolation_scope_context = ( |
485 | | - nullcontext() |
486 | | - if isolation_scope is None |
487 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
488 | | - ) |
489 | | - current_scope_context = ( |
490 | | - nullcontext() |
491 | | - if current_scope is None |
492 | | - else sentry_sdk.scope.use_scope(current_scope) |
493 | | - ) |
494 | | - |
495 | 494 | # Get request ID, session ID, and transport from context |
496 | 495 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
497 | 496 |
|
498 | 497 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
499 | 498 |
|
500 | 499 | # Start span and execute |
501 | | - with isolation_scope_context, current_scope_context: |
| 500 | + with _with_active_http_scopes(ctx=ctx): |
502 | 501 | span_mgr: "Union[Span, StreamedSpan]" |
503 | 502 | if span_streaming: |
504 | 503 | span_mgr = sentry_sdk.traces.start_span( |
@@ -590,35 +589,13 @@ async def _prompt_handler_wrapper( |
590 | 589 | "prompt", original_args, original_kwargs |
591 | 590 | ) |
592 | 591 |
|
593 | | - scopes = _get_active_http_scopes(ctx=ctx) |
594 | | - |
595 | | - isolation_scope_context: "ContextManager[Any]" |
596 | | - current_scope_context: "ContextManager[Any]" |
597 | | - |
598 | | - if scopes is None: |
599 | | - isolation_scope_context = nullcontext() |
600 | | - current_scope_context = nullcontext() |
601 | | - else: |
602 | | - isolation_scope, current_scope = scopes |
603 | | - |
604 | | - isolation_scope_context = ( |
605 | | - nullcontext() |
606 | | - if isolation_scope is None |
607 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
608 | | - ) |
609 | | - current_scope_context = ( |
610 | | - nullcontext() |
611 | | - if current_scope is None |
612 | | - else sentry_sdk.scope.use_scope(current_scope) |
613 | | - ) |
614 | | - |
615 | 592 | # Get request ID, session ID, and transport from context |
616 | 593 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
617 | 594 |
|
618 | 595 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
619 | 596 |
|
620 | 597 | # Start span and execute |
621 | | - with isolation_scope_context, current_scope_context: |
| 598 | + with _with_active_http_scopes(ctx=ctx): |
622 | 599 | span_mgr: "Union[Span, StreamedSpan]" |
623 | 600 | if span_streaming: |
624 | 601 | span_mgr = sentry_sdk.traces.start_span( |
@@ -710,35 +687,13 @@ async def _resource_handler_wrapper( |
710 | 687 | "resource", original_args, original_kwargs |
711 | 688 | ) |
712 | 689 |
|
713 | | - scopes = _get_active_http_scopes(ctx=ctx) |
714 | | - |
715 | | - isolation_scope_context: "ContextManager[Any]" |
716 | | - current_scope_context: "ContextManager[Any]" |
717 | | - |
718 | | - if scopes is None: |
719 | | - isolation_scope_context = nullcontext() |
720 | | - current_scope_context = nullcontext() |
721 | | - else: |
722 | | - isolation_scope, current_scope = scopes |
723 | | - |
724 | | - isolation_scope_context = ( |
725 | | - nullcontext() |
726 | | - if isolation_scope is None |
727 | | - else sentry_sdk.scope.use_isolation_scope(isolation_scope) |
728 | | - ) |
729 | | - current_scope_context = ( |
730 | | - nullcontext() |
731 | | - if current_scope is None |
732 | | - else sentry_sdk.scope.use_scope(current_scope) |
733 | | - ) |
734 | | - |
735 | 690 | # Get request ID, session ID, and transport from context |
736 | 691 | request_id, session_id, mcp_transport = _get_request_context_data(ctx=ctx) |
737 | 692 |
|
738 | 693 | span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options) |
739 | 694 |
|
740 | 695 | # Start span and execute |
741 | | - with isolation_scope_context, current_scope_context: |
| 696 | + with _with_active_http_scopes(ctx=ctx): |
742 | 697 | span_mgr: "Union[Span, StreamedSpan]" |
743 | 698 | if span_streaming: |
744 | 699 | span_mgr = sentry_sdk.traces.start_span( |
|
0 commit comments