5
5
python -m mcp_simple_auth.server --port=8001
6
6
"""
7
7
8
+ import asyncio
8
9
import logging
9
10
from typing import Any , Literal
10
11
11
12
import click
12
13
import httpx
13
14
from pydantic import AnyHttpUrl
14
15
from pydantic_settings import BaseSettings , SettingsConfigDict
16
+ from starlette .applications import Starlette
17
+ from starlette .routing import Mount , Route
18
+ from uvicorn import Config , Server
15
19
20
+ from mcp .server .auth .handlers .metadata import MetadataHandler
16
21
from mcp .server .auth .middleware .auth_context import get_access_token
22
+ from mcp .server .auth .routes import cors_middleware
17
23
from mcp .server .auth .settings import AuthSettings
18
24
from mcp .server .fastmcp .server import FastMCP
25
+ from mcp .shared .auth import OAuthMetadata
19
26
20
27
from .token_verifier import IntrospectionTokenVerifier
21
28
@@ -33,9 +40,10 @@ class ResourceServerSettings(BaseSettings):
33
40
host : str = "localhost"
34
41
port : int = 8001
35
42
server_url : AnyHttpUrl = AnyHttpUrl ("http://localhost:8001" )
43
+ transport : Literal ["sse" , "streamable-http" ] = "streamable-http"
36
44
37
45
# Authorization Server settings
38
- auth_server_url : AnyHttpUrl = AnyHttpUrl ("http://localhost:9000 " )
46
+ auth_server_url : AnyHttpUrl = AnyHttpUrl ("http://localhost:8001 " )
39
47
auth_server_introspection_endpoint : str = f"{ API_ENDPOINT } /oauth2/@me"
40
48
auth_server_discord_user_endpoint : str = f"{ API_ENDPOINT } /users/@me"
41
49
@@ -47,7 +55,7 @@ def __init__(self, **data):
47
55
super ().__init__ (** data )
48
56
49
57
50
- def create_resource_server (settings : ResourceServerSettings ) -> FastMCP :
58
+ def create_resource_server (settings : ResourceServerSettings ) -> Starlette :
51
59
"""
52
60
Create MCP Resource Server.
53
61
"""
@@ -59,10 +67,8 @@ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
59
67
)
60
68
61
69
# Create FastMCP server as a Resource Server
62
- app = FastMCP (
70
+ resource_server = FastMCP (
63
71
name = "MCP Resource Server" ,
64
- host = settings .host ,
65
- port = settings .port ,
66
72
debug = True ,
67
73
token_verifier = token_verifier ,
68
74
auth = AuthSettings (
@@ -93,14 +99,14 @@ async def get_discord_user_data() -> dict[str, Any]:
93
99
94
100
return response .json ()
95
101
96
- @app .tool ()
102
+ @resource_server .tool ()
97
103
async def get_user_profile () -> dict [str , Any ]:
98
104
"""
99
105
Get the authenticated user's Discord profile information.
100
106
"""
101
107
return await get_discord_user_data ()
102
108
103
- @app .tool ()
109
+ @resource_server .tool ()
104
110
async def get_user_info () -> dict [str , Any ]:
105
111
"""
106
112
Get information about the currently authenticated user.
@@ -121,12 +127,53 @@ async def get_user_info() -> dict[str, Any]:
121
127
"authorization_server" : str (settings .auth_server_url ),
122
128
}
123
129
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
+
124
159
return app
125
160
126
161
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
+
127
174
@click .command ()
128
175
@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" )
130
177
@click .option (
131
178
"--transport" ,
132
179
default = "streamable-http" ,
@@ -153,15 +200,14 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http
153
200
auth_server_url = auth_server_url ,
154
201
auth_server_introspection_endpoint = f"{ API_ENDPOINT } /oauth2/@me" ,
155
202
auth_server_discord_user_endpoint = f"{ API_ENDPOINT } /users/@me" ,
203
+ transport = transport ,
156
204
)
157
205
except ValueError as e :
158
206
logger .error (f"Configuration error: { e } " )
159
207
logger .error ("Make sure to provide a valid Authorization Server URL" )
160
208
return 1
161
209
162
210
try :
163
- mcp_server = create_resource_server (settings )
164
-
165
211
logger .info ("=" * 80 )
166
212
logger .info ("📦 MCP RESOURCE SERVER" )
167
213
logger .info ("=" * 80 )
@@ -182,7 +228,7 @@ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http
182
228
logger .info ("=" * 80 )
183
229
184
230
# Run the server - this should block and keep running
185
- mcp_server .run (transport = transport )
231
+ asyncio .run (run_server ( settings ) )
186
232
logger .info ("Server stopped" )
187
233
return 0
188
234
except Exception as e :
0 commit comments