1515import httpx
1616from pydantic import AnyHttpUrl
1717from pydantic_settings import BaseSettings , SettingsConfigDict
18- from starlette .authentication import AuthCredentials , AuthenticationBackend
19- from starlette .requests import HTTPConnection
20- from starlette .responses import JSONResponse
2118
2219from mcp .server .auth .middleware .auth_context import get_access_token
23- from mcp .server .auth .middleware . bearer_auth import AuthenticatedUser
24- from mcp .server .auth .provider import AccessToken
20+ from mcp .server .auth .settings import AuthSettings
21+ from mcp .server .auth .token_verifier import IntrospectionTokenVerifier
2522from mcp .server .fastmcp .server import FastMCP
26- from mcp .shared .auth import ProtectedResourceMetadata
2723
2824logger = logging .getLogger (__name__ )
2925
@@ -51,63 +47,6 @@ def __init__(self, **data):
5147 super ().__init__ (** data )
5248
5349
54- class TokenIntrospectionAuthBackend (AuthenticationBackend ):
55- """
56- Authentication backend for Resource Server that validates tokens via AS introspection.
57-
58- This backend:
59- 1. Extracts Bearer tokens from Authorization header
60- 2. Calls Authorization Server's introspection endpoint
61- 3. Creates AuthenticatedUser from token info
62- """
63-
64- def __init__ (self , settings : ResourceServerSettings ):
65- self .settings = settings
66- self .introspection_endpoint = settings .auth_server_introspection_endpoint
67-
68- async def authenticate (self , conn : HTTPConnection ):
69- auth_header = next (
70- (conn .headers .get (key ) for key in conn .headers if key .lower () == "authorization" ),
71- None ,
72- )
73- if not auth_header or not auth_header .lower ().startswith ("bearer " ):
74- return None
75-
76- token = auth_header [7 :] # Remove "Bearer " prefix
77-
78- # Introspect token with Authorization Server
79- async with httpx .AsyncClient () as client :
80- try :
81- response = await client .post (
82- self .introspection_endpoint ,
83- data = {"token" : token },
84- headers = {"Content-Type" : "application/x-www-form-urlencoded" },
85- )
86-
87- if response .status_code != 200 :
88- logger .debug (f"Token introspection failed with status { response .status_code } " )
89- return None
90-
91- data = response .json ()
92- if not data .get ("active" , False ):
93- logger .debug ("Token is not active" )
94- return None
95-
96- # Create auth info from introspection response
97- auth_info = AccessToken (
98- token = token ,
99- client_id = data .get ("client_id" , "unknown" ),
100- scopes = data .get ("scope" , "" ).split () if data .get ("scope" ) else [],
101- expires_at = data .get ("exp" ),
102- )
103-
104- return AuthCredentials (auth_info .scopes ), AuthenticatedUser (auth_info )
105-
106- except Exception :
107- logger .exception ("Token introspection failed" )
108- return None
109-
110-
11150def create_resource_server (settings : ResourceServerSettings ) -> FastMCP :
11251 """
11352 Create MCP Resource Server with token introspection.
@@ -117,35 +56,24 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
11756 2. Validates tokens via Authorization Server introspection
11857 3. Serves MCP tools and resources
11958 """
120- # Create FastMCP server WITHOUT auth settings (since we'll use custom middleware)
121- # This avoids the FastMCP validation error that requires auth_server_provider
59+ # Create token verifier for introspection
60+ token_verifier = IntrospectionTokenVerifier (settings .auth_server_introspection_endpoint )
61+
62+ # Create FastMCP server as a Resource Server
12263 app = FastMCP (
12364 name = "MCP Resource Server" ,
12465 instructions = "Resource Server that validates tokens via Authorization Server introspection" ,
12566 host = settings .host ,
12667 port = settings .port ,
12768 debug = True ,
128- # No auth settings - this is RS, not AS
129- )
130-
131- # Add the protected resource metadata route using FastMCP's custom_route
132- @app .custom_route ("/.well-known/oauth-protected-resource" , methods = ["GET" , "OPTIONS" ])
133- async def protected_resource_metadata (_request ):
134- """Handle requests for protected resource metadata."""
135- metadata = ProtectedResourceMetadata (
136- resource = settings .server_url ,
69+ # Auth configuration for RS mode
70+ token_verifier = token_verifier ,
71+ auth = AuthSettings (
72+ issuer_url = settings .server_url ,
73+ required_scopes = [settings .mcp_scope ],
13774 authorization_servers = [settings .auth_server_url ],
138- scopes_supported = [settings .mcp_scope ],
139- bearer_methods_supported = ["header" ],
140- )
141- # Convert to dict with string URLs for JSON serialization
142- response_data = {
143- "resource" : str (metadata .resource ),
144- "authorization_servers" : [str (url ) for url in metadata .authorization_servers ],
145- "scopes_supported" : metadata .scopes_supported ,
146- "bearer_methods_supported" : metadata .bearer_methods_supported ,
147- }
148- return JSONResponse (response_data )
75+ ),
76+ )
14977
15078 async def get_github_user_data () -> dict [str , Any ]:
15179 """
0 commit comments