@@ -42,9 +42,10 @@ async def main():
4242from collections .abc import AsyncIterator , Awaitable , Callable
4343from contextlib import AbstractAsyncContextManager , AsyncExitStack , asynccontextmanager
4444from importlib .metadata import version as importlib_version
45- from typing import Any , Generic
45+ from typing import Any , Generic , cast
4646
4747import anyio
48+ from opentelemetry .trace import SpanKind , StatusCode
4849from starlette .applications import Starlette
4950from starlette .middleware import Middleware
5051from starlette .middleware .authentication import AuthenticationMiddleware
@@ -65,6 +66,7 @@ async def main():
6566from mcp .server .streamable_http import EventStore
6667from mcp .server .streamable_http_manager import StreamableHTTPASGIApp , StreamableHTTPSessionManager
6768from mcp .server .transport_security import TransportSecuritySettings
69+ from mcp .shared ._otel import extract_trace_context , otel_span
6870from mcp .shared ._stream_protocols import ReadStream , WriteStream
6971from mcp .shared .exceptions import MCPError
7072from mcp .shared .message import ServerMessageMetadata , SessionMessage
@@ -446,72 +448,90 @@ async def _handle_request(
446448 ):
447449 logger .info ("Processing request of type %s" , type (req ).__name__ )
448450
449- if handler := self . _request_handlers . get (req .method ):
450- logger . debug ( "Dispatching request of type %s" , type ( req ). __name__ )
451+ target = getattr (req .params , "name" , None ) if req . params else None
452+ span_name = f"MCP handle { req . method } { target } " if target else f"MCP handle { req . method } "
451453
452- try :
453- # Extract request context and close_sse_stream from message metadata
454- request_data = None
455- close_sse_stream_cb = None
456- close_standalone_sse_stream_cb = None
457- if message .message_metadata is not None and isinstance (message .message_metadata , ServerMessageMetadata ):
458- request_data = message .message_metadata .request_context
459- close_sse_stream_cb = message .message_metadata .close_sse_stream
460- close_standalone_sse_stream_cb = message .message_metadata .close_standalone_sse_stream
454+ # Extract W3C trace context from _meta (SEP-414).
455+ meta = cast (dict [str , Any ] | None , getattr (req .params , "meta" , None )) if req .params else None
456+ parent_context = extract_trace_context (meta ) if meta is not None else None
461457
462- client_capabilities = session .client_params .capabilities if session .client_params else None
463- task_support = self ._experimental_handlers .task_support if self ._experimental_handlers else None
464- # Get task metadata from request params if present
465- task_metadata = None
466- if hasattr (req , "params" ) and req .params is not None :
467- task_metadata = getattr (req .params , "task" , None )
468- ctx = ServerRequestContext (
469- request_id = message .request_id ,
470- meta = message .request_meta ,
471- session = session ,
472- lifespan_context = lifespan_context ,
473- experimental = Experimental (
474- task_metadata = task_metadata ,
475- _client_capabilities = client_capabilities ,
476- _session = session ,
477- _task_support = task_support ,
478- ),
479- request = request_data ,
480- close_sse_stream = close_sse_stream_cb ,
481- close_standalone_sse_stream = close_standalone_sse_stream_cb ,
482- )
483- response = await handler (ctx , req .params )
484- except MCPError as err :
485- response = err .error
486- except anyio .get_cancelled_exc_class ():
487- if message .cancelled :
488- # Client sent CancelledNotification; responder.cancel() already
489- # sent an error response, so skip the duplicate.
490- logger .info ("Request %s cancelled - duplicate response suppressed" , message .request_id )
491- return
492- # Transport-close cancellation from the TG in run(); re-raise so the
493- # TG swallows its own cancellation.
494- raise
495- except Exception as err :
496- if raise_exceptions : # pragma: no cover
497- raise err
498- response = types .ErrorData (code = 0 , message = str (err ))
499- else : # pragma: no cover
500- response = types .ErrorData (code = types .METHOD_NOT_FOUND , message = "Method not found" )
501-
502- try :
503- await message .respond (response )
504- except (anyio .BrokenResourceError , anyio .ClosedResourceError ):
505- # Transport closed between handler unblocking and respond. Happens
506- # when _receive_loop's finally wakes a handler blocked on
507- # send_request: the handler runs to respond() before run()'s TG
508- # cancel fires, but after the write stream closed. Closed if our
509- # end closed (_receive_loop's async-with exit); Broken if the peer
510- # end closed first (streamable_http terminate()).
511- logger .debug ("Response for %s dropped - transport closed" , message .request_id )
512- return
513-
514- logger .debug ("Response sent" )
458+ with otel_span (
459+ span_name ,
460+ kind = SpanKind .SERVER ,
461+ attributes = {"mcp.method.name" : req .method , "jsonrpc.request.id" : message .request_id },
462+ context = parent_context ,
463+ ) as span :
464+ if handler := self ._request_handlers .get (req .method ):
465+ logger .debug ("Dispatching request of type %s" , type (req ).__name__ )
466+
467+ try :
468+ # Extract request context and close_sse_stream from message metadata
469+ request_data = None
470+ close_sse_stream_cb = None
471+ close_standalone_sse_stream_cb = None
472+ if message .message_metadata is not None and isinstance (
473+ message .message_metadata , ServerMessageMetadata
474+ ):
475+ request_data = message .message_metadata .request_context
476+ close_sse_stream_cb = message .message_metadata .close_sse_stream
477+ close_standalone_sse_stream_cb = message .message_metadata .close_standalone_sse_stream
478+
479+ client_capabilities = session .client_params .capabilities if session .client_params else None
480+ task_support = self ._experimental_handlers .task_support if self ._experimental_handlers else None
481+ # Get task metadata from request params if present
482+ task_metadata = None
483+ if hasattr (req , "params" ) and req .params is not None : # pragma: no branch
484+ task_metadata = getattr (req .params , "task" , None )
485+ ctx = ServerRequestContext (
486+ request_id = message .request_id ,
487+ meta = message .request_meta ,
488+ session = session ,
489+ lifespan_context = lifespan_context ,
490+ experimental = Experimental (
491+ task_metadata = task_metadata ,
492+ _client_capabilities = client_capabilities ,
493+ _session = session ,
494+ _task_support = task_support ,
495+ ),
496+ request = request_data ,
497+ close_sse_stream = close_sse_stream_cb ,
498+ close_standalone_sse_stream = close_standalone_sse_stream_cb ,
499+ )
500+ response = await handler (ctx , req .params )
501+ except MCPError as err :
502+ response = err .error
503+ except anyio .get_cancelled_exc_class ():
504+ if message .cancelled :
505+ # Client sent CancelledNotification; responder.cancel() already
506+ # sent an error response, so skip the duplicate.
507+ logger .info ("Request %s cancelled - duplicate response suppressed" , message .request_id )
508+ return
509+ # Transport-close cancellation from the TG in run(); re-raise so the
510+ # TG swallows its own cancellation.
511+ raise
512+ except Exception as err :
513+ if raise_exceptions : # pragma: no cover
514+ raise err
515+ response = types .ErrorData (code = 0 , message = str (err ))
516+ else : # pragma: no cover
517+ response = types .ErrorData (code = types .METHOD_NOT_FOUND , message = "Method not found" )
518+
519+ if isinstance (response , types .ErrorData ) and span is not None :
520+ span .set_status (StatusCode .ERROR , response .message )
521+
522+ try :
523+ await message .respond (response )
524+ except (anyio .BrokenResourceError , anyio .ClosedResourceError ):
525+ # Transport closed between handler unblocking and respond. Happens
526+ # when _receive_loop's finally wakes a handler blocked on
527+ # send_request: the handler runs to respond() before run()'s TG
528+ # cancel fires, but after the write stream closed. Closed if our
529+ # end closed (_receive_loop's async-with exit); Broken if the peer
530+ # end closed first (streamable_http terminate()).
531+ logger .debug ("Response for %s dropped - transport closed" , message .request_id )
532+ return
533+
534+ logger .debug ("Response sent" )
515535
516536 async def _handle_notification (
517537 self ,
0 commit comments