mirror of
https://github.com/aljazceru/mcp-python-sdk.git
synced 2025-12-19 14:54:24 +01:00
Support Cursor OAuth client registration (#895)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user