
    `gj                        U d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
mZ ddlmZmZ  ej                  e      ZddZe G d d	             ZddZ e       Zd
ed<    G d d      Zdaded<    ej2                         ZddZddZy)u?  Central manager for per-server MCP OAuth state.

One instance shared across the process. Holds per-server OAuth provider
instances and coordinates:

- **Cross-process token reload** via mtime-based disk watch. When an external
  process (e.g. a user cron job) refreshes tokens on disk, the next auth flow
  picks them up without requiring a process restart.
- **401 deduplication** via in-flight futures. When N concurrent tool calls
  all hit 401 with the same access_token, only one recovery attempt fires;
  the rest await the same result.
- **Reconnect signalling** for long-lived MCP sessions. The manager itself
  does not drive reconnection — the `MCPServerTask` in `mcp_tool.py` does —
  but the manager is the single source of truth that decides when reconnect
  is warranted.

Replaces what used to be scattered across eight call sites in `mcp_oauth.py`,
`mcp_tool.py`, and `hermes_cli/mcp_config.py`. This module is the ONLY place
that instantiates the MCP SDK's `OAuthClientProvider` — all other code paths
go through `get_manager()`.

Design reference:

- Claude Code's ``invalidateOAuthCacheIfDiskChanged``
  (``claude-code/src/utils/auth.ts:1320``, CC-1096 / GH#24317). Identical
  external-refresh staleness bug class.
- Codex's ``refresh_oauth_if_needed`` / ``persist_if_needed``
  (``codex-rs/rmcp-client/src/rmcp_client.rs:805``). We lean on the MCP SDK's
  lazy refresh rather than calling refresh before every op, because one
  ``stat()`` per tool call is cheaper than an ``await`` + potential refresh
  round-trip, and the SDK's in-memory expiry path is already correct.
    )annotationsN)	dataclassfield)Path)AnyOptionalc                `   ddl m} 	  ||        ||      }}|j                  |j                  k(  xrn |j                  j                         |j                  j                         k(  xr7 |j                  j                  d      |j                  j                  d      k(  S # t        $ r Y yw xY w)a  Return True if two URLs target the same endpoint (ignoring query/fragment).

    Compares scheme, host (case-insensitive), and path. Used to confirm a
    rejected response actually came from the OAuth token endpoint before we
    act on an ``invalid_client`` body.
    r   )urlsplitF/)urllib.parser
   
ValueErrorschemenetloclowerpathrstrip)abr
   papbs        J/root/.hermes/venv/lib/python3.12/site-packages/tools/mcp_oauth_manager.py_same_endpointr   0   s     &!hqkB 			RYY 	7IIOO!22	7GGNN3277>>##66  s   B! !	B-,B-c                      e Zd ZU dZded<   ded<   dZded<   d	Zd
ed<    eej                        Z
ded<    ee      Zded<   y)_ProviderEntrya  Per-server OAuth state tracked by the manager.

    Fields:
        server_url: The MCP server URL used to build the provider. Tracked
            so we can discard a cached provider if the URL changes.
        oauth_config: Optional dict from ``mcp_servers.<name>.oauth``.
        provider: The ``httpx.Auth``-compatible provider wrapping the MCP
            SDK. None until first use.
        last_mtime_ns: Last-seen ``st_mtime_ns`` of the on-disk tokens file.
            Zero if never read. Used by :meth:`MCPOAuthManager.invalidate_if_disk_changed`
            to detect external refreshes.
        lock: Serialises concurrent access to this entry's state. Bound to
            whichever asyncio loop first awaits it (the MCP event loop).
        pending_401: In-flight 401-handler futures keyed by the failed
            access_token, for deduplicating thundering-herd 401s. Mirrors
            Claude Code's ``pending401Handlers`` map.
    str
