@@ -13,13 +13,16 @@ use axum::{
1313 http:: { Request , StatusCode } ,
1414 middleware,
1515 response:: { IntoResponse , Response } ,
16- routing:: post,
16+ routing:: { get , post} ,
1717 Router ,
1818} ;
1919use mcpmux_core:: { DomainEvent , ServerDiscoveryService , ServerLogManager } ;
2020use mcpmux_gateway:: {
2121 mcp:: mcp_oauth_middleware,
22- server:: { DependenciesBuilder , GatewayDependencies , GatewayState , ServiceContainer } ,
22+ server:: {
23+ oauth_metadata, resource_metadata, AppState , DependenciesBuilder , GatewayDependencies ,
24+ GatewayState , ServiceContainer ,
25+ } ,
2326} ;
2427use mcpmux_storage:: SqliteSpaceRepository ;
2528use std:: sync:: Arc ;
@@ -44,6 +47,7 @@ async fn echo_client_id(req: Request<Body>) -> Response {
4447
4548struct Harness {
4649 url : String ,
50+ base : String ,
4751 ct : CancellationToken ,
4852}
4953
@@ -112,10 +116,30 @@ impl Harness {
112116 gateway_state,
113117 ) ) ;
114118
115- let router = Router :: new ( ) . route ( "/mcp" , post ( echo_client_id) ) . layer (
119+ let mcp_router = Router :: new ( ) . route ( "/mcp" , post ( echo_client_id) ) . layer (
116120 middleware:: from_fn_with_state ( services. clone ( ) , mcp_oauth_middleware) ,
117121 ) ;
118122
123+ // Mount the OAuth-discovery endpoints so we can assert they 404 when
124+ // inbound auth is disabled (don't advertise auth the gateway won't ask
125+ // for).
126+ let app_state = AppState {
127+ gateway_state : services. gateway_state . clone ( ) ,
128+ services : services. clone ( ) ,
129+ base_url : "http://127.0.0.1:0" . to_string ( ) ,
130+ } ;
131+ let discovery_router = Router :: new ( )
132+ . route (
133+ "/.well-known/oauth-protected-resource" ,
134+ get ( resource_metadata) ,
135+ )
136+ . route (
137+ "/.well-known/oauth-authorization-server" ,
138+ get ( oauth_metadata) ,
139+ )
140+ . with_state ( app_state) ;
141+ let router = mcp_router. merge ( discovery_router) ;
142+
119143 let listener = tokio:: net:: TcpListener :: bind ( "127.0.0.1:0" )
120144 . await
121145 . expect ( "bind" ) ;
@@ -131,6 +155,7 @@ impl Harness {
131155
132156 Self {
133157 url : format ! ( "http://127.0.0.1:{port}/mcp" ) ,
158+ base : format ! ( "http://127.0.0.1:{port}" ) ,
134159 ct,
135160 }
136161 }
@@ -178,3 +203,39 @@ async fn auth_required_gateway_rejects_request_without_token() {
178203 "default gateway must reject a tokenless request"
179204 ) ;
180205}
206+
207+ #[ tokio:: test]
208+ async fn authless_gateway_does_not_advertise_oauth_discovery ( ) {
209+ // With inbound auth disabled, the OAuth-discovery endpoints must 404 so MCP
210+ // clients don't start an OAuth flow against a gateway that accepts them
211+ // without a token.
212+ let h = Harness :: start ( true ) . await ;
213+ let client = reqwest:: Client :: new ( ) ;
214+ for path in [
215+ "/.well-known/oauth-protected-resource" ,
216+ "/.well-known/oauth-authorization-server" ,
217+ ] {
218+ let resp = client
219+ . get ( format ! ( "{}{path}" , h. base) )
220+ . send ( )
221+ . await
222+ . expect ( "request" ) ;
223+ assert_eq ! (
224+ resp. status( ) ,
225+ reqwest:: StatusCode :: NOT_FOUND ,
226+ "{path} must 404 when auth is disabled"
227+ ) ;
228+ }
229+ }
230+
231+ #[ tokio:: test]
232+ async fn auth_required_gateway_advertises_oauth_discovery ( ) {
233+ // The default (auth required) still serves discovery so real OAuth works.
234+ let h = Harness :: start ( false ) . await ;
235+ let resp = reqwest:: Client :: new ( )
236+ . get ( format ! ( "{}/.well-known/oauth-protected-resource" , h. base) )
237+ . send ( )
238+ . await
239+ . expect ( "request" ) ;
240+ assert_eq ! ( resp. status( ) , reqwest:: StatusCode :: OK ) ;
241+ }
0 commit comments