1616import httpx
1717from httpx_sse import ServerSentEvent , aconnect_sse
1818from starlette .applications import Starlette
19+ from starlette .middleware import Middleware
20+ from starlette .middleware .authentication import AuthenticationMiddleware
1921from starlette .requests import Request
2022from starlette .responses import Response
2123from starlette .routing import Mount , Route
2426from mcp .client .sse import sse_client
2527from mcp .client .streamable_http import streamable_http_client
2628from mcp .server import Server
27- from mcp .server .auth .provider import OAuthAuthorizationServerProvider , TokenVerifier
29+ from mcp .server .auth .middleware .auth_context import AuthContextMiddleware
30+ from mcp .server .auth .middleware .bearer_auth import BearerAuthBackend , RequireAuthMiddleware
31+ from mcp .server .auth .provider import OAuthAuthorizationServerProvider , ProviderTokenVerifier , TokenVerifier
32+ from mcp .server .auth .routes import build_resource_metadata_url , create_auth_routes , create_protected_resource_routes
2833from mcp .server .auth .settings import AuthSettings
2934from mcp .server .fastmcp import FastMCP
3035from mcp .server .fastmcp .server import StreamableHTTPASGIApp
@@ -139,6 +144,9 @@ def build_streamable_http_app(
139144 `/mcp` is mounted via `Route(path, endpoint=<class instance>)` with no `methods=`, exactly
140145 as FastMCP does — Starlette treats a class-instance endpoint as raw ASGI and matches all
141146 verbs, which is what the transport requires.
147+
148+ Unlike `FastMCP.__init__`, this does not enforce `auth_server_provider` XOR
149+ `token_verifier`; the AS-handler tests pass both.
142150 """
143151 manager = StreamableHTTPSessionManager (
144152 app = _lowlevel (server ),
@@ -150,14 +158,55 @@ def build_streamable_http_app(
150158 )
151159 asgi = StreamableHTTPASGIApp (manager )
152160
161+ # FastMCP derives a verifier from the provider at construction time when no explicit verifier
162+ # is given (mcp/server/fastmcp/server.py:230); the harness has no construction step, so the
163+ # same derivation runs here so the gating below sees the same verifier FastMCP would.
164+ verifier = token_verifier
165+ if auth_server_provider is not None and token_verifier is None :
166+ verifier = ProviderTokenVerifier (auth_server_provider )
167+
153168 routes : list [Route ] = []
154- # Auth routing (middleware, AS routes, RequireAuthMiddleware wrap, PRM routes) is added in
155- # H5; until then `auth` / `token_verifier` / `auth_server_provider` are accepted but ignored
156- # so callers that don't pass them work today.
157- assert auth is None and token_verifier is None and auth_server_provider is None , "auth branch lands in H5"
158- routes .append (Route ("/mcp" , endpoint = asgi ))
169+ middleware : list [Middleware ] = []
170+ required_scopes : list [str ] = []
171+
172+ if auth is not None :
173+ required_scopes = auth .required_scopes or []
174+ if verifier is not None :
175+ middleware = [
176+ Middleware (AuthenticationMiddleware , backend = BearerAuthBackend (verifier )),
177+ Middleware (AuthContextMiddleware ),
178+ ]
179+ if auth_server_provider is not None :
180+ routes .extend (
181+ create_auth_routes (
182+ provider = auth_server_provider ,
183+ issuer_url = auth .issuer_url ,
184+ service_documentation_url = auth .service_documentation_url ,
185+ client_registration_options = auth .client_registration_options ,
186+ revocation_options = auth .revocation_options ,
187+ )
188+ )
189+
190+ if verifier is not None :
191+ resource_metadata_url = (
192+ build_resource_metadata_url (auth .resource_server_url )
193+ if auth is not None and auth .resource_server_url
194+ else None
195+ )
196+ routes .append (Route ("/mcp" , endpoint = RequireAuthMiddleware (asgi , required_scopes , resource_metadata_url )))
197+ else :
198+ routes .append (Route ("/mcp" , endpoint = asgi ))
199+
200+ if auth is not None and auth .resource_server_url :
201+ routes .extend (
202+ create_protected_resource_routes (
203+ resource_url = auth .resource_server_url ,
204+ authorization_servers = [auth .issuer_url ],
205+ scopes_supported = auth .required_scopes ,
206+ )
207+ )
159208
160- return Starlette (routes = routes ), manager
209+ return Starlette (routes = routes , middleware = middleware ), manager
161210
162211
163212@asynccontextmanager
@@ -230,15 +279,15 @@ async def mounted_app(
230279 Yields the httpx client (rooted at the in-process origin) and the live session manager. Tests
231280 use this in two ways: for raw-httpx assertions (status codes, headers, SSE bytes) the test
232281 speaks HTTP through the yielded client directly; for client-driven assertions the test wraps
233- that client in `client_via_http(http)`, which lets several `Client `s share the one mounted
234- session manager. `on_request` records every outgoing HTTP request before it leaves the
282+ that client in `client_via_http(http)`, which lets several `ClientSession `s share the one
283+ mounted session manager. `on_request` records every outgoing HTTP request before it leaves the
235284 yielded client.
236285
237286 DNS-rebinding protection is disabled by default; pass explicit settings (or `None` for the
238287 localhost auto-enable behaviour) to test the protection itself.
239288 """
240- lowlevel = server . _lowlevel_server if isinstance ( server , MCPServer ) else server # noqa: F821 -- body rewritten in H5
241- app = lowlevel . streamable_http_app (
289+ app , manager = build_streamable_http_app (
290+ server ,
242291 stateless_http = stateless_http ,
243292 json_response = json_response ,
244293 event_store = event_store ,
@@ -250,12 +299,12 @@ async def mounted_app(
250299 )
251300 event_hooks = {"request" : [on_request ]} if on_request is not None else None
252301 async with (
253- server . session_manager .run (),
302+ manager .run (),
254303 httpx .AsyncClient (
255304 transport = StreamingASGITransport (app ), base_url = BASE_URL , event_hooks = event_hooks , headers = headers
256305 ) as http_client ,
257306 ):
258- yield http_client , server . session_manager
307+ yield http_client , manager
259308
260309
261310@asynccontextmanager
0 commit comments