Improve OAuth protected resource metadata URL construction per RFC 97… · modelcontextprotocol/python-sdk@df3e428 · GitHub
Skip to content

Commit df3e428

Browse files
authored
Improve OAuth protected resource metadata URL construction per RFC 9728 (#1407)
1 parent 814c9c0 commit df3e428

4 files changed

Lines changed: 192 additions & 32 deletions

File tree

‎examples/servers/simple-auth/mcp_simple_auth/server.py‎

Lines changed: 2 additions & 2 deletions

‎src/mcp/server/auth/routes.py‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from collections.abc import Awaitable, Callable
22
from typing import Any
3+
from urllib.parse import urlparse
34

45
from pydantic import AnyHttpUrl
56
from starlette.middleware.cors import CORSMiddleware
@@ -186,6 +187,25 @@ def build_metadata(
186187
return metadata
187188

188189

190+
def build_resource_metadata_url(resource_server_url: AnyHttpUrl) -> AnyHttpUrl:
191+
"""
192+
Build RFC 9728 compliant protected resource metadata URL.
193+
194+
Inserts /.well-known/oauth-protected-resource between host and resource path
195+
as specified in RFC 9728 §3.1.
196+
197+
Args:
198+
resource_server_url: The resource server URL (e.g., https://example.com/mcp)
199+
200+
Returns:
201+
The metadata URL (e.g., https://example.com/.well-known/oauth-protected-resource/mcp)
202+
"""
203+
parsed = urlparse(str(resource_server_url))
204+
# Handle trailing slash: if path is just "/", treat as empty
205+
resource_path = parsed.path if parsed.path != "/" else ""
206+
return AnyHttpUrl(f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}")
207+
208+
189209
def create_protected_resource_routes(
190210
resource_url: AnyHttpUrl,
191211
authorization_servers: list[AnyHttpUrl],
@@ -218,9 +238,15 @@ def create_protected_resource_routes(
218238

219239
handler = ProtectedResourceMetadataHandler(metadata)
220240

241+
# RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path
242+
metadata_url = build_resource_metadata_url(resource_url)
243+
# Extract just the path part for route registration
244+
parsed = urlparse(str(metadata_url))
245+
well_known_path = parsed.path
246+
221247
return [
222248
Route(
223-
"/.well-known/oauth-protected-resource",
249+
well_known_path,
224250
endpoint=cors_middleware(handler.handle, ["GET", "OPTIONS"]),
225251
methods=["GET", "OPTIONS"],
226252
)

‎src/mcp/server/fastmcp/server.py‎

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -825,11 +825,10 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send):
825825
# Determine resource metadata URL
826826
resource_metadata_url = None
827827
if self.settings.auth and self.settings.auth.resource_server_url:
828-
from pydantic import AnyHttpUrl
828+
from mcp.server.auth.routes import build_resource_metadata_url
829829

830-
resource_metadata_url = AnyHttpUrl(
831-
str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
832-
)
830+
# Build compliant metadata URL for WWW-Authenticate header
831+
resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url)
833832

834833
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
835834
routes.append(
@@ -938,11 +937,10 @@ def streamable_http_app(self) -> Starlette:
938937
# Determine resource metadata URL
939938
resource_metadata_url = None
940939
if self.settings.auth and self.settings.auth.resource_server_url:
941-
from pydantic import AnyHttpUrl
940+
from mcp.server.auth.routes import build_resource_metadata_url
942941

943-
resource_metadata_url = AnyHttpUrl(
944-
str(self.settings.auth.resource_server_url).rstrip("/") + "/.well-known/oauth-protected-resource"
945-
)
942+
# Build compliant metadata URL for WWW-Authenticate header
943+
resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url)
946944

947945
routes.append(
948946
Route(
@@ -961,23 +959,13 @@ def streamable_http_app(self) -> Starlette:
961959

962960
# Add protected resource metadata endpoint if configured as RS
963961
if self.settings.auth and self.settings.auth.resource_server_url:
964-
from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler
965-
from mcp.server.auth.routes import cors_middleware
966-
from mcp.shared.auth import ProtectedResourceMetadata
967-
968-
protected_resource_metadata = ProtectedResourceMetadata(
969-
resource=self.settings.auth.resource_server_url,
970-
authorization_servers=[self.settings.auth.issuer_url],
971-
scopes_supported=self.settings.auth.required_scopes,
972-
)
973-
routes.append(
974-
Route(
975-
"/.well-known/oauth-protected-resource",
976-
endpoint=cors_middleware(
977-
ProtectedResourceMetadataHandler(protected_resource_metadata).handle,
978-
["GET", "OPTIONS"],
979-
),
980-
methods=["GET", "OPTIONS"],
962+
from mcp.server.auth.routes import create_protected_resource_routes
963+
964+
routes.extend(
965+
create_protected_resource_routes(
966+
resource_url=self.settings.auth.resource_server_url,
967+
authorization_servers=[self.settings.auth.issuer_url],
968+
scopes_supported=self.settings.auth.required_scopes,
981969
)
982970
)
983971

‎tests/server/auth/test_protected_resource.py‎

Lines changed: 150 additions & 4 deletions

0 commit comments

Comments
 (0)