@@ -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
101+ from mcp .server .streamable_http_manager import StreamableHTTPASGIApp , 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 : # pragma: no cover
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 # pragma: no cover
287+
261288 def list_prompts (self ):
262289 def decorator (
263290 func : Callable [[], Awaitable [list [types .Prompt ]]]
@@ -791,6 +818,118 @@ async def _handle_notification(self, notify: Any):
791818 except Exception : # pragma: no cover
792819 logger .exception ("Uncaught exception in notification handler" )
793820
821+ def streamable_http_app (
822+ self ,
823+ * ,
824+ streamable_http_path : str = "/mcp" ,
825+ json_response : bool = False ,
826+ stateless_http : bool = False ,
827+ event_store : EventStore | None = None ,
828+ retry_interval : int | None = None ,
829+ transport_security : TransportSecuritySettings | None = None ,
830+ host : str = "127.0.0.1" ,
831+ auth : AuthSettings | None = None ,
832+ token_verifier : TokenVerifier | None = None ,
833+ auth_server_provider : (OAuthAuthorizationServerProvider [Any , Any , Any ] | None ) = None ,
834+ custom_starlette_routes : list [Route ] | None = None ,
835+ debug : bool = False ,
836+ ) -> Starlette :
837+ """Return an instance of the StreamableHTTP server app."""
838+ # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6)
839+ if transport_security is None and host in ("127.0.0.1" , "localhost" , "::1" ):
840+ transport_security = TransportSecuritySettings (
841+ enable_dns_rebinding_protection = True ,
842+ allowed_hosts = ["127.0.0.1:*" , "localhost:*" , "[::1]:*" ],
843+ allowed_origins = ["http://127.0.0.1:*" , "http://localhost:*" , "http://[::1]:*" ],
844+ )
845+
846+ session_manager = StreamableHTTPSessionManager (
847+ app = self ,
848+ event_store = event_store ,
849+ retry_interval = retry_interval ,
850+ json_response = json_response ,
851+ stateless = stateless_http ,
852+ security_settings = transport_security ,
853+ )
854+ self ._session_manager = session_manager
855+
856+ # Create the ASGI handler
857+ streamable_http_app = StreamableHTTPASGIApp (session_manager )
858+
859+ # Create routes
860+ routes : list [Route | Mount ] = []
861+ middleware : list [Middleware ] = []
862+ required_scopes : list [str ] = []
863+
864+ # Set up auth if configured
865+ if auth : # pragma: no cover
866+ required_scopes = auth .required_scopes or []
867+
868+ # Add auth middleware if token verifier is available
869+ if token_verifier :
870+ middleware = [
871+ Middleware (
872+ AuthenticationMiddleware ,
873+ backend = BearerAuthBackend (token_verifier ),
874+ ),
875+ Middleware (AuthContextMiddleware ),
876+ ]
877+
878+ # Add auth endpoints if auth server provider is configured
879+ if auth_server_provider :
880+ routes .extend (
881+ create_auth_routes (
882+ provider = auth_server_provider ,
883+ issuer_url = auth .issuer_url ,
884+ service_documentation_url = auth .service_documentation_url ,
885+ client_registration_options = auth .client_registration_options ,
886+ revocation_options = auth .revocation_options ,
887+ )
888+ )
889+
890+ # Set up routes with or without auth
891+ if token_verifier : # pragma: no cover
892+ # Determine resource metadata URL
893+ resource_metadata_url = None
894+ if auth and auth .resource_server_url :
895+ # Build compliant metadata URL for WWW-Authenticate header
896+ resource_metadata_url = build_resource_metadata_url (auth .resource_server_url )
897+
898+ routes .append (
899+ Route (
900+ streamable_http_path ,
901+ endpoint = RequireAuthMiddleware (streamable_http_app , required_scopes , resource_metadata_url ),
902+ )
903+ )
904+ else :
905+ # Auth is disabled, no wrapper needed
906+ routes .append (
907+ Route (
908+ streamable_http_path ,
909+ endpoint = streamable_http_app ,
910+ )
911+ )
912+
913+ # Add protected resource metadata endpoint if configured as RS
914+ if auth and auth .resource_server_url : # pragma: no cover
915+ routes .extend (
916+ create_protected_resource_routes (
917+ resource_url = auth .resource_server_url ,
918+ authorization_servers = [auth .issuer_url ],
919+ scopes_supported = auth .required_scopes ,
920+ )
921+ )
922+
923+ if custom_starlette_routes : # pragma: no cover
924+ routes .extend (custom_starlette_routes )
925+
926+ return Starlette (
927+ debug = debug ,
928+ routes = routes ,
929+ middleware = middleware ,
930+ lifespan = lambda app : session_manager .run (),
931+ )
932+
794933
795934async def _ping_handler (request : types .PingRequest ) -> types .ServerResult :
796935 return types .EmptyResult ()
0 commit comments