server_urlOptional[dict]oauth_configNOptional[Any]providerr   intlast_mtime_ns)default_factoryzasyncio.Locklockz!dict[str, 'asyncio.Future[bool]']pending_401)__name__
__module____qualname____doc____annotations__r    r"   r   asyncioLockr$   dictr%        r   r   r   I   sK    $ O  "Hm"M3w||<D,<5:45PK2Pr/   r   Optional[type]c                 H    	 ddl m}   G d d|       }|S # t        $ r Y yw xY w)zLazy-import the SDK base class and return our subclass.

    Wrapped in a function so this module imports cleanly even when the
    MCP SDK's OAuth module is unavailable (e.g. older mcp versions).
    r   )OAuthClientProviderNc                  h     e Zd ZdZddd	 	 	 	 	 	 	 d fdZd fdZddZddZdd	Z fd
Z	 xZ
S );_make_hermes_provider_class.<locals>.HermesMCPOAuthProvideraL  OAuthClientProvider with pre-flow disk-mtime reload.

        Before every ``async_auth_flow`` invocation, asks the manager to
        check whether the tokens file on disk has been modified externally.
        If so, the manager resets ``_initialized`` so the next flow
        re-reads from storage.

        This makes external-process refreshes (cron, another CLI instance)
        visible to the running MCP session without requiring a restart.

        Reference: Claude Code's ``invalidateOAuthCacheIfDiskChanged``
        (``src/utils/auth.ts:1320``, CC-1096 / GH#24317).
         F)server_namepreregisteredc               N    t        |   |i | || _        d| _        || _        y )Nr5   )super__init___hermes_server_name_hermes_home_hermes_preregistered)selfr6   r7   argskwargs	__class__s        r   r:   zD_make_hermes_provider_class.<locals>.HermesMCPOAuthProvider.__init__   s1     Gd-f-'2D$ "D *7D&r/   c                  K   t         |           d{    | j                  j                  }|'|j                  | j                  j                  |       | j                  j                  }ddlm} t        ||      rd| j                  j                  N|j                         }|<|| j                  _
        t        j                  d| j                  |j                         |1| j                  j                  	 | j!                          d{    yyy7 7 
# t"        $ r+}t        j                  d| j                  |       Y d}~yd}~ww xY ww)uQ  Load stored tokens + client info AND seed token_expiry_time.

            Also eagerly fetches OAuth authorization-server metadata (PRM +
            ASM) when we have stored tokens but no cached metadata, so the
            SDK's ``_refresh_token`` can build the correct token_endpoint
            URL on the preemptive-refresh path. Without this, the SDK
            falls back to ``{mcp_server_url}/token`` (wrong for providers
            whose AS is a different origin — BetterStack's MCP lives at
            ``https://mcp.betterstack.com`` but its token endpoint is at
            ``https://betterstack.com/oauth/token``), the refresh 404s, and
            we drop through to full browser reauth.

            The SDK's base ``_initialize`` populates ``current_tokens`` but
            does NOT call ``update_token_expiry``, so ``token_expiry_time``
            stays ``None`` and ``is_token_valid()`` returns True for any
            loaded token regardless of actual age. After a process restart
            this ships stale Bearer tokens to the server; some providers
            return HTTP 401 (caught by the 401 handler), others return 200
            with an app-level auth error (invisible to the transport layer,
            e.g. BetterStack returning "No teams found. Please check your
            authentication.").

            Seeding ``token_expiry_time`` from the reloaded token fixes that:
            ``is_token_valid()`` correctly reports False for expired tokens,
            ``async_auth_flow`` takes the ``can_refresh_token()`` branch,
            and the SDK quietly refreshes before the first real request.

            Paired with :class:`HermesTokenStorage` persisting an absolute
            ``expires_at`` timestamp (``mcp_oauth.py:set_tokens``) so the
            remaining TTL we compute here reflects real wall-clock age.
            Nr   HermesTokenStoragez?MCP OAuth '%s': restored metadata from disk (token_endpoint=%s)zDMCP OAuth '%s': pre-flight metadata discovery failed (non-fatal): %s)r9   _initializecontextcurrent_tokens
expires_inupdate_token_expirystoragetools.mcp_oauthrD   
isinstanceoauth_metadataload_oauth_metadataloggerdebugr;   token_endpoint_prefetch_oauth_metadata	Exception)r>   tokensrJ   rD   metaexcrA   s         r   rE   zG_make_hermes_provider_class.<locals>.HermesMCPOAuthProvider._initialize   s7    @ '%'''\\00F!f&7&7&C008 ll**G:7$67LL//7224#26DLL/LL.00++	 "LL//7	77999 8 #A (H :   LL100# sL   EDC'E>D DD ED 	E'!EEEEc           	       K   ddl }ddlm}m}m}m}m} | j                  j                  }|j                  d      4 d{   } |d|      D ]|  }	 ||	      }
	 |j                  |
       d{   } ||       d{   }|s8|| j                  _        |j                   r't#        |j                   d         | j                  _         n  || j                  j$                  |      D ]  }	 ||	      }
	 |j                  |
       d{   } ||       d{   \  }}|s n{|s?|| j                  _        | j                  j(                  }ddlm} t/        ||      r|j1                  |       t        j                  d	| j                  |j2                          n ddd      d{    y7 r7 G# |j                  $ r-}t        j                  d| j                  |	|       Y d}~d}~ww xY w7 y7 # |j                  $ r-}t        j                  d| j                  |	|       Y d}~Id}~ww xY w7 !7 # 1 d{  7  sw Y   yxY ww)
a  Fetch PRM + ASM from the well-known endpoints, cache on context.

            Mirrors the SDK's 401-branch discovery (oauth2.py ~line 511-551)
            but runs synchronously before the first request instead of
            inside the httpx auth_flow generator. Uses the SDK's own URL
            builders and response handlers so we track whatever the SDK
            version we're pinned to expects.
            r   N)8build_oauth_authorization_server_metadata_discovery_urls0build_protected_resource_metadata_discovery_urlscreate_oauth_metadata_requesthandle_auth_metadata_response"handle_protected_resource_responseg      $@)timeoutz.MCP OAuth '%s': PRM discovery to %s failed: %sz.MCP OAuth '%s': ASM discovery to %s failed: %srC   z;MCP OAuth '%s': pre-flight ASM discovered token_endpoint=%s)httpxmcp.client.auth.utilsrX   rY   rZ   r[   r\   rF   r   AsyncClientsend	HTTPErrorrO   rP   r;   protected_resource_metadataauthorization_serversr   auth_server_urlrM   rJ   rK   rD   rL   save_oauth_metadatarQ   )r>   r^   rX   rY   rZ   r[   r\   r   clienturlreqresprV   prmokasmrJ   rD   s                     r   rR   zT_make_hermes_provider_class.<locals>.HermesMCPOAuthProvider._prefetch_oauth_metadata   s7       00J(((6 5 5&K* C 8<C!%+[[%55 !C4 HHCCF@44;> # 9 9! <<DLL8 '. TLL00* C 8<C!%+[[%55 %B$$GGGB693 #',,"6"6F%g/AB#77<0 44c6H6H
 935 5 5  6 ?? !L 44c3 !! I   6 ?? !L 44c3 !! HK5 5 5 5s   ?IF3IIF9/F60F94I?G8 IA/I7G=G;G=IH<I*A8I"I-H?.I6F99G5"G0*I0G55I;G==H9"H4.I4H99I?III
