Skip to content

Commit f2876ea

Browse files
committed
Add OAuth Protected Resource Metadata support
- Introduced OAuthProtectedResourceMetadata class for enhanced resource metadata handling. - Updated create_auth_routes to include resource_server_url and resource_name parameters. - Modified AuthSettings to include resource_server_url and resource_name fields. - Adjusted MetadataHandler to accept both OAuthMetadata and OAuthProtectedResourceMetadata. - Updated FastMCP to utilize new resource metadata features. Signed-off-by: Xin Fu <[email protected]>
1 parent f2f4dbd commit f2876ea

File tree

5 files changed

+52
-5
lines changed

5 files changed

+52
-5
lines changed

src/mcp/server/auth/handlers/metadata.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from starlette.responses import Response
55

66
from mcp.server.auth.json_response import PydanticJSONResponse
7-
from mcp.shared.auth import OAuthMetadata
7+
from mcp.shared.auth import OAuthMetadata, OAuthProtectedResourceMetadata
88

99

1010
@dataclass
1111
class MetadataHandler:
12-
metadata: OAuthMetadata
12+
metadata: OAuthMetadata | OAuthProtectedResourceMetadata
1313

1414
async def handle(self, request: Request) -> Response:
1515
return PydanticJSONResponse(

src/mcp/server/auth/routes.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from mcp.server.auth.middleware.client_auth import ClientAuthenticator
1717
from mcp.server.auth.provider import OAuthAuthorizationServerProvider
1818
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
19-
from mcp.shared.auth import OAuthMetadata
19+
from mcp.shared.auth import OAuthMetadata, OAuthProtectedResourceMetadata
2020

2121

2222
def validate_issuer_url(url: AnyHttpUrl):
@@ -67,9 +67,11 @@ def cors_middleware(
6767
def create_auth_routes(
6868
provider: OAuthAuthorizationServerProvider[Any, Any, Any],
6969
issuer_url: AnyHttpUrl,
70+
resource_server_url: AnyHttpUrl,
7071
service_documentation_url: AnyHttpUrl | None = None,
7172
client_registration_options: ClientRegistrationOptions | None = None,
7273
revocation_options: RevocationOptions | None = None,
74+
resource_name: str | None = None,
7375
) -> list[Route]:
7476
validate_issuer_url(issuer_url)
7577

@@ -85,11 +87,27 @@ def create_auth_routes(
8587
)
8688
client_authenticator = ClientAuthenticator(provider)
8789

90+
protected_resource_metadata = OAuthProtectedResourceMetadata(
91+
resource=resource_server_url,
92+
authorization_servers=[metadata.issuer],
93+
scopes_supported=metadata.scopes_supported,
94+
resource_name=resource_name,
95+
resource_documentation= service_documentation_url,
96+
)
97+
8898
# Create routes
8999
# Allow CORS requests for endpoints meant to be hit by the OAuth client
90100
# (with the client secret). This is intended to support things like MCP Inspector,
91101
# where the client runs in a web browser.
92102
routes = [
103+
Route(
104+
"/.well-known/oauth-protected-resource",
105+
endpoint=cors_middleware(
106+
MetadataHandler(protected_resource_metadata).handle,
107+
["GET", "OPTIONS"],
108+
),
109+
methods=["GET", "OPTIONS"],
110+
),
93111
Route(
94112
"/.well-known/oauth-authorization-server",
95113
endpoint=cors_middleware(

src/mcp/server/auth/settings.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@ class RevocationOptions(BaseModel):
1515
class AuthSettings(BaseModel):
1616
issuer_url: AnyHttpUrl = Field(
1717
...,
18-
description="URL advertised as OAuth issuer; this should be the URL the server "
19-
"is reachable at",
18+
description="The authorization server's issuer identifier",
19+
)
20+
resource_server_url: AnyHttpUrl = Field(
21+
..., description="URL of the MCP server, for use in protected resource metadata"
2022
)
2123
service_documentation_url: AnyHttpUrl | None = None
2224
client_registration_options: ClientRegistrationOptions | None = None
2325
revocation_options: RevocationOptions | None = None
2426
required_scopes: list[str] | None = None
27+
resource_name: str | None = Field(
28+
None, description="Optional resource name to display in resource metadata"
29+
)

src/mcp/server/fastmcp/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,9 +810,11 @@ async def handle_streamable_http(
810810
create_auth_routes(
811811
provider=self._auth_server_provider,
812812
issuer_url=self.settings.auth.issuer_url,
813+
resource_server_url=self.settings.auth.resource_server_url,
813814
service_documentation_url=self.settings.auth.service_documentation_url,
814815
client_registration_options=self.settings.auth.client_registration_options,
815816
revocation_options=self.settings.auth.revocation_options,
817+
resource_name=self.settings.auth.resource_name,
816818
)
817819
)
818820
routes.append(

src/mcp/shared/auth.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,3 +135,25 @@ class OAuthMetadata(BaseModel):
135135
) = None
136136
introspection_endpoint_auth_signing_alg_values_supported: None = None
137137
code_challenge_methods_supported: list[Literal["S256"]] | None = None
138+
139+
140+
class OAuthProtectedResourceMetadata(BaseModel):
141+
"""
142+
RFC 9728 OAuth Protected Resource Metadata
143+
See https://datatracker.ietf.org/doc/html/rfc9728
144+
"""
145+
146+
resource: AnyHttpUrl
147+
authorization_servers: list[AnyHttpUrl] | None = None
148+
jwks_uri: AnyHttpUrl | None = None
149+
scopes_supported: list[str] | None = None
150+
bearer_methods_supported: list[str] | None = None
151+
resource_signing_alg_values_supported: list[str] | None = None
152+
resource_name: str | None = None
153+
resource_documentation: str | None = None
154+
resource_policy_uri: AnyHttpUrl | None = None
155+
resource_tos_uri: AnyHttpUrl | None = None
156+
tls_client_certificate_bound_access_tokens: bool | None = None
157+
authorization_details_types_supported: list[str] | None = None
158+
dpop_signing_alg_values_supported: list[str] | None = None
159+
dpop_bound_access_tokens_required: bool | None = None

0 commit comments

Comments
 (0)