Skip to content

Commit d3f8564

Browse files
committed
refactor: encapsulate resource metadata URL construction in a dedicated function
- Introduced `build_resource_metadata_url` to create RFC 9728 compliant metadata URLs. - Updated `FastMCP` to utilize the new function for constructing resource metadata URLs. - Improved code readability by removing redundant URL parsing logic from `server.py`.
1 parent 2023262 commit d3f8564

File tree

2 files changed

+39
-41
lines changed

2 files changed

+39
-41
lines changed

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: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -824,11 +824,10 @@ async def handle_sse(scope: Scope, receive: Receive, send: Send):
824824
# Determine resource metadata URL
825825
resource_metadata_url = None
826826
if self.settings.auth and self.settings.auth.resource_server_url:
827-
from pydantic import AnyHttpUrl
827+
from mcp.server.auth.routes import build_resource_metadata_url
828828

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

833832
# Auth is enabled, wrap the endpoints with RequireAuthMiddleware
834833
routes.append(
@@ -937,18 +936,10 @@ def streamable_http_app(self) -> Starlette:
937936
# Determine resource metadata URL
938937
resource_metadata_url = None
939938
if self.settings.auth and self.settings.auth.resource_server_url:
940-
from urllib.parse import urlparse
941-
942-
from pydantic import AnyHttpUrl
939+
from mcp.server.auth.routes import build_resource_metadata_url
943940

944-
# RFC 9728 §3.1: Insert /.well-known/oauth-protected-resource between host and resource path
945-
# This URL will be used in WWW-Authenticate header for client discovery
946-
parsed = urlparse(str(self.settings.auth.resource_server_url))
947-
# Handle trailing slash: if path is just "/", treat as empty
948-
resource_path = parsed.path if parsed.path != "/" else ""
949-
resource_metadata_url = AnyHttpUrl(
950-
f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}"
951-
)
941+
# Build compliant metadata URL for WWW-Authenticate header
942+
resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url)
952943

953944
routes.append(
954945
Route(
@@ -967,32 +958,13 @@ def streamable_http_app(self) -> Starlette:
967958

968959
# Add protected resource metadata endpoint if configured as RS
969960
if self.settings.auth and self.settings.auth.resource_server_url:
970-
from urllib.parse import urlparse
971-
972-
from mcp.server.auth.handlers.metadata import ProtectedResourceMetadataHandler
973-
from mcp.server.auth.routes import cors_middleware
974-
from mcp.shared.auth import ProtectedResourceMetadata
975-
976-
protected_resource_metadata = ProtectedResourceMetadata(
977-
resource=self.settings.auth.resource_server_url,
978-
authorization_servers=[self.settings.auth.issuer_url],
979-
scopes_supported=self.settings.auth.required_scopes,
980-
)
981-
982-
# RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path
983-
parsed = urlparse(str(self.settings.auth.resource_server_url))
984-
# Handle trailing slash: if path is just "/", treat as empty
985-
resource_path = parsed.path if parsed.path != "/" else ""
986-
well_known_path = f"/.well-known/oauth-protected-resource{resource_path}"
961+
from mcp.server.auth.routes import create_protected_resource_routes
987962

988-
routes.append(
989-
Route(
990-
well_known_path,
991-
endpoint=cors_middleware(
992-
ProtectedResourceMetadataHandler(protected_resource_metadata).handle,
993-
["GET", "OPTIONS"],
994-
),
995-
methods=["GET", "OPTIONS"],
963+
routes.extend(
964+
create_protected_resource_routes(
965+
resource_url=self.settings.auth.resource_server_url,
966+
authorization_servers=[self.settings.auth.issuer_url],
967+
scopes_supported=self.settings.auth.required_scopes,
996968
)
997969
)
998970

0 commit comments

Comments
 (0)