@@ -79,15 +79,27 @@ async def main():
7979import anyio
8080import jsonschema
8181from anyio .streams .memory import MemoryObjectReceiveStream , MemoryObjectSendStream
82+ from starlette .applications import Starlette
83+ from starlette .middleware import Middleware
84+ from starlette .middleware .authentication import AuthenticationMiddleware
85+ from starlette .routing import Mount , Route
8286from typing_extensions import TypeVar
8387
8488import mcp .types as types
89+ from mcp .server .auth .middleware .auth_context import AuthContextMiddleware
90+ from mcp .server .auth .middleware .bearer_auth import BearerAuthBackend , RequireAuthMiddleware
91+ from mcp .server .auth .provider import OAuthAuthorizationServerProvider , TokenVerifier
92+ from mcp .server .auth .routes import build_resource_metadata_url , create_auth_routes , create_protected_resource_routes
93+ from mcp .server .auth .settings import AuthSettings
8594from mcp .server .experimental .request_context import Experimental
8695from mcp .server .lowlevel .experimental import ExperimentalHandlers
8796from mcp .server .lowlevel .func_inspection import create_call_wrapper
8897from mcp .server .lowlevel .helper_types import ReadResourceContents
8998from mcp .server .models import InitializationOptions
9099from mcp .server .session import ServerSession
100+ from mcp .server .streamable_http import EventStore , StreamableHTTPASGIApp
101+ from mcp .server .streamable_http_manager import StreamableHTTPSessionManager
102+ from mcp .server .transport_security import TransportSecuritySettings
91103from mcp .shared .context import RequestContext
92104from mcp .shared .exceptions import McpError , UrlElicitationRequiredError
93105from mcp .shared .message import ServerMessageMetadata , SessionMessage
@@ -162,6 +174,7 @@ def __init__(
162174 self .notification_handlers : dict [type , Callable [..., Awaitable [None ]]] = {}
163175 self ._tool_cache : dict [str , types .Tool ] = {}
164176 self ._experimental_handlers : ExperimentalHandlers | None = None
177+ self ._session_manager : StreamableHTTPSessionManager | None = None
165178 logger .debug ("Initializing server %r" , name )
166179
167180 def create_initialization_options (
@@ -258,6 +271,20 @@ def experimental(self) -> ExperimentalHandlers:
258271 self ._experimental_handlers = ExperimentalHandlers (self , self .request_handlers , self .notification_handlers )
259272 return self ._experimental_handlers
260273
274+ @property
275+ def session_manager (self ) -> StreamableHTTPSessionManager :
276+ """Get the StreamableHTTP session manager.
277+
278+ Raises:
279+ RuntimeError: If called before streamable_http_app() has been called.
280+ """
281+ if self ._session_manager is None :
282+ raise RuntimeError (
283+ "Session manager can only be accessed after calling streamable_http_app(). "
284+ "The session manager is created lazily to avoid unnecessary initialization."
285+ )
286+ return self ._session_manager
287+
261288 def list_prompts (self ):
262289 def decorator (
263290 func : Callable [[], Awaitable [list [types .Prompt ]]]
@@ -801,6 +828,118 @@ async def _handle_notification(self, notify: Any):
801828 except Exception : # pragma: no cover
802829 logger .exception ("Uncaught exception in notification handler" )
803830
831+ def streamable_http_app (
832+ self ,
833+ * ,
834+ streamable_http_path : str = "/mcp" ,
835+ json_response : bool = False ,
836+ stateless_http : bool = False ,
837+ event_store : EventStore | None = None ,
838+ retry_interval : int | None = None ,
839+ transport_security : TransportSecuritySettings | None = None ,
840+ host : str = "127.0.0.1" ,
841+ auth : AuthSettings | None = None ,
842+ token_verifier : TokenVerifier | None = None ,
843+ auth_server_provider : (OAuthAuthorizationServerProvider [Any , Any , Any ] | None ) = None ,
844+ custom_starlette_routes : list [Route ] | None = None ,
845+ debug : bool = False ,
846+ ) -> Starlette :
847+ """Return an instance of the StreamableHTTP server app."""
848+ # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6)
849+ if transport_security is None and host in ("127.0.0.1" , "localhost" , "::1" ):
850+ transport_security = TransportSecuritySettings (
851+ enable_dns_rebinding_protection = True ,
852+ allowed_hosts = ["127.0.0.1:*" , "localhost:*" , "[::1]:*" ],
853+ allowed_origins = ["http://127.0.0.1:*" , "http://localhost:*" , "http://[::1]:*" ],
854+ )
855+
856+ session_manager = StreamableHTTPSessionManager (
857+ app = self ,
858+ event_store = event_store ,
859+ retry_interval = retry_interval ,
860+ json_response = json_response ,
861+ stateless = stateless_http ,
862+ security_settings = transport_security ,
863+ )
864+ self ._session_manager = session_manager
865+
866+ # Create the ASGI handler
867+ streamable_http_app = StreamableHTTPASGIApp (session_manager )
868+
869+ # Create routes
870+ routes : list [Route | Mount ] = []
871+ middleware : list [Middleware ] = []
872+ required_scopes : list [str ] = []
873+
874+ # Set up auth if configured
875+ if auth : # pragma: no cover
876+ required_scopes = auth .required_scopes or []
877+
878+ # Add auth middleware if token verifier is available
879+ if token_verifier :
880+ middleware = [
881+ Middleware (
882+ AuthenticationMiddleware ,
883+ backend = BearerAuthBackend (token_verifier ),
884+ ),
885+ Middleware (AuthContextMiddleware ),
886+ ]
887+
888+ # Add auth endpoints if auth server provider is configured
889+ if auth_server_provider :
890+ routes .extend (
891+ create_auth_routes (
892+ provider = auth_server_provider ,
893+ issuer_url = auth .issuer_url ,
894+ service_documentation_url = auth .service_documentation_url ,
895+ client_registration_options = auth .client_registration_options ,
896+ revocation_options = auth .revocation_options ,
897+ )
898+ )
899+
900+ # Set up routes with or without auth
901+ if token_verifier : # pragma: no cover
902+ # Determine resource metadata URL
903+ resource_metadata_url = None
904+ if auth and auth .resource_server_url :
905+ # Build compliant metadata URL for WWW-Authenticate header
906+ resource_metadata_url = build_resource_metadata_url (auth .resource_server_url )
907+
908+ routes .append (
909+ Route (
910+ streamable_http_path ,
911+ endpoint = RequireAuthMiddleware (streamable_http_app , required_scopes , resource_metadata_url ),
912+ )
913+ )
914+ else :
915+ # Auth is disabled, no wrapper needed
916+ routes .append (
917+ Route (
918+ streamable_http_path ,
919+ endpoint = streamable_http_app ,
920+ )
921+ )
922+
923+ # Add protected resource metadata endpoint if configured as RS
924+ if auth and auth .resource_server_url : # pragma: no cover
925+ routes .extend (
926+ create_protected_resource_routes (
927+ resource_url = auth .resource_server_url ,
928+ authorization_servers = [auth .issuer_url ],
929+ scopes_supported = auth .required_scopes ,
930+ )
931+ )
932+
933+ if custom_starlette_routes :
934+ routes .extend (custom_starlette_routes )
935+
936+ return Starlette (
937+ debug = debug ,
938+ routes = routes ,
939+ middleware = middleware ,
940+ lifespan = lambda app : session_manager .run (),
941+ )
942+
804943
805944async def _ping_handler (request : types .PingRequest ) -> types .ServerResult :
806945 return types .ServerResult (types .EmptyResult ())
0 commit comments