@@ -195,25 +195,69 @@ const REDIRECT_URI: &str = "http://localhost:3000/callback";
195195const SCOPE_STEP_UP_INITIAL_SCOPES : & [ & str ] = & [ "mcp:basic" ] ;
196196const SCOPE_STEP_UP_ESCALATED_SCOPES : & [ & str ] = & [ "mcp:basic" , "mcp:write" ] ;
197197
198- /// Perform the headless OAuth authorization-code flow.
198+ /// Attempt the real connection unauthenticated and return the server's
199+ /// `WWW-Authenticate` challenge from the 401 — the reactive discovery
200+ /// trigger. Returns `None` when the server accepts unauthenticated clients
201+ /// (the connection is closed cleanly in that case).
202+ async fn initialize_challenge (
203+ server_url : & str ,
204+ lifecycle : ClientLifecycleMode ,
205+ ) -> anyhow:: Result < Option < String > > {
206+ let transport = StreamableHttpClientTransport :: from_uri ( server_url) ;
207+ match BasicClientHandler
208+ . serve_with_lifecycle ( transport, lifecycle)
209+ . await
210+ {
211+ Ok ( client) => {
212+ client. cancel ( ) . await . ok ( ) ;
213+ Ok ( None )
214+ }
215+ Err ( error) => match error. auth_challenge ( ) {
216+ Some ( challenge) => Ok ( Some ( challenge. to_string ( ) ) ) ,
217+ None => Err ( error. into ( ) ) ,
218+ } ,
219+ }
220+ }
221+
222+ fn with_optional_challenge (
223+ request : AuthorizationRequest ,
224+ challenge : Option < String > ,
225+ ) -> AuthorizationRequest {
226+ match challenge {
227+ Some ( challenge) => request. with_challenge ( challenge) ,
228+ None => request,
229+ }
230+ }
231+
232+ /// Perform the headless OAuth authorization-code flow, reactively:
199233///
200- /// 1. Discover metadata, register (or use CIMD), get auth URL
201- /// 2. Fetch the auth URL with redirect:manual → extract code from Location header
202- /// 3. Exchange code for token
203- /// 4. Return an `AuthClient` wrapping `reqwest::Client`
234+ /// 1. Attempt the real connection; take the 401's WWW-Authenticate challenge
235+ /// 2. Discover from the challenge, register (or use CIMD), get auth URL
236+ /// 3. Fetch the auth URL with redirect:manual → extract code from Location header
237+ /// 4. Exchange code for token
238+ /// 5. Return an `AuthClient` wrapping `reqwest::Client`
204239async fn perform_oauth_flow (
205240 server_url : & str ,
206241 _ctx : & ConformanceContext ,
207242) -> anyhow:: Result < AuthClient < reqwest:: Client > > {
243+ // Always the discover lifecycle here (not `conformance_lifecycle()`):
244+ // this flow serves `run_auth_client`, whose 2026-07-28 auth mocks require
245+ // the per-request MCP-Protocol-Version negotiation.
246+ let challenge = initialize_challenge (
247+ server_url,
248+ ClientLifecycleMode :: Discover {
249+ preferred_versions : preferred_protocol_versions ( ) ,
250+ } ,
251+ )
252+ . await ?;
208253 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
209254
210255 // Discover + register + get auth URL
256+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
257+ . with_client_name ( "conformance-client" )
258+ . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ;
211259 oauth
212- . start_authorization (
213- AuthorizationRequest :: new ( REDIRECT_URI )
214- . with_client_name ( "conformance-client" )
215- . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ,
216- )
260+ . start_authorization ( with_optional_challenge ( request, challenge) )
217261 . await ?;
218262
219263 let auth_url = oauth. get_authorization_url ( ) . await ?;
@@ -255,14 +299,14 @@ async fn perform_oauth_flow_preregistered(
255299 client_id : & str ,
256300 client_secret : & str ,
257301) -> anyhow:: Result < AuthClient < reqwest:: Client > > {
302+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
258303 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
259304
305+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
306+ . with_preregistered_client ( client_id)
307+ . with_client_secret ( client_secret) ;
260308 oauth
261- . start_authorization (
262- AuthorizationRequest :: new ( REDIRECT_URI )
263- . with_preregistered_client ( client_id)
264- . with_client_secret ( client_secret) ,
265- )
309+ . start_authorization ( with_optional_challenge ( request, challenge) )
266310 . await ?;
267311
268312 let auth_url = oauth. get_authorization_url ( ) . await ?;
@@ -325,14 +369,14 @@ async fn run_auth_scope_step_up_client(
325369 server_url : & str ,
326370 _ctx : & ConformanceContext ,
327371) -> anyhow:: Result < ( ) > {
372+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
328373 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
374+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
375+ . with_scopes ( SCOPE_STEP_UP_INITIAL_SCOPES . iter ( ) . copied ( ) )
376+ . with_client_name ( "conformance-client" )
377+ . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ;
329378 oauth
330- . start_authorization (
331- AuthorizationRequest :: new ( REDIRECT_URI )
332- . with_scopes ( SCOPE_STEP_UP_INITIAL_SCOPES . iter ( ) . copied ( ) )
333- . with_client_name ( "conformance-client" )
334- . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ,
335- )
379+ . start_authorization ( with_optional_challenge ( request, challenge) )
336380 . await ?;
337381
338382 let auth_url = oauth. get_authorization_url ( ) . await ?;
@@ -427,15 +471,15 @@ async fn run_auth_scope_retry_limit_client(
427471) -> anyhow:: Result < ( ) > {
428472 let max_retries = 3u32 ;
429473 let mut attempt = 0u32 ;
474+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
430475
431476 loop {
432477 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
478+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
479+ . with_client_name ( "conformance-client" )
480+ . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ;
433481 oauth
434- . start_authorization (
435- AuthorizationRequest :: new ( REDIRECT_URI )
436- . with_client_name ( "conformance-client" )
437- . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ,
438- )
482+ . start_authorization ( with_optional_challenge ( request, challenge. clone ( ) ) )
439483 . await ?;
440484 let auth_url = oauth. get_authorization_url ( ) . await ?;
441485 let callback = headless_authorize ( & auth_url) . await ?;
@@ -509,7 +553,10 @@ async fn migration_token(
509553 return Ok ( manager. get_access_token ( ) . await ?) ;
510554 }
511555
512- let resolution = manager. resolve_metadata ( ) . await ?;
556+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
557+ let resolution = manager
558+ . resolve_metadata_from_challenge ( challenge. as_deref ( ) )
559+ . await ?;
513560 manager. set_metadata ( resolution. metadata ) ;
514561 manager
515562 . register_client ( "conformance-client" , REDIRECT_URI , & [ ] )
@@ -609,7 +656,10 @@ async fn run_client_credentials_basic(
609656 . unwrap_or ( "conformance-test-secret" ) ;
610657
611658 let mut manager = AuthorizationManager :: new ( server_url) . await ?;
612- let resolution = manager. resolve_metadata ( ) . await ?;
659+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
660+ let resolution = manager
661+ . resolve_metadata_from_challenge ( challenge. as_deref ( ) )
662+ . await ?;
613663 let token_endpoint = resolution. metadata . token_endpoint . clone ( ) ;
614664 manager. set_metadata ( resolution. metadata ) ;
615665
0 commit comments