@@ -36,17 +36,13 @@ async def main():
3636
3737from __future__ import annotations
3838
39- import contextvars
4039import logging
41- import warnings
4240from collections .abc import AsyncIterator , Awaitable , Callable
43- from contextlib import AbstractAsyncContextManager , AsyncExitStack , asynccontextmanager
41+ from contextlib import AbstractAsyncContextManager , asynccontextmanager
4442from dataclasses import dataclass
4543from importlib .metadata import version as importlib_version
46- from typing import Any , Generic , cast
44+ from typing import Any , Generic
4745
48- import anyio
49- from opentelemetry .trace import SpanKind , StatusCode
5046from pydantic import BaseModel
5147from starlette .applications import Starlette
5248from starlette .middleware import Middleware
@@ -61,18 +57,16 @@ async def main():
6157from mcp .server .auth .routes import build_resource_metadata_url , create_auth_routes , create_protected_resource_routes
6258from mcp .server .auth .settings import AuthSettings
6359from mcp .server .context import HandlerResult , ServerMiddleware , ServerRequestContext
64- from mcp .server .experimental .request_context import Experimental
6560from mcp .server .lowlevel .experimental import ExperimentalHandlers
6661from mcp .server .models import InitializationOptions
67- from mcp .server .session import ServerSession
62+ from mcp .server .runner import ServerRunner , otel_middleware
6863from mcp .server .streamable_http import EventStore
6964from mcp .server .streamable_http_manager import StreamableHTTPASGIApp , StreamableHTTPSessionManager
7065from mcp .server .transport_security import TransportSecuritySettings
71- from mcp .shared ._otel import extract_trace_context , otel_span
7266from mcp .shared ._stream_protocols import ReadStream , WriteStream
73- from mcp .shared .exceptions import MCPError
74- from mcp .shared .message import ServerMessageMetadata , SessionMessage
75- from mcp .shared .session import RequestResponder
67+ from mcp .shared .jsonrpc_dispatcher import JSONRPCDispatcher
68+ from mcp .shared .message import SessionMessage
69+ from mcp .shared .transport_context import TransportContext
7670
7771logger = logging .getLogger (__name__ )
7872
@@ -432,196 +426,23 @@ async def run(
432426 # the initialization lifecycle, but can do so with any available node
433427 # rather than requiring initialization for each connection.
434428 stateless : bool = False ,
435- ):
436- async with AsyncExitStack () as stack :
437- lifespan_context = await stack .enter_async_context (self .lifespan (self ))
438- session = await stack .enter_async_context (
439- ServerSession (
440- read_stream ,
441- write_stream ,
442- initialization_options ,
443- stateless = stateless ,
444- )
445- )
446-
447- # Configure task support for this session if enabled
448- task_support = self ._experimental_handlers .task_support if self ._experimental_handlers else None
449- if task_support is not None :
450- task_support .configure_session (session )
451- await stack .enter_async_context (task_support .run ())
452-
453- async with anyio .create_task_group () as tg :
454- try :
455- async for message in session .incoming_messages :
456- logger .debug ("Received message: %s" , message )
457-
458- if isinstance (message , RequestResponder ) and message .context is not None :
459- context = message .context
460- else :
461- context = contextvars .copy_context ()
462-
463- context .run (
464- tg .start_soon ,
465- self ._handle_message ,
466- message ,
467- session ,
468- lifespan_context ,
469- raise_exceptions ,
470- )
471- finally :
472- # Transport closed: cancel in-flight handlers. Without this the
473- # TG join waits for them, and when they eventually try to
474- # respond they hit a closed write stream (the session's
475- # _receive_loop closed it when the read stream ended).
476- tg .cancel_scope .cancel ()
477-
478- async def _handle_message (
479- self ,
480- message : RequestResponder [types .ClientRequest , types .ServerResult ] | types .ClientNotification | Exception ,
481- session : ServerSession ,
482- lifespan_context : LifespanResultT ,
483- raise_exceptions : bool = False ,
484- ):
485- with warnings .catch_warnings (record = True ) as w :
486- match message :
487- case RequestResponder () as responder :
488- with responder :
489- await self ._handle_request (
490- message , responder .request , session , lifespan_context , raise_exceptions
491- )
492- case Exception ():
493- logger .error (f"Received exception from stream: { message } " )
494- if raise_exceptions :
495- raise message
496- case _:
497- await self ._handle_notification (message , session , lifespan_context )
498-
499- for warning in w : # pragma: lax no cover
500- logger .info ("Warning: %s: %s" , warning .category .__name__ , warning .message )
501-
502- async def _handle_request (
503- self ,
504- message : RequestResponder [types .ClientRequest , types .ServerResult ],
505- req : types .ClientRequest ,
506- session : ServerSession ,
507- lifespan_context : LifespanResultT ,
508- raise_exceptions : bool ,
509- ):
510- logger .info ("Processing request of type %s" , type (req ).__name__ )
511-
512- target = getattr (req .params , "name" , None ) if req .params else None
513- span_name = f"MCP handle { req .method } { target } " if target else f"MCP handle { req .method } "
514-
515- # Extract W3C trace context from _meta (SEP-414).
516- meta = cast (dict [str , Any ] | None , getattr (req .params , "meta" , None )) if req .params else None
517- parent_context = extract_trace_context (meta ) if meta is not None else None
518-
519- with otel_span (
520- span_name ,
521- kind = SpanKind .SERVER ,
522- attributes = {"mcp.method.name" : req .method , "jsonrpc.request.id" : message .request_id },
523- context = parent_context ,
524- ) as span :
525- if entry := self ._request_handlers .get (req .method ):
526- handler = entry .handler
527- logger .debug ("Dispatching request of type %s" , type (req ).__name__ )
528-
529- try :
530- # Extract request context and close_sse_stream from message metadata
531- request_data = None
532- close_sse_stream_cb = None
533- close_standalone_sse_stream_cb = None
534- if message .message_metadata is not None and isinstance (
535- message .message_metadata , ServerMessageMetadata
536- ):
537- request_data = message .message_metadata .request_context
538- close_sse_stream_cb = message .message_metadata .close_sse_stream
539- close_standalone_sse_stream_cb = message .message_metadata .close_standalone_sse_stream
540-
541- client_capabilities = session .client_params .capabilities if session .client_params else None
542- task_support = self ._experimental_handlers .task_support if self ._experimental_handlers else None
543- # Get task metadata from request params if present
544- task_metadata = None
545- if hasattr (req , "params" ) and req .params is not None : # pragma: no branch
546- task_metadata = getattr (req .params , "task" , None )
547- ctx = ServerRequestContext (
548- request_id = message .request_id ,
549- meta = message .request_meta ,
550- session = session ,
551- lifespan_context = lifespan_context ,
552- experimental = Experimental (
553- task_metadata = task_metadata ,
554- _client_capabilities = client_capabilities ,
555- _session = session ,
556- _task_support = task_support ,
557- ),
558- request = request_data ,
559- close_sse_stream = close_sse_stream_cb ,
560- close_standalone_sse_stream = close_standalone_sse_stream_cb ,
561- )
562- response = await handler (ctx , req .params )
563- except MCPError as err :
564- response = err .error
565- except anyio .get_cancelled_exc_class ():
566- if message .cancelled :
567- # Client sent CancelledNotification; responder.cancel() already
568- # sent an error response, so skip the duplicate.
569- logger .info ("Request %s cancelled - duplicate response suppressed" , message .request_id )
570- return
571- # Transport-close cancellation from the TG in run(); re-raise so the
572- # TG swallows its own cancellation.
573- raise
574- except Exception as err :
575- if raise_exceptions : # pragma: no cover
576- raise err
577- response = types .ErrorData (code = 0 , message = str (err ))
578- else :
579- response = types .ErrorData (code = types .METHOD_NOT_FOUND , message = "Method not found" )
580-
581- if isinstance (response , types .ErrorData ) and span is not None :
582- span .set_status (StatusCode .ERROR , response .message )
583-
584- try :
585- # TODO: cast goes away when `_handle_request` is deleted.
586- await message .respond (cast (types .ServerResult | types .ErrorData , response ))
587- except (anyio .BrokenResourceError , anyio .ClosedResourceError ):
588- # Transport closed between handler unblocking and respond. Happens
589- # when _receive_loop's finally wakes a handler blocked on
590- # send_request: the handler runs to respond() before run()'s TG
591- # cancel fires, but after the write stream closed. Closed if our
592- # end closed (_receive_loop's async-with exit); Broken if the peer
593- # end closed first (streamable_http terminate()).
594- logger .debug ("Response for %s dropped - transport closed" , message .request_id )
595- return
596-
597- logger .debug ("Response sent" )
598-
599- async def _handle_notification (
600- self ,
601- notify : types .ClientNotification ,
602- session : ServerSession ,
603- lifespan_context : LifespanResultT ,
604429 ) -> None :
605- if entry := self ._notification_handlers .get (notify .method ):
606- handler = entry .handler
607- logger .debug ("Dispatching notification of type %s" , type (notify ).__name__ )
608-
609- try :
610- client_capabilities = session .client_params .capabilities if session .client_params else None
611- task_support = self ._experimental_handlers .task_support if self ._experimental_handlers else None
612- ctx = ServerRequestContext (
613- session = session ,
614- lifespan_context = lifespan_context ,
615- experimental = Experimental (
616- task_metadata = None ,
617- _client_capabilities = client_capabilities ,
618- _session = session ,
619- _task_support = task_support ,
620- ),
621- )
622- await handler (ctx , notify .params )
623- except Exception : # pragma: no cover
624- logger .exception ("Uncaught exception in notification handler" )
430+ async with self .lifespan (self ) as lifespan_context :
431+ dispatcher : JSONRPCDispatcher [TransportContext ] = JSONRPCDispatcher (
432+ read_stream ,
433+ write_stream ,
434+ raise_handler_exceptions = raise_exceptions ,
435+ )
436+ runner = ServerRunner (
437+ server = self ,
438+ dispatcher = dispatcher ,
439+ lifespan_state = lifespan_context ,
440+ init_options = initialization_options ,
441+ has_standalone_channel = True ,
442+ stateless = stateless ,
443+ dispatch_middleware = [otel_middleware ],
444+ )
445+ await runner .run ()
625446
626447 def streamable_http_app (
627448 self ,
0 commit comments