IIc                &   | j                   j                  }|y| j                   j                  }ddlm} t        ||      sy|j                         }|+t        |j                        t        |j                        k7  r|j                  |       yy)a0  Persist discovered OAuth metadata for future process restarts.

            Called after the SDK's normal 401-branch auth flow completes so
            metadata discovered via the lazy path (not pre-flight) is also
            saved. No-op when nothing to persist or metadata hasn't changed.
            Nr   rC   )
rF   rM   rJ   rK   rD   rL   rN   r   rQ   rf   )r>   rU   rJ   rD   existings        r   "_persist_oauth_metadata_if_changedz^_make_hermes_provider_class.<locals>.HermesMCPOAuthProvider._persist_oauth_metadata_if_changed-  s     <<..D|ll**G:g'9:224H x../3t7J7J3KK++D1 Lr/   c                  K   	 | j                   ryt        |dd      }|dvryt        | j                  dd      }|"t        |dd      rt        |j                        nd}t        |dd      }|t        |j
                        nd}|r|syt        ||      sy|j                          d{   }t        j                  d|j                               sy| j                  j                  }dd	lm}	 t        ||	      r|j                          d| j                  _        d
| _        y7 z# t$        $ r+}
t&        j)                  d| j*                  |
       Y d}
~
yd}
~
ww xY ww)u  Detect a dead client registration and force re-registration.

            When the IdP rejects our ``client_id`` with ``invalid_client`` on
            the token endpoint (token exchange or refresh), the cached client
            registration is provably dead server-side. We delete ``client.json``
            (+ stale metadata) so the SDK's next ``async_auth_flow`` takes the
            ``if not client_info`` branch and re-runs RFC 7591 dynamic client
            registration. This addresses the recurring manual-reset ritual in
            GH#36767 for the auto-detectable subset (token-endpoint rejection);
            the browser-side "Redirect URI Mismatch" case has no HTTP signal
            and is handled by ``hermes mcp reauth``.

            Conservative by construction — acts ONLY when all hold:
              * status is 400/401,
              * the request hit the discovered ``token_endpoint`` (the only
                request carrying our ``client_id``), and
              * the body carries the ``invalid_client`` error code
                (word-boundary match, so RFC 7591's ``invalid_client_metadata``
                registration error does not trip it).
            Pre-registered (config-supplied) clients are never poisoned.
            Fully best-effort: any failure here is swallowed so a detection
            miss never breaks the live auth flow.

            Covers both the authorization-code token exchange and the
            preemptive refresh — but only when ``token_endpoint`` was
            discovered (``_initialize`` prefetches it on cold-load). If that
            discovery was skipped, the guard returns early and the user falls
            back to ``hermes mcp reauth``.
            Nstatus_code)i  i  rM   rQ   requests   \binvalid_client\br   rC   Fz?MCP OAuth '%s': invalid_client detection failed (non-fatal): %s)r=   getattrrF   r   rQ   rh   r   areadresearchr   rJ   rK   rD   rL   poison_client_registrationclient_info_initializedrS   rO   rP   r;   )r>   responsestatusrU   rQ   ri   req_urlbodyrJ   rD   rV   s              r   _maybe_flag_poisoned_clientzW_make_hermes_provider_class.<locals>.HermesMCPOAuthProvider._maybe_flag_poisoned_clientB  sB    <$-- =$?+t||-=tD 'GD:JD,Q ++, 
 h	48*-/#cgg,t%W%g~>%^^-- yy!7F,,..>g'9:668+/($)! .  U,,c sz   ED( ED( EA'D( 
