Support Cursor OAuth client registration (#895)

This commit is contained in:
Sam Tombury
2025-06-07 15:24:11 +01:00
committed by GitHub
parent 8276632caa
commit 2bce10bdb1
5 changed files with 16 additions and 16 deletions

View File

@@ -2,7 +2,7 @@ import logging
from dataclasses import dataclass
from typing import Any, Literal
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel, ValidationError
from pydantic import AnyUrl, BaseModel, Field, RootModel, ValidationError
from starlette.datastructures import FormData, QueryParams
from starlette.requests import Request
from starlette.responses import RedirectResponse, Response
@@ -29,7 +29,7 @@ logger = logging.getLogger(__name__)
class AuthorizationRequest(BaseModel):
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
client_id: str = Field(..., description="The client ID")
redirect_uri: AnyHttpUrl | None = Field(
redirect_uri: AnyUrl | None = Field(
None, description="URL to redirect to after authorization"
)
@@ -68,8 +68,8 @@ def best_effort_extract_string(
return None
class AnyHttpUrlModel(RootModel[AnyHttpUrl]):
root: AnyHttpUrl
class AnyUrlModel(RootModel[AnyUrl]):
root: AnyUrl
@dataclass
@@ -116,7 +116,7 @@ class AuthorizationHandler:
if params is not None and "redirect_uri" not in params:
raw_redirect_uri = None
else:
raw_redirect_uri = AnyHttpUrlModel.model_validate(
raw_redirect_uri = AnyUrlModel.model_validate(
best_effort_extract_string("redirect_uri", params)
).root
redirect_uri = client.validate_redirect_uri(raw_redirect_uri)

View File

@@ -4,7 +4,7 @@ import time
from dataclasses import dataclass
from typing import Annotated, Any, Literal
from pydantic import AnyHttpUrl, BaseModel, Field, RootModel, ValidationError
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, RootModel, ValidationError
from starlette.requests import Request
from mcp.server.auth.errors import (
@@ -27,7 +27,7 @@ class AuthorizationCodeRequest(BaseModel):
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
grant_type: Literal["authorization_code"]
code: str = Field(..., description="The authorization code")
redirect_uri: AnyHttpUrl | None = Field(
redirect_uri: AnyUrl | None = Field(
None, description="Must be the same as redirect URI provided in /authorize"
)
client_id: str

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from typing import Generic, Literal, Protocol, TypeVar
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
from pydantic import AnyHttpUrl, BaseModel
from pydantic import AnyUrl, BaseModel
from mcp.shared.auth import (
OAuthClientInformationFull,
@@ -14,7 +14,7 @@ class AuthorizationParams(BaseModel):
state: str | None
scopes: list[str] | None
code_challenge: str
redirect_uri: AnyHttpUrl
redirect_uri: AnyUrl
redirect_uri_provided_explicitly: bool
@@ -24,7 +24,7 @@ class AuthorizationCode(BaseModel):
expires_at: float
client_id: str
code_challenge: str
redirect_uri: AnyHttpUrl
redirect_uri: AnyUrl
redirect_uri_provided_explicitly: bool

View File

@@ -1,6 +1,6 @@
from typing import Any, Literal
from pydantic import AnyHttpUrl, BaseModel, Field
from pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field
class OAuthToken(BaseModel):
@@ -32,7 +32,7 @@ class OAuthClientMetadata(BaseModel):
for the full specification.
"""
redirect_uris: list[AnyHttpUrl] = Field(..., min_length=1)
redirect_uris: list[AnyUrl] = Field(..., min_length=1)
# token_endpoint_auth_method: this implementation only supports none &
# client_secret_post;
# ie: we do not support client_secret_basic
@@ -71,7 +71,7 @@ class OAuthClientMetadata(BaseModel):
raise InvalidScopeError(f"Client was not registered with scope {scope}")
return requested_scopes
def validate_redirect_uri(self, redirect_uri: AnyHttpUrl | None) -> AnyHttpUrl:
def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:
if redirect_uri is not None:
# Validate redirect_uri against client's registered redirect URIs
if redirect_uri not in self.redirect_uris:

View File

@@ -11,7 +11,7 @@ from urllib.parse import parse_qs, urlparse
import httpx
import pytest
from inline_snapshot import snapshot
from pydantic import AnyHttpUrl
from pydantic import AnyHttpUrl, AnyUrl
from mcp.client.auth import OAuthClientProvider
from mcp.server.auth.routes import build_metadata
@@ -52,7 +52,7 @@ def mock_storage():
@pytest.fixture
def client_metadata():
return OAuthClientMetadata(
redirect_uris=[AnyHttpUrl("http://localhost:3000/callback")],
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
client_name="Test Client",
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],
@@ -79,7 +79,7 @@ def oauth_client_info():
return OAuthClientInformationFull(
client_id="test_client_id",
client_secret="test_client_secret",
redirect_uris=[AnyHttpUrl("http://localhost:3000/callback")],
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
client_name="Test Client",
grant_types=["authorization_code", "refresh_token"],
response_types=["code"],