diff --git a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py index 2f5973ca371..f5b8349ac40 100644 --- a/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/byok_oauth_endpoints.py @@ -600,28 +600,32 @@ def _build_authorize_html( @router.get("/.well-known/oauth-authorization-server", include_in_schema=False) async def oauth_authorization_server_metadata(request: Request) -> JSONResponse: """RFC 8414 Authorization Server Metadata for the BYOK OAuth flow.""" + from mcp.shared.auth import OAuthMetadata + base_url = get_request_base_url(request) return JSONResponse( - { - "issuer": base_url, - "authorization_endpoint": f"{base_url}/v1/mcp/oauth/authorize", - "token_endpoint": f"{base_url}/v1/mcp/oauth/token", - "response_types_supported": ["code"], - "grant_types_supported": ["authorization_code"], - "code_challenge_methods_supported": ["S256"], - } + OAuthMetadata( + issuer=base_url, # type: ignore[arg-type] + authorization_endpoint=f"{base_url}/v1/mcp/oauth/authorize", # type: ignore[arg-type] + token_endpoint=f"{base_url}/v1/mcp/oauth/token", # type: ignore[arg-type] + response_types_supported=["code"], + grant_types_supported=["authorization_code"], + code_challenge_methods_supported=["S256"], + ).model_dump(mode="json", exclude_none=True) ) @router.get("/.well-known/oauth-protected-resource", include_in_schema=False) async def oauth_protected_resource_metadata(request: Request) -> JSONResponse: """RFC 9728 Protected Resource Metadata pointing back at this server.""" + from mcp.shared.auth import ProtectedResourceMetadata + base_url = get_request_base_url(request) return JSONResponse( - { - "resource": base_url, - "authorization_servers": [base_url], - } + ProtectedResourceMetadata( + resource=base_url, # type: ignore[arg-type] + authorization_servers=[base_url], # type: ignore[list-item] + ).model_dump(mode="json", exclude_none=True) ) diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index 3beddd2c435..1446edddf00 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -1043,19 +1043,21 @@ async def _build_oauth_protected_resource_response( ), ) - return { - "authorization_servers": [ + from mcp.shared.auth import ProtectedResourceMetadata + + return ProtectedResourceMetadata( + resource=resource_url, # type: ignore[arg-type] + authorization_servers=[ ( f"{request_base_url}/{mcp_server_name}" if mcp_server_name else f"{request_base_url}" ) - ], - "resource": resource_url, - "scopes_supported": ( + ], # type: ignore[list-item] + scopes_supported=( mcp_server.scopes if mcp_server and mcp_server.scopes else [] ), - } + ).model_dump(mode="json", exclude_none=True) # Standard MCP pattern: /.well-known/oauth-protected-resource/mcp/{server_name} @@ -1145,24 +1147,26 @@ def _build_oauth_authorization_server_response( mcp_server_name, client_ip=client_ip ) - return { - "issuer": request_base_url, # point to your proxy - "authorization_endpoint": authorization_endpoint, - "token_endpoint": token_endpoint, - "response_types_supported": ["code"], - "scopes_supported": ( + from mcp.shared.auth import OAuthMetadata + + return OAuthMetadata( + issuer=request_base_url, # type: ignore[arg-type] + authorization_endpoint=authorization_endpoint, # type: ignore[arg-type] + token_endpoint=token_endpoint, # type: ignore[arg-type] + response_types_supported=["code"], + scopes_supported=( mcp_server.scopes if mcp_server and mcp_server.scopes else [] ), - "grant_types_supported": ["authorization_code", "refresh_token"], - "code_challenge_methods_supported": ["S256"], - "token_endpoint_auth_methods_supported": ["client_secret_post"], + grant_types_supported=["authorization_code", "refresh_token"], + code_challenge_methods_supported=["S256"], + token_endpoint_auth_methods_supported=["client_secret_post"], # Claude expects a registration endpoint, even if we just fake it - "registration_endpoint": ( + registration_endpoint=( # type: ignore[arg-type] f"{request_base_url}/{mcp_server_name}/register" if mcp_server_name else f"{request_base_url}/register" ), - } + ).model_dump(mode="json", exclude_none=True) # Standard MCP pattern: /.well-known/oauth-authorization-server/mcp/{server_name} diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py index 6fd935e3364..a5802982d5e 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py @@ -2015,7 +2015,9 @@ async def test_discovery_root_does_not_expose_private_server_for_external_client assert "/test_oauth/" not in authorization_response["authorization_endpoint"] assert "/test_oauth/" not in authorization_response["token_endpoint"] assert authorization_response["scopes_supported"] == [] - assert resource_response["authorization_servers"] == ["https://llm.example.com"] + assert resource_response["authorization_servers"] == [ + "https://llm.example.com/" + ] assert resource_response["scopes_supported"] == [] finally: global_mcp_server_manager.registry.clear()