ED( ED( +D&,(D( EAD( %E&D( (	E1!EEEEc                 K   	 t               j                  | j                  | j                         d {    t        | !  |      }	 |j                          d {   }	 |}| j                  |       d {    |j                  |       d {   }87 e# t        $ r+}t
        j                  d| j                  |       Y d }~d }~ww xY w7 v7 Y7 B# t        $ r | j                          Y y w xY ww)Nhermes_homez:MCP OAuth '%s': pre-flow disk-watch failed (non-fatal): %s)get_managerinvalidate_if_disk_changedr;   r<   rS   rO   rP   r9   async_auth_flow	__anext__r   asendStopAsyncIterationrp   )r>   rs   rV   inneroutgoingincomingrA   s         r   r   zK_make_hermes_provider_class.<locals>.HermesMCPOAuthProvider.async_auth_flow  s     	!m>>,, $ 1 1 ?   0 G+G4E!&!22%-~H ::8DDD%*[[%::H 7  P,,c , 3
 E:%  779	s   C;2B BB C;C C C >C?C CC B 	C(!C	C;CC;C C C C85C;7C88C;)r?   r   r6   r   r7   boolr@   r   returnNone)r{   r   r   r   )r&   r'   r(   r)   r:   rE   rR   rp   r   r   __classcell__)rA   s   @r   HermesMCPOAuthProviderr4   u   s_    	"  ""'		7	7 	7  		7
 	7"L	\H	T	2*B	H*	 *	r/   r   )mcp.client.auth.oauth2r2   ImportError)r2   r   s     r   _make_hermes_provider_classr   j   s7    >{!4 {z	 "!A
  s    	!!_HERMES_PROVIDER_CLSc                      e Zd ZdZddZ	 	 	 	 	 	 	 	 ddZe	 d	 	 	 	 	 dd       Z	 	 	 	 	 	 ddZdd	 	 	 	 	 ddZ	dd	 	 	 	 	 	 	 dd	Z
dd	 	 	 	 	 dd
Zdd	 	 	 	 	 ddZ	 d	 	 	 	 	 ddZy)MCPOAuthManagera  Single source of truth for per-server MCP OAuth state.

    Thread-safe: the ``_entries`` dict is guarded by ``_entries_lock`` for
    get-or-create semantics. Per-entry state is guarded by the entry's own
    ``asyncio.Lock`` (used from the MCP event loop thread).
    c                b    i | _         t        j                         | _        t	               | _        y N)_entries	threadingr,   _entries_lockset_inflight_tasks)r>   s    r   r:   zMCPOAuthManager.__init__  s$    ?A&^^- 36%r/   c                   | j                  |      }| j                  5  | j                  j                  |      }|3|j                  |k7  r$t
        j                  d||j                  |       d}|t        ||      }|| j                  |<   |j                  7| j                  ||      |_        |j                  |d   |j                  _
        |j                  cddd       S # 1 sw Y   yxY w)aV  Return a cached OAuth provider for ``server_name`` or build one.

        Idempotent: repeat calls with the same name return the same instance.
        If ``server_url`` changes for a given name, the cached entry is
        discarded and a fresh provider is built.

        Returns None if the MCP SDK's OAuth support is unavailable.
        Nz;MCP OAuth '%s': URL changed from %s to %s, discarding cache)r   r   r   )_keyr   r   getr   rO   infor   r    _build_providerr<   )r>   r6   r   r   keyentrys         r   get_or_build_providerz%MCPOAuthManager.get_or_build_provider  s     ii$ 	"MM%%c*E U%5%5%CQ!1!1: }&)!- &+c"~~%!%!5!5k5!I>>-25a&ENN/>>+	" 	" 	"s   B=C%%C.Nc                    ddl m} |t        |      n |       }t        |j	                         j                  d            | fS )Nr   )get_hermes_homeF)strict)hermes_constantsr   r   r   
