Skip to content

Commit 7f0f463

Browse files
authored
feat(mcp): Migrate to span first (#6131)
Add span streaming support to the MCP integration. Refs PY-2340 Fixes #6038
1 parent bb7865d commit 7f0f463

2 files changed

Lines changed: 824 additions & 486 deletions

File tree

sentry_sdk/integrations/mcp.py

Lines changed: 63 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
from typing import TYPE_CHECKING
1313

1414
import sentry_sdk
15-
from sentry_sdk.ai.utils import get_start_span_function
15+
from sentry_sdk.ai.utils import _set_span_data_attribute, get_start_span_function
1616
from sentry_sdk.consts import OP, SPANDATA
1717
from sentry_sdk.integrations import Integration, DidNotEnable
18+
from sentry_sdk.traces import StreamedSpan
19+
from sentry_sdk.tracing_utils import has_span_streaming_enabled
1820
from sentry_sdk.utils import safe_serialize
1921
from sentry_sdk.scope import should_send_default_pii
2022
from sentry_sdk.integrations._wsgi_common import nullcontext
@@ -33,8 +35,10 @@
3335

3436

3537
if TYPE_CHECKING:
36-
from typing import Any, Callable, Optional, Tuple, ContextManager
38+
from typing import Any, Callable, Optional, Tuple, Union, ContextManager
3739

40+
from sentry_sdk.tracing import Span
41+
from sentry_sdk.traces import StreamedSpan
3842
from starlette.types import Receive, Scope, Send # type: ignore[import-not-found]
3943

4044

@@ -156,7 +160,7 @@ def _get_span_config(
156160

157161

158162
def _set_span_input_data(
159-
span: "Any",
163+
span: "Union[StreamedSpan, Span]",
160164
handler_name: str,
161165
span_data_key: str,
162166
mcp_method_name: str,
@@ -168,26 +172,28 @@ def _set_span_input_data(
168172
"""Set input span data for MCP handlers."""
169173

170174
# Set handler identifier
171-
span.set_data(span_data_key, handler_name)
172-
span.set_data(SPANDATA.MCP_METHOD_NAME, mcp_method_name)
175+
_set_span_data_attribute(span, span_data_key, handler_name)
176+
_set_span_data_attribute(span, SPANDATA.MCP_METHOD_NAME, mcp_method_name)
173177

174178
# Set transport/MCP transport type
175-
span.set_data(
176-
SPANDATA.NETWORK_TRANSPORT, "pipe" if mcp_transport == "stdio" else "tcp"
179+
_set_span_data_attribute(
180+
span,
181+
SPANDATA.NETWORK_TRANSPORT,
182+
"pipe" if mcp_transport == "stdio" else "tcp",
177183
)
178-
span.set_data(SPANDATA.MCP_TRANSPORT, mcp_transport)
184+
_set_span_data_attribute(span, SPANDATA.MCP_TRANSPORT, mcp_transport)
179185

180186
# Set request_id if provided
181187
if request_id:
182-
span.set_data(SPANDATA.MCP_REQUEST_ID, request_id)
188+
_set_span_data_attribute(span, SPANDATA.MCP_REQUEST_ID, request_id)
183189

184190
# Set session_id if provided
185191
if session_id:
186-
span.set_data(SPANDATA.MCP_SESSION_ID, session_id)
192+
_set_span_data_attribute(span, SPANDATA.MCP_SESSION_ID, session_id)
187193

188194
# Set request arguments (excluding common request context objects)
189195
for k, v in arguments.items():
190-
span.set_data(f"mcp.request.argument.{k}", safe_serialize(v))
196+
_set_span_data_attribute(span, f"mcp.request.argument.{k}", safe_serialize(v))
191197

192198

193199
def _extract_tool_result_content(result: "Any") -> "Any":
@@ -231,7 +237,10 @@ def _extract_tool_result_content(result: "Any") -> "Any":
231237

232238

233239
def _set_span_output_data(
234-
span: "Any", result: "Any", result_data_key: "Optional[str]", handler_type: str
240+
span: "Union[StreamedSpan, Span]",
241+
result: "Any",
242+
result_data_key: "Optional[str]",
243+
handler_type: str,
235244
) -> None:
236245
"""Set output span data for MCP handlers."""
237246
if result is None:
@@ -248,11 +257,17 @@ def _set_span_output_data(
248257
# For tools, extract the meaningful content
249258
if handler_type == "tool":
250259
extracted = _extract_tool_result_content(result)
251-
if extracted is not None and should_include_data:
252-
span.set_data(result_data_key, safe_serialize(extracted))
260+
if (
261+
extracted is not None
262+
and should_include_data
263+
and result_data_key is not None
264+
):
265+
_set_span_data_attribute(span, result_data_key, safe_serialize(extracted))
253266
# Set content count if result is a dict
254267
if isinstance(extracted, dict):
255-
span.set_data(SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted))
268+
_set_span_data_attribute(
269+
span, SPANDATA.MCP_TOOL_RESULT_CONTENT_COUNT, len(extracted)
270+
)
256271
elif handler_type == "prompt":
257272
# For prompts, count messages and set role/content only for single-message prompts
258273
try:
@@ -270,7 +285,9 @@ def _set_span_output_data(
270285

271286
# Always set message count if we found messages
272287
if message_count > 0:
273-
span.set_data(SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count)
288+
_set_span_data_attribute(
289+
span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_COUNT, message_count
290+
)
274291

275292
# Only set role and content for single-message prompts if PII is allowed
276293
if message_count == 1 and should_include_data and messages:
@@ -283,7 +300,9 @@ def _set_span_output_data(
283300
role = first_message["role"]
284301

285302
if role:
286-
span.set_data(SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role)
303+
_set_span_data_attribute(
304+
span, SPANDATA.MCP_PROMPT_RESULT_MESSAGE_ROLE, role
305+
)
287306

288307
# Extract content text
289308
content_text = None
@@ -303,8 +322,8 @@ def _set_span_output_data(
303322
elif isinstance(msg_content, str):
304323
content_text = msg_content
305324

306-
if content_text:
307-
span.set_data(result_data_key, content_text)
325+
if content_text and result_data_key is not None:
326+
_set_span_data_attribute(span, result_data_key, content_text)
308327
except Exception:
309328
# Silently ignore if we can't extract message info
310329
pass
@@ -434,14 +453,28 @@ async def _handler_wrapper(
434453
# Get request ID, session ID, and transport from context
435454
request_id, session_id, mcp_transport = _get_request_context_data()
436455

456+
span_streaming = has_span_streaming_enabled(sentry_sdk.get_client().options)
457+
437458
# Start span and execute
438459
with isolation_scope_context:
439460
with current_scope_context:
440-
with get_start_span_function()(
441-
op=OP.MCP_SERVER,
442-
name=span_name,
443-
origin=MCPIntegration.origin,
444-
) as span:
461+
span_mgr: "Union[Span, StreamedSpan]"
462+
if span_streaming:
463+
span_mgr = sentry_sdk.traces.start_span(
464+
name=span_name,
465+
attributes={
466+
"sentry.op": OP.MCP_SERVER,
467+
"sentry.origin": MCPIntegration.origin,
468+
},
469+
)
470+
else:
471+
span_mgr = get_start_span_function()(
472+
op=OP.MCP_SERVER,
473+
name=span_name,
474+
origin=MCPIntegration.origin,
475+
)
476+
477+
with span_mgr as span:
445478
# Set input span data
446479
_set_span_input_data(
447480
span,
@@ -467,7 +500,9 @@ async def _handler_wrapper(
467500
elif handler_name and "://" in handler_name:
468501
protocol = handler_name.split("://")[0]
469502
if protocol:
470-
span.set_data(SPANDATA.MCP_RESOURCE_PROTOCOL, protocol)
503+
_set_span_data_attribute(
504+
span, SPANDATA.MCP_RESOURCE_PROTOCOL, protocol
505+
)
471506

472507
try:
473508
# Execute the async handler
@@ -481,7 +516,9 @@ async def _handler_wrapper(
481516
except Exception as e:
482517
# Set error flag for tools
483518
if handler_type == "tool":
484-
span.set_data(SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True)
519+
_set_span_data_attribute(
520+
span, SPANDATA.MCP_TOOL_RESULT_IS_ERROR, True
521+
)
485522
sentry_sdk.capture_exception(e)
486523
raise
487524

0 commit comments

Comments
 (0)