Skip to content

Commit efad41b

Browse files
committed
fix: improved low-level mcp instrumentation
1 parent 0e93d4c commit efad41b

File tree

2 files changed

+47
-17
lines changed

2 files changed

+47
-17
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -875,9 +875,7 @@ class OP:
875875
WEBSOCKET_SERVER = "websocket.server"
876876
SOCKET_CONNECTION = "socket.connection"
877877
SOCKET_DNS = "socket.dns"
878-
MCP_TOOL = "mcp.tool"
879-
MCP_PROMPT = "mcp.prompt"
880-
MCP_RESOURCE = "mcp.resource"
878+
MCP_SERVER = "mcp.server"
881879

882880

883881
# This type exists to trick mypy and PyCharm into thinking `init` and `Client`

sentry_sdk/integrations/mcp/lowlevel.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,49 @@
1212
from sentry_sdk.integrations.mcp import MCPIntegration
1313
from sentry_sdk.utils import safe_serialize
1414

15+
from mcp.server.lowlevel import Server
16+
from mcp.server.lowlevel.server import request_ctx
17+
18+
1519
if TYPE_CHECKING:
1620
from typing import Any, Callable
1721

1822

1923
def _get_span_config(handler_type, handler_name):
20-
# type: (str, str) -> tuple[str, str, str, str]
24+
# type: (str, str) -> tuple[str, str, str]
2125
"""
2226
Get span configuration based on handler type.
2327
2428
Returns:
25-
Tuple of (op, span_data_key, span_name, mcp_method_name)
29+
Tuple of (span_data_key, span_name, mcp_method_name)
2630
"""
2731
if handler_type == "tool":
28-
op = OP.MCP_TOOL
2932
span_data_key = SPANDATA.MCP_TOOL_NAME
3033
mcp_method_name = "tools/call"
3134
elif handler_type == "prompt":
32-
op = OP.MCP_PROMPT
3335
span_data_key = SPANDATA.MCP_PROMPT_NAME
3436
mcp_method_name = "prompts/get"
3537
else: # resource
36-
op = OP.MCP_RESOURCE
3738
span_data_key = SPANDATA.MCP_RESOURCE_URI
3839
mcp_method_name = "resources/read"
3940

4041
span_name = f"{handler_type} {handler_name}"
41-
return op, span_data_key, span_name, mcp_method_name
42+
return span_data_key, span_name, mcp_method_name
4243

4344

44-
def _set_span_data(span, handler_name, span_data_key, mcp_method_name, kwargs):
45-
# type: (Any, str, str, str, dict[str, Any]) -> None
45+
def _set_span_data(
46+
span, handler_name, span_data_key, mcp_method_name, kwargs, request_id=None
47+
):
48+
# type: (Any, str, str, str, dict[str, Any], str | None) -> None
4649
"""Set common span data for MCP handlers."""
4750
# Set handler identifier
4851
span.set_data(span_data_key, handler_name)
4952
span.set_data(SPANDATA.MCP_METHOD_NAME, mcp_method_name)
5053

54+
# Set request_id if provided
55+
if request_id:
56+
span.set_data(SPANDATA.MCP_REQUEST_ID, request_id)
57+
5158
# Set request arguments (excluding common request context objects)
5259
for k, v in kwargs.items():
5360
span.set_data(f"mcp.request.argument.{k}", safe_serialize(v))
@@ -66,7 +73,7 @@ def wrap_handler(original_handler, handler_type, handler_name):
6673
Returns:
6774
Wrapped handler function
6875
"""
69-
op, span_data_key, span_name, mcp_method_name = _get_span_config(
76+
span_data_key, span_name, mcp_method_name = _get_span_config(
7077
handler_type, handler_name
7178
)
7279

@@ -76,12 +83,25 @@ def wrap_handler(original_handler, handler_type, handler_name):
7683
async def async_wrapper(*args, **kwargs):
7784
# type: (*Any, **Any) -> Any
7885
with get_start_span_function()(
79-
op=op,
86+
op=OP.MCP_SERVER,
8087
name=span_name,
8188
origin=MCPIntegration.origin,
8289
) as span:
90+
# Extract request_id from RequestContext context variable
91+
request_id = None
92+
try:
93+
ctx = request_ctx.get()
94+
request_id = ctx.request_id
95+
except LookupError:
96+
# Not in a request context, request_id will remain None
97+
pass
8398
_set_span_data(
84-
span, handler_name, span_data_key, mcp_method_name, kwargs
99+
span,
100+
handler_name,
101+
span_data_key,
102+
mcp_method_name,
103+
kwargs,
104+
request_id,
85105
)
86106
try:
87107
return await original_handler(*args, **kwargs)
@@ -96,12 +116,25 @@ async def async_wrapper(*args, **kwargs):
96116
def sync_wrapper(*args, **kwargs):
97117
# type: (*Any, **Any) -> Any
98118
with get_start_span_function()(
99-
op=op,
119+
op=OP.MCP_SERVER,
100120
name=span_name,
101121
origin=MCPIntegration.origin,
102122
) as span:
123+
# Extract request_id from RequestContext context variable
124+
request_id = None
125+
try:
126+
ctx = request_ctx.get()
127+
request_id = ctx.request_id
128+
except LookupError:
129+
# Not in a request context, request_id will remain None
130+
pass
103131
_set_span_data(
104-
span, handler_name, span_data_key, mcp_method_name, kwargs
132+
span,
133+
handler_name,
134+
span_data_key,
135+
mcp_method_name,
136+
kwargs,
137+
request_id,
105138
)
106139
try:
107140
return original_handler(*args, **kwargs)
@@ -117,7 +150,6 @@ def patch_lowlevel_server():
117150
"""
118151
Patches the mcp.server.lowlevel.Server class to instrument handler execution.
119152
"""
120-
from mcp.server.lowlevel import Server
121153

122154
# Patch call_tool decorator
123155
original_call_tool = Server.call_tool

0 commit comments

Comments
 (0)