55 python -m mcp_simple_auth.server --port=8001
66"""
77
8+ import asyncio
89import logging
910from typing import Any , Literal
1011
1112import click
1213import httpx
1314from pydantic import AnyHttpUrl
1415from pydantic_settings import BaseSettings , SettingsConfigDict
16+ from starlette .applications import Starlette
17+ from starlette .routing import Mount , Route
18+ from uvicorn import Config , Server
1519
20+ from mcp .server .auth .handlers .metadata import MetadataHandler
1621from mcp .server .auth .middleware .auth_context import get_access_token
22+ from mcp .server .auth .routes import cors_middleware
1723from mcp .server .auth .settings import AuthSettings
1824from mcp .server .fastmcp .server import FastMCP
25+ from mcp .shared .auth import OAuthMetadata
1926
2027from .token_verifier import IntrospectionTokenVerifier
2128
@@ -33,9 +40,10 @@ class ResourceServerSettings(BaseSettings):
3340 host : str = "localhost"
3441 port : int = 8001
3542 server_url : AnyHttpUrl = AnyHttpUrl ("http://localhost:8001" )
43+ transport : Literal ["sse" , "streamable-http" ] = "streamable-http"
3644
3745 # Authorization Server settings
38- auth_server_url : AnyHttpUrl = AnyHttpUrl ("http://localhost:9000 " )
46+ auth_server_url : AnyHttpUrl = AnyHttpUrl ("http://localhost:8001 " )
3947 auth_server_introspection_endpoint : str = f"{ API_ENDPOINT } /oauth2/@me"
4048 auth_server_discord_user_endpoint : str = f"{ API_ENDPOINT } /users/@me"
4149
@@ -47,7 +55,7 @@ def __init__(self, **data):
4755 super ().__init__ (** data )
4856
4957
50- def create_resource_server (settings : ResourceServerSettings ) -> FastMCP :
58+ def create_resource_server (settings : ResourceServerSettings ) -> Starlette :
5159 """
5260 Create MCP Resource Server.
5361 """
@@ -59,10 +67,8 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
5967 )
6068
6169 # Create FastMCP server as a Resource Server
62- app = FastMCP (
70+ resource_server = FastMCP (
6371 name = "MCP Resource Server" ,
64- host = settings .host ,
65- port = settings .port ,
6672 debug = True ,
6773 token_verifier = token_verifier ,
6874 auth = AuthSettings (
@@ -93,14 +99,14 @@ async def get_discord_user_data() -> dict[str, Any]:
9399
94100 return response .json ()
95101
96- @app .tool ()
102+ @resource_server .tool ()
97103 async def get_user_profile () -> dict [str , Any ]:
98104 """
99105 Get the authenticated user's Discord profile information.
100106 """
101107 return await get_discord_user_data ()
102108
103- @app .tool ()
109+ @resource_server .tool ()
104110 async def get_user_info () -> dict [str , Any ]:
105111 """
106112 Get information about the currently authenticated user.
@@ -121,12 +127,53 @@ async def get_user_info() -> dict[str, Any]:
121127 "authorization_server" : str (settings .auth_server_url ),
122128 }
123129
130+ # Create Starlette app to mount the MCP server and host RFC8414
131+ # metadata to jump to Discord's authorization server
132+ app = Starlette (
133+ debug = True ,
134+ routes = [
135+ Route (
136+ "/.well-known/oauth-authorization-server" ,
137+ endpoint = cors_middleware (
138+ MetadataHandler (metadata = OAuthMetadata (
139+ issuer = settings .server_url ,
140+ authorization_endpoint = AnyHttpUrl (f"{ API_ENDPOINT } /oauth2/authorize" ),
141+ token_endpoint = AnyHttpUrl (f"{ API_ENDPOINT } /oauth2/token" ),
142+ token_endpoint_auth_methods_supported = ["client_secret_basic" ],
143+ response_types_supported = ["code" ],
144+ grant_types_supported = ["client_credentials" ],
145+ scopes_supported = ["identify" ]
146+ )).handle ,
147+ ["GET" , "OPTIONS" ],
148+ ),
149+ methods = ["GET" , "OPTIONS" ],
150+ ),
151+ Mount (
152+ "/" ,
153+ app = resource_server .streamable_http_app () if settings .transport == "streamable-http" else resource_server .sse_app ()
154+ ),
155+ ],
156+ lifespan = lambda app : resource_server .session_manager .run (),
157+ )
158+
124159 return app
125160
126161
162+ async def run_server (settings : ResourceServerSettings ):
163+ mcp_server = create_resource_server (settings )
164+ config = Config (
165+ mcp_server ,
166+ host = settings .host ,
167+ port = settings .port ,
168+ log_level = "info" ,
169+ )
170+ server = Server (config )
171+ await server .serve ()
172+
173+
127174@click .command ()
128175@click .option ("--port" , default = 8001 , help = "Port to listen on" )
129- @click .option ("--auth-server" , default = "http://localhost:9000 " , help = "Authorization Server URL" )
176+ @click .option ("--auth-server" , default = "http://localhost:8001 " , help = "Authorization Server URL" )
130177@click .option (
131178 "--transport" ,
132179 default = "streamable-http" ,
@@ -153,15 +200,14 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http
153200 auth_server_url = auth_server_url ,
154201 auth_server_introspection_endpoint = f"{ API_ENDPOINT } /oauth2/@me" ,
155202 auth_server_discord_user_endpoint = f"{ API_ENDPOINT } /users/@me" ,
203+ transport = transport ,
156204 )
157205 except ValueError as e :
158206 logger .error (f"Configuration error: { e } " )
159207 logger .error ("Make sure to provide a valid Authorization Server URL" )
160208 return 1
161209
162210 try :
163- mcp_server = create_resource_server (settings )
164-
165211 logger .info ("=" * 80 )
166212 logger .info ("📦 MCP RESOURCE SERVER" )
167213 logger .info ("=" * 80 )
@@ -182,7 +228,7 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http
182228 logger .info ("=" * 80 )
183229
184230 # Run the server - this should block and keep running
185- mcp_server .run (transport = transport )
231+ asyncio .run (run_server ( settings ) )
186232 logger .info ("Server stopped" )
187233 return 0
188234 except Exception as e :
0 commit comments