@@ -65,6 +65,7 @@ async def main():
6565from mcp .server .streamable_http import EventStore
6666from mcp .server .streamable_http_manager import StreamableHTTPASGIApp , StreamableHTTPSessionManager
6767from mcp .server .transport_security import TransportSecuritySettings
68+ from mcp .shared ._otel import otel_span
6869from mcp .shared ._stream_protocols import ReadStream , WriteStream
6970from mcp .shared .exceptions import MCPError
7071from mcp .shared .message import ServerMessageMetadata , SessionMessage
@@ -446,72 +447,82 @@ async def _handle_request(
446447 ):
447448 logger .info ("Processing request of type %s" , type (req ).__name__ )
448449
449- if handler := self . _request_handlers . get (req .method ):
450- logger . debug ( "Dispatching request of type %s" , type ( req ). __name__ )
450+ target = getattr (req .params , "name" , None ) if req . params else None
451+ span_name = f"MCP handle { req . method } { target } " if target else f"MCP handle { req . method } "
451452
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
453+ with otel_span (
454+ span_name ,
455+ kind = "SERVER" ,
456+ attributes = {"mcp.method.name" : req .method , "jsonrpc.request.id" : message .request_id },
457+ ):
458+ if handler := self ._request_handlers .get (req .method ):
459+ logger .debug ("Dispatching request of type %s" , type (req ).__name__ )
461460
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" )
461+ try :
462+ # Extract request context and close_sse_stream from message metadata
463+ request_data = None
464+ close_sse_stream_cb = None
465+ close_standalone_sse_stream_cb = None
466+ if message .message_metadata is not None and isinstance (
467+ message .message_metadata , ServerMessageMetadata
468+ ):
469+ request_data = message .message_metadata .request_context
470+ close_sse_stream_cb = message .message_metadata .close_sse_stream
471+ close_standalone_sse_stream_cb = message .message_metadata .close_standalone_sse_stream
472+
473+ client_capabilities = session .client_params .capabilities if session .client_params else None
474+ task_support = self ._experimental_handlers .task_support if self ._experimental_handlers else None
475+ # Get task metadata from request params if present
476+ task_metadata = None
477+ if hasattr (req , "params" ) and req .params is not None :
478+ task_metadata = getattr (req .params , "task" , None )
479+ ctx = ServerRequestContext (
480+ request_id = message .request_id ,
481+ meta = message .request_meta ,
482+ session = session ,
483+ lifespan_context = lifespan_context ,
484+ experimental = Experimental (
485+ task_metadata = task_metadata ,
486+ _client_capabilities = client_capabilities ,
487+ _session = session ,
488+ _task_support = task_support ,
489+ ),
490+ request = request_data ,
491+ close_sse_stream = close_sse_stream_cb ,
492+ close_standalone_sse_stream = close_standalone_sse_stream_cb ,
493+ )
494+ response = await handler (ctx , req .params )
495+ except MCPError as err :
496+ response = err .error
497+ except anyio .get_cancelled_exc_class ():
498+ if message .cancelled :
499+ # Client sent CancelledNotification; responder.cancel() already
500+ # sent an error response, so skip the duplicate.
501+ logger .info ("Request %s cancelled - duplicate response suppressed" , message .request_id )
502+ return
503+ # Transport-close cancellation from the TG in run(); re-raise so the
504+ # TG swallows its own cancellation.
505+ raise
506+ except Exception as err :
507+ if raise_exceptions : # pragma: no cover
508+ raise err
509+ response = types .ErrorData (code = 0 , message = str (err ))
510+ else : # pragma: no cover
511+ response = types .ErrorData (code = types .METHOD_NOT_FOUND , message = "Method not found" )
512+
513+ try :
514+ await message .respond (response )
515+ except (anyio .BrokenResourceError , anyio .ClosedResourceError ):
516+ # Transport closed between handler unblocking and respond. Happens
517+ # when _receive_loop's finally wakes a handler blocked on
518+ # send_request: the handler runs to respond() before run()'s TG
519+ # cancel fires, but after the write stream closed. Closed if our
520+ # end closed (_receive_loop's async-with exit); Broken if the peer
521+ # end closed first (streamable_http terminate()).
522+ logger .debug ("Response for %s dropped - transport closed" , message .request_id )
523+ return
524+
525+ logger .debug ("Response sent" )
515526
516527 async def _handle_notification (
517528 self ,
0 commit comments