diff --git a/README.v2.md b/README.v2.md index d0851c04e5..20567e534e 100644 --- a/README.v2.md +++ b/README.v2.md @@ -1021,10 +1021,14 @@ mcp = MCPServer( "Weather Service", # Token verifier for authentication token_verifier=SimpleTokenVerifier(), - # Auth settings for RFC 9728 Protected Resource Metadata + # Auth settings for RFC 9728 Protected Resource Metadata. + # `resource_server_url` MUST be the full public URL of the MCP endpoint, including + # the transport path (e.g. `/mcp` for streamable-http, `/sse` for sse). RFC 9728 + # section 3.3 requires strict equality between the client's resource identifier and + # the `resource` value advertised in the protected resource metadata. auth=AuthSettings( issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL - resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL + resource_server_url=AnyHttpUrl("http://localhost:3001/mcp"), # Public MCP endpoint URL required_scopes=["user"], ), ) diff --git a/examples/servers/simple-auth/mcp_simple_auth/server.py b/examples/servers/simple-auth/mcp_simple_auth/server.py index 0320871b12..b33a40eaeb 100644 --- a/examples/servers/simple-auth/mcp_simple_auth/server.py +++ b/examples/servers/simple-auth/mcp_simple_auth/server.py @@ -126,9 +126,12 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http # Parse auth server URL auth_server_url = AnyHttpUrl(auth_server) - # Create settings + # Create settings. server_url is the public URL of the MCP endpoint and must + # include the transport path so it matches the URL the client used to reach + # the server (RFC 9728 section 3.3 strict equality). host = "localhost" - server_url = f"http://{host}:{port}/mcp" + transport_path = "/sse" if transport == "sse" else "/mcp" + server_url = f"http://{host}:{port}{transport_path}" settings = ResourceServerSettings( host=host, port=port, diff --git a/examples/snippets/servers/oauth_server.py b/examples/snippets/servers/oauth_server.py index 962ef0615e..dfd5de58ab 100644 --- a/examples/snippets/servers/oauth_server.py +++ b/examples/snippets/servers/oauth_server.py @@ -21,10 +21,14 @@ async def verify_token(self, token: str) -> AccessToken | None: "Weather Service", # Token verifier for authentication token_verifier=SimpleTokenVerifier(), - # Auth settings for RFC 9728 Protected Resource Metadata + # Auth settings for RFC 9728 Protected Resource Metadata. + # `resource_server_url` MUST be the full public URL of the MCP endpoint, including + # the transport path (e.g. `/mcp` for streamable-http, `/sse` for sse). RFC 9728 + # section 3.3 requires strict equality between the client's resource identifier and + # the `resource` value advertised in the protected resource metadata. auth=AuthSettings( issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL - resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL + resource_server_url=AnyHttpUrl("http://localhost:3001/mcp"), # Public MCP endpoint URL required_scopes=["user"], ), ) diff --git a/src/mcp/server/auth/settings.py b/src/mcp/server/auth/settings.py index 1649826db2..d54e9898e3 100644 --- a/src/mcp/server/auth/settings.py +++ b/src/mcp/server/auth/settings.py @@ -25,6 +25,13 @@ class AuthSettings(BaseModel): # Resource Server settings (when operating as RS only) resource_server_url: AnyHttpUrl | None = Field( ..., - description="The URL of the MCP server to be used as the resource identifier " - "and base route to look up OAuth Protected Resource Metadata.", + description=( + "The full public URL of this MCP server, used as the resource identifier " + "and base route to look up OAuth Protected Resource Metadata (RFC 9728). " + "Must include the transport path (e.g. https://example.com/mcp for " + "streamable-http, https://example.com/sse for sse) so that the value " + "advertised in protected resource metadata exactly matches the URL the " + "client used to reach the server. RFC 9728 section 3.3 requires strict " + "equality between the client's resource identifier and this value." + ), )