@@ -22,6 +22,7 @@ use codex_exec_server::HttpResponseBodyStream;
2222use futures:: StreamExt ;
2323use futures:: stream;
2424use futures:: stream:: BoxStream ;
25+ use oauth2:: AccessToken ;
2526use reqwest:: StatusCode ;
2627use reqwest:: header:: ACCEPT ;
2728use reqwest:: header:: AUTHORIZATION ;
@@ -61,6 +62,10 @@ pub(crate) struct StreamableHttpClientAdapter {
6162pub ( crate ) enum StreamableHttpClientAdapterError {
6263 #[ error( "streamable HTTP session expired with 404 Not Found" ) ]
6364 SessionExpired404 ,
65+ #[ error( "MCP server rejected the access token with HTTP 401 Unauthorized" ) ]
66+ AccessTokenRejected { rejected_access_token : AccessToken } ,
67+ #[ error( "MCP OAuth operation failed: {0:#}" ) ]
68+ OAuth ( #[ source] anyhow:: Error ) ,
6469 #[ error( transparent) ]
6570 HttpRequest ( #[ from] ExecServerError ) ,
6671 #[ error( "invalid HTTP header: {0}" ) ]
@@ -109,7 +114,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
109114 JSON_MIME_TYPE . to_string ( ) ,
110115 StreamableHttpClientAdapterError :: Header ,
111116 ) ?;
112- if let Some ( auth_token) = auth_token {
117+ if let Some ( auth_token) = auth_token. as_deref ( ) {
113118 insert_header (
114119 & mut headers,
115120 AUTHORIZATION ,
@@ -162,6 +167,11 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
162167 StreamableHttpClientAdapterError :: SessionExpired404 ,
163168 ) ) ;
164169 }
170+ if response. status == StatusCode :: UNAUTHORIZED . as_u16 ( )
171+ && let Some ( error) = access_token_rejected ( auth_token. as_deref ( ) )
172+ {
173+ return Err ( error) ;
174+ }
165175 if response. status == StatusCode :: UNAUTHORIZED . as_u16 ( )
166176 && let Some ( header) =
167177 response_header ( & response. headers , reqwest:: header:: WWW_AUTHENTICATE )
@@ -240,7 +250,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
240250 let mut headers = self . default_headers . clone ( ) ;
241251 headers. extend ( custom_headers) ;
242252 self . add_auth_headers ( & mut headers) ;
243- if let Some ( auth_token) = auth_token {
253+ if let Some ( auth_token) = auth_token. as_deref ( ) {
244254 insert_header (
245255 & mut headers,
246256 AUTHORIZATION ,
@@ -274,6 +284,11 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
274284 if response. status == StatusCode :: METHOD_NOT_ALLOWED . as_u16 ( ) {
275285 return Ok ( ( ) ) ;
276286 }
287+ if response. status == StatusCode :: UNAUTHORIZED . as_u16 ( )
288+ && let Some ( error) = access_token_rejected ( auth_token. as_deref ( ) )
289+ {
290+ return Err ( error) ;
291+ }
277292 if !status_is_success ( response. status ) {
278293 return Err ( StreamableHttpError :: UnexpectedServerResponse (
279294 format ! ( "DELETE returned HTTP {}" , response. status) . into ( ) ,
@@ -316,7 +331,7 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
316331 StreamableHttpClientAdapterError :: Header ,
317332 ) ?;
318333 }
319- if let Some ( auth_token) = auth_token {
334+ if let Some ( auth_token) = auth_token. as_deref ( ) {
320335 insert_header (
321336 & mut headers,
322337 AUTHORIZATION ,
@@ -349,6 +364,11 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
349364 StreamableHttpClientAdapterError :: SessionExpired404 ,
350365 ) ) ;
351366 }
367+ if response. status == StatusCode :: UNAUTHORIZED . as_u16 ( )
368+ && let Some ( error) = access_token_rejected ( auth_token. as_deref ( ) )
369+ {
370+ return Err ( error) ;
371+ }
352372 if !status_is_success ( response. status ) {
353373 return Err ( StreamableHttpError :: UnexpectedServerResponse (
354374 format ! ( "GET returned HTTP {}" , response. status) . into ( ) ,
@@ -371,6 +391,20 @@ impl StreamableHttpClient for StreamableHttpClientAdapter {
371391 }
372392}
373393
394+ fn access_token_rejected (
395+ auth_token : Option < & str > ,
396+ ) -> Option < StreamableHttpError < StreamableHttpClientAdapterError > > {
397+ // Preserve the token associated with this response. Reading the current credential after a
398+ // delayed 401 is racy: another concurrent request may already have refreshed A to B, in which
399+ // case recovery must retry B rather than refresh B a second time. AccessToken's Debug
400+ // implementation redacts the secret if this error is logged.
401+ auth_token. map ( |rejected_access_token| {
402+ StreamableHttpError :: Client ( StreamableHttpClientAdapterError :: AccessTokenRejected {
403+ rejected_access_token : AccessToken :: new ( rejected_access_token. to_string ( ) ) ,
404+ } )
405+ } )
406+ }
407+
374408impl StreamableHttpClientAdapter {
375409 fn add_auth_headers ( & self , headers : & mut HeaderMap ) {
376410 if let Some ( auth_provider) = & self . auth_provider {
0 commit comments