Skip to content

Commit ebf64d3

Browse files
ref(mcp): Inline _prepare_handler_data
1 parent 247f5d4 commit ebf64d3

1 file changed

Lines changed: 145 additions & 89 deletions

File tree

sentry_sdk/integrations/mcp.py

Lines changed: 145 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
if TYPE_CHECKING:
5656
from typing import Any, Awaitable, Callable, ContextManager, Optional, Tuple, Union
5757

58-
from mcp.server.context import CallNext, HandlerResult
5958
from mcp_types import (
6059
CallToolResult,
6160
GetPromptResult,
@@ -458,50 +457,6 @@ def _extract_handler_data_from_args(
458457
return handler_name, arguments
459458

460459

461-
def _prepare_handler_data(
462-
handler_type: str,
463-
original_args: "tuple[Any, ...]",
464-
original_kwargs: "Optional[dict[str, Any]]" = None,
465-
params: "Optional[Any]" = None,
466-
) -> "tuple[str, dict[str, Any], str, str, str, Optional[str]]":
467-
"""
468-
Prepare common handler data for both v1 and v2 MCP SDK.
469-
470-
Args:
471-
handler_type: "tool", "prompt", or "resource"
472-
original_args: Original positional args (v1 path)
473-
original_kwargs: Original keyword args (v1 path)
474-
params: Typed params object from v2 ServerRequestContext path
475-
476-
Returns:
477-
Tuple of (handler_name, arguments, span_data_key, span_name, mcp_method_name, result_data_key)
478-
"""
479-
if params is not None:
480-
handler_name, arguments = _extract_handler_data_from_params(
481-
handler_type, params
482-
)
483-
elif _is_v2_context(original_args):
484-
handler_name = "unknown"
485-
arguments = {}
486-
else:
487-
handler_name, arguments = _extract_handler_data_from_args(
488-
handler_type, original_args, original_kwargs
489-
)
490-
491-
span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config(
492-
handler_type, handler_name
493-
)
494-
495-
return (
496-
handler_name,
497-
arguments,
498-
span_data_key,
499-
span_name,
500-
mcp_method_name,
501-
result_data_key,
502-
)
503-
504-
505460
async def _tool_handler_wrapper(
506461
func: "Callable[..., Awaitable[Union[CallToolResult, InputRequiredResult]]]",
507462
original_args: "tuple[Any, ...]",
@@ -531,16 +486,20 @@ async def _tool_handler_wrapper(
531486
and isinstance(original_args[0], ServerRequestContext)
532487
):
533488
ctx = original_args[0]
534-
params = original_args[1] if len(original_args) > 1 else None
489+
if len(original_args) > 1:
490+
params = original_args[1]
491+
handler_name, arguments = _extract_handler_data_from_params("tool", params)
492+
else:
493+
handler_name = "unknown"
494+
arguments = {}
495+
else:
496+
handler_name, arguments = _extract_handler_data_from_args(
497+
"tool", original_args, original_kwargs
498+
)
535499

536-
(
537-
handler_name,
538-
arguments,
539-
span_data_key,
540-
span_name,
541-
mcp_method_name,
542-
result_data_key,
543-
) = _prepare_handler_data("tool", original_args, original_kwargs, params=params)
500+
span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config(
501+
"tool", handler_name
502+
)
544503

545504
scopes = _get_active_http_scopes(ctx=ctx)
546505

@@ -642,23 +601,27 @@ async def _prompt_handler_wrapper(
642601

643602
# Detect v1 vs v2: MCP SDK v2 passes (ServerRequestContext, params) to handlers
644603
ctx: "Optional[Any]" = None
645-
params: "Optional[Any]" = None
646604
if (
647605
ServerRequestContext is not None
648606
and original_args
649607
and isinstance(original_args[0], ServerRequestContext)
650608
):
651609
ctx = original_args[0]
652-
params = original_args[1] if len(original_args) > 1 else None
610+
if len(original_args) > 1:
611+
handler_name, arguments = _extract_handler_data_from_params(
612+
"prompt", original_args[1]
613+
)
614+
else:
615+
handler_name = "unknown"
616+
arguments = {}
617+
else:
618+
handler_name, arguments = _extract_handler_data_from_args(
619+
"prompt", original_args, original_kwargs
620+
)
653621

654-
(
655-
handler_name,
656-
arguments,
657-
span_data_key,
658-
span_name,
659-
mcp_method_name,
660-
result_data_key,
661-
) = _prepare_handler_data("prompt", original_args, original_kwargs, params=params)
622+
span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config(
623+
"prompt", handler_name
624+
)
662625

663626
scopes = _get_active_http_scopes(ctx=ctx)
664627

@@ -765,16 +728,22 @@ async def _resource_handler_wrapper(
765728
and isinstance(original_args[0], ServerRequestContext)
766729
):
767730
ctx = original_args[0]
768-
params = original_args[1] if len(original_args) > 1 else None
731+
if len(original_args) > 1:
732+
params = original_args[1]
733+
handler_name, arguments = _extract_handler_data_from_params(
734+
"resource", params
735+
)
736+
else:
737+
handler_name = "unknown"
738+
arguments = {}
739+
else:
740+
handler_name, arguments = _extract_handler_data_from_args(
741+
"resource", original_args, original_kwargs
742+
)
769743

770-
(
771-
handler_name,
772-
arguments,
773-
span_data_key,
774-
span_name,
775-
mcp_method_name,
776-
result_data_key,
777-
) = _prepare_handler_data("resource", original_args, original_kwargs, params=params)
744+
span_data_key, span_name, mcp_method_name, result_data_key = _get_span_config(
745+
"resource", handler_name
746+
)
778747

779748
scopes = _get_active_http_scopes(ctx=ctx)
780749

@@ -944,21 +913,6 @@ async def wrapper(*args: "Any") -> "Any":
944913
Server.read_resource = patched_read_resource # type: ignore[attr-defined]
945914

946915

947-
async def _sentry_middleware(
948-
ctx: "ServerRequestContext[Any, Any]", call_next: "CallNext"
949-
) -> "HandlerResult":
950-
if ctx.method == "tools/call":
951-
return await _tool_handler_wrapper(call_next, ctx)
952-
953-
if ctx.method == "prompts/get":
954-
return await _prompt_handler_wrapper(call_next, ctx)
955-
956-
if ctx.method == "resources/read":
957-
return await _resource_handler_wrapper(call_next, ctx)
958-
959-
return await call_next(ctx)
960-
961-
962916
def _patch_lowlevel_server_v2() -> None:
963917
"""Patches the v2 Server to wrap tool/prompt/resource handlers.
964918
@@ -971,11 +925,113 @@ def _patch_lowlevel_server_v2() -> None:
971925

972926
@wraps(original_init)
973927
def patched_init(self: "Server", *args: "Any", **kwargs: "Any") -> None:
928+
on_tool_call = kwargs.get("on_call_tool")
929+
if on_tool_call is not None and not getattr(
930+
on_tool_call, "__sentry_mcp_wrapped__", False
931+
):
932+
933+
@wraps(on_tool_call)
934+
async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
935+
return await _tool_handler_wrapper(
936+
on_tool_call, args, kwargs, force_await=False
937+
)
938+
939+
wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined]
940+
kwargs["on_call_tool"] = wrapper
941+
942+
on_get_prompt = kwargs.get("on_get_prompt")
943+
if on_get_prompt is not None and not getattr(
944+
on_get_prompt, "__sentry_mcp_wrapped__", False
945+
):
946+
947+
@wraps(on_get_prompt)
948+
async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
949+
return await _prompt_handler_wrapper(
950+
on_get_prompt, args, kwargs, force_await=False
951+
)
952+
953+
wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined]
954+
kwargs["on_get_prompt"] = wrapper
955+
956+
on_read_resource = kwargs.get("on_read_resource")
957+
if on_read_resource is not None and not getattr(
958+
on_read_resource, "__sentry_mcp_wrapped__", False
959+
):
960+
961+
@wraps(on_read_resource)
962+
async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
963+
return await _resource_handler_wrapper(
964+
on_read_resource, args, kwargs, force_await=False
965+
)
966+
967+
wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined]
968+
kwargs["on_read_resource"] = wrapper
969+
974970
original_init(self, *args, **kwargs)
975-
self.middleware.append(_sentry_middleware)
976971

977972
Server.__init__ = patched_init # type: ignore[method-assign]
978973

974+
original_add_request_handler = Server.add_request_handler
975+
976+
def patched_add_request_handler(
977+
self: "Server",
978+
method: str,
979+
params_type: "Any",
980+
handler: "Callable[..., Any]",
981+
*args: "Any",
982+
**kwargs: "Any",
983+
) -> None:
984+
if getattr(handler, "__sentry_mcp_wrapped__", False):
985+
return original_add_request_handler(
986+
self, method, params_type, handler, *args, **kwargs
987+
)
988+
989+
if method == "tools/call":
990+
991+
@wraps(handler)
992+
async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
993+
return await _tool_handler_wrapper(
994+
handler, args, kwargs, force_await=False
995+
)
996+
997+
wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined]
998+
999+
return original_add_request_handler(
1000+
self, method, params_type, wrapper, *args, **kwargs
1001+
)
1002+
if method == "prompts/get":
1003+
1004+
@wraps(handler)
1005+
async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
1006+
return await _prompt_handler_wrapper(
1007+
handler, args, kwargs, force_await=False
1008+
)
1009+
1010+
wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined]
1011+
1012+
return original_add_request_handler(
1013+
self, method, params_type, wrapper, *args, **kwargs
1014+
)
1015+
if method == "resources/read":
1016+
1017+
@wraps(handler)
1018+
async def wrapper(*args: "Any", **kwargs: "Any") -> "Any":
1019+
return await _resource_handler_wrapper(
1020+
handler, args, kwargs, force_await=False
1021+
)
1022+
1023+
wrapper.__sentry_mcp_wrapped__ = True # type: ignore[attr-defined]
1024+
1025+
return original_add_request_handler(
1026+
self, method, params_type, wrapper, *args, **kwargs
1027+
)
1028+
1029+
original_add_request_handler(
1030+
self, method, params_type, handler, *args, **kwargs
1031+
)
1032+
1033+
Server.add_request_handler = patched_add_request_handler # type: ignore[method-assign]
1034+
9791035

9801036
def _patch_handle_request() -> None:
9811037
original_handle_request = StreamableHTTPServerTransport.handle_request

0 commit comments

Comments
 (0)