1+ # pyright: reportUnknownVariableType=false
2+ # pyright: reportUnknownMemberType=false
3+ # pyright: reportAttributeAccessIssue=false
4+ # pyright: reportUnknownArgumentType=false
5+ # pyright: reportCallIssue=false
6+
17from __future__ import annotations
28
9+ import logging
10+ import os
11+ import time
12+ import uuid
13+ from collections .abc import Mapping
14+ from typing import Any , cast
15+ from urllib .parse import urlencode
16+
17+ import httpx # type: ignore
18+ from pydantic import AnyHttpUrl , AnyUrl , Field
19+ from pydantic_settings import BaseSettings , SettingsConfigDict
20+ from starlette .responses import Response
21+ from starlette .routing import Route
22+
23+ from mcp .server .auth .handlers .token import TokenHandler
24+ from mcp .server .auth .middleware .client_auth import ClientAuthenticator
25+ from mcp .server .auth .provider import (
26+ AccessToken ,
27+ AuthorizationCode ,
28+ AuthorizationParams ,
29+ OAuthAuthorizationServerProvider ,
30+ )
31+ from mcp .server .auth .proxy .routes import create_proxy_routes
32+ from mcp .server .auth .routes import create_auth_routes
33+ from mcp .server .auth .settings import ClientRegistrationOptions
34+ from mcp .server .fastmcp .utilities .logging import redact_sensitive_data
35+ from mcp .shared .auth import OAuthClientInformationFull , OAuthToken
36+
337"""Transparent OAuth proxy provider for FastMCP (Anthropic SDK).
438
539This provider mimics the behaviour of fastapi_mcp's `setup_proxies=True` and the
2357A simple helper ``TransparentOAuthProxyProvider.from_env()`` reads these vars.
2458"""
2559
26- import os
27- import time
28- import uuid
29- import json
30- import urllib .parse
31- from typing import Any , cast
32- from collections .abc import Mapping
33- from urllib .parse import urlencode
34-
35- import httpx # type: ignore
36- import logging
37- from pydantic import AnyHttpUrl , Field , AnyUrl
38- from pydantic_settings import BaseSettings , SettingsConfigDict
39- from starlette .routing import Route
40- from starlette .requests import Request
41- from starlette .responses import Response
42-
43- from mcp .server .auth .provider import (
44- AccessToken ,
45- AuthorizationCode ,
46- AuthorizationParams ,
47- OAuthAuthorizationServerProvider ,
48- )
49- from mcp .server .auth .settings import ClientRegistrationOptions
50- from mcp .shared .auth import OAuthClientInformationFull , OAuthToken
51- from mcp .server .auth .routes import create_auth_routes
52- from mcp .server .auth .middleware .client_auth import ClientAuthenticator
53- from mcp .server .auth .handlers .token import TokenHandler
54- from starlette .requests import Request
55- from starlette .responses import Response
56-
57- # New: route factory for proxy endpoints
58- from mcp .server .auth .proxy .routes import create_proxy_routes # noqa: E402
59-
60- from mcp .server .fastmcp .utilities .logging import configure_logging , redact_sensitive_data
61-
6260__all__ = ["TransparentOAuthProxyProvider" ]
6361
6462logger = logging .getLogger ("transparent_oauth_proxy" )
@@ -74,15 +72,13 @@ class ProxyTokenHandler(TokenHandler):
7472 back to the caller.
7573 """
7674
77- def __init__ (self , provider : " TransparentOAuthProxyProvider" ):
75+ def __init__ (self , provider : TransparentOAuthProxyProvider ):
7876 # We provide a dummy ClientAuthenticator that will accept any client –
7977 # we are not going to invoke the base-class logic anyway.
8078 super ().__init__ (provider = provider , client_authenticator = ClientAuthenticator (provider ))
8179 self .provider = provider # keep for easy access
8280
8381 async def handle (self , request ): # type: ignore[override]
84- from starlette .responses import Response
85-
8682 correlation_id = str (uuid .uuid4 ())[:8 ]
8783 start_time = time .time ()
8884
@@ -157,7 +153,7 @@ class ProxySettings(BaseSettings):
157153 default_scope : str = Field ("openid" , alias = "PROXY_DEFAULT_SCOPE" )
158154
159155 @classmethod
160- def load (cls ) -> " ProxySettings" :
156+ def load (cls ) -> ProxySettings :
161157 """Instantiate settings from environment variables (for backwards compatibility)."""
162158 return cls ()
163159
@@ -174,7 +170,7 @@ def __init__(self, *, settings: ProxySettings):
174170 if settings .client_id is None :
175171 settings .client_id = os .getenv ("PROXY_CLIENT_ID" , "demo-client-id" ) # type: ignore[assignment]
176172 assert settings .client_id is not None , "client_id must be provided"
177- self ._s = settings
173+ self ._s : ProxySettings = settings
178174 # simple in-memory auth-code store (maps code→AuthorizationCode)
179175 self ._codes : dict [str , AuthorizationCode ] = {}
180176 # always the same client info returned by /register
@@ -342,7 +338,7 @@ async def revoke_token(self, token: object) -> None: # noqa: D401
342338 # ------------------------------------------------------------------
343339
344340 @classmethod
345- def from_env (cls ) -> " TransparentOAuthProxyProvider" :
341+ def from_env (cls ) -> TransparentOAuthProxyProvider :
346342 """Construct provider using :class:`ProxySettings` populated from the environment."""
347343 return cls (settings = ProxySettings .load ())
348344
@@ -356,24 +352,30 @@ def client_registration_options(self) -> ClientRegistrationOptions: # type: ign
356352 # ------------------------------------------------------------------
357353
358354 def get_auth_routes (self ): # type: ignore[override]
359- """Return full auth+proxy route list for FastMCP."""
360-
361- routes = create_auth_routes (
362- provider = self ,
363- issuer_url = AnyHttpUrl ("http://localhost:8000" ), # placeholder; FastMCP rewrites host
364- client_registration_options = self .client_registration_options ,
365- revocation_options = None ,
366- service_documentation_url = None ,
355+ """Create authentication routes for the transparent proxy provider.
356+
357+ Returns:
358+ List of Starlette routes for OAuth endpoints
359+ """
360+ # First, get the standard OAuth routes
361+ routes = create_auth_routes (self )
362+
363+ # Then add our proxy routes (/.well-known/oauth-authorization-server)
364+ proxy_routes = create_proxy_routes (
365+ upstream_authorization_endpoint = str (self ._s .upstream_authorize ),
366+ upstream_token_endpoint = str (self ._s .upstream_token ),
367+ jwks_uri = self ._s .jwks_uri ,
368+ scopes_supported = self ._s .default_scope .split (),
367369 )
368370
369- # Drop default /token and /authorize handlers – we provide custom ones.
370- routes = [r for r in routes if not ( isinstance ( r , Route ) and r . path in { "/token" , "/authorize" }) ]
371-
372- # Insert proxy /token handler first for high precedence
373- proxy_handler = ProxyTokenHandler ( self )
374- routes . insert ( 0 , Route ( "/token" , endpoint = proxy_handler . handle , methods = [ "POST" ]))
375-
376- # Append additional proxy endpoints (metadata, register, authorize, revoke…)
377- routes . extend ( create_proxy_routes ( self ))
378-
379- return routes
371+ # Replace any route that has the same path as a proxy route
372+ final_routes = []
373+ for route in routes :
374+ # Skip if this is a route that we're replacing with a proxy route
375+ if any ( r . path == route . path for r in proxy_routes if isinstance ( r , Route )):
376+ continue
377+ final_routes . append ( route )
378+
379+ # Add all proxy routes
380+ final_routes . extend ( proxy_routes )
381+ return final_routes
0 commit comments