expanduserresolve)r6   r   r   homes       r   r   zMCPOAuthManager._key  s@    
 	5$/$;tK ARDOO%--U-;<kJJr/   c                ,   t         t        j                  d|       yddlm}m}m}m}m}m	}m
}	m}
m} |syt        |j                  xs i       } ||      }ddlm}  |       & |       s|j#                         s |d| d| d       |||        ||      } |	|||       |j%                  d	d      } ||      } |
|      }t        |t'        |j%                  d
            |j(                  ||||t+        |j%                  dd                  S )a  Build the underlying OAuth provider.

        Constructs :class:`HermesMCPOAuthProvider` directly using the helpers
        extracted from ``tools.mcp_oauth``. The subclass injects a pre-flow
        disk-watch hook so external token refreshes (cron, other CLI
        instances) are visible to running MCP sessions.

        Returns None if the MCP SDK's OAuth support is unavailable.
        Nz+MCP OAuth '%s': SDK auth module unavailabler   )	rD   OAuthNonInteractiveError_OAUTH_AVAILABLE_build_client_metadata_configure_callback_port_is_interactive_maybe_preregister_client_make_callback_waiter_make_redirect_handler)get_dashboard_oauth_flowzMCP OAuth for 'zQ': non-interactive environment and no cached tokens found. Run `hermes mcp login z8` interactively first to complete initial authorization._resolved_port	client_idr]   i,  )r6   r7   r   client_metadatarJ   redirect_handlercallback_handlerr]   )r   rO   warningrK   rD   r   r   r   r   r   r   r   r   r-   r   tools.mcp_dashboard_oauthr   has_cached_tokensr   r   r   float)r>   r6   r   rD   r   r   r   r   r   r   r   r   cfgrJ   r   r   resolved_portr   r   s                      r   r   zMCPOAuthManager._build_provider   s3     'NN={ 
	
 
	
 
	
  5%%+,$[1F %&.#%--/*= !>-  !!  	!g.05!'3@ 0!41-@0?##sww{34''+--#'')S12	
 		
r/   r   c                   | j                   5  | j                  j                  | j                  ||      d      }ddd       ddlm}  |||       t        j                  d|       S # 1 sw Y   1xY w)zEvict the provider from cache AND delete tokens from disk.

        Called by ``hermes mcp remove <name>`` and (indirectly) by
        ``hermes mcp login <name>`` during forced re-auth.
        Nr   )remove_oauth_tokensr   z8MCP OAuth '%s': evicted from cache and removed from disk)r   r   popr   rK   r   rO   r   )r>   r6   r   r   r   s        r   removezMCPOAuthManager.removeI  so      	QMM%%dii[&I4PE	Q 	8K[AF	
 	Q 	Qs   -A**A3c                   |y| j                   5  | j                  j                  | j                  ||      |       ddd       y# 1 sw Y   yxY w)z>Restore a provider entry removed for a failed reauthorization.N)r   r   
setdefaultr   )r>   r6   r   r   s       r   restore_entryzMCPOAuthManager.restore_entry_  sM     = 	QMM$$TYY{K%H%P	Q 	Q 	Qs   -AAc                   | j                   5  | j                  j                  | j                  ||      d       ddd       y# 1 sw Y   yxY w)zDDrop only the in-process provider, preserving persisted OAuth state.N)r   r   r   r   )r>   r6   r   s      r   evictzMCPOAuthManager.evictl  sE      	IMMdii[A4H	I 	I 	Is   -AAc                 K   ddl m}m} | j                  j	                  | j                  ||            }||j                  y|j                  4 d{     ||       ||       dz  }	 |j                         j                  }||j                  k7  rd|j                  }||_        t        |j                  d      rd|j                  _        t        j!                  d|||       	 ddd      d{    y	 ddd      d{    y7 # t        t        f$ r Y ddd      d{  7   yw xY w7 ?7 /# 1 d{  7  sw Y   yxY ww)	a  If the tokens file on disk has a newer mtime than last-seen, force
        the MCP SDK provider to reload its in-memory state.

        Returns True if the cache was invalidated (mtime differed). This is
        the core fix for the external-refresh workflow: a cron job writes
        fresh tokens to disk, and on the next tool call the running MCP
        session picks them up without a restart.
        r   )_get_token_dir_safe_filenameNFz.jsonrz   zDMCP OAuth '%s': tokens file changed (mtime %d -> %d), forcing reloadT)rK   r   r   r   r   r   r    r$   statst_mtime_nsFileNotFoundErrorOSErrorr"   hasattrrz   rO   r   )	r>   r6   r   r   r   r   tokens_pathmtime_nsolds	            r   r   z*MCPOAuthManager.invalidate_if_disk_changedx  sE     	C!!$))K"EF=ENN2:: 	 	(5>+;V:WW\8]]K&++-99 5...))&.# 5>>>:27ENN/%h
 )	 	 	* +	 	 	 &w/ 	 	 			 	 	 	 	s   AEDED:/D	A"D:+E6D67E<D:=ED8	ED3D: E+D.,E2D33D:6E8E:E EEEc                   	K    j                   j                   j                              j                  y|xs dt	        j
                         }j                  4 d{    j                  j                        		|j                         		j                  <   d	 fd}t	        j                   |             } j                  j                  |       |j                   j                  j                         ddd      d{    	 	 d{   S 7 7 # 1 d{  7  sw Y   !xY w7 # t        $ r!}t        j!                  d|       Y d}~yd}~ww xY ww)ux  Handle a 401 from a tool call, deduplicated across concurrent callers.

        Returns:
            True  if a (possibly new) access token is now available — caller
                  should trigger a reconnect and retry the operation.
            False if no recovery path exists — caller should surface a
                  ``needs_reauth`` error to the model so it stops hallucinating
                  manual refresh attempts.

        Thundering-herd protection: if N concurrent tool calls hit 401 with
        the same ``failed_access_token``, only one recovery attempt fires.
        Others await the same future.
        NFz	<unknown>c                   K   	 	j                  
       d {   } | r?j                         sj                  d       	 j                  j	                  d        y j
                  }t        |dd       }d}|)t        |dd       }t        |      r	 t         |             }j                         sj                  |       j                  j	                  d        y 7 # t        $ r d}Y Mw xY w# t        $ rB}t        j                  d
|       j                         sj                  d       Y d }~vd }~ww xY w# j                  j	                  d        w xY ww)NTrF   Fcan_refresh_tokenz&MCP OAuth '%s': 401 handler failed: %s)r   done
set_resultr%   r   r    rt   callabler   rS   rO   r   )disk_changedr    ctxcan_refreshcan_refresh_fnrV   r   r   pendingr>   r6   s         r   
_do_handlez.MCPOAuthManager.handle_401.<locals>._do_handle  sJ    !9-1-L-L'. ( (#*<<> ' 2 24 8"2 ))--c48) $)>>%h	4@&+?-4S:Mt-TN'7!826~7G2HK  '||~#..{; ))--c48?(& (1 !827K!8 % 6D'  '||~#..u56 ))--c48sz   E%C6 C#'C6 E%5C6 C% %!C6 E%#C6 %C30C6 2C33C6 6	E?8D<7E <EE E""E%z/MCP OAuth '%s': awaiting 401 handler failed: %sr   )r   r   r   r    r+   get_running_loopr$   r%   create_futurecreate_taskr   addadd_done_callbackdiscardrS   rO   r   )
r>   r6   failed_access_tokenloopr   taskrV   r   r   r   s
   ``     @@@r   
handle_401zMCPOAuthManager.handle_401  sS    $ !!$))K"89=ENN2!0[''):: ,	E ,	E''++C0G,,.)0!!#&"9 "9H **:<8$$((.&&t';';'C'CDY,	E ,	E\	 = _,	E ,	E ,	E ,	E ,	E^ ! 	NNAS 	s   A$E3+D+,E3/B!D/E3D-E3!E &E'E *E3-E3/E5D86E=E3E 	E0E+&E3+E00E3r   )r6   r   r   r   r   r   r   r   r   )r6   r   r   str | Path | Noner   ztuple[str, str])r6   r   r   r   r   r   )r6   r   r   r   r   _ProviderEntry | None)r6   r   r   r   r   r   r   r   )r6   r   r   r   r   r   )r6   r   r   r   r   r   )r6   r   r   zOptional[str]r   r   )r&   r'   r(   r)   r:   r   staticmethodr   r   r   r   r   r   r   r.   r/   r   r   r     sl   8$"$" $" %	$"
 
$"L  *.KK&K 
K KG
G
 G
 
	G
Z *.	 '	
 
6 *.QQ %Q
 'Q 
Q" *.	II '	I
 
I  *.	)) '	)
 
)` .2NN +N 
	Nr/   r   zOptional[MCPOAuthManager]_MANAGERc                 f    t         5  t        
t               at        cddd       S # 1 sw Y   yxY w)z;Return the process-wide :class:`MCPOAuthManager` singleton.N)_MANAGER_LOCKr   r   r.   r/   r   r   r     s-     
 &(H  s   '0c                 >    t         5  daddd       y# 1 sw Y   yxY w)z=Test-only helper: drop the singleton so fixtures start clean.N)r   r   r.   r/   r   reset_manager_for_testsr     s!     
   s   )r   r   r   r   r   r   )r   r0   )r   r   r   )r)   
__future__r   r+   loggingrv   r   dataclassesr   r   pathlibr   typingr   r   	getLoggerr&   rO   r   r   r   r   r*   r   r   r,   r   r   r   r.   r/   r   <module>r      s   B #   	  (   			8	$2 Q Q Q@H"X
 (C'D n Du uz	 '+
# *	 r/   