@@ -195,25 +195,73 @@ 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.
199201///
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`
202+ /// `None` (server accepted the unauthenticated connection, which is then
203+ /// closed cleanly) is a legitimate outcome, not an error: the scope-step-up
204+ /// and scope-retry-limit mocks allow unauthenticated `initialize` and only
205+ /// enforce authorization on tool calls.
206+ async fn initialize_challenge (
207+ server_url : & str ,
208+ lifecycle : ClientLifecycleMode ,
209+ ) -> anyhow:: Result < Option < String > > {
210+ let transport = StreamableHttpClientTransport :: from_uri ( server_url) ;
211+ match BasicClientHandler
212+ . serve_with_lifecycle ( transport, lifecycle)
213+ . await
214+ {
215+ Ok ( client) => {
216+ client. cancel ( ) . await . ok ( ) ;
217+ Ok ( None )
218+ }
219+ Err ( error) => match error. auth_challenge ( ) {
220+ Some ( challenge) => Ok ( Some ( challenge. to_string ( ) ) ) ,
221+ None => Err ( error. into ( ) ) ,
222+ } ,
223+ }
224+ }
225+
226+ fn with_optional_challenge (
227+ request : AuthorizationRequest ,
228+ challenge : Option < String > ,
229+ ) -> AuthorizationRequest {
230+ match challenge {
231+ Some ( challenge) => request. with_challenge ( challenge) ,
232+ None => request,
233+ }
234+ }
235+
236+ /// Perform the headless OAuth authorization-code flow, reactively:
237+ ///
238+ /// 1. Attempt the real connection; take the 401's WWW-Authenticate challenge
239+ /// 2. Discover from the challenge, register (or use CIMD), get auth URL
240+ /// 3. Fetch the auth URL with redirect:manual → extract code from Location header
241+ /// 4. Exchange code for token
242+ /// 5. Return an `AuthClient` wrapping `reqwest::Client`
204243async fn perform_oauth_flow (
205244 server_url : & str ,
206245 _ctx : & ConformanceContext ,
207246) -> anyhow:: Result < AuthClient < reqwest:: Client > > {
247+ // Always the discover lifecycle here (not `conformance_lifecycle()`):
248+ // this flow serves `run_auth_client`, whose 2026-07-28 auth mocks require
249+ // the per-request MCP-Protocol-Version negotiation.
250+ let challenge = initialize_challenge (
251+ server_url,
252+ ClientLifecycleMode :: Discover {
253+ preferred_versions : preferred_protocol_versions ( ) ,
254+ } ,
255+ )
256+ . await ?;
208257 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
209258
210259 // Discover + register + get auth URL
260+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
261+ . with_client_name ( "conformance-client" )
262+ . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ;
211263 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- )
264+ . start_authorization ( with_optional_challenge ( request, challenge) )
217265 . await ?;
218266
219267 let auth_url = oauth. get_authorization_url ( ) . await ?;
@@ -255,14 +303,14 @@ async fn perform_oauth_flow_preregistered(
255303 client_id : & str ,
256304 client_secret : & str ,
257305) -> anyhow:: Result < AuthClient < reqwest:: Client > > {
306+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
258307 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
259308
309+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
310+ . with_preregistered_client ( client_id)
311+ . with_client_secret ( client_secret) ;
260312 oauth
261- . start_authorization (
262- AuthorizationRequest :: new ( REDIRECT_URI )
263- . with_preregistered_client ( client_id)
264- . with_client_secret ( client_secret) ,
265- )
313+ . start_authorization ( with_optional_challenge ( request, challenge) )
266314 . await ?;
267315
268316 let auth_url = oauth. get_authorization_url ( ) . await ?;
@@ -325,14 +373,14 @@ async fn run_auth_scope_step_up_client(
325373 server_url : & str ,
326374 _ctx : & ConformanceContext ,
327375) -> anyhow:: Result < ( ) > {
376+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
328377 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
378+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
379+ . with_scopes ( SCOPE_STEP_UP_INITIAL_SCOPES . iter ( ) . copied ( ) )
380+ . with_client_name ( "conformance-client" )
381+ . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ;
329382 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- )
383+ . start_authorization ( with_optional_challenge ( request, challenge) )
336384 . await ?;
337385
338386 let auth_url = oauth. get_authorization_url ( ) . await ?;
@@ -427,15 +475,15 @@ async fn run_auth_scope_retry_limit_client(
427475) -> anyhow:: Result < ( ) > {
428476 let max_retries = 3u32 ;
429477 let mut attempt = 0u32 ;
478+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
430479
431480 loop {
432481 let mut oauth = OAuthState :: new ( server_url, None ) . await ?;
482+ let request = AuthorizationRequest :: new ( REDIRECT_URI )
483+ . with_client_name ( "conformance-client" )
484+ . with_client_metadata_url ( CIMD_CLIENT_METADATA_URL ) ;
433485 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- )
486+ . start_authorization ( with_optional_challenge ( request, challenge. clone ( ) ) )
439487 . await ?;
440488 let auth_url = oauth. get_authorization_url ( ) . await ?;
441489 let callback = headless_authorize ( & auth_url) . await ?;
@@ -509,7 +557,10 @@ async fn migration_token(
509557 return Ok ( manager. get_access_token ( ) . await ?) ;
510558 }
511559
512- let resolution = manager. resolve_metadata ( ) . await ?;
560+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
561+ let resolution = manager
562+ . resolve_metadata_from_challenge ( challenge. as_deref ( ) )
563+ . await ?;
513564 manager. set_metadata ( resolution. metadata ) ;
514565 manager
515566 . register_client ( "conformance-client" , REDIRECT_URI , & [ ] )
@@ -609,7 +660,10 @@ async fn run_client_credentials_basic(
609660 . unwrap_or ( "conformance-test-secret" ) ;
610661
611662 let mut manager = AuthorizationManager :: new ( server_url) . await ?;
612- let resolution = manager. resolve_metadata ( ) . await ?;
663+ let challenge = initialize_challenge ( server_url, conformance_lifecycle ( ) ) . await ?;
664+ let resolution = manager
665+ . resolve_metadata_from_challenge ( challenge. as_deref ( ) )
666+ . await ?;
613667 let token_endpoint = resolution. metadata . token_endpoint . clone ( ) ;
614668 manager. set_metadata ( resolution. metadata ) ;
615669
0 commit comments