
    .cjDR                    D   U d Z ddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddlZddlZddlZddlZddlZddlmZ ddlmZ ddlmZ ddlmZmZmZmZmZ ddlmZ  ej        e          ZdZ da!ee         e"d	<    ej#                    Z$d
efdZ%de&d
dfdZ'dZ(dZ)dZ*dZ+dZ,dZ-dZ.	 ddl/m0Z0m1Z1 ddl2m3Z3 dZ(	 ddl4m5Z5 dZ)n# e6$ r dZ)Y nw xY w	 ddl4m7Z7 dZ8n# e6$ r dZ8Y nw xY w	 ddl9m.Z. n # e6$ r e:                    d           Y nw xY w	 ddl;m<Z< n"# e6$ r dZ<e:                    d           Y nw xY w	 ddl9m=Z=m>Z>m?Z?m@Z@mAZAmBZBmCZC dZ*n # e6$ r e:                    d           Y nw xY w	 ddl9mDZDmEZE dZ,n # e6$ r e:                    d           Y nw xY w	 ddl9mFZFmGZGmHZHmIZI dZ+n # e6$ r e:                    d           Y nw xY wn # e6$ r e:                    d           Y nw xY wd
eJfd ZK eK            Z-e(re-se:                    d!           d
eJfd"ZL eL            ZMejN        ejO        ejO        ejP        ejQ        ejQ        ejQ        ejQ        d#ZRd$ZSd%ZTd&ZUd'ZVd%ZWd$ZXd(ZYd)ZZd*e[d
e[fd+Z\d,Z]d&Z^ e_h d-          Z` e_h d.          Za ejb        d/ejc                  Zd ejb        d0          Zed1e&d
e&fd2Zfd3eeg         d
egfd4Zhd5e&d
e&fd6Zid7ejd
e&fd8Zk	 dd9l9mlZm n# en$ r d:ZmY nw xY wd7ejd
eJfd;Zo ejb        d<ejp                  d=f ejb        d>ejp                  d?f ejb        d@ejp                  dAf ejb        dBejp                  dCf ejb        dDejp                  dEf ejb        dFejp                  dGf ejb        dHejp                  dIf ejb        dJejp                  dKf ejb        dLejp                  dMf ejb        dNejp                  dOfg
Zqde&dPe&dQe&d
ee&         fdRZrdSegdTe&d
egfdUZsdVZtdWe&de&fdXZudYe&dSegd
eve&egf         fdZZwdYe&d[exd
eve&exf         fd\Zyd]e&d
e&fd^Zzd
e&fd_Z{d`Z|e|daz  d'z  daz   Z}dbe&d]e&d
e&fdcZ~d
e&fddZdde&d
e&fdfZ G dg dhe          Z G di dje          Zd7ejd
ejfdkZd7ejd
eJfdlZd7ejd
e&fdmZde&dned
e&fdoZde&dpegfdqZd7ejd
e&fdrZedsfdtZ G du dv          Zdwegde&d
e&fdxZ G dy dz          Z G d{ d|          Zi Zee&ef         e"d}<    e            Zee&         e"d~<   i Zee&e&f         e"d<   i Zee&e[f         e"d<   i Zee&ef         e"d<   dZdZde&d
dfdZde&d
dfdZde&d
eJfdZi Zee&ef         e"d<   i Zee&e[f         e"d<   d'ZdZde&d
dfdZde&d
dfdZded
eJfdZde&d
eJfdZdd(ddd|dede[d
eJfdZd(dde&dd|de&de[d
eJf
dZdaeve"d<   d
evfdZd7ejd
eJfdZde&d7ejde&fdZdZeve"d<   dZd7ejd
eJfdZde&d7ejde&fdZ e            Zee"d<   i Zee&e&f         e"d<   daeej                 e"d<   daeej                 e"d<    ej#                    Zi Zeee&f         e"d<    e            Zee"d<   i Zeee&f         e"d<   i Zeeef         e"d<   d
efdZdZeve&df         e"d<   ded
efdZd Zd ZddZd Zdde[fdZd
e&fdZd Zdee&egf         d
ee&egf         fdZd
ee&egf         fdZde&dpegd
efdZde&ded
eJfdZde&d
ee         fdZded
dfdZde&dPe&de[fdZde&de[fdÄZde&de[fdĄZde&de[fdńZde&de[fdƄZde&fdǄZdwegdz  d
egfdȄZde&d
e&fdʄZdZdZde&dPe&d
e&fd̈́Zde&d
egfd΄Zde&d
eeg         fdτZdede&d
ee&         fdфZddedeJd
eJfdӄZdpegde&d
ee[         fdՄZdddddڜZdddddڜZdPe&de&d
dfd݄ZdPe&d
dfdބZde&dedpegd
eeg         fd߄Zd
ee&         fdZde&dedpegd
ee&         fdZde&dpegd
ee&         fdZdee&egf         d
ee&         fdZd
ee&         fdZdPe&d
eJfdZd
eeg         fdZd
ee&eev         f         fdZ ej#                    Zd
eJfdZd
efdZdddddeJd
efdZdexded
efdZd Z	 	 ddeJdee&         d
dfdZd
eJfdZdddeJd
eJfdZdS )a'  
MCP (Model Context Protocol) Client Support

Connects to external MCP servers via stdio, HTTP/StreamableHTTP, or SSE
transport, discovers their tools, and registers them into the hermes-agent
tool registry so the agent can call them like any built-in tool.

Configuration is read from ~/.hermes/config.yaml under the ``mcp_servers`` key.
The ``mcp`` Python package is optional -- if not installed, this module is a
no-op and logs a debug message.

Example config::

    mcp_servers:
      filesystem:
        command: "npx"
        args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
        env: {}
        timeout: 120         # per-tool-call timeout in seconds (default: 300)
        connect_timeout: 60  # initial connection timeout (default: 60)
        keepalive_interval: 10  # liveness ping cadence in seconds (default:
                                # 180). Set below the server's session TTL for
                                # servers that GC idle sessions quickly (e.g.
                                # Unreal Engine editor MCP, ~15s). Floored at 5s.
        idle_timeout_seconds: 3600      # optional stdio recycle after idle
        max_lifetime_seconds: 86400     # optional stdio recycle after age
        # The recycle settings may also live under lifecycle: {...}.
        # Use 0 to disable either recycle limit.
      github:
        command: "npx"
        args: ["-y", "@modelcontextprotocol/server-github"]
        env:
          GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_..."
        supports_parallel_tool_calls: true  # tools from this server may run concurrently
      remote_api:
        url: "https://my-mcp-server.example.com/mcp"
        headers:
          Authorization: "Bearer sk-..."
        timeout: 180
        skip_preflight: true  # bypass the content-type probe for a valid
                              # Streamable HTTP endpoint that answers HEAD/GET
                              # with a non-MCP content type but serves real
                              # MCP over POST. Default: false.
      searxng:
        url: "http://localhost:8000/sse"
        transport: sse       # use SSE transport instead of Streamable HTTP
        timeout: 180
        connect_timeout: 10
        command: "npx"
        args: ["-y", "analysis-server"]
        sampling:                    # server-initiated LLM requests
          enabled: true              # default: true
          model: "gemini-3-flash"    # override model (optional)
          max_tokens_cap: 4096       # max tokens per request
          timeout: 30                # LLM call timeout (seconds)
          max_rpm: 10                # max requests per minute
          allowed_models: []         # model whitelist (empty = all)
          max_tool_rounds: 5         # tool loop limit (0 = disable)
          log_level: "info"          # audit verbosity

Features:
    - Stdio transport (command + args) and HTTP/StreamableHTTP transport (url)
    - SSE transport (transport: sse) for MCP servers using the SSE protocol
    - Automatic reconnection with exponential backoff (up to 5 retries)
    - Environment variable filtering for stdio subprocesses (security)
    - Credential stripping in error messages returned to the LLM
    - Configurable per-server timeouts for tool calls and connections
    - Thread-safe architecture with dedicated background event loop
    - Sampling support: MCP servers can request LLM completions via
      sampling/createMessage (text and tool-use responses)
    - Parallel tool call opt-in: per-server ``supports_parallel_tool_calls``
      flag allows concurrent execution of tools from the same server

Architecture:
    A dedicated background event loop (_mcp_loop) runs in a daemon thread.
    Each MCP server runs as a long-lived asyncio Task on this loop, keeping
    its transport context alive. Tool call coroutines are scheduled onto the
    loop via ``run_coroutine_threadsafe()``.

    On shutdown, each server Task is signalled to exit its ``async with``
    block, ensuring the anyio cancel-scope cleanup happens in the *same*
    Task that opened the connection (required by anyio).

Thread safety:
    _servers and _mcp_loop/_mcp_thread are accessed from both the MCP
    background thread and caller threads.  All mutations are protected by
    _lock so the code is safe regardless of GIL presence (e.g. Python 3.13+
    free-threading).
    N)SimpleNamespace)Callable)datetime)Any	CoroutineDictListOptional)urlparseg      (@_mcp_stderr_log_fhreturnc                     t           5  t          t          cddd           S 	 ddlm}   |             dz  }|                    dd           |dz  }t          |dd	d
d          }|                                 |anl# t          $ r_}t          	                    d|           	 t          t          j        dd	          an# t          $ r t          j        aY nw xY wY d}~nd}~ww xY wt          cddd           S # 1 swxY w Y   dS )a^  Return a shared append-mode file handle for MCP subprocess stderr.

    Opened once per process and reused for every stdio server.  Must have a
    real OS-level file descriptor (``fileno()``) because asyncio's subprocess
    machinery wires the child's stderr directly to that fd.  Falls back to
    ``/dev/null`` if opening the log file fails.
    Nr   )get_hermes_homelogsT)parentsexist_okzmcp-stderr.logautf-8replace   )encodingerrors	bufferingz0Failed to open MCP stderr log, using devnull: %swr   )_mcp_stderr_log_lockr   hermes_constantsr   mkdiropenfileno	Exceptionloggerdebugosdevnullsysstderr)r   log_dirlog_pathfhexcs        0/home/ice/.hermes/hermes-agent/tools/mcp_tool.py_get_mcp_stderr_logr-      s    
 " ")%" " " " " " " "	0888888%o''&0GMM$M666!11H hgiSTUUUBIIKKK!# 	0 	0 	0LLKSQQQ0%)"*cG%L%L%L"" 0 0 0 &)Z"""0		0 "/" " " " " " " " " " " " " " " " " "s_   C=AA>=C=>
C'C"$C C"CC"CC"C="C''	C==DDserver_namec                     t                      }	 t          j                                        d          }|                    d| d|  d           |                                 dS # t          $ r Y dS w xY w)a  Write a human-readable session marker before launching a server.

    Gives operators a way to find each server's output in the shared
    ``mcp-stderr.log`` file without needing per-line prefixes (which would
    require a pipe + reader thread and complicate shutdown).
    z%Y-%m-%d %H:%M:%Sz
===== [z] starting MCP server 'z' =====
N)r-   r   nowstrftimewriteflushr!   )r.   r*   tss      r,   _write_stderr_log_headerr5      s     
		B\^^$$%899
NRNNNNNOOO





   s   AA( (
A65A6Fz
2025-03-26)ClientSessionStdioServerParameters)stdio_clientT)streamablehttp_client)streamable_http_client)LATEST_PROTOCOL_VERSIONzRmcp.types.LATEST_PROTOCOL_VERSION not available -- using fallback protocol version)
sse_clientzAmcp.client.sse.sse_client not available -- SSE transport disabled)CreateMessageResultCreateMessageResultWithTools	ErrorDataSamplingCapabilitySamplingToolsCapabilityTextContentToolUseContentz5MCP sampling types not available -- sampling disabled)ElicitRequestParamsElicitResultz;MCP elicitation types not available -- elicitation disabled)ServerNotificationToolListChangedNotificationPromptListChangedNotificationResourceListChangedNotificationzGMCP notification types not available -- dynamic tool discovery disabledz6mcp package not installed -- MCP tool support disabledc                      t           sdS 	 dt          j        t                    j        v S # t
          t          f$ r Y dS w xY w)zCheck if ClientSession accepts ``message_handler`` kwarg.

    Inspects the constructor signature for backward compatibility with older
    MCP SDK versions that don't support notification handlers.
    Fmessage_handler_MCP_AVAILABLEinspect	signaturer6   
parameters	TypeError
ValueError     r,   _check_message_handler_supportrU     sV      u G$5m$D$D$OOOz"   uu   + A A zKMCP SDK does not support message_handler -- dynamic tool discovery disabledc                      t           sdS 	 dt          j        t                    j        v S # t
          t          f$ r Y dS w xY w)ae  Check if ClientSession accepts the ``logging_callback`` kwarg.

    Mirrors ``_check_message_handler_support`` for backward compatibility
    with older MCP SDK versions.  Without a logging_callback, the SDK's
    default handler silently discards every ``notifications/message`` a
    server emits, so server-side diagnostics never reach Hermes' logs.
    Flogging_callbackrL   rS   rT   r,   _check_logging_callback_supportrY   '  sV      u!W%6}%E%E%PPPz"   uurV   )r#   infonoticewarningerrorcriticalalert	emergency,  <         g      .@g?secondsc           	      n    t          d| t          j        dt          z
  dt          z             z            S )z<Return ``seconds`` with +/-20% uniform jitter, floored at 0.              ?)maxrandomuniform_BACKOFF_JITTER)re   s    r,   	_jitteredrm   [  s@    sGfnS?-B-0?-BD D D E E ErT      >   HOMELANGPATHTERMUSERSHELLLC_ALLTMPDIR>   PROGRAMFILES(X86)COMMONPROGRAMFILES(X86)OSTMPTEMPPUBLICWINDIRAPPDATACOMSPECPATHEXTHOMEPATHUSERNAME	HOMEDRIVE
SYSTEMROOT
USERDOMAINPROGRAMDATASYSTEMDRIVEUSERPROFILECOMPUTERNAMELOCALAPPDATAPROGRAMFILESPROGRAMW6432ALLUSERSPROFILECOMMONPROGRAMFILESCOMMONPROGRAMW6432NUMBER_OF_PROCESSORSPROCESSOR_ARCHITECTUREz(?:ghp_[A-Za-z0-9_]{1,255}|sk-[A-Za-z0-9_]{1,255}|Bearer\s+\S+|token=[^\s&,;\"']{1,255}|key=[^\s&,;\"']{1,255}|API_KEY=[^\s&,;\"']{1,255}|password=[^\s&,;\"']{1,255}|secret=[^\s&,;\"']{1,255})z\$\{([^}]+)\}refc                     |                                  } |                     d          r)| t          d          d                                          } | S )a  Normalize a ``${...}`` reference body into an env-var name.

    Accepts Cursor-style ``${env:VAR}`` in addition to plain ``${VAR}`` by
    stripping a leading ``env:`` prefix. The result is the bare variable name
    to look up in the secret scope / ``os.environ``.
    zenv:N)strip
startswithlen)r   s    r,   _env_ref_namer     sJ     ))++C
~~f (#f++,,%%''JrT   user_envc                 J   	 ddl m} n# t          $ r d}Y nw xY wi }t          j                                        D ]P\  }}|t          v s=|                                t          v s"|	                    d          s| ||          r|||<   Q| r|
                    |            |S )a  Build a filtered environment dict for stdio subprocesses.

    Only passes through safe baseline variables (PATH, HOME, etc.) and XDG_*
    variables from the current process environment, secrets injected by an
    external secret source (Bitwarden, 1Password, plugin backends) that
    Hermes explicitly tagged during dotenv loading, plus any variables
    explicitly specified by the user in the server config.

    This prevents accidentally leaking secrets like API keys, tokens, or
    credentials to MCP server subprocesses.  Secret-source-injected vars are
    an exception: users configured that backend specifically so Hermes and
    its subprocesses can consume those credentials without duplicating them
    in every MCP server's ``env:`` block.
    r   )get_secret_sourceNXDG_)hermes_cli.env_loaderr   r!   r$   environitems_SAFE_ENV_KEYSupper_SAFE_ENV_KEYS_CASE_INSENSITIVEr   update)r   r   envkeyvalues        r,   _build_safe_envr     s    !;;;;;;; ! ! ! !
Cj&&((  
U>!!yy{{===~~f%% >!-2C2CC2H2H-CH 

8J   	 textc                 8    t                               d|           S )zStrip credential-like patterns from error text before returning to LLM.

    Replaces tokens, keys, and other secrets with [REDACTED] to prevent
    accidental credential exposure in tool error responses.
    z
[REDACTED])_CREDENTIAL_PATTERNsubr   s    r,   _sanitize_errorr     s     ""<666rT   r+   c                 j    t          |                                           }|r|nt          |           S )aX  Return a non-empty human-readable string for *exc*.

    Some exception classes (e.g. ``anyio.ClosedResourceError``) are raised
    without a message argument, so ``str(exc)`` is ``""``.  This helper
    falls back to ``repr(exc)`` so that error messages shown to the user
    and logged to disk always carry *some* diagnostic information.
    )strr   repr)r+   r   s     r,   _exc_strr     s.     s88>>D&44T#YY&rT   )METHOD_NOT_FOUNDic                     t          | dd          }t          |dd          }|t          k    rdS t          |                                           }|sdS t          t                    |v pd|v pd|v pd|v S )	u  Return True if *exc* is a JSON-RPC ``method not found`` (-32601).

    ``ping`` is an *optional* MCP utility (spec: "optional ping mechanism").
    A server that doesn't implement it answers a ping with -32601 rather than
    an empty result. Structurally inspect ``McpError.error.code`` first, then
    fall back to a substring match so detection survives SDK version drift and
    servers that surface the condition as a plain message.

    The substring fallback matters when a server reports method-not-found
    without a structural ``-32601`` code (e.g. surfaced as a plain exception
    string). Besides the canonical "method not found", many JSON-RPC
    implementations phrase it as "Unknown method: <name>" — agentmemory's MCP
    server is one such case (#50028). Without matching that phrasing the
    ping→list_tools fallback never latches and the keepalive reconnect-loops.
    r]   NcodeTFzmethod not foundzunknown methodznot found: ping)getattr_JSONRPC_METHOD_NOT_FOUNDr   lower)r+   errr   msgs       r,   _is_method_not_found_errorr     s    " #w
%
%C3%%D(((t
c((..

C u%&&#- 	$$	$s"	$ #	rT   z)ignore\s+(all\s+)?previous\s+instructionsz8prompt override attempt ('ignore previous instructions')zyou\s+are\s+now\s+az.identity override attempt ('you are now a...')z1your\s+new\s+(task|role|instructions?)\s+(is|are)ztask override attemptzsystem\s*:\s*zsystem prompt injection attemptz <\s*(system|human|assistant)\s*>zrole tag injection attemptz'do\s+not\s+(tell|inform|mention|reveal)zconcealment instructionz(curl|wget|fetch)\s+https?://znetwork command in descriptionzbase64\.(b64decode|decodebytes)zbase64 decode referencezexec\s*\(|eval\s*\(zcode execution referencez&import\s+(subprocess|os|shutil|socket)zdangerous import reference	tool_namedescriptionc                     g }|s|S t           D ]/\  }}|                    |          r|                    |           0|r1t                              d| |d                    |          |           |S )zxScan an MCP tool description for prompt injection patterns.

    Returns a list of finding strings (empty = clean).
    uU   MCP server '%s' tool '%s': suspicious description content — %s. Description: %.200s; )_MCP_INJECTION_PATTERNSsearchappendr"   r\   join)r.   r   r   findingspatternreasons         r,   _scan_mcp_descriptionr   5  s    
 H 2 $ $>>+&& 	$OOF### 
"DIIh$7$7		
 	
 	
 OrT   r   	directoryc                    t          | pi           }|s|S |                    dd          }d |                    t          j                  D             }||vr|g|}|rt          j                            |          n||d<   |S )z=Prepend *directory* to env PATH if it is not already present.rq    c                     g | ]}||S rS   rS   ).0parts     r,   
<listcomp>z!_prepend_path.<locals>.<listcomp>Q  s    AAAdDATAAArT   )dictgetsplitr$   pathsepr   )r   r   updatedexistingpartss        r,   _prepend_pathr   J  s    39"ooG {{62&&HAAhnnRZ88AAAE#U#05Dbjooe,,,9GFONrT   2   
items_attrc           	      |  K   g }d}t          t                    D ]p}|r | |          n	 |              d{V }|                    t          ||d          pg            t          |dd          }t	          |t
                    r|s n1qt                              d||t          t          |                     |S )u  Drain a paginated MCP ``list_*`` call by following ``nextCursor``.

    The MCP spec allows servers to paginate ``tools/list``,
    ``resources/list``, and ``prompts/list`` responses via an opaque
    ``nextCursor`` token. The Python SDK's ``ClientSession.list_*`` methods
    fetch exactly one page per call, so a client that never passes the
    cursor back silently sees only the first page — on a paginated server
    every tool/resource/prompt past page 1 would be invisible to the agent.

    Args:
        list_method: Bound ``session.list_tools`` / ``list_resources`` /
            ``list_prompts`` coroutine function.
        items_attr: Result attribute holding the page's items
            (``"tools"``, ``"resources"``, or ``"prompts"``).
        server_name: For log messages.

    Returns:
        Combined list of items across all pages. Callers must hold the
        server's ``_rpc_lock`` for the duration so pages come from a
        consistent snapshot.
    N)cursor
nextCursorzHMCP server '%s': %s pagination exceeded %d pages; truncating at %d items)	range_MCP_LIST_MAX_PAGESextendr   
isinstancer   r"   r\   r   )list_methodr   r.   r   r   _results          r,   _paginate_full_listr   ^  s      , EF&'' 
 
6<O62222++--PPPPPPWVZ66<"===t44 &#&& 	f 	E	 	%%8#e**	
 	
 	

 LrT   commandc           
         t           j                            t          |                                                     }t          |pi           }t           j        |vrpd|v r|d         nd}t          j        ||          }|r|}nF|dv rAt           j                            t          j	        dt           j        
                    t           j                            d          d                              }t           j        
                    |dd	|          t           j        
                    t           j                            d          d
d	|          t           j        
                    t           j        ddd	|          g}|D ]D}t           j                            |          r#t          j        |t           j                  r|} nEt           j                            |          }	|	rt          ||	          }||fS )zResolve a stdio MCP command against the exact subprocess environment.

    This primarily exists to make bare ``npx``/``npm``/``node`` commands work
    reliably even when MCP subprocesses run under a filtered PATH.
    rq   N)path>   npmnpxnodeHERMES_HOME~z.hermesr   binz.localusrlocal)r$   r   
expanduserr   r   r   sepshutilwhichgetenvr   isfileaccessX_OKdirnamer   )
r   r   resolved_commandresolved_envpath_arg	which_hithermes_home
candidates	candidatecommand_dirs
             r,   _resolve_stdio_commandr     s    w))#g,,*<*<*>*>??	r??L	v%%%+1\+A+A<''tL!1AAA	 	(!777',,	!27<<0B0B30G0G#S#S  K [&%9IJJRW//44hGWXX RVUGU<LMMJ (  	7>>),, 9bg1N1N '0$E'//"233K @$\;??\))rT   argsc                 ~   t           j        dk    r| |fS 	 t          j                    }n# t          $ r | |fcY S w xY wt           j                            t           j                            t           j                            t                              d          dt          |          d| g|}t          j        |fS )a&  Wrap a stdio MCP server command in the parent-death watchdog supervisor.

    On POSIX, the watchdog records this process's PID and later detects parent
    death directly through ``getppid()``. Returns the (command, args) unchanged
    on non-POSIX platforms or if the PID cannot be read.
    posixzmcp_stdio_watchdog.pyz--ppidz--)r$   namegetpidr!   r   r   r   abspath__file__r   r&   
executable)r   r   my_pidwatchdog_argss       r,   _wrap_command_with_watchdogr
    s     
w'
 }   } 	RW__RW__X%>%>??AXYY#f++	
 
M >=((s   * ;;	mime_typec                     ddl }| pd                    dd          d                                                                         }|dv rdS |                    |          pdS )	z>Return a reasonable file extension for an MCP image MIME type.r   Nr   ;r   >   	image/jpg
image/jpegz.jpgz.png)	mimetypesr   r   r   guess_extension)r  r  
normalizeds      r,   "_mcp_image_extension_for_mime_typer    sn    /r((a00399;;AACCJ000v$$Z00:F:rT   c                    ddl }t          | dd          }t          | dd          }t          |pd                              dd          d                                                                         }||                    d          sdS 	 |                    |          }n;# t          t          f$ r'}t                              d	||           Y d}~dS d}~ww xY w	 dd
lm}  ||t          |                    }nY# t          $ r t                              d           Y dS t"          $ r&}t                              d|           Y d}~dS d}~ww xY wd| S )a  Cache an MCP ``ImageContent`` block to the shared image cache and
    return a ``MEDIA:<path>`` tag that Hermes gateways know how to render.

    Returns an empty string when *block* is not an image, when the base64
    payload is malformed, or when the cache helper rejects the bytes (e.g.
    non-image MIME masquerading as an image). Errors are logged, not raised:
    a single bad block shouldn't kill the tool result, and the caller will
    fall through to any text blocks that did parse.
    r   NdatamimeTyper   r  r   zimage/z&MCP image block decode failed (%s): %s)cache_image_from_bytesextu@   MCP image caching skipped — gateway.platforms.base unavailablez MCP image block cache failed: %sMEDIA:)base64r   r   r   r   r   r   	b64decoderQ   rR   r"   r\   gateway.platforms.baser  r  ImportErrorr#   r!   )	blockr  r  r  normalized_mime	raw_bytesr+   r  
image_paths	            r,   _cache_mcp_image_blockr#    s    MMM5&$''Dz400I)/r**00a88;AACCIIKKO|?55h??|r$$T**		z"   ?RUVVVrrrrrAAAAAA++2?CC
 
 


     	WXXXrr   93???rrrrr !J   s6   B& &C7CC" D $E*	E3EEi      uric           	         ddl }ddl}ddlm} ddlm}m} d}| rQ	  | | |t          |                     j        pd                    j	        }n# t          t          f$ r d}Y nw xY w|                    dd|                                          }t          |          dk    rf|                    d          \  }}	}
|	rAdt          |
          cxk     rd	k    r'n n$|ddt          |
          z
  d
z
           dz   |
z   }n
|dd         }|r|dv r^|pd                    dd
          d                                                                         }|                    |          pd}
d|
 }|S )u  Derive a safe display filename for an MCP resource.

    Only the last path segment of the URI is considered, and only as a
    *name hint* — `cache_document_from_bytes` re-sanitizes and prefixes it,
    so remote path components can't influence the cache location.
    r   N)Path)r   unquoter   z[\x00-\x1f\x7f]   .   r   >   ..r*  r  z.binresource)r  repathlibr'  urllib.parser   r(  r   r   r  rR   rQ   r   r   r   
rpartitionr   r   r  )r%  r  r  _rer'  r   r(  r  stemdotr  r  s               r,   _mcp_resource_filenamer5    s    ........D
 	4S 2 2 7 =2>>??DDDI& 	 	 	DDD	
 77%r4006688D
4yy3--c3 	1s3xx%%%%2%%%%%,#C.1,,-3c9DD:D  4;&&o2,,S!44Q7==??EEGG
''
33=v#Ks   6A A)(A)c                    ddl }t          | dd          }t          t          | dd          pd                              dd          d                                                                         }||                    d          sdS t          |          t          k    rd	t          |          d
z  dz   dS 	 |	                    |          }n;# t          t          f$ r'}t                              d||           Y d}~dS d}~ww xY wt          |          t          k    rdt          |           dS 	 ddlm} ddl}dddd                    |          p|                    |          pd} |||          }	nY# t(          $ r t                              d           Y dS t,          $ r&}t                              d|           Y d}~dS d}~ww xY wd|	 S )u   Cache an MCP ``AudioContent`` block and return a ``MEDIA:`` tag.

    Returns an empty string when *block* is not audio or on any failure —
    same fail-open contract as ``_cache_mcp_image_block``.
    r   Nr  r  r   r  r   zaudio/z)[MCP audio resource too large to cache: ~rd   r$   bytes]z&MCP audio block decode failed (%s): %sz([MCP audio resource too large to cache: )cache_audio_from_bytesz.wav)z	audio/wavzaudio/x-wavz
audio/wavez.oggr  u@   MCP audio caching skipped — gateway.platforms.base unavailablez MCP audio block cache failed: %sr  )r  r   r   r   r   r   r   r   _MCP_RESOURCE_MAX_B64_CHARSr  rQ   rR   r"   r\   _MCP_RESOURCE_MAX_BYTESr  r8  r  r   r  r  r#   r!   )
r  r  r  r  r!  r+   r8  r  r  
audio_paths
             r,   _cache_mcp_audio_blockr<  <  s>    MMM5&$''DGE:t44:;;AA#qII!LRRTTZZ\\I|9//99|r
4yy...V3t99q=A;MVVVV$$T**		z"   ?CPPPrrrrr 9~~///Q#i..QQQQAAAAAA !vNNRRS\]] ((33 	
 ,+I3???

   WXXXrr   93???rrrrr !J   s7   ?C D&DD<AF $G+	G4GGr   c                 d   t          | dd          }|dk    s&t          | d          rt          | d          sw|dk    rqt          | dd          }|sdS t          | dd          pd}t          | d	d          pd}d
| }|r|d| z  }|r|d| z  }|rt          |d          nd}d| d| dS t          | dd          }|dS t          |dd          }	|	t          |	          S t          |dd          }
|
dS ddl}t          t          |dd          pd          }t          t          |d	d          pd          }t          |
          t          k    rdt          |
          dz  dz   d| dS 	 |                    |
          }nD# t          t          f$ r0}t                              d|p||           d|p| dcY d}~S d}~ww xY wt          |          t          k    rdt          |           d| dS 	 ddlm}  ||t          ||                    }nw# t           $ r5 t                              d           dt          |           d|pd d cY S t$          $ r-}t                              d!|           d"|p| dcY d}~S d}~ww xY w|pd}d#| d$| d%t          |           d&S )'u  Render an MCP ``ResourceLink`` or ``EmbeddedResource`` block as text.

    - ``EmbeddedResource`` with text contents → the text itself.
    - ``EmbeddedResource`` with blob contents → bytes are decoded (size-capped)
      and materialized into the Hermes document cache; returns a marker with
      the local path so file/terminal tools can consume it.
    - ``ResourceLink`` → the URI plus a pointer at the server's read_resource
      tool. No network fetch happens here; the link is only readable through
      the originating MCP session.

    Returns an empty string for non-resource blocks. Failures are logged and
    reported inline rather than silently dropping the block.
    typer   resource_linkr%  r-  r   Nr  r  zuri=z, name=z, mimeType=read_resourcez#the MCP server's read_resource toolz[MCP resource link: u    — fetch it with ]blobr   z,[MCP embedded resource too large to cache: ~rd   r$  z bytes, uri=z,MCP embedded resource decode failed (%s): %sz-[MCP embedded resource could not be decoded: z+[MCP embedded resource too large to cache: )cache_document_from_bytesuC   MCP resource caching skipped — gateway.platforms.base unavailablez![MCP embedded resource received (z bytes, zunknown typez1) but document cache unavailable in this process]z&MCP embedded resource cache failed: %sz,[MCP embedded resource could not be cached: z[MCP resource saved to  (, u5    bytes) — read it with read_file or terminal tools])r   hasattrmcp_prefixed_tool_namer   r  r   r9  r  rQ   rR   r"   r\   r:  r  rC  r5  r  r#   r!   )r  r.   
block_typer%  r  mimedetailsreaderr-  r   rB  r  r!  r+   rC  r   details                    r,   _render_mcp_resource_blockrM  d  s    ++J_$$u %&-eZ&@&@ %EOSYEYEYeUD)) 	2ufb))/Ruj"--3,, 	(''''G 	,+T+++G 7";@@@6 	
 LgKK&KKKKuj$//Hr8VT**D4yy8VT**D|rMMM
ghr**0b
1
1CwxR006B77D
4yy...dc$ii!mq>Pdd^addddN$$T**		z" N N NEt{sTWXXXMt{sMMMMMMMMMN 9~~///_S^^__Y\____	MDDDDDD((4J3PT4U4UVV U U UZ[[[ U3y>>  U  U4KaSa  U  U  U  	U  	U  	U M M M?EEELdkcLLLLLLLLLM #^F|T||V||s9~~||||sB   4F
 
G%G GG= H <J	J%"JJJc                       e Zd ZdZdS )InvalidMcpUrlErrora  Raised when a remote MCP server's ``url`` cannot be parsed as http(s)://.

    Validated once at startup so we fail fast with a clear message instead of
    burning through the reconnect-backoff loop on every attempt.  (Ported from
    anomalyco/opencode#25019.)
    N__name__
__module____qualname____doc__rS   rT   r,   rO  rO    s           rT   rO  c                       e Zd ZdZdS )NonMcpEndpointErroran  Raised when an HTTP MCP URL serves a non-MCP response.

    A genuine MCP Streamable-HTTP endpoint answers with ``application/json``
    or ``text/event-stream``.  Anything else on a 2xx response (typically
    ``text/html`` from a web-app root) means the configured ``url`` points at
    the wrong place.  This is non-retryable: every attempt returns the same
    page, so the reconnect-backoff loop is skipped and the server is reported
    failed immediately with an actionable message.

    Subclasses :class:`ConnectionError` so callers that only catch the broad
    class still treat it as a connection problem.
    NrP  rS   rT   r,   rV  rV    s           rT   rV  c                    t          | t                    r| j        r|                     t          t
          f          \  }}|I|}t          |t                    r0|j        r)|j        d         }t          |t                    r|j        )|| j        d         }| j        D ]}t          |          s|} n|} t          | t                    r| j        | S )u  Extract the root-cause exception from anyio TaskGroup wrappers.

    The MCP SDK uses anyio task groups, which wrap errors in
    ``BaseExceptionGroup`` / ``ExceptionGroup``. Their ``str()`` is opaque —
    "unhandled errors in a TaskGroup (1 sub-exception)" — so log sites must
    unwrap to surface the real cause (e.g. ``BrokenPipeError`` on a dead
    stdio pipe, "401 Unauthorized" on an auth failure).

    Adapted from :func:`hermes_cli.mcp_config._unwrap_exception_group` with
    two extra behaviours needed on the runtime path:

    - **Fatal leaves re-raise.** A ``KeyboardInterrupt`` / ``SystemExit``
      anywhere in the (possibly nested) group must propagate to the
      interpreter, never be flattened into a loggable error.
    - **Prefer non-cancellation leaves.** When a group carries both a real
      error and the ``CancelledError``s that anyio cancellation sprays across
      sibling tasks, the real error is the root cause worth logging.
    Nr   )r   BaseExceptionGroup
exceptionsr   KeyboardInterrupt
SystemExit_contains_only_cancellation)r+   fatal_restleafchosenr   s         r,   _unwrap_exception_groupra    s    & S,
-
- #. yy"3Z!@AAu"'DT#566 *4? *q) T#566 *4? *J "> 	 	C.s33   S,
-
- #.   JrT   c                     t          | t                    rt          d | j        D                       S t          | t          j                  S )z?True if ``exc`` is (or a group containing only) CancelledError.c              3   4   K   | ]}t          |          V  d S N)r\  )r   r   s     r,   	<genexpr>z._contains_only_cancellation.<locals>.<genexpr>  s+      NN.s33NNNNNNrT   )r   rX  allrY  asyncioCancelledError)r+   s    r,   r\  r\    sH    #)** ONNs~NNNNNNc71222rT   c                 h   t          |           }t          |          rdS t          |t          t          f          rdS t          |t
                    rdS t          |t                    r!t          |dd          t          j	        k    rdS t          t          |dd          dd          }|dv rdS dS )u  Classify an MCP connection failure as ``'permanent'`` or ``'transient'``.

    Permanent failures are deterministic — every retry hits the same wall, so
    burning the retry ladder (and log lines) on them is pure noise; ``run()``
    parks them immediately:

    - auth failures (401/403) — need new credentials, not a retry;
    - :class:`NonMcpEndpointError` — the URL serves a web page, not MCP;
    - :class:`InvalidMcpUrlError` — unusable config;
    - ``FileNotFoundError`` / ``ENOENT`` — the stdio command doesn't exist.

    Everything else (network blips, EOF, ``ClosedResourceError``, transport
    TaskGroup drops, timeouts) is transient and keeps the normal
    retry-with-backoff ladder.
    	permanenterrnoNresponsestatus_code)  i  	transient)
ra  _is_auth_errorr   rV  rO  FileNotFoundErrorOSErrorr   rk  ENOENT)r+   rootstatuss      r,   _classify_mcp_failurerv    s      #3''Dd {$,.@ABB {$)** {$   WT7D%A%AU\%Q%Q{ WT:t44mTJJF{;rT   urlc           
      0   t          |t                    s't          d|  dt          |          j                   |                                }|st          d|  d          	 t          |          }n,# t          $ r}t          d|  d|d| d          |d}~ww xY w|j        	                                dvrt          d|  d	|j        d|d          |j
        st          d|  d
|d          |j        st          d|  d|d          |S )ut  Return the URL as a string if it's a valid http(s) remote MCP URL.

    Raises :class:`InvalidMcpUrlError` otherwise with a message naming the
    offending server, so users can spot the bad entry in their config.

    Accepts:
    - ``http://host`` / ``https://host`` with optional port, path, query
    - IPv4, IPv6 (bracketed), DNS hostnames

    Rejects:
    - Non-string values (``None``, dicts, ints)
    - Missing scheme (``example.com/mcp``)
    - Non-http(s) schemes (``file://``, ``ws://``, ``stdio:`` — stdio servers
      use the ``command`` key, not ``url``)
    - Empty host (``http://``, ``https:///path``)
    zInvalid MCP URL for 'z': expected a string, got z': empty url': rD  )N>   httphttpsz%': scheme must be http or https, got z': missing host (z': missing hostname ()r   r   rO  r>  rQ  r   r   r!   schemer   netlochostname)r.   rw  strippedparsedr+   s        r,   _validate_remote_mcp_urlr    s   " c3 
 $K $ $Cyy!$ $
 
 	
 yy{{H 
 =K===
 
 	
(##    HKHHHHH#HHH
 
	 }$555 ;K ; ; -; ;-5; ; ;
 
 	
 = 
 OKOO(OOO
 
 	

 ? 
 K    
 
 	
 Os   'A7 7
B BB configc                     |                     d          }|                     d          }||dS dt          dt          dt          f fd}t          |t          t
          f          r|t          d  d	          t          |          d
k    r( ||d         d          } ||d         d          }||fS t          |          dk    rY ||d         d          } ||d         d          }|d
         }t          |t                    st          d  d          |||fS t          d  dt          |           d           ||d          }| ||d          }||fS |S )us  Resolve the ``client_cert`` / ``client_key`` config for mTLS.

    Returns whatever ``httpx``'s ``cert=`` parameter accepts, or ``None`` when
    no client certificate is configured:

      - ``None`` if neither ``client_cert`` nor ``client_key`` is set.
      - A single absolute path string if ``client_cert`` is a string and
        ``client_key`` is unset (PEM file with cert + key combined).
      - A ``(cert_path, key_path)`` tuple when both are set, or when
        ``client_cert`` is a 2-element list/tuple.
      - A ``(cert_path, key_path, password)`` tuple when ``client_cert`` is
        a 3-element list/tuple — the third element is the key passphrase.

    User paths support ``~`` expansion. Missing files raise ``FileNotFoundError``
    with a server-scoped message so the failure surfaces as a clear setup
    error rather than an opaque TLS handshake error.
    client_cert
client_keyNr   labelr   c           
         t          | t                    r|                                 s+t          d d| dt	          |           j         d          t          j                            |                                           }t          j        	                    |          st          d d| d|          |S )NMCP server 'ry  z& must be a non-empty string path (got rz  z not found at )r   r   r   rR   r>  rQ  r$   r   r   r   rq  )r   r  expandedr.   s      r,   _expandz%_resolve_client_cert.<locals>._expandg  s    $$$ 	DJJLL 	;{ ; ;u ; ;$(JJ$7; ; ;   7%%djjll33w~~h'' 	# {    u          rT   r  zY': specify either client_cert as a list [cert, key] OR client_cert + client_key, not both   r   zclient_cert[0]r   zclient_cert[1]rd   z3': client_cert[2] (key passphrase) must be a stringz8': client_cert list form must have 2 or 3 elements (got rz  )r   r   r   r   listtuplerR   r   )r.   r  raw_certraw_keyr  	cert_pathkey_pathpasswords   `       r,   _resolve_client_certr  O  s   $ zz-((Hjj&&GGOtc # #       (T5M** 
L{ L L L   x==A-=>>Iwx{,<==Hx((x==A-=>>Iwx{,<==H{Hh,,  4; 4 4 4   x223; 3 3"%h--3 3 3
 
 	
 -00I77L118$$rT   c                    dt           dt          t                   ffddt           dt          t                   ffd |           }|r;d| d}t          j                            |          dv r|dz  }t          |          S g } |           D ]}||vr|                    |           t          d		                    |d
d                             S )zERender nested MCP connection errors into an actionable short message.currentr   c                    t          | dd           }|r|D ]} |          }|r|c S d S t          | t                    r^t          | dd           rt          | j                  S t          j        dt          |                     }|r|                    d          S dD ]9}t          | |d           }t          |t                    r |          }|r|c S :d S )NrY  filenamez$No such file or directory: '([^']+)'r   	__cause____context__)	r   r   rq  r   r  r.  r   groupBaseException)r  nestedchildmissingmatchattr
nested_exc_find_missings          r,   r  z,_format_connect_error.<locals>._find_missing  s   ,55 	 # #'-.. #"NNN#4g011 	&w
D11 -7+,,,IEs7||TTE &{{1~~%0 	# 	#D $55J*m44 #'-
33 #"NNNtrT   c                    t          | dd           }|r'g }|D ] }|                     |                     !|S g }t          |                                           }|r|                    |           dD ]F}t          | |d           }t          |t                    r|                     |                     G|p| j        j        gS )NrY  r  )	r   r   r   r   r   r   r  	__class__rQ  )	r  r  	flattenedr  messagesr   r  r  _flatten_messagess	           r,   r  z0_format_connect_error.<locals>._flatten_messages  s    ,55 	#%I ; ;  !2!25!9!9::::7||!!## 	"OOD!!!0 	? 	?D $55J*m44 ? 1 1* = =>>>7G-677rT   zmissing executable ''>   r   r   r   z (ensure Node.js is installed and PATH includes its bin directory, or set mcp_servers.<name>.command to an absolute path and include that directory in mcp_servers.<name>.env.PATH)r   Nrd   )
r  r
   r   r	   r$   r   basenamer   r   r   )r+   r  messagededupeditemr  r  s        @@r,   _format_connect_errorr    s(   } #      ,8= 8T#Y 8 8 8 8 8 8" mC  G (33337G$$(>>>AG
 w'''G!!#&& ! !wNN4   499WRaR[11222rT   r   c                     	  ||           }t          |t                    rt          j        |          s|S t	          ||          S # t
          t          t          f$ r |cY S w xY w)zCoerce a config value to a numeric type, returning *default* on failure.

    Handles string values from YAML (e.g. ``"10"`` instead of ``10``),
    non-finite floats, and values below *minimum*.
    )r   floatmathisfiniteri   rQ   rR   OverflowError)r   defaultcoerceminimumr   s        r,   _safe_numericr    sw    fe$$ 	T]6-B-B 	N67###z=1   s   5A A A$#A$c                       e Zd ZdZddddZdedefdZd	efd
Z	d	e
e         fdZed	efd            Zd	ee         fdZeddedefd            Zd Zd Zd	efdZd ZdS )SamplingHandlera+  Handles sampling/createMessage requests for a single MCP server.

    Each MCPServerTask that has sampling enabled creates one SamplingHandler.
    The handler is callable and passed directly to ``ClientSession`` as
    the ``sampling_callback``.  All state (rate-limit timestamps, metrics,
    tool-loop counters) lives on the instance -- no module-level globals.

    The callback is async and runs on the MCP background event loop.  The
    sync LLM call is offloaded to a thread via ``asyncio.to_thread()`` so
    it doesn't block the event loop.
    endTurn	maxTokenstoolUse)stoplength
tool_callsr.   r  c                 &   || _         t          |                    dd          dt                    | _        t          |                    dd          dt
                    | _        t          |                    dd          dt                    | _        t          |                    dd          dt          d	
          | _        |                    d          | _	        |                    dg           | _
        t          j        t          j        t          j        d}|                    t          |                    dd                                                    t          j                  | _        g | _        d	| _        d	d	d	d	d| _        d S )Nmax_rpm
   timeout   max_tokens_capi   max_tool_roundsrc   r   )r  modelallowed_models)r#   rZ   r\   	log_levelrZ   )requestsr   tokens_usedtool_use_count)r.   r  r   intr  r  r  r  r  model_overrider  loggingDEBUGINFOWARNINGr   r   audit_level_rate_timestamps_tool_loop_countmetrics)selfr.   r  _log_levelss       r,   __init__zSamplingHandler.__init__  sM   &$VZZ	2%>%>CHH$VZZ	2%>%>EJJ+FJJ7G,N,NPTVYZZ,JJ(!,,aa 
  
  
 %jj11$jj)92>> 'w|PWP_``&??

;//006688',
 

 .0 !$%1XYZZrT   r   c                     t          j                     }|dz
  fd| j        D             | j        dd<   t          | j                  | j        k    rdS | j                            |           dS )zASliding-window rate limiter.  Returns True if request is allowed.rb   c                      g | ]
}|k    |S rS   rS   )r   twindows     r,   r   z5SamplingHandler._check_rate_limit.<locals>.<listcomp>  s    #S#S#S!F

A


rT   NFT)timer  r   r  r   )r  r0   r  s     @r,   _check_rate_limitz!SamplingHandler._check_rate_limit  sx    ikkr#S#S#S#St/D#S#S#Saaa t$%%555$$S)))trT   c                     | j         r| j         S |rAt          |d          r1|j        r*|j        D ]"}t          |d          r|j        r	|j        c S #dS )z3Config override > server hint > None (use default).hintsr  N)r  rF  r  r  )r  preferenceshints      r,   _resolve_modelzSamplingHandler._resolve_model  sz     	'&& 	%7;88 	%[=N 	%#) % %4(( %TY %9$$$trT   c                     t          | d          r| j        dS t          | j        t                    r| j        n| j        g}d                    d |D                       S )z,Extract text from a ToolResultContent block.contentNr   
c              3   D   K   | ]}t          |d           |j        V  dS )r   NrF  r   r   r  s     r,   re  z<SamplingHandler._extract_tool_result_text.<locals>.<genexpr>+  s3      NNtf8M8MNNNNNNNrT   )rF  r  r   r  r   )r  r   s     r,   _extract_tool_result_textz)SamplingHandler._extract_tool_result_text%  sc     ui(( 	EM,A2!+EM4!@!@Uu}oyyNNuNNNNNNrT   c                 H   g }|j         D ]}t          |d          r|j        n(t          |j        t
                    r|j        n|j        g}d |D             }d |D             }d |D             }|D ]3}|                    d|j        |                     |          d           4|rg }	|D ]}
|	                    t          |
ddt          |	                     d	|
j        t          |
j        t                    rt          j        |
j        d
          nt!          |
j                  dd           |j        |	d}d |D             }|rd                    |          |d<   |                    |           t|rt          |          dk    r@t          |d         d          r*|                    |j        |d         j        d           g }|D ]}t          |d          r|                    d|j        d           0t          |d          r;t          |d          r+|                    ddd|j         d|j         id           {t,                              dt1          |          j                   |r|                    |j        |d           |S )aL  Convert MCP SamplingMessages to OpenAI format.

        Uses ``msg.content_as_list`` (SDK helper) so single-block and
        list-of-blocks are handled uniformly.  Dispatches per block type
        with ``isinstance`` on real SDK types when available, falling back
        to duck-typing via ``hasattr`` for compatibility.
        content_as_listc                 2    g | ]}t          |d           |S )	toolUseIdrF  r   bs     r,   r   z5SamplingHandler._convert_messages.<locals>.<listcomp><  s'    III!K1H1HIAIIIrT   c                 r    g | ]4}t          |d           t          |d          "t          |d          2|5S )r  inputr  r  r  s     r,   r   z5SamplingHandler._convert_messages.<locals>.<listcomp>=  sI    yyyqga.@.@yWQPWEXEXyahijlwaxaxyyyyrT   c                 r    g | ]4}t          |d           t          |d          rt          |d          2|5S )r  r  r  r  r  s     r,   r   z5SamplingHandler._convert_messages.<locals>.<listcomp>>  sr      E  E  EAwq+7N7N  EX_`aciXjXj  Eovwx  {B  pC  pC  Ea  E  E  ErT   tool)roletool_call_idr  idcall_functionFensure_asciir  	arguments)r  r>  r  )r  r  c                 <    g | ]}t          |d           |j        S r   r  r  s     r,   r   z5SamplingHandler._convert_messages.<locals>.<listcomp>V  s)    SSS6@R@RSafSSSrT   r  r  r   r   r   r  r  r>  r   r  r  	image_urlrw  zdata:z;base64,)r>  r  z5Unsupported sampling content block type: %s (skipped))r  rF  r  r   r  r  r   r  r  r   r   r  r  r   jsondumpsr   r  r   r   r  r  r"   r\   r>  rQ  )r  paramsr  r   blockstool_results	tool_usescontent_blockstrtc_listtumsg_dict
text_partsr   r  s                  r,   _convert_messagesz!SamplingHandler._convert_messages-  si     "? 8	N 8	NC,3C9J,K,K S(()#+t<<O3;- 
 JIvIIILyyFyyyI E  E  E  E  EN #  "$&L#==bAA! !      %N#  BNN%b$0FG0F0FGG *$&GU_`b`hjnUoUo  *CBH5)Q)Q)Q)Quxy{  zB  vC  vC% %$ $     +.('!J!JSSnSSS
 @*.))J*?*?HY')))) N~&&!++q8I60R0R+OOSX.QRBSBX$Y$YZZZZE!/  "5&11 !LL&%*)M)MNNNN$UF33 	z8R8R 	!LL(3.35aU^5a5aUZU_5a5a-b* *    
 #NN W $U 4     N e(L(LMMMrT   r  r   c                 P    t           rt          ||           S t          |           )z1Return ErrorData (MCP spec) or raise as fallback.)r   r  )_MCP_SAMPLING_TYPESr?   r!   )r  r   s     r,   _errorzSamplingHandler._errort  s-      	9$8888   rT   c                    | j         dxx         dz  cc<   | j        dk    r%d| _        |                     d| j         d          S | xj        dz  c_        | j        | j        k    r-d| _        |                     d| j         d| j         d          S g }|j        j        D ]}|j        j        }t          |t                    rW	 t          j        |          }ni# t          j        t          f$ r( t                              d	| j        |           d
|i}Y n,w xY wt          |t"                    r|nd
t          |          i}|                    t'          d|j        |j        j        |                     t                              | j        d| j        |j        t3          t3          |dd          dd          t5          |                     t7          d||j        d          S )zEBuild a CreateMessageResultWithTools from an LLM tool_calls response.r  r   r   z Tool loops disabled for server 'z' (max_tool_rounds=0)z%Tool loop limit exceeded for server 'z' (max z rounds)zRMCP server '%s': malformed tool_calls arguments from LLM (wrapping as raw): %.100s_rawtool_use)r>  r  r  r  zEMCP server '%s' sampling response: model=%s, tokens=%s, tool_calls=%dusageNtotal_tokens?	assistantr  r  r  r  
stopReason)r  r  r  r  r.   r  r  r  r   r   r   r  loadsJSONDecodeErrorrR   r"   r\   r   r   rC   r  r  logr  r  r   r   r>   )r  choicerl  r  tcr   r  s          r,   _build_tool_use_resultz&SamplingHandler._build_tool_use_result}  sO   %&&&!+&&& 1$$$%D!;;Z43CZZZ   	" 4#777$%D!;;78H 7 7,7 7 7  
 .+ 	 	B;(D$$$ Q,!Z--FF,j9 , , ,NN=($  
 %d^FFF, ",D$!7!7Pfc$ii=P!!.5[%	# # #     	

ShnGHgt44ncJJ	
 	
 	
 ,". 	
 
 
 	
s   C;DDc                 f   d| _         |j        j        pd}t                              | j        d| j        |j        t          t          |dd          dd                     t          dt          d	t          |          
          |j        | j                            |j        d                    S )z8Build a CreateMessageResult from a normal text response.r   r   z6MCP server '%s' sampling response: model=%s, tokens=%sr  Nr  r  r  r   r  r  r  )r  r  r  r"   r!  r  r.   r  r   r=   rB   r   _STOP_REASON_MAPr   finish_reason)r  r"  rl  response_texts       r,   _build_text_resultz"SamplingHandler._build_text_result  s     !.4"

DhnGHgt44ncJJ		
 	
 	
 #V/-2P2PQQQ.,001EyQQ	
 
 
 	
rT   c                 @    | t          t                                dS )z<Return kwargs to pass to ClientSession for sampling support.)tools)sampling_callbacksampling_capabilities)r@   rA   r  s    r,   session_kwargszSamplingHandler.session_kwargs  s1     "&%7-//& & &
 
 	
rT   c           
         
K                                     sat                              d j         j                    j        dxx         dz  cc<                        d j         d j         d          S                      t          |dd                    }d	d
l	m

 |p j        pd j        r|rz j        vrqt                              d j                    j        dxx         dz  cc<                        d d j         dd                     j                             S                      |          t          |d          r%|j        r                    d	d|j        d           t%          |j         j                  dt          |d          r|j        |j        dt          |dd          }|rd |D             t                               j        d j        t1                               
 fd}	 t3          j        t3          j        |           j                   d{V }n# t2          j        $ r>  j        dxx         dz  cc<                        d j         d j         d          cY S t<          $ rQ} j        dxx         dz  cc<                        dt?          tA          |                               cY d}~S d}~ww xY wt          |dd          s3 j        dxx         dz  cc<                        d j         d          S |j!        d	         } j        d xx         dz  cc<   t          t          |d!d          d"d	          }	tE          |	tF                    r j        d#xx         |	z  cc<   |j$        d$k    r7t          |j%        d$          r"|j%        j&        r '                    ||          S  (                    ||          S )%zSampling callback invoked by the MCP SDK.

        Conforms to ``SamplingFnT`` protocol.  Returns
        ``CreateMessageResult``, ``CreateMessageResultWithTools``, or
        ``ErrorData``.
        z5MCP server '%s' sampling rate limit exceeded (%d/min)r   r   z)Sampling rate limit exceeded for server 'z' (z requests/minute)modelPreferencesNr   )call_llmr   z:MCP server '%s' requested model '%s' not in allowed_modelszModel 'z' not allowed for server 'z'. Allowed: rE  systemPromptsystemr  temperaturer+  c                     g | ]G}d t          |dd          t          |dd          pdt          t          |dd                    ddHS )r  r  r   r   inputSchemaNr  r   rP   r>  r  )r   _normalize_mcp_input_schemar   r  s     r,   r   z,SamplingHandler.__call__.<locals>.<listcomp>  s}         ' '62 6 6'.q-'D'D'J&A#A}d;;' '! !	 	  rT   zFMCP server '%s' sampling request: model=%s, max_tokens=%d, messages=%dc            	      6      dpd j                   S )Nmcp)taskr  r  r5  
max_tokensr+  r  r  )r2  call_temperature
call_toolsr?  r  resolved_modelr  s   r,   
_sync_callz,SamplingHandler.__call__.<locals>._sync_call!  s8    8$,!,%    rT   r@  z"Sampling LLM call timed out after zs for server 'r  zSampling LLM call failed: choicesz5LLM returned empty response (no choices) for server 'r  r  r  r  r  ))r  r"   r\   r.   r  r  r  r  r   agent.auxiliary_clientr2  r  r  r   r  rF  r3  insertminr  r  r5  r!  r  r   rg  wait_for	to_threadr  TimeoutErrorr!   r   r   rE  r   r  r'  r  r  r$  r)  )r  contextr  r  server_toolsrD  rl  r+   r"  r  r2  rA  rB  r?  r  rC  s   `         @@@@@@r,   __call__zSamplingHandler.__call__  st      %%'' 		NNG $,   L"""a'""";;4D<L 4 4L4 4 4   ##GF4F$M$MNN 	433333 ;$"5; 		> 		nDL_6_6_NNL .   L"""a'""";;S. S S$S S26))D<O2P2PS S   ))&116>** 	Sv/B 	SOOAV=PQQRRR )4+>??
6=)) 	2f.@.L%1 
vw55 	  &  J 	

Tnj#h--	
 	
 	
		 		 		 		 		 		 		 		 		 		 			$-!*--t|        HH # 	 	 	L"""a'""";;3T\ 3 3#/3 3 3      	 	 	L"""a'""";;M_Xc]]-K-KMM       	 xD11 	L"""a'""";;($( ( (   !!$Z   A%   wx$??QRSSlC(( 	8L'''<7'''  L0055 1) 1 ..vx@@@&&vx888s&   3I A
K8	K8'AK3-K83K8N)r  )rQ  rR  rS  rT  r&  r   r   r  boolr  r
   r  staticmethodr  r	   r  r  r  r$  r)  r/  rN  rS   rT   r,   r  r    se       
 
 !*[PYZZ[C [ [ [ [ [.4    Xc]     OC O O O \OC4: C C C CN ! ! !3 ! ! ! \!7
 7
 7
r
 
 
*
 
 
 
 
}9 }9 }9 }9 }9rT   r  schemac                 N   t          | t                    r|                     d          nd}t          |t                    r|sd| dS d| dg}|                                D ]\  }}d}d}t          |t                    rJt	          |                    dd          pd          }t	          |                    d	d          pd          }|rd
| dnd}|r|                    d| | d|            |                    d| |            d                    |          S )a%  Render a JSON-schema-ish requested_schema to a human-readable field list.

    Elicitation schemas are restricted to a flat object with named top-level
    properties. We surface field names, types, and descriptions so the user
    can tell what the server is asking for before approving.
    
propertiesNz"Approval requested by MCP server 'z'.z Fields requested by MCP server 'z':r   r>  r   rD  rz  z  - : r  )r   r   r   r   r   r   r   )	rQ  r.   propslines
field_name
field_spec
field_type
field_descsuffixs	            r,   "_format_elicitation_schema_summaryr\  Z  sd    )364(@(@JFJJ|$$$dEeT"" D% DCKCCCC????@E"'++-- 
6 
6
J

j$'' 	FZ^^FB77=2>>JZ^^M2>>D"EEJ'19#j####r 	6LLB
BFBBjBBCCCCLL4
4F44555599UrT   c                   J    e Zd ZdZdZddededed         fdZd	efd
Z	d Z
dS )ElicitationHandlera  Handles ``elicitation/create`` requests for a single MCP server.

    Each ``MCPServerTask`` that has elicitation enabled creates one handler.
    The handler is callable and passed directly to ``ClientSession`` as the
    ``elicitation_callback`` (added in mcp Python SDK 1.11.0).

    Elicitation lets a server ask the client to collect structured input from
    the user mid-tool-call (e.g. payment authorization, OAuth confirmation).
    Form-mode elicitations are routed through Hermes' existing approval
    system (``tools.approval.prompt_dangerous_approval``), which surfaces
    the prompt on whichever surface the active session uses -- CLI, TUI,
    Telegram, Slack, etc. URL-mode elicitations are declined as unsupported.

    Failure modes are fail-closed: any timeout, exception, or unexpected
    state returns ``decline``/``cancel`` rather than silently accepting.
    The server treats this as the user not approving.
    rc   Nr.   r  ownerMCPServerTaskc                     || _         t          |                    dd          dt                    | _        || _        ddddd| _        d S )Nr  ra   r   )r  accepteddeclinedr   )r.   r  r   r  r  r_  r  )r  r.   r  r_  s       r,   r  zElicitationHandler.__init__  sT    & %VZZ	3%?%?eLL 
	
 
rT   r   c                 
    d| iS )z?Return kwargs to pass to ClientSession for elicitation support.elicitation_callbackrS   r.  s    r,   r/  z!ElicitationHandler.session_kwargs  s    &--rT   c                    	
K    j         dxx         dz  cc<   t          |dd          }|dk    rEt                              d j                    j         dxx         dz  cc<   t          d	          S t          |d
d          p
d j         d
t          |di           pi }t          | j                  	t                              d j        t          
          dd                    	 ddlm	 n]# t          $ rP}t                              d j        |            j         dxx         dz  cc<   t          d	          cY d}~S d}~ww xY w j        rt           j        dd          nddt          f	
 fd}	 t          j        t          j        |           j         j        z              d{V }n# t          j        $ r[ t                              d j        t+           j                              j         dxx         dz  cc<   t          d	          cY S t          $ rR}t                              d j        |d            j         dxx         dz  cc<   t          d	          cY d}~S d}~ww xY w|dk    r& j         d xx         dz  cc<   t          di !          S |dk    r% j         dxx         dz  cc<   t          d	          S  j         dxx         dz  cc<   t          d	          S )"zElicitation callback invoked by the MCP SDK.

        Conforms to ``ElicitationFnT`` protocol. Returns ``ElicitResult``
        or ``ErrorData``.
        r  r   modeformrw  z`MCP server '%s' requested URL-mode elicitation; declining (URL-mode elicitation not implemented)rc  decline)actionr  r   r  z' is requesting your approvalrequested_schemaz'MCP server '%s' elicitation request: %sN   r   )request_elicitation_consentz<MCP server '%s' elicitation: approval system unavailable: %sr   _pending_call_contextr   c                       ) t          j                  dj                   S                                                      t          j                  dj                   S )Nzmcp-elicitation/)timeout_secondssurface)r  r  r.   copyrun)capturedr   r  rm  r  s   r,   _invoke_consentz4ElicitationHandler.__call__.<locals>._invoke_consent  s    22$'$5$5At/?AA	    ==??&&+ #DL 1 1=4+;== '   rT   r@  z/MCP server '%s' elicitation timed out after %dscancelz&MCP server '%s' elicitation failed: %sTexc_infoacceptrb  )rj  r  )r  r   r"   rZ   r.   rE   r\  r   tools.approvalrm  r!   r]   r_  r   rg  rI  rJ  r  _OUTER_TIMEOUT_GRACE_SECONDSrK  r\   r  )r  rL  r  rg  rQ  r+   ru  answerrt  r   r  rm  s   `       @@@@r,   rN  zElicitationHandler.__call__  s      	Z   A%    vvv..5==KKC   
 L$$$)$$$y1111&)R00 
J4+JJJ 	 !3R88>B8AQRR5og66tt<	
 	
 	
	2BBBBBBB 	2 	2 	2LLN #   L"""a'"""y111111111	2* JN]74:'>EEEY]	 	 	 	 	 	 	 	 	 	 	$	2"+!/22t'HH        FF # 	1 	1 	1NNA #dl"3"3   L"""a'"""x000000 	2 	2 	2LL8 #     L"""a'"""y111111111	2 XL$$$)$$$x<<<<XL"""a'"""x0000Z   A%   9----sE   4C; ;
EAE
EE	;G A'J	.	J	7AJ>J	J	rd  )rQ  rR  rS  rT  r{  r   r   r
   r  r/  rN  rS   rT   r,   r^  r^  t  s         , $% 
 
C 
 
h>W 
 
 
 
". . . . .l. l. l. l. l.rT   r^  c                      e Zd ZdZdZdefdZdefdZdefdZ	defdZ
d/d
Zd/dZd0dee         dee         fdZdee         fdZdedd	fdZd Zdej        fdZd Zd Zd Zd/dZd/dZdefdZ	 d0dee         defdZdefdZdZd	dd	dd d!ed"ee         d#ededd	f
d$Z d%e!defd&Z"defd'Z#d( Z$d/d)Z%defd*Z&defd+Z'd, Z(d/d-Z)d/d.Z*d	S )1r`  ac  Manages a single MCP server connection in a dedicated asyncio Task.

    The entire connection lifecycle (connect, discover, serve, disconnect)
    runs inside one asyncio Task so that anyio cancel-scopes created by
    the transport client are entered and exited in the same Task context.

    Supports both stdio and HTTP/StreamableHTTP transports.
    )r  sessiontool_timeout_task_ready_shutdown_event_reconnect_event_toolsr  _config	_sampling_elicitation_registered_tool_names
_auth_type_refresh_lock	_rpc_lock_pending_refresh_tasksrn  _lifecycle_started_at_last_tool_call_at_idle_timeout_seconds_max_lifetime_seconds_recycled_reasoninitialize_result_ping_unsupported_reconnect_retries_session_proven_was_parkedr  c                    || _         d | _        t          | _        d | _        t          j                    | _        t          j                    | _        t          j                    | _	        g | _
        d | _        i | _        d | _        d | _        g | _        d| _        d| _        d| _        d| _        t          j                    | _        t          j                    | _        t/                      | _        d | _        t5          j                    }|| _        || _        d | _        d | _        d | _         d | _!        d| _"        d S )Nr   Fr   )#r  r~  _DEFAULT_TOOL_TIMEOUTr  r  rg  Eventr  r  r  r  r  r  r  r  r  r  r  r  r  Lockr  r  setr  rn  r  	monotonicr  r  r  r  r  r  r  )r  r  r0   s      r,   r  zMCPServerTask.__init__-  s   	&*#8-1
moo&} !(+/48:>13#'( &+ "'!$\^^ !9<# EI"n,/"),6:"6:"/3 15 (-rT   r   c                     d| j         v S )z)Check if this server uses HTTP transport.rw  )r  r.  s    r,   _is_httpzMCPServerTask._is_httpt  s    $$rT   c                 h    | j         }|t          |dd          nd}|dS t          |dd          duS )u  Whether the server advertises the ``tools`` capability.

        Per the MCP spec, ``InitializeResult.capabilities.tools`` is non-None
        iff the server implements the ``tools/*`` request family. Prompt-only
        or resource-only servers omit it, and calling ``tools/list`` against
        them raises ``McpError(-32601 Method not found)`` — which previously
        killed the connection during discovery and made every keepalive fail.
        (Ported from anomalyco/opencode#31271.)

        Returns True when no capability info was captured (legacy fallback:
        preserve the old always-call-list_tools behavior rather than regress
        any server that was working before this gate).
        NcapabilitiesTr+  )r  r   )r  init_resultcapss      r,   _advertises_toolszMCPServerTask._advertises_toolsx  sI     ,=H=Tw{ND999Z^<4tWd++477rT   c                 >    |                                   o| j        duS )z;Return True when a stdio server was intentionally recycled.N)r  r  r.  s    r,   _is_recycled_stdioz MCPServerTask._is_recycled_stdio  s     ==??"Ht'<D'HHrT   Nc                 6    t          j                    | _        dS )z5Record that a user-visible MCP operation is starting.N)r  r  r  r.  s    r,   mark_tool_callzMCPServerTask.mark_tool_call  s    "&."2"2rT   c                 V    t          j                    }|| _        || _        d | _        d S rd  )r  r  r  r  r  r  r0   s     r,   _mark_lifecycle_startedz%MCPServerTask._mark_lifecycle_started  s-    n%(""% $rT   r0   c                    |                                  s| j                                        rdS |t          j                    n|}| j        || j        z
  | j        k    rdS | j        || j        z
  | j        k    rdS dS )z@Return the stdio recycle reason if idle/age limits have elapsed.Nmax_lifetime_secondsidle_timeout_seconds)	r  r  lockedr  r  r  r  r  r  r  s     r,   _stdio_recycle_reasonz#MCPServerTask._stdio_recycle_reason  s    ==?? 	dn3355 	4"%+dn3&2d00D4NNN))&2d--1KKK))trT   c                 .   |                                  s| j                                        rdS g }| j        "|                    | j        | j        z              | j        "|                    | j        | j        z              |rt          |          ndS )z=Return the next monotonic recycle deadline for stdio, if any.N)	r  r  r  r  r   r  r  r  rH  )r  	deadliness     r,   _next_stdio_recycle_deadlinez*MCPServerTask._next_stdio_recycle_deadline  s    ==?? 	dn3355 	4	%1T7$:TTUUU%1T4t7QQRRR!*4s9~~~4rT   r   c                 "    || _         d| _        dS )zCMark a stdio session dormant before its transport finishes closing.N)r  r~  )r  r   s     r,   _mark_stdio_recycledz"MCPServerTask._mark_stdio_recycled  s     &rT   c                    K   	 |                                   d{V  dS # t          j        $ r  t          $ r$ t                              d| j                   Y dS w xY w)zBRun a dynamic tool refresh and log failures from background tasks.Nz,MCP server '%s': dynamic tool refresh failed)_refresh_toolsrg  rh  r!   r"   	exceptionr  r.  s    r,   _refresh_tools_taskz!MCPServerTask._refresh_tools_task  s      	X%%'''''''''''% 	 	 	 	X 	X 	XKTYWWWWWW	Xs     9AAc                     t          j        |                                           }| j                            |           |                    | j        j                   |S )zCSchedule a background tool refresh and keep it strongly referenced.)rg  create_taskr  r  addadd_done_callbackdiscardr  r>  s     r,   _schedule_tools_refreshz%MCPServerTask._schedule_tools_refresh  sT    "4#;#;#=#=>>#''---t:BCCCrT   c                       fd}|S )a  Build a ``logging_callback`` for ``ClientSession``.

        Routes MCP ``notifications/message`` log notifications from the
        server into Hermes' logging (agent.log via hermes_logging), tagged
        with the server name.  Without this, the SDK's default callback
        silently discards them, so server-side warnings/errors during a
        tool call were invisible.  Port of anomalyco/opencode#34529.
        c           	        K   	 t                               t          t          | dd                                                    t
          j                  }t          | dd           }t          |t                    sD	 t          j	        |dt                    }n&# t          t          f$ r t          |          }Y nw xY wt          |          dk    r|d d         dz   }t          | dd           }|rj         d	| nj        }t                              |d
||           d S # t           $ r& t                              dj        d           Y d S w xY w)NlevelrZ   r  F)r  r  i  z... [truncated]r"   /zMCP server log [%s]: %sz/Failed to handle MCP log notification from '%s'Trw  )_MCP_LOG_LEVEL_MAPr   r   r   r   r  r  r   r  r  rQ   rR   r   r  r"   r!  r!   r#   )r  r  r  logger_nameoriginr  s        r,   _on_logz5MCPServerTask._make_logging_callback.<locals>._on_log  s     *..8899??AA7<  vvt44!$,, ))#z$UCPPP%z2 ) ) )"4yy) t99t##;)::D%fh==9DSDI55555$)

5";VTJJJJJ   EI       s7   A9D' ?B D'  B?<D' >B??A&D' ',EErS   )r  r  s   ` r,   _make_logging_callbackz$MCPServerTask._make_logging_callback  s#    	 	 	 	 	. rT   c                       fd}|S )a  Build a ``message_handler`` callback for ``ClientSession``.

        Dispatches on notification type.  Only ``ToolListChangedNotification``
        triggers a refresh; prompt and resource change notifications are
        logged as stubs for future work.
        c                   K   	 t          | t                    r#t                              dj        |            d S t
          rt          | t                    r| j        xt          d xS\    t          	                    dj                   
                                 t          j        d           d {V  d S  xt          d x%\    t                              dj                   d S  t          d x$\   t                              dj                   d S  	 d S d S d S # t          $ r$ t                              dj                   Y d S w xY w)Nz'MCP message handler (%s): exception: %srS   z9MCP server '%s': received tools/list_changed notificationr   z/MCP server '%s': prompts/list_changed (ignored)z1MCP server '%s': resources/list_changed (ignored)z%Error in MCP message handler for '%s')r   r!   r"   r#   r  _MCP_NOTIFICATION_TYPESrF   rt  rG   rZ   r  rg  sleeprH   rI   r  )r  r  s    r,   _handlerz5MCPServerTask._make_message_handler.<locals>._handler  s      Ugy11 LL!JDIW^___F* !z'CU/V/V !!,:8::::::"KK [ $	   !88::: #*-"2"2222222222% ;& =:<<<<<<"LL)Z\`\efffff =<>>>>>"LL)\^b^ghhhhh ? D5! ! ! !6  U U U  !H$)TTTTTTUs)   6D% A>D% =/D% .-D% D% %*EErS   )r  r  s   ` r,   _make_message_handlerz#MCPServerTask._make_message_handler  s)    !	U !	U !	U !	U !	UD rT   c           	      <   K   ddl m}                                  sdS  j        4 d{V  t	           j                  } j        4 d{V  t           j        j	        d j
                   d{V }ddd          d{V  n# 1 d{V swxY w Y   | fd|D             z
  }|D ]&}|                    |           t          |           '| _        t           j
          j                   _        t	           j                  }||z
  }||z
  }g }	|r8|	                    dd                    t%          |                                |r8|	                    dd                    t%          |                                |	r5t&                              d	 j
        d
                    |	                     n3t&                              d j
        t-           j                             ddd          d{V  dS # 1 d{V swxY w Y   dS )ua  Re-fetch tools from the server and update the registry.

        Called when the server sends ``notifications/tools/list_changed``.
        The lock prevents overlapping refreshes from rapid-fire notifications.
        After the initial ``await`` (list_tools), all mutations are synchronous
        — atomic from the event loop's perspective.
        r   registryNr+  c                 D    h | ]}t          j        |j                  S rS   )rG  r  )r   r  r  s     r,   	<setcomp>z/MCPServerTask._refresh_tools.<locals>.<setcomp>;  s7     1 1 1 'ty$)<<1 1 1rT   zadded: rE  z	removed: uU   MCP server '%s': tools changed dynamically — %s. Verify these changes are expected.r   z>MCP server '%s': dynamically refreshed %d tool(s) (no changes))tools.registryr  r  r  r  r  r  r   r~  
list_toolsr  
deregister_forget_mcp_tool_serverr  _register_server_toolsr  r   r   sortedr"   r\   rZ   r   )
r  r  old_tool_namesnew_mcp_toolsstale_tool_namesr   new_tool_namesaddedremovedchangess
   `         r,   r  zMCPServerTask._refresh_tools  s      	,+++++%%'' 	 F% 2	 2	 2	 2	 2	 2	 2	 2	 !<==N ~        &9L+Wdi' ' ! ! ! ! ! !                            . 1 1 1 1)1 1 1   . 3 3	##I...'	2222 (DK*@	4+ +D'
 !!<==N"^3E$~5GG EC6%==)A)ACCDDD IG499VG__+E+EGGHHH 
9Ityy11    TIs4#>??  _2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	 2	s6   "H'B6H
B	HB	E"H
HHc                   K   | j         s	 t          j        | j                                        d           d{V  dS # t
          $ rV}t          |          s |                                 s d| _         t          	                    d| j
                   Y d}~nd}~ww xY wt          j        | j                                        d           d{V  dS )ub  Exercise the session to detect a stale/expired connection.

        Uses ``ping`` (cheap, transport-agnostic liveness) by default. ``ping``
        is an OPTIONAL MCP utility: a server that doesn't implement it answers
        JSON-RPC -32601. The first time that happens we latch
        ``_ping_unsupported`` and fall back to the pre-ping probe — capability
        permitting, ``list_tools``; otherwise ``ping`` is the only option and
        the -32601 propagates (a server advertising neither a working ping nor
        tools has no liveness primitive left). The latch resets on each fresh
        transport connection so a server that gains ping support after a
        reconnect is re-probed with the cheap path.

        Raises on a genuine connection failure so the caller triggers a
        reconnect; returns normally when the session is alive.
              >@r@  NTz~MCP server '%s': does not implement the optional 'ping' utility (-32601); using 'list_tools' for keepalive on this connection.)r  rg  rI  r~  	send_pingr!   r   r  r"   rZ   r  r  )r  r+   s     r,   _keepalive_probezMCPServerTask._keepalive_probe^  s       % 	&t|'='='?'?NNNNNNNNNN    2#66 --// )-&' I	       $ t|6688$GGGGGGGGGGGGs   3A   
B 
ABB c                     | j         s>d| _         d| _        | j        r+d| _        t                              d| j                   dS dS dS )u  Record that the current session demonstrated real health.

        Called from the keepalive success path (session survived at least one
        full keepalive interval) and the tool-call success path. Only then is
        the reconnect budget cleared: a handshake that completes but drops
        moments later must keep consuming ``_reconnect_retries`` so a flapping
        transport still reaches the park instead of respawning forever
        (#62212 — 6212 spawns in 63h).
        Tr   Fu^   MCP server '%s': revived — session healthy again after parking (state: parked → connected)N)r  r  r  r"   r\   r  r.  s    r,   _mark_session_provenz"MCPServerTask._mark_session_proven  sp     # 		#'D &'D# #( <I    		 		 rT   c           	      Z  K   t          t          t          | j                            dt
                                        }t          j        | j        	                                          }t          j        | j
        	                                          }	 	 |                                 }|m|                     |           	 ||fD ]P}|                                s:|                                 	 | d{V  4# t          j        t           f$ r Y Lw xY wQdS |}|                                 }|2t          dt%          ||t'          j                    z
                      }t          j	        ||h|t          j                   d{V \  }}	|rn/|                                 }|m|                     |           	 ||fD ]P}|                                s:|                                 	 | d{V  4# t          j        t           f$ r Y Lw xY wQdS | j        r	 |                                  d{V  ns# t           $ rf}
t1          |
          }t2                              d| j        t9          |          j        |           | j
                                         Y d}
~
nd}
~
ww xY w|                                  ,||fD ]P}|                                s:|                                 	 | d{V  4# t          j        t           f$ r Y Lw xY wQnZ# ||fD ]P}|                                s:|                                 	 | d{V  4# t          j        t           f$ r Y Lw xY wQw xY w| j                                         rdS | j
        !                                 d	S )
u  Block until either _shutdown_event or _reconnect_event fires.

        Returns:
            "shutdown"  if the server should exit the run loop entirely.
            "reconnect" if the server should tear down the current MCP
                        session and re-enter the transport (fresh OAuth
                        tokens, new session ID, etc.). The reconnect event
                        is cleared before return so the next cycle starts
                        with a fresh signal.
            "recycle"   if a stdio idle/max-lifetime limit elapsed. The
                        current transport is torn down and restarted lazily
                        on the next tool call.

        Shutdown takes precedence if both events are set simultaneously.

        Periodically sends a lightweight keepalive (``ping``, with a
        ``list_tools`` fallback for servers that don't implement the optional
        ping utility — see :meth:`_keepalive_probe`) to prevent TCP/session
        state from going stale during idle periods (#17003). If the keepalive
        fails, triggers a reconnect.

        The cadence is ``keepalive_interval`` from server config (default
        :data:`_DEFAULT_KEEPALIVE_INTERVAL`, floored at
        :data:`_MIN_KEEPALIVE_INTERVAL`). Servers that GC idle sessions on a
        short TTL (e.g. Unreal Engine's editor MCP, ~15s) need an interval
        below that TTL, otherwise every idle tool call lands on an
        already-expired session and pays the full reconnect path.
        keepalive_intervalTNrecyclerg   )r  return_whenu^   MCP server '%s' keepalive failed, triggering reconnect (state: connected → degraded): %s: %sshutdown	reconnect)"ri   _MIN_KEEPALIVE_INTERVALr  r  r   _DEFAULT_KEEPALIVE_INTERVALrg  r  r  waitr  r  r  donerv  rh  r!   r  rH  r  r  FIRST_COMPLETEDr~  r  ra  r"   r\   r  r>  rQ  r  r  is_setclear)r  r  shutdown_taskreconnect_taskrecycle_reasonr  r  recycle_deadliner  _pendingr+   rt  s               r,   _wait_for_lifecycle_eventz'MCPServerTask._wait_for_lifecycle_event  s     F !#$,""#79TUUVV
 

  +D,@,E,E,G,GHH ,T-B-G-G-I-IJJ8	/0!%!;!;!=!=!---n===$Z $^4  vvxx HHJJJ#2I>   	 W -#'#D#D#F#F #/!#s74Dt~GWGW4W'X'XYYG'.|"N3# ' 7( ( ( " " " " " "h
  !%!;!;!=!=!---n===$6 $^4  vvxx HHJJJ#2I>   	 # < 0
"335555555555$   6s;;P ItDzz':D  
 -11333 --///_/0b $^4  vvxx HHJJJ#2I>   	m^4  vvxx HHJJJ#2I>   	 &&(( 	:##%%%{s   ,L 6C??DDB%L 3G<<HHL #H> =L >
J.AJ)$L )J..L 6K??LL/M4MM4M/	,M4.M/	/M4r  c                   K   t          j        | j                                                  }t          j        | j                                                  }	 t          j        ||ht           j        |           d{V  ||fD ]P}|                                s:|                                 	 | d{V  4# t           j        t          f$ r Y Lw xY wQnZ# ||fD ]P}|                                s:|                                 	 | d{V  4# t           j        t          f$ r Y Lw xY wQw xY w| j        
                                rdS | j                                         dS )u<  Block until a reconnect or shutdown is requested while parked.

        Used by :meth:`run` after the reconnect budget is exhausted. The
        task stays alive (so ``_reconnect_event`` always has a listener) but
        does no work until something explicitly asks it to come back —
        OAuth recovery, a manual ``/mcp`` refresh — or, when ``timeout`` is
        given, until the timeout elapses (a periodic self-probe). The timed
        wake matters because parking deregisters this server's tools, so
        no tool call can ever reach the circuit-breaker's half-open probe
        or ``_signal_reconnect`` — without a self-probe a parked server
        would be unrevivable short of a full reload.

        Returns:
            ``"shutdown"`` if the server should exit the run loop entirely,
            ``"reconnect"`` if it should rebuild the transport (explicit
            request or self-probe timeout). The reconnect event is cleared
            before returning so the next park cycle starts from a fresh
            signal. Shutdown takes precedence.
        )r  r  Nr  r  )rg  ensure_futurer  r  r  r  r  rv  rh  r!   r  r  )r  r  r  r  r  s        r,   _wait_for_reconnect_or_shutdownz-MCPServerTask._wait_for_reconnect_or_shutdown	  s     ,  -d.B.G.G.I.IJJ .t/D/I/I/K/KLL	,/#3          $^4  vvxx HHJJJ#2I>   	m^4  vvxx HHJJJ#2I>   	 &&(( 	:##%%%{sB   )C 2B;;CC/D0	DD0D+	(D0*D+	+D0r  c           	        K   t           st          d| j         d          |                    d          }|                    dg           }|                    d          }|st	          d| j         d          t          |          }t          ||          \  }}ddlm} 	 t          j
        t          j        |||          t          	           d
{V }n=# t          j        $ r+ t                              d| j        t                     d
}Y nw xY w|rt	          d| j         d|           t!          ||          \  }}t#          |||r|nd
          }| j        r| j                                        ni }	| j        r,|	                    | j                                                   t,          rt.          r|                                 |	d<   t2          r|                                 |	d<   t          j        t6                     d
{V  t9                      }
t;                      }t=          | j                   t?                      }	 tA          ||          4 d
{V \  }}tC          t9                      |
z
            }|ri }|D ]7}	 tE          j#        |          ||<   # tH          tJ          tL          f$ r Y 4w xY wtN          5  |D ]}| j        tP          |<   tR                              |           d
d
d
           n# 1 swxY w Y   tU          ||fi |	4 d
{V 	 }tW          |                    dtX                              }t          j
        |-                                |	           d
{V | _.        || _/        | 0                                 | 1                                 d
{V  | j2                                         tg          | j                   d| _4        | 5                                 d
{V cd
d
d
          d
{V  cd
d
d
          d
{V  |rddl6m7} tq          tD          dd
          }tN          5  |D ]}tP          9                    |d
           |D ]} ||          }d}tR                              |          }|s3|1|/	  ||d           d}n# tJ          tt          tL          f$ r d}Y nw xY w|s|r*tv          <                    |           | j        tz          |<   tR          9                    |d
           	 d
d
d
           S # 1 swxY w Y   S S # 1 d
{V swxY w Y   	 d
d
d
          d
{V  n# 1 d
{V swxY w Y   |r	ddl6m7} tq          tD          dd
          }tN          5  |D ]}tP          9                    |d
           |D ]} ||          }d}tR                              |          }|s3|1|/	  ||d           d}n# tJ          tt          tL          f$ r d}Y nw xY w|s|r*tv          <                    |           | j        tz          |<   tR          9                    |d
           	 d
d
d
           d
S # 1 swxY w Y   d
S d
S # |rddl6m7} tq          tD          dd
          }tN          5  |D ]}tP          9                    |d
           |D ]} ||          }d}tR                              |          }|s3|1|/	  ||d           d}n# tJ          tt          tL          f$ r d}Y nw xY w|s|r*tv          <                    |           | j        tz          |<   tR          9                    |d
           	 d
d
d
           w # 1 swxY w Y   w w xY w)z%Run the server using stdio transport.r  zp' requires the 'mcp' Python SDK, but it is not installed. Run `hermes setup` to install MCP support, then retry.r   r   r   z' has no 'command' in configr   )check_package_for_malwarer@  Nuy   MCP server '%s': OSV malware preflight timed out after %.0fs (network slow/unreachable) — proceeding without the check.ry  )r   r   r   rK   rX   )errlogconnect_timeoutF_pid_existskillpgT)>rM   r  r  r   rR   r   r   tools.osv_checkr  rg  rI  rJ  _OSV_MALWARE_CHECK_TIMEOUT_SrK  r"   r\   r
  r7   r  r/  r  r   r  _MCP_MESSAGE_HANDLER_SUPPORTEDr  _MCP_LOGGING_CALLBACK_SUPPORTEDr  _kill_orphaned_mcp_children_snapshot_child_pidsr  r5   r-   r8   _filter_mcp_childrenr$   getpgidAttributeErrorProcessLookupErrorrr  _lock_stdio_pids_stdio_pgidsr6   r  _DEFAULT_CONNECT_TIMEOUT
initializer  r~  r  _discover_toolsr  _reset_server_errorr  r  gateway.statusr  r   popPermissionError_orphan_stdio_pidsr  _orphan_stdio_pid_servers)r  r  r   r   r   safe_envr  malware_errorserver_paramssampling_kwargspids_beforenew_pids_errlogread_streamwrite_stream	new_pgids_pidr~  r   r  _killpgpid	pid_alivepgroup_alivepgids                            r,   
_run_stdiozMCPServerTask._run_stdio/	  s=      	ty      **Y''zz&"%%::e$$ 	FtyFFF   #8,,27HEE 	>=====	!")"2!";WdKK4# # #      MM # 	! 	! 	!NNO	7  
 !MMM	!  	<ty<<]<<   4GTBB-$.$
 
 
 >B^S$.77999QS 	G""4#4#C#C#E#EFFF" 	N'E 	N151K1K1M1MO-.* 	P262M2M2O2OO./  ;<<<<<<<<< +,,
 	!+++%''h	8#M'BBB DB DB DB DB DB DB DB G 0(**[8   7
 13I ( ! !!.0j.>.>IdOO .0BGL ! ! ! !D!  7 7$, : :D04	K--$++I6667 7 7 7 7 7 7 7 7 7 7 7 7 7 7 ) 1@  %B %B %B %B %B %B %B %B ',

#46NOO' 'O 4;3C**,,o4 4 4 . . . . . .D* $+DL00222..000000000KOO%%% (	222 ,1D( "&!?!?!A!AAAAAAAK%B %B %B %B %B %B %B %B %B %B %B %B %B?DB DB DB DB DB DB DB DB DB DB DB DB DBV  8666666!"h55 8 8 ( 4 4#d3333' 8 8 %0K$4$4	',+//44( 	5T-='BU
5 'a 0 0 0/3$6#Q 5 5 5/45$ 8 8.223777=AY5c:: ),,S$7777-88 8 8 8 8 8 8 8 8 8 8 8 8 8 8 88Y%B %B %B %B %B %B %B %B %B %B %B %B %B %B %B?DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DB DBV  8666666!"h55 8 8 ( 4 4#d3333' 8 8 %0K$4$4	',+//44( 	5T-='BU
5 'a 0 0 0/3$6#Q 5 5 5/45$ 8 8.223777=AY5c:: ),,S$7777-88 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 88 8x 8666666!"h55 8 8 ( 4 4#d3333' 8 8 %0K$4$4	',+//44( 	5T-='BU
5 'a 0 0 0/3$6#Q 5 5 5/45$ 8 8.223777=AY5c:: ),,S$7777-88 8 8 8 8 8 8 8 8 8 8 8 8 8 8 88so  %5C 7DDY< **UJ-,U-KUK
U/L UL	UL	U,CT9
UY< AT,'R65T,6S	T,S	AT,,T0	3T0	9
U	UU	UY< 
U''Y< *U'+Y< AY-'W65Y-6XY-XAY--Y14Y1<'^#A];6\];\!]; \!!A];/^;]??^]?^)application/jsonztext/event-streamT      @)headers
ssl_verifyr  r  rw  r-  r.  c                L  K   	 ddl }n# t          $ r Y dS w xY w|d|                    |          d}|||d<   |rt          |          ni }	  |j        di |4 d{V }	|	                    ||           d{V }
|
j        dv r|	                    ||           d{V }
|
j                            dd	          	                    d
          d         
                                                                }|r|| j        vrd|
j        cxk    rdk     rn n|	                    |i |dddd           d{V }d|j        cxk    rdk     rfn nc|j                            dd	          	                    d
          d         
                                                                }|| j        v r|}
	 ddd          d{V  n# 1 d{V swxY w Y   n# |j        $ r Y dS w xY wd|
j        cxk    rdk     sn dS |
j                            dd	          	                    d
          d         
                                                                }|sdS || j        v rdS t          d| j         d| d| dd                    | j                   d	          )u  Probe *url* for an MCP-shaped response before the SDK connects.

        A misconfigured ``mcp_servers.<name>.url`` pointed at a plain web app
        returns HTML (or some other non-MCP body). The MCP SDK then sits on
        the connection for the full ``connect_timeout`` (default 60 s) before
        surfacing an opaque ``CancelledError``. A cheap, short-timeout probe
        here catches that in ≤ ``timeout`` seconds and raises
        :class:`NonMcpEndpointError` with an actionable message.

        Detection is allow-list based: a 2xx response is rejected only when it
        carries a definite content type that is NOT one an MCP endpoint uses
        (``application/json`` / ``text/event-stream``).  When HEAD/GET returns
        a non-MCP content type (e.g. ``text/html``), a lightweight JSON-RPC
        ``initialize`` POST is attempted before giving up — some servers
        (e.g. DocuSeal) serve a web UI on GET but speak Streamable HTTP only
        via POST.

        A missing or empty content type, non-2xx status, or any
        network/transport error passes through silently — the probe is
        strictly best-effort, and the real handshake remains the source of
        truth for everything except the unambiguous "this is a web page,
        not MCP" case.

        Runs on its own httpx client OUTSIDE the SDK's anyio task group, so the
        raised error propagates as itself rather than being wrapped in an
        ``ExceptionGroup`` (which is what defeats hooks installed inside the
        SDK transport).
        r   NT)verifyfollow_redirectsr  cert)r-  )i  i  zcontent-typer   r  rl  ra   r+  z#application/json, text/event-stream)zContent-TypeAcceptz{"jsonrpc":"2.0","id":"_probe","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"hermes-probe","version":"0.1"}}})r-  r  r  z' at z returned Content-Type 'z)', not an MCP response (expected one of: rE  u   ). The URL most likely points at a web page rather than an MCP endpoint — check it resolves to a Streamable HTTP / SSE endpoint (e.g. https://host/mcp, not https://host/).rS   )httpxr  Timeoutr   AsyncClientheadrm  r   r-  r   r   r   _MCP_CONTENT_TYPESpost	HTTPErrorrV  r  r   )r  rw  r-  r.  r  r  _httpxclient_kwargsprobe_headersclientrespct	post_resppost_ctct_bases                  r,   _preflight_content_typez%MCPServerTask._preflight_content_type	  s     J	""""" 	 	 	FF	 ! $~~g..
 

 "$/M&!)08Wb1	)v)::M:: .- .- .- .- .- .- .-f $[[m[DDDDDDDD#z11!'C!G!GGGGGGGD L$$^R88U3ZZ#UWWUWW	  -$"999t/5555#55555&,kk!+!,>&K! ! !1 '2 ' ' ! ! ! ! ! !I  i39999c99999%-11."EE"U3ZZ+"UWW"UWW	   #d&===#,D,].- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .- .-^  	 	 	FF	
 t'----#----F,"">266<<SAA!DJJLLRRTT 	Fd---F!:49 : :3 : :: :yy011: : :
 
 	
sC   	 
G !EG;G 
GG GG 
G-,G-egc                 P   | j                                         r||                    t          t          f          \  }}|||                    t
          j                  \  }}||| j                                        s|t          	                    d| j
        |           dS )u  Map an SDK transport TaskGroup failure to a clean ``"reconnect"``.

        Streamable-HTTP / SSE transports run their stream pump inside an anyio
        TaskGroup. A transient stream drop (idle timeout, brief backend blip,
        server-side TCP close) surfaces as a ``BaseExceptionGroup`` escaping the
        transport context manager. Left unwrapped it reaches ``run()``'s error
        path, which applies exponential backoff and eventually *parks* the
        server for 300s and deregisters its tools — a multi-minute tool outage
        for what is usually a sub-second glitch while the POST path stays
        healthy (issue #66092).

        Returning ``"reconnect"`` instead lets ``run()`` rebuild the session
        immediately with no backoff, no park, and no tool deregistration.

        Re-raise (rather than mask) when the failure is not a transient drop:
        - shutdown is in progress (``shutdown()`` sets ``_shutdown_event``
          before it ever cancels the task);
        - the group carries a ``KeyboardInterrupt`` / ``SystemExit`` — fatal
          signals must propagate to the interpreter, never be converted into
          a reconnect;
        - the group carries a real ``CancelledError`` (task cancellation must
          propagate to asyncio, mirroring the ``run()`` guard for #9930);
        - we never reached a live session this attempt (``_ready`` unset) — a
          connect/handshake failure SHOULD fall through to ``run()``'s backoff
          rather than hot-loop reconnects against a broken endpoint.
        Nuy   MCP server '%s': transport TaskGroup exited after a live session (%r) — reconnecting immediately instead of backing offr  )r  r  r   rZ  r[  rg  rh  r  r"   r#   r  )r  rE  r]  r^  	cancelleds        r,   _reconnect_or_reraise_groupz)MCPServerTask._reconnect_or_reraise_groupv
  s    6 &&(( 	Hxx!2J ?@@uH88G$:;;	5 H{!!## 	HGIr	
 	
 	

 {rT   c           	        K   t           st          d| j         d          |d         }t          |                    d          pi           }t          d |D                       s
t          |d<   |                    dt                    }|                    dd	          }t          | j        |          }d
}| j	        dk    rs	 ddl
m}  |                                | j        ||                    d                    }n4# t          $ r'}	t                              d| j        |	            d
}	~	ww xY w| j        r| j                                        ni }
| j        r,|
                    | j                                                   t(          rt*          r|                                 |
d<   t.          r|                                 |
d<   |                    d          dk    rt2          t          d| j         d          ||pd
t5          |          dd}|||d<   ||d	urdd
l||	 d&fd	}||d<   	 t3          d'i |4 d
{V \  }}t9          ||fi |
4 d
{V 	 }t;          j        |                                t5          |                     d
{V | _         || _!        | "                                 d
{V  | j#        $                                 tK          | j                   d| _&        | '                                 d
{V }|dk    r t          (                    d| j                   d
d
d
          d
{V  n# 1 d
{V swxY w Y   d
d
d
          d
{V  n# 1 d
{V swxY w Y   n,# tR          $ r}| *                    |          }Y d
}~nd
}~ww xY w|S tV          rdd
l}|,                    |          fd}d	|-                    t5          |          d          |d|gid }|r||d<   |||d<   |||d!<   	  |j.        d'i |4 d
{V }t_          ||"          4 d
{V \  }}}t9          ||fi |
4 d
{V }t;          j        |                                t5          |                     d
{V | _         || _!        | "                                 d
{V  | j#        $                                 tK          | j                   d| _&        | '                                 d
{V }|dk    r t          (                    d#| j                   d
d
d
          d
{V  n# 1 d
{V swxY w Y   d
d
d
          d
{V  n# 1 d
{V swxY w Y   d
d
d
          d
{V  n# 1 d
{V swxY w Y   n,# tR          $ r}| *                    |          }Y d
}~nd
}~ww xY w|S |t5          |          |d$}|||d<   	 ta          |fi |4 d
{V \  }}}t9          ||fi |
4 d
{V }t;          j        |                                t5          |                     d
{V | _         || _!        | "                                 d
{V  | j#        $                                 tK          | j                   d| _&        | '                                 d
{V }|dk    r t          (                    d%| j                   d
d
d
          d
{V  n# 1 d
{V swxY w Y   d
d
d
          d
{V  n# 1 d
{V swxY w Y   n,# tR          $ r}| *                    |          }Y d
}~nd
}~ww xY w|S )(z3Run the server using HTTP/StreamableHTTP transport.r  zw' requires HTTP transport but mcp.client.streamable_http is not available. Upgrade the mcp package to get HTTP support.rw  r-  c              3   F   K   | ]}|                                 d k    V  dS )mcp-protocol-versionN)r   )r   r   s     r,   re  z*MCPServerTask._run_http.<locals>.<genexpr>
  s0      LLS399;;"88LLLLLLrT   rK  r   r.  TNoauthr   get_managerz#MCP OAuth setup failed for '%s': %srK   rX   	transportssezt' requires SSE transport but mcp.client.sse.sse_client is not available. Upgrade the mcp package to get SSE support.     r@)rw  r-  r  sse_read_timeoutauthc                     dd}|||d<   n                     dd          |d<   | | |d<   |||d<   |d	<    j        d
i |S )NT)r1  r0  r  r  rQ  readr-  rS  r2  rS   )r5  r6  )r-  r  rS  kwargs_cert_for_factory
_httpx_mod_verify_for_factorys       r,   _mcp_http_client_factoryz9MCPServerTask._run_http.<locals>._mcp_http_client_factory
  s     -1"5$ $F *,3y)),6,>,>t%,>,P,Py)*,3y)')-v(4):v1:1;;F;;;rT   httpx_client_factoryr@  Fr  uA   MCP server '%s': reconnect requested — tearing down SSE sessionc                    K   | j         r| j        rz| j        j        }|j        |j        |j        fj        j        j        fk    rF| j        j                            dd           | j        j                            dd           dS dS dS dS )zBStrip Authorization headers when redirected to a different origin.authorizationNAuthorization)is_redirectnext_requestrw  r}  hostportr-  r  )rl  target_original_urls     r,   $_strip_auth_on_cross_origin_redirectzEMCPServerTask._run_http.<locals>._strip_auth_on_cross_origin_redirect4  s      ' QH,A Q%26Fv{FK@%,m.@-BTE   !-599/4PPP -599/4PPPPPQ Q Q Q rT   rU  rl  )r1  r  r0  event_hooksr2  )http_clientuB   MCP server '%s': reconnect requested — tearing down HTTP session)r-  r  r0  uI   MCP server '%s': reconnect requested — tearing down legacy HTTP session)NNNrS   )1_MCP_HTTP_AVAILABLEr  r  r   r   anyr;   r  r  r  tools.mcp_oauth_managerrN  get_or_build_providerr!   r"   r\   r  r/  r  r   r  r  r  r  r  r<   r  r4  r6   rg  rI  r  r  r~  r  r  r  r  r  r  rZ   rX  rH  _MCP_NEW_HTTPURLr5  r6  r:   r9   )r  r  rw  r-  r   r.  r  _oauth_authrN  r+   r  _sse_kwargsr[  r!  r"  r~  r   _egr4  rf  r<  rh  _get_session_id_http_kwargsrX  rY  re  rZ  s                           @@@@r,   	_run_httpzMCPServerTask._run_http
  s      " 	?ty ? ? ?   Umvzz),,233
 LLGLLLLL 	F.EG*+ **%68PQQZZd33
*49f== ?g%%??????)kmmAAIsFJJw$7$7     DdiQTUUU >B^S$.77999QS 	G""4#4#C#C#E#EFFF" 	N'E 	N151K1K1M1MO-.* 	P262M2M2O2OO./
 ::k""e++!!B49 B B B   "?d 11$)	! !K & '2F#&*D*@*@ +***$/!&0# 6:< < < < < < < <& 7O23?%4444       8Sl,#\   5D            
 8?7G#..00%:P:P8 8 8 2 2 2 2 2 2. (/"22444444444))) ,DI666/4,'+'E'E'G'G!G!G!G!G!G!G![00"KK!;<@I  +                                                    4 & ? ? ? 99#>>? M c	 LLL!IIcNNMQ Q Q Q Q %) ==)?)?e=LL$ *-Q,RS	# #M  3+2i(&(3f%&(3f%?,5,==}== " " " " " " "5c{SSS " " " " " " " X#\?#0l#^#^o#^#^ " " " " " " "bi;B;K ' 2 2 4 4eO>T>T< < < 6 6 6 6 6 6D2 ,3DL"&"6"6"8"88888888 KOO--- 0	:::38D0+/+I+I+K+K%K%K%K%K%K%KF%44 &%@AE!" !" !"!" " " " " " " " " " " " " " " " " " " " " " " " " " "" " " " " " " " " " " " " " " " " " " " " " " " " " "" " " " " " " " " " " " " " " " " " " " " " " " " " "0 & ? ? ? 99#>>? M # 11$" "L
 &'2V$?0EEEE        J,[,ZZ/ZZ       ^e7>7G#..00%:P:P8 8 8 2 2 2 2 2 2. (/"22444444444))) ,DI666/4,'+'E'E'G'G!G!G!G!G!G!G![00"KK!CDHI  !                                                    . & ? ? ? 99#>>? Ms_  	=D 
D8"D33D8N M<1CMM<
M#	#M<&M#	'M<*N <
NN 	N
N 
N7N22N7$V( 7VU3(CU>U3
UU3UU3!V3
U=	=V U=	VV( 
V  V( #V $V( (
W2WW1\: \(C\3\(
\	\(\	\(\: (
\22\: 5\26\: :
]#]]#c                   K   d| _         | j        dS |                                 s=t                              d| j                   g | _        |                                  dS | j        4 d{V  t          | j        j
        d| j                   d{V | _        ddd          d{V  n# 1 d{V swxY w Y   |                                  dS )u  Discover tools from the connected session.

        Capability-gated: prompt-only / resource-only MCP servers don't
        implement ``tools/list``, and calling it raises ``McpError(-32601)``,
        which previously aborted the connection — those servers could never
        stay connected for their prompts/resources. Skip the call when the
        server doesn't advertise the ``tools`` capability.
        (Ported from anomalyco/opencode#31271.)
        FNus   MCP server '%s': does not advertise 'tools' capability — skipping tools/list (prompts/resources remain available)r+  )r  r~  r  r"   rZ   r  r  $_register_discovered_tools_if_neededr  r   r  r.  s    r,   r  zMCPServerTask._discover_tools  s      "'<F%%'' 	KKK	  
 DK55777F> 	 	 	 	 	 	 	 	 3'$)! !      DK	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	1133333s   1,B//
B9<B9c                 (   | j         rdS | j                                        sOt          5  t                              | j                  | ur	 ddd           dS 	 ddd           n# 1 swxY w Y   t          | j        | | j                  | _         dS )a  Re-register tools after a post-ready reconnect if needed.

        Initial registration is performed by ``_discover_and_register_server``
        after ``start()`` completes. During a later reconnect, outage handling
        may clear ``_ready`` before discovery and may deregister stale tools.
        A managed server can still be identified by its entry in ``_servers``;
        publish its freshly discovered tools before transport readiness is
        restored so a successful revival cannot come back with zero tools.
        N)	r  r  r  r  _serversr   r  r  r  r.  s    r,   rv  z2MCPServerTask._register_discovered_tools_if_needed  s     & 	F{!!## 	  <<	**$66       6               '=ItT\'
 '
###s   #A''A+.A+c                 ,  K   || _         |                    dt                    | _        |                    d          pd                                                                | _        t          |d          | _        t          |d          | _	        |                    di           }|                    dd          r"t          rt          | j        |          | _        nd	| _        |                    d
i           }|                    dd          r$t          rt          | j        ||           | _        nd	| _        d|v r$d|v r t"                              d| j                   |                                 r	 t)          | j        |                    d                     nS# t*          $ rF}t"                              d|           || _        | j                                         Y d	}~d	S d	}~ww xY w|                    d          dk    r|                    d          s| j                                        s| j        dk    r	 t5          |                    d          pi           }|                     |d         ||                    dd          t9          | j        |                     d	{V  nS# t:          $ rF}t"                              d|           || _        | j                                         Y d	}~d	S d	}~ww xY wd| _        d}d}	 	 |                                 r|                     |           d	{V }n|                      |           d	{V }| j!                                        r
	 d	| _"        d	S |dk    rt"          #                    d| j        | j$                   d	| _"        | %                                 d	{V  | j!                                        r
	 d	| _"        d	S | j&        '                                 	 d	| _"        t"          (                    d| j                   | j)        r
d| _        d}n| xj        dz  c_        | j        tT          k    rt"                              d| j        tT          tV                     d| _,        | -                                 | j&        '                                 | .                    tV                     d	{V }	|	dk    r
	 d	| _"        d	S t"          (                    d | j                   tT          | _        d}| j        '                                 d	| _"        	 d	| _"        =# t^          j0        $ r	 d	| _"         tb          $ r}d	| _"        te          |          }
tg          |
          }| 4                                r;t"                              d!| j        tk          |
          j6        |
           d	| _$        | j                                        sto          |
          rat"                              d"| j        tk          |
          j6        |
           || _        | j                                         Y d	}~d	| _"        d	S |d#k    rt"                              d$| j        tk          |
          j6        |
           || _        | j                                         d| _,        | -                                 | j&        '                                 | .                    tV                     d	{V }	|	dk    rY d	}~d	| _"        d	S t"          (                    d%| j                   d}d| _        d}d	| _        | j        '                                 Y d	}~d	| _"        w|dz  }|tp          k    rt"                              d&| j        tp          tk          |
          j6        |
           || _        | j                                         d| _,        | -                                 | j&        '                                 | .                    tV                     d	{V }	|	dk    rY d	}~d	| _"        d	S t"          (                    d'| j                   d}d| _        d}d	| _        | j        '                                 Y d	}~d	| _"        t"          (                    d(| j        |tp          |tk          |
          j6        |
           t_          j9        tu          |                     d	{V  tw          |d)z  tx                    }| j!                                        r-|| _        | j                                         Y d	}~d	| _"        d	S Y d	}~d	| _"        p| j!                                        rAt"          (                    d*| j        tk          |
          j6        |
           Y d	}~d	| _"        d	S |d#k    rt"                              d+| j        tV          tk          |
          j6        |
           d| _,        | -                                 | j&        '                                 | .                    tV                     d	{V }	|	dk    rY d	}~d	| _"        d	S t"          (                    d,| j                   tT          | _        d}Y d	}~d	| _"        | xj        dz  c_        | j        tT          k    rt"                              d-| j        tT          tV          tk          |
          j6        |
           d| _,        | -                                 | j&        '                                 | .                    tV                     d	{V }	|	dk    rY d	}~d	| _"        d	S t"          (                    d | j                   tT          | _        d}Y d	}~d	| _"        t"          (                    d.| j        | j        tT          |tk          |
          j6        |
           t_          j9        tu          |                     d	{V  tw          |d)z  tx                    }| j!                                        rY d	}~d	| _"        d	S Y d	}~nd	}~ww xY w	 d	| _"        n# d	| _"        w xY w	y)/zLong-lived coroutine: connect, discover tools, wait, disconnect.

        Includes automatic reconnection with exponential backoff if the
        connection drops unexpectedly (unless shutdown was requested).
        r  rS  r   r  r  samplingenabledTNelicitation)r_  rw  r   zMCP server '%s' has both 'url' and 'command' in config. Using HTTP transport ('url'). Remove 'command' to silence this warning.z%srO  rP  skip_preflightrL  r-  r.  )r-  r.  r  r   rh   r  zLMCP server '%s': stdio session recycled after %s; waiting for lazy reconnectz@MCP server '%s': reconnecting (OAuth recovery or manual refresh)r   u   MCP server '%s': %d consecutive reconnects without a healthy session (rapid-drop budget exhausted), parking; will self-probe every %ds until it recovers (state: degraded → parked)r@  r  zwMCP server '%s': attempting revival from parked state (self-probe or explicit reconnect request); rebuilding transport.zfMCP server '%s': lazy reconnect after stdio recycle failed, marking unavailable while retrying: %s: %szWMCP server '%s' failed initial OAuth authentication, not retrying automatically: %s: %srj  u   MCP server '%s' failed initial connection with a permanent error, parking without retries (state: connecting → parked): %s: %szMCP server '%s': attempting revival after permanent initial failure (self-probe or explicit reconnect request); rebuilding transport.u   MCP server '%s' failed initial connection after %d attempts, parking until a reconnect is requested (state: connecting → parked): %s: %szMCP server '%s': attempting revival after initial connection failures (self-probe or explicit reconnect request); rebuilding transport.zTMCP server '%s' initial connection failed (attempt %d/%d), retrying in %.0fs: %s: %sr  z4MCP server '%s' disconnected during shutdown: %s: %su   MCP server '%s' hit a permanent error, parking without retries; will self-probe every %ds (state: connected → parked): %s: %szMCP server '%s': attempting revival from parked state (permanent error; self-probe or explicit reconnect request); rebuilding transport.u   MCP server '%s' failed after %d reconnection attempts, parking; will self-probe every %ds until it recovers (state: degraded → parked): %s: %szNMCP server '%s' connection lost (attempt %d/%d), reconnecting in %.0fs: %s: %s)=r  r   r  r  r   r   r  _get_lifecycle_secondsr  r  r  r  r  r  _MCP_ELICITATION_TYPESr^  r  r"   r\   r  r  rO  r  r  r  r  r   rD  r  rV  r  rt  r*  r  r~  rZ   r  _wait_for_lazy_reconnectr  r  r#   r  _MAX_RECONNECT_RETRIES_PARKED_RETRY_INTERVALr  _deregister_toolsr  rg  rh  r!   ra  rv  r  r>  rQ  rp  _MAX_INITIAL_CONNECT_RETRIESr  rm   rH  _MAX_BACKOFF_SECONDS)r  r  sampling_configelicitation_configr+   _probe_headersinitial_retriesbackofflifecycle_reasonparkedrt  failure_classs               r,   rs  zMCPServerTask.run  sq      "JJy2GHH!::f--3::<<BBDD%;FDZ%[%["%;FDZ%[%[" !**Z44y$// 	"4G 	",TYHHDNN!DN $ZZr::!!)T22 	%7M 	% 249>PX\ ] ] ]D $D F??yF22NN  		   ==?? "	(FJJu4E4EFFFF%   tS)))!!!!	$ zz+&&%//

CS8T8T/]a]h]o]o]q]q/vz  wF  JQ  wQ  wQ%)&**Y*?*?*E2%F%FN66u .#)::lD#A#A$8F$K$K	 7           +   NN4---"%DKKOO%%%FFFFF	 #$s	$r$==?? E-1^^F-C-C'C'C'C'C'C'C$$-1__V-D-D'D'D'D'D'D'D$ '..00 N	  $M	 $y00KK5	4#8  
 $(DL77999999999+2244 z  $y )//111v  $o &I   ' !&./D+!GG ++q0++.1GGGM !I'=2   ,0(..000-33555'+'K'K$: (L ( ( " " " " " " "Z//!d  $c 4 !I	   3I/"%
 !!####B  $A )     $ R R R# /s33 5d ; ;**,, 1NNM	4::#6  
 -1D) {))++ \%d++ A ItDzz':D  
 '*)))j  $g %33
 E !ItDzz':D	   '*)))+/(..000-33555'+'K'K$: (L ( ( " " " " " " "Z//"FFF~  $} H !I	   +,23/"%&*))+++ f  $c $q(O&)EEEO !I'C JJ/   '*)))+/(..000-33555'+'K'K$: (L ( ( " " " " " " "Z//"FFF~  $} H !I	   +,23/"%&*))+++ f  $c LLE	?4gT

+T   "-	'(:(:;;;;;;;;;!'A+/CDDG +2244 &))))F  $E HHHD  $ '..00 LLN	4::#6   FFFt  $q !K//
 NN@ 	#9T

+T   (,D$**,,,)//111#'#G#G 6 $H $ $      F ++J  $I LL: 		   /ED+!GHHHx  $u ''1,''*-CCCNN? 	#9.T

+T  & (,D$**,,,)//111#'#G#G 6 $H $ $      F ++<  $; LL0 		   /ED+!GHHH$  $ 4It68NT$ZZ0$	   mIg$6$6777777777gk+?@@ '..00 FFF#    cRb  $t####g	s	$s  >(F' '
G71;G22G7A0J> >
L;L		LA$U A&U <U CU AU !q<:C1q7+r 8B6q7.r ;Aq7r Cq7r &Aq71r >B4q72r ?r Aq7r &Bq7r .q7<r 	B;q7r .q7?r Bq7%r 2r 7q<<r 	rc                 Z  K   t          j        |                     |                    | _        	 | j                                         d{V  nM# t           j        $ r; | j        r2| j                                        s| j                                          w xY w| j	        r| j	        dS )z<Create the background Task and wait until ready (or failed).N)
rg  r  rs  r  r  r  rh  r  rv  r  )r  r  s     r,   startzMCPServerTask.startM  s      *488F+;+;<<
	+""$$$$$$$$$$% 
	 
	 
	 z $$*//"3"3 $
!!###
	 ; 	+	 	s   A A
Bc                   K   | j                                          | j                                         | j        r| j                                        s	 t          j        | j        d           d{V  nr# t
          j        $ r` t          	                    d| j
                   | j                                         	 | j         d{V  n# t
          j        $ r Y nw xY wY nw xY w| j        r`t          | j                  D ]}|                                 t          j        | j        ddi d{V  | j                                         |                                  d| _        dS )z=Signal the Task to exit and wait for clean resource teardown.r  r@  Nz3MCP server '%s' shutdown timed out, cancelling taskreturn_exceptionsT)r  r  r  r  r  rg  rI  rK  r"   r\   r  rv  rh  r  r  gatherr  r  r~  r  s     r,   r  zMCPServerTask.shutdown`  s       """ 	!!###: 	djoo// 	&tz2>>>>>>>>>>>' 	 	 	II   
!!###*$$$$$$$$-   D	 & 	0T899  .$"=VQUVVVVVVVVV'--///   s7   !A8 8AC'CC'C!C' C!!C'&C'c                     ddl m} t          t          | dg                     D ]&}|                    |           t          |           'g | _        dS )a  Drop this server's tools from the global registry (idempotent).

        Pulls the server's tool schemas out of the registry so the agent
        stops advertising them to the model. Called on shutdown AND when the
        reconnect budget is exhausted, so a dead server never leaves phantom
        tool definitions bloating the prompt cache and producing "not
        connected" errors on every turn.
        r   r  r  N)r  r  r  r   r  r  r  )r  r  r   s      r,   r  zMCPServerTask._deregister_tools~  sm     	,+++++gd,DbIIJJ 	/ 	/I	***#I....&(###rT   c                 f  K   t          j        | j                                                  }t          j        | j                                                  }	 t          j        ||ht           j                   d{V  ||fD ]P}|                                s:|                                 	 | d{V  4# t           j        t          f$ r Y Lw xY wQdS # ||fD ]P}|                                s:|                                 	 | d{V  4# t           j        t          f$ r Y Lw xY wQw xY w)z=Wait while an intentionally recycled stdio server is dormant.)r  N)
rg  r  r  r  r  r  r  rv  rh  r!   )r  r  r  r>  s       r,   r  z&MCPServerTask._wait_for_lazy_reconnect  s     +D,@,E,E,G,GHH ,T-B-G-G-I-IJJ	,/#3         
 '7  yy{{ KKMMM"







#2I>   	 7  yy{{ KKMMM"







#2I>   	sB   (C 1B::CC/D0	DD0D+	(D0*D+	+D0r   Nrd  )+rQ  rR  rS  rT  	__slots__r   r  rO  r  r  r  r  r  r
   r  r  r  r  r  rg  Taskr  r  r  r  r  r  r  r  r   r*  r8  rD  rX  rH  rt  r  rv  rs  r  r  r  r  rS   rT   r,   r`  r`    s        IE-S E- E- E- E-N%$ % % % %84 8 8 8 8(ID I I I I3 3 3 3% % % % % HSM    "	5huo 	5 	5 	5 	53 4    X X X         D) ) )VB B BH&H &H &H &HP   *g g g g gT *.) ))	) ) ) )VF8t F8 F8 F8 F8V C #'x
 x
 x
x
 $	x

 x
 x
 
x
 x
 x
 x
t*.@ *S * * * *Xnd n n n n`4 4 4>
 
 
 
(F$ F$ F$ F$ F$P$    &  <) ) ) )      rT   r`  rx  _server_connecting_server_connect_errors_server_connect_retry_after_server_connect_failuresr  g     @c                     t                               | d          dz   }|t           | <   t          t          d|dz
  z  z  t                    }t          j                    |z   t          | <   dS )a  Stamp an exponential-backoff cooldown after a failed connect.

    Called (under ``_lock``) when a server fails its discovery/connect
    attempt. The cooldown grows geometrically with the consecutive
    failure count and is capped at :data:`_CONNECT_RETRY_MAX_BACKOFF_SEC`,
    so a permanently-broken server settles into infrequent retries
    rather than a tight respawn loop.
    r   r   r  N)r  r   rH  _CONNECT_RETRY_BASE_BACKOFF_SEC_CONNECT_RETRY_MAX_BACKOFF_SECr  r  r  )r.   nr  s      r,   _record_connect_failurer    sk     	!$$[!44q8A,-[)'1Q<8& G 04~/?/?'/I,,,rT   c                 r    t                               | d           t                              | d           dS )z?Clear the connect-cooldown state after a successful connection.N)r  r  r  r.   s    r,   _clear_connect_failurer    s4      d333##K66666rT   c                 l    t                               |           }|duot          j                    |k     S )zBReturn True if ``server_name`` is still within its retry cooldown.N)r  r   r  r  )r.   deadlines     r,   _connect_cooldown_activer    s2    *..{;;H4?DN$4$4x$??rT   _server_error_counts_server_breaker_opened_atg      N@c                     t                               | d          dz   }|t           | <   |t          k    rt          j                    t
          | <   dS dS )a  Increment the consecutive-failure count for ``server_name``.

    When the count crosses :data:`_CIRCUIT_BREAKER_THRESHOLD`, stamp the
    breaker-open timestamp so the cooldown clock starts (or re-starts,
    for probe failures in the half-open state).
    r   r   N)r  r   _CIRCUIT_BREAKER_THRESHOLDr  r  r  )r.   r  s     r,   _bump_server_errorr    sV     	  a0014A()%&&&151A1A!+... '&rT   c                 P    dt           | <   t                              | d           dS )zFully close the breaker for ``server_name``.

    Clears both the failure count and the breaker-open timestamp. Call
    this on any unambiguous success signal (successful tool call,
    successful reconnect, manual /mcp refresh).
    r   N)r  r  r  r  s    r,   r  r    s+     )*%!!+t44444rT   serverc                     t          | dd          }|dS t          }|/|                                r|                    |j                   n|                                 dS )a*  Ask a server task to rebuild its transport, thread-safely.

    The tool handlers run on caller threads, while the server task and its
    ``_reconnect_event`` live on the background MCP loop. Setting an
    asyncio.Event from another thread must go through
    ``loop.call_soon_threadsafe``; only fall back to a direct ``.set()``
    when the loop isn't running (e.g. unit tests that drive the handler
    synchronously).

    Returns True if a reconnect signal was delivered, False if the server
    has no reconnect machinery (nothing to revive).
    r  NFT)r   	_mcp_loop
is_runningcall_soon_threadsafer  )r  eventloops      r,   _signal_reconnectr    se     F.55E}uDDOO--!!%),,,,		4rT   c                     t           5  t                              |           }ddd           n# 1 swxY w Y   |dS t          |          S )zBAsk a currently-live MCP server to rebuild after external re-auth.NF)r  rx  r   r  r.   r  s     r,   reconnect_mcp_serverr  '  s    	 + +k**+ + + + + + + + + + + + + + +~uV$$$s   /33old_sessionr  srvr  r  c          
         d}t          dt          t          t          |          d          |z                      }t          |          D ]}t	          | dd          }t	          | dd          }d}|Et          |d          r5	 t          |                                          }n# t          $ r d}Y nw xY w|	||ur|r dS ||dz
  k     rt          j
        |           d	S )
a  Wait for an MCP server to expose a usable session.

    Tool handlers run in normal worker threads while the MCP transport lives on
    the module's background asyncio loop. During a reconnect there is a short
    window where ``srv.session`` is ``None`` (or still points at the stale
    session until the lifecycle coroutine has left the transport context). A
    handler that blindly retries in that window can burn circuit-breaker strikes
    and return ``not connected`` even though the reconnect is already in
    progress.

    When ``old_session`` is supplied, require the observed session object to be
    different so callers do not mistake the pre-reconnect, stale session for a
    fresh one.
    g      ?r   rg   r~  Nr  Tr  F)ri   r  r  r   r   rF  rO  r  r!   r  r  )	r  r  r  poll_interval
iterationsir~  readyis_readys	            r,   _wait_for_server_session_readyr  0  s	   . MQCg44}DEEFFJ: & &#y$//Xt,,!9!9 //       7+#=#=(#=44zA~J}%%%5s   !B((B76B7r@  op_descriptionc                    t           }||                                sdS t          dd          }dfd}t                              d| |           |                    |           t          ||          S )	a#  Ask a live MCP server task to rebuild its transport session.

    The important detail is clearing ``_ready`` on the MCP event loop before
    setting ``_reconnect_event``. Older code left ``_ready`` set across
    reconnects, so the caller's readiness poll could return immediately and
    retry against the same dead HTTP/stream session. That was observed as
    repeated ``Session terminated`` / ``not connected`` / circuit-breaker
    failures in long-lived gateway sessions even though a fresh CLI process
    could connect successfully.
    NFr~  r   c                      t          dd           } | $t          | d          r|                                  t          dd           }|&t          |d          r|                                 d S d S d S )Nr  r  r  r  )r   rF  r  r  )r  reconnect_eventr  s     r,   _request_reconnectz6_signal_reconnect_and_wait.<locals>._request_reconnectp  s    Xt,,!8!8KKMMM!#'94@@&7?E+J+J&!!!!! '&&&rT   z2MCP server '%s': %s requesting transport reconnectr  r  )r  r  r   r"   rZ   r  r  )r.   r  r  r  r  r  r  s    `     r,   _signal_reconnect_and_waitr  Y  s    " D|4??,,|u#y$//K" " " " " " KK<^   	0111)   rT   rS   _AUTH_ERROR_TYPESc                     t           rt           S g } 	 ddlm}m} |                     ||g           n# t
          $ r Y nw xY w	 ddlm} |                     |           n# t
          $ r Y nw xY w	 ddlm	} |                     |           n# t
          $ r Y nw xY w	 ddl
}|                     |j                   n# t
          $ r Y nw xY wt          |           a t           S )u  Return a tuple of exception types that indicate MCP OAuth failure.

    Cached after first call. Includes:
      - ``mcp.client.auth.OAuthFlowError`` / ``OAuthTokenError`` — raised by
        the SDK's auth flow when discovery, refresh, or full re-auth fails.
      - ``mcp.client.auth.UnauthorizedError`` (older MCP SDKs) — kept as an
        optional import for forward/backward compatibility.
      - ``tools.mcp_oauth.OAuthNonInteractiveError`` — raised by our callback
        handler when no user is present to complete a browser flow.
      - ``httpx.HTTPStatusError`` — caller must additionally check
        ``status_code == 401`` via :func:`_is_auth_error`.
    r   )OAuthFlowErrorOAuthTokenError)UnauthorizedError)OAuthNonInteractiveErrorN)r  mcp.client.authr  r  r   r  r  r   tools.mcp_oauthr  r4  HTTPStatusErrorr  )typesr  r  r  r  r4  s         r,   _get_auth_error_typesr    s`     !  ECCCCCCCCno67777   555555&''''   <<<<<<-....   U*++++   esB   2 
??A 
A,+A,0B 
BBB< <
C	C	c                     t                      }|rt          | |          sdS 	 ddl}t          | |j                  rt	          | j        dd          dk    S n# t          $ r Y nw xY wdS )zReturn True if ``exc`` indicates an MCP OAuth failure.

    ``httpx.HTTPStatusError`` is only treated as auth-related when the
    response status code is 401. Other HTTP errors fall through to the
    generic error path in the tool handlers.
    Fr   Nrm  rn  T)r  r   r4  r  r   rl  r  )r+   r  r4  s      r,   rp  rp    s     "##E 
3.. uc5011 	E3<==DD	E   4s   2A 
A%$A%c                 b    t          |          sdS ddlm}  |             fd}	 t          |d          }n5# t          $ r(}t
                              d |           d}Y d}~nd}~ww xY w|rt          5  t          	                               }ddd           n# 1 swxY w Y   d}	|&t          |d	          rt           || d
d          }	|	rt                      	  |            }
	 t          j        |
          }d|vrt                      |
S n-# t          j        t           f$ r t                      |
cY S w xY wn4# t          $ r'}t
                              d ||           Y d}~nd}~ww xY wt#                      t          j        d  d  dd dd          S )aQ  Attempt auth recovery and one retry; return None to fall through.

    Called by the 5 MCP tool handlers when ``session.<op>()`` raises an
    auth-related exception. Workflow:

      1. Ask :class:`tools.mcp_oauth_manager.MCPOAuthManager.handle_401` if
         recovery is viable (i.e., disk has fresh tokens, or the SDK can
         refresh in-place).
      2. If yes, set the server's ``_reconnect_event`` so the server task
         tears down the current MCP session and rebuilds it with fresh
         credentials. Wait briefly for ``_ready`` to re-fire.
      3. Retry the operation once. Return the retry result if it produced
         a non-error JSON payload. Otherwise return the ``needs_reauth``
         error dict so the model stops hallucinating manual refresh.
      4. Return None if ``exc`` is not an auth error, signalling the
         caller to use the generic error path.

    Args:
        server_name: Name of the MCP server that raised.
        exc: The exception from the failed tool call.
        retry_call: Zero-arg callable that re-runs the tool call, returning
            the same JSON string format as the handler.
        op_description: Human-readable name of the operation (for logs).

    Returns:
        A JSON string if auth recovery was attempted, or None to fall
        through to the caller's generic error path.
    Nr   rM  c                  @   K                         d            d {V S rd  )
handle_401)managerr.   s   r,   _recoverz._handle_auth_error_and_retry.<locals>._recover  s/      ''T:::::::::rT   r  r@  z+MCP OAuth '%s': recovery attempt failed: %sFr  z after OAuth recovery   r  r  r]   z.MCP %s/%s retry after auth recovery failed: %sr  z4' requires re-authentication. Run `hermes mcp login u   ` (or delete the tokens file under ~/.hermes/mcp-tokens/ and restart). Do NOT retry this tool — ask the user to re-authenticate.T)r]   needs_reauthr  r  )rp  rk  rN  _run_on_mcp_loopr!   r"   r\   r  rx  r   rF  r  r  r  r  r   rQ   r  r  )r.   r+   
retry_callr  rN  r  	recoveredrec_excr  reconnectedr   r  	retry_excr  s   `            @r,   _handle_auth_error_and_retryr    s   D # t333333kmmG; ; ; ; ; ;$Xr:::		   9	
 	
 	
 						  $ 	, 	,,,{++C	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	, 	,?ws,>???4"0GGG	  K  	-,,,	Z\\FF++&(('444!M ) ()4   #K000 )  	 	 	NN@^Y       	 {###:>; > >%0> > >
 	 	 	 	 	 	sc   = 
A/A**A/<B##B'*B'*
E 5(D E 'E	E E		E 
E>E99E>)zinvalid or expired sessionzexpired sessionzsession expiredzsession not foundzunknown sessionzsession terminatedclosedresourceerrorzclosed resourceztransport is closedzconnection closedzbroken pipezend of file_SESSION_EXPIRED_MARKERSi'  c                    	 ddl m}m}m} |||f}n# t          $ r d}Y nw xY w| g}t                      }d}t          }|r/|dk    r(|                                }	|	!t          |	          }
|
|v r5|	                    |
           |dz  }t          |	t                    rdS t          |	|          rd}t          |	                                          r"t          fdt          D                       rd}|                    t#          |	d	d                     |                    t#          |	d
d                     |                    t#          |	dd                     |r|dk    (|S )u  Return True if ``exc`` looks like an MCP transport session expiry.

    Streamable HTTP MCP servers may garbage-collect server-side session
    state while the OAuth token remains valid — idle TTL, server
    restart, horizontal-scaling pod rotation, etc.  The SDK surfaces
    this as a JSON-RPC error whose message contains phrases like
    ``"Invalid or expired session"``.  This class of failure is
    distinct from :func:`_is_auth_error`: re-running the OAuth refresh
    flow would be pointless because the access token is fine.  What's
    needed is a transport reconnect — tear down and rebuild the
    ``streamablehttp_client`` + ``ClientSession`` pair, which is
    exactly what ``MCPServerTask._reconnect_event`` triggers.
    r   )BrokenResourceErrorClosedResourceErrorEndOfStreamrS   FNr   Tc              3       K   | ]}|v V  	d S rd  rS   )r   markerr   s     r,   re  z,_is_session_expired_error.<locals>.<genexpr>  s'      LLv}LLLLLLrT   rY  r  r  )anyior  r  r  r  r  _EXC_TRAVERSAL_MAX_NODESr  r  r  r   InterruptedErrorr   r   rj  r  r   r   r   )r+   r  r  r  transport_error_typesstackseentransport_error_foundbudgetr  identityr   s              @r,   _is_session_expired_errorr  P  s   "	#OOOOOOOOOO  !

  # # # "# ,/%EUUD!%F
 <FQJJ))++?g;;t!g/00 	5g455 	)$(! 'll  "" 	)3LLLL3KLLLLL 	)$(!WWlB77888WWk488999WWmT::;;;3  <FQJJ6 ! s    ""c                    t          |          sdS t          5  t                              |           }ddd           n# 1 swxY w Y   |t	          |d          sdS t
          }||                                sdS t                              d| ||           t          | ||d          st          
                    d|            dS 	  |            }	 t          j        |          }d|vrt          |            |S n-# t          j        t          f$ r t          |            |cY S w xY wn4# t           $ r'}t          
                    d| ||           Y d}~nd}~ww xY wdS )	uO  Trigger a transport reconnect and retry once on session expiry.

    Unlike :func:`_handle_auth_error_and_retry`, this does **not** call
    the OAuth manager's ``handle_401`` — the access token is still
    valid, only the server-side session state is stale.  Setting
    ``_reconnect_event`` causes the server task's lifecycle loop to
    tear down the current ``streamablehttp_client`` + ``ClientSession``
    and rebuild them, reusing the existing OAuth provider instance.
    See #13383.

    Args:
        server_name: Name of the MCP server that raised.
        exc: The exception from the failed call.
        retry_call: Zero-arg callable that re-runs the operation,
            returning the same JSON string format as the handler.
        op_description: Human-readable name of the operation (logs).

    Returns:
        A JSON string if reconnect + retry was attempted and produced
        a response, or ``None`` to fall through to the caller's
        generic error path (not a session-expired error, no server
        record, reconnect didn't ready in time, or retry also failed).
    Nr  zmMCP server '%s': %s failed with session-expired error (%s); signalling transport reconnect and retrying once.r  r  zsMCP server '%s': reconnect did not ready within 15s after session-expired error; falling through to error response.r]   z2MCP %s/%s retry after session reconnect failed: %s)r  r  rx  r   rF  r  r  r"   rZ   r  r\   r  r  r  r   rQ   r!   )	r.   r+   r  r  r  r  r   r  r  s	            r,   !_handle_session_expired_and_retryr    s   : %S)) t	 ( (ll;''( ( ( ( ( ( ( ( ( ( ( ( ( ( (
{'#'9::{tD|4??,,|t
KK	<^S   &%	    	H	
 	
 	

 t
	Z''Ff$$#K000 % $i0 	 	 	,,,MMM	 %  
 
 
@	
 	
 	
 	
 	
 	
 	
 	


 4sM   A  AA
D/ (D  D/ 'D+(D/ *D++D/ /
E 9EE _parallel_safe_servers_mcp_tool_server_namesr  _mcp_threadr  r  r  r  c                     t          j                    } 	 d|  d|  d}t          |d          5 }d |                                                                D             cddd           S # 1 swxY w Y   n# t
          t          t          f$ r Y nw xY w	 ddl}d	 |	                    |           
                                D             S # t          $ r Y nw xY wt                      S )
zReturn a set of current child process PIDs.

    Uses /proc on Linux, falls back to psutil, then empty set.
    Used by _run_stdio to identify the subprocess spawned by stdio_client.
    z/proc/z/task/z	/childrenr   r   c                 T    h | ]%}|                                 t          |          &S rS   )r   r  )r   ps     r,   r  z'_snapshot_child_pids.<locals>.<setcomp>&  s+    BBBq		BCFFBBBrT   Nr   c                     h | ]	}|j         
S rS   )r&  )r   cs     r,   r  z'_snapshot_child_pids.<locals>.<setcomp>-  s    AAA!AAArT   )r$   r  r   rV  r   rq  rr  rR   psutilProcesschildrenr!   r  )r  children_pathfr  s       r,   r	  r	    s\    Y[[F@@@v@@@-'222 	CaBBAFFHHNN$4$4BBB	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	C 	Cw
3   AAv~~f55>>@@AAAA    55LsF   A8 0A,A8 ,A00A8 3A04A8 8BB4C 
CC)ztui_gateway.slash_workerztui_gateway.entryz-dorg.eclipse.equinox.launcherzeclipse.jdt.lszorg.eclipse.equinox.launcher_._NON_MCP_CHILD_CMDLINE_MARKERSpidsc                 ^   | s| S 	 ddl }n# t          $ r | cY S w xY wt                      }| D ]}	 |                    |                                          }n# |j        |j        t          f$ r Y Dw xY wt          d |dd         D                       rj|	                    |           |S )a  Remove non-MCP children from a PID snapshot delta.

    _snapshot_child_pids() returns *all* direct children of the gateway. When
    a stdio MCP server spawns concurrently with a slash_worker or LSP server
    spawn, the delta ``_snapshot_child_pids() - pids_before`` can include
    PIDs that are NOT the MCP server. Tracking those PIDs in _stdio_pgids is
    catastrophic if a future child lacks start_new_session: its pgid can be the
    TUI parent's PID, so the shutdown sweep's killpg() kills the TUI itself.
    r   Nc              3   2   K   | ]}t           D ]}||v V  	d S rd  )r  )r   argr  s      r,   re  z'_filter_mcp_children.<locals>.<genexpr>\  sQ       
 
8
 
  cM
 
 
 
 
 
 
rT   r   )
r  r  r  r   cmdlineNoSuchProcessAccessDeniedrr  rj  r  )r  r  filteredr&  argvs        r,   r
  r
  C  s	         EEH  	>>#&&..00DD$f&97C 	 	 	 H	  
 
ABBx
 
 
 
 
 	
 SOs    'AA21A2c                     |                     d          }t          |t                    rdt          |          v rdS |                     |           dS )a  Suppress benign 'Event loop is closed' noise during shutdown.

    When the MCP event loop is stopped and closed, httpx/httpcore async
    transports may fire __del__ finalizers that call call_soon() on the
    dead loop.  asyncio catches that RuntimeError and routes it here.
    We silence it because the connection is being torn down anyway; all
    other exceptions are forwarded to the default handler.
    r  zEvent loop is closedN)r   r   RuntimeErrorr   default_exception_handler)r  rL  r+   s      r,   _mcp_loop_exception_handlerr  f  sW     ++k
"
"C#|$$ )?3s88)K)K""7+++++rT   c                  z   t           5  t          't                                          r	 ddd           dS t          j                    at                              t                     t          j        t          j	        dd          a
t                                           ddd           dS # 1 swxY w Y   dS )z>Start the background event loop thread if not already running.Nzmcp-event-loopT)rd  r  daemon)r  r  r  rg  new_event_loopset_exception_handlerr  	threadingThreadrun_foreverr  r  rS   rT   r,   _ensure_mcp_loopr  u  s    
 
 
 Y%9%9%;%; 
 
 
 
 
 
 
 
 *,,	''(CDDD&(!
 
 

 	
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
s   "B0A,B00B47B4coror   c                      	 ddl m}mm  |            n# t          $ r  cY S w xY ws S  fd} |            S )uW  Carry the caller's context-local HERMES_HOME override into ``coro``.

    Returns ``coro`` unchanged when no override is active. Otherwise wraps
    it so the override is set inside the coroutine's own (task-local)
    context on the MCP loop and reset when it completes — concurrent calls
    carrying different scopes don't interfere.
    r   )get_hermes_home_overridereset_hermes_home_overrideset_hermes_home_overridec                  h   K              } 	  d {V 	  |            S #  |            w xY wrd  rS   )tokenr  home_overrider  r  s    r,   _scopedz)_wrap_with_home_override.<locals>._scoped  sZ      ((77	.::::::&&u----&&u----s   $ 1)r   r  r  r  r!   )r  r  r"  r!  r  r  s   `  @@@r,   _wrap_with_home_overrider#    s    		
 	
 	
 	
 	
 	
 	
 	
 	
 	
 1022    . . . . . . . . 799s    **c                 ~     	 ddl mm}  |            n# t          $ r  cY S w xY w S  fd} |            S )zBPropagate a dashboard OAuth flow onto the dedicated MCP loop task.r   )dashboard_oauth_flowget_dashboard_oauth_flowNc                  b   K              5    d {V cd d d            S # 1 swxY w Y   d S rd  rS   )r  r%  flows   r,   r"  z0_wrap_with_dashboard_oauth_flow.<locals>._scoped  s      !!$'' 	 	::::::	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	s   $(()tools.mcp_dashboard_oauthr%  r&  r!   )r  r&  r"  r%  r(  s   `  @@r,   _wrap_with_dashboard_oauth_flowr*    s    	
 	
 	
 	
 	
 	
 	
 	

 ('))   |       799s    ''r  c                    ddl m} ddlm} t          5  t
          }ddd           n# 1 swxY w Y   ||                                s7t          j        |           r| 	                                 t          d          t          |           r
 |             n| }t          |          }t          |          } |||t          d          }|t          d          t          j                    }|dn||z   }	  |            r#|                                 t%          d
          d}	|{|t          j                    z
  }
|
dk    rO|                                 t          j                    |z
  }t'          d|ddt)          |          dd          t+          |	|
          }		 |                    |	          S # t.          j        j        $ r. |                                r|                                cY S Y w xY w)a  Schedule a coroutine on the MCP event loop and block until done.

    Accepts either a coroutine object or a zero-arg callable that returns one.
    Callers can pass a factory to avoid constructing coroutine objects when
    the MCP loop is unavailable (which would otherwise leak the coroutine
    frame and emit ``"coroutine was never awaited"`` warnings).

    Poll in short intervals so the calling agent thread can honor user
    interrupts while the MCP work is still running on the background loop.
    r   )is_interruptedsafe_schedule_threadsafeNzMCP event loop is not runningzMCP scheduling failedr"   log_messagez/MCP event loop unavailable (failed to schedule)TzUser sent a new messageg?zMCP call timed out after z.1fzs (configured timeout: zs)r@  )tools.interruptr,  agent.async_utilsr.  r  r  r  rg  iscoroutinecloser  callabler#  r*  r"   r  r  rv  r  rK  r  rH  r   
concurrentfuturesr  )coro_or_factoryr  r,  r.  r  r  future
start_timer  wait_timeout	remainingelapseds               r,   r  r    s    /.....::::::	                |4??,,|// 	$!!###:;;; ( 9 9N??D $D))D*400D%%d+  F
 ~LMMM!!JttJ,@H> 	>MMOOO"#<=== 4>#3#33IA~~.**Z7"C? C C,1'NNBC C C   |Y77L
	===666!. 	 	 	 {{}} '}}&&&H	s   (,,(F> ><H ?H c                  2    t          j        ddid          S )z=Standardized JSON error for a user-interrupted MCP tool call.r]   z-MCP call interrupted: user sent a new messageFr  )r  r  rS   rT   r,   _interrupted_call_resultr?    s)    :@   rT   c                 &   ddl m t          | t                    r fd}t                              ||           S t          | t                    rd |                                 D             S t          | t                    rd | D             S | S )u9  Recursively resolve ``${VAR}`` placeholders.

    Both ``${VAR}`` and Cursor-style ``${env:VAR}`` are accepted — the
    ``env:`` prefix is stripped so a doc copied from a Cursor / Claude MCP
    config resolves the same secret. Resolves from the active profile's secret
    scope when multiplexing is on (so an MCP server config's ``${API_KEY}``
    picks up the routed profile's value, not the process-global ``os.environ``
    which may hold another profile's), falling back to ``os.environ``
    otherwise. Unset vars keep the literal placeholder, as before.
    r   )
get_secretc                     t          |                     d                    } ||                     d                    p|                     d          S )Nr   r   )r   r  )mr  _get_secrets     r,   _replacez'_interpolate_env_vars.<locals>._replace  sE     ,,D;tQWWQZZ00>AGGAJJ>rT   c                 4    i | ]\  }}|t          |          S rS   _interpolate_env_varsr   kvs      r,   
<dictcomp>z)_interpolate_env_vars.<locals>.<dictcomp>#  s'    FFF1(++FFFrT   c                 ,    g | ]}t          |          S rS   rG  )r   rK  s     r,   r   z)_interpolate_env_vars.<locals>.<listcomp>%  s!    888Q%a((888rT   )	agent.secret_scoperA  r   r   _ENV_VAR_PATTERNr   r   r   r  )r   rE  rD  s     @r,   rH  rH    s     =<<<<<% 5	? 	? 	? 	? 	?  ##He444% GFFFFFF% 988%8888LrT   serversc                 6   	 ddl m} n# t          $ r d}Y nw xY w|| S i }|                                 D ]c\  }}t	          |t
                    s|||<     |||          }|r0t                              d|d                    |                     ^|||<   d|S )zADrop exfiltration-shaped MCP configs before any stdio spawn path.r   )validate_mcp_server_entryNz'Skipping suspicious MCP server '%s': %sr   )	hermes_cli.mcp_securityrR  r!   r   r   r   r"   r\   r   )rP  _validate_mcp_server_entrysafe_serversr  cfgissuess         r,   _filter_suspicious_mcp_serversrX  )  s    ]ccccccc ] ] ]X\"""] ")L]]__ ! !	c#t$$ 	!$L++D#66 	NN9		&!!  
  Tr   c                     	 ddl m}  ddlm}  |d          ri S  |             }|                    d          }|rt          |t                    si S 	 ddlm}  |             n# t          $ r Y nw xY wi }t          |                                          D ].\  }}t          |          }t          |t                    r|||<   /|S # t          $ r'}	t                              d|	           i cY d}	~	S d}	~	ww xY w)	a  Read ``mcp_servers`` from the Hermes config file.

    Returns a dict of ``{server_name: server_config}`` or empty dict.
    Server config can contain either ``command``/``args``/``env`` for stdio
    transport or ``url``/``headers`` for HTTP transport, plus optional
    ``timeout``, ``connect_timeout``, and ``auth`` overrides.

    ``${ENV_VAR}`` placeholders in string values are resolved from
    ``os.environ`` (which includes ``~/.hermes/.env`` loaded at startup).
    r   )load_config)env_var_enabledHERMES_SAFE_MODEmcp_servers)load_hermes_dotenvzFailed to load MCP config: %sN)hermes_cli.configrZ  utilsr[  r   r   r   r   r^  r!   rX  r   rH  r"   r#   )
rZ  _env_enabledr  rP  r^  rU  r  rV  interpolatedr+   s
             r,   _load_mcp_configrc  D  se   111111999999<*++ 	I**]++ 	j$77 	I	@@@@@@     	 	 	D	(*7@@FFHH 	2 	2ID#055L,-- 2%1T"   4c:::						sG   C	 7C	 A% $C	 %
A2/C	 1A22AC	 	
C:C5/C:5C:r  c                 ^   K   t          |           }|                    |           d{V  |S )a  Create an MCPServerTask, start it, and return when ready.

    The server Task keeps the connection alive in the background.
    Call ``server.shutdown()`` (on the same event loop) to tear it down.

    Raises:
        ValueError: if required config keys are missing.
        ImportError: if HTTP transport is needed but not available.
        Exception: on connection or initialization failure.
    N)r`  r  )r  r  r  s      r,   _connect_serverre  n  s>       4  F
,,v

MrT   c                                                     sdS t          5  t          }ddd           n# 1 swxY w Y   ||                                sdS dfd}|                    |           dt
          ffd}	 t          t          |t                              S # t          $ r'}t          
                    d| |           Y d}~dS d}~ww xY w)	zBWake a recycled stdio server and wait briefly for a fresh session.FNr   c                  l     j                                           j                                         d S rd  )r  r  r  r  )r  s   r,   r  z2_request_lazy_reconnect.<locals>._signal_reconnect  s2    ##%%%%%rT   c                    K   t          j                    t          z   } t          j                    | k     rSj        j                                        rdS t          j        d           d {V  t          j                    | k     SdS )NTg?F)r  r  _RECYCLED_RECONNECT_TIMEOUTr~  r  r  rg  r  )r  r  s    r,   _await_readyz-_request_lazy_reconnect.<locals>._await_ready  s      >##&AAn))~)fm.B.B.D.D)t-%%%%%%%%% n)) urT   r@  z>MCP server '%s': lazy reconnect after stdio recycle failed: %sr  )r  r  r  r  r  rO  r  ri  r!   r"   r\   )r.   r  r  r  rj  r+   s    `    r,   _request_lazy_reconnectrk    s\   $$&& u	                |4??,,|u& & & & & & 	/000      $\;VWWWXXX   L	
 	
 	
 uuuuus$   377?"B" "
C,CCc                 D   t           5  t                              |           }ddd           n# 1 swxY w Y   |d|j        ]|                                rIt          | |           t           5  t                              |           }ddd           n# 1 swxY w Y   |S )zDReturn a connected server, lazily reconnecting recycled stdio state.N)r  rx  r   r~  r  rk  r  s     r,   _get_connected_server_for_callrm    s   	 + +k**+ + + + + + + + + + + + + + +fn49R9R9T9T4V444 	/ 	/\\+..F	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/Ms   /33.BBBc                 ^    t          | dd          }t          |          r |             dS dS )z@Record a user-visible MCP operation when the server supports it.r  N)r   r5  )r  r  s     r,   _mark_server_call_startedro    sB    V%5t<<N  rT   r  c                 8     dt           dt          f fd}|S )zReturn a sync handler that calls an MCP tool via the background loop.

    The handler conforms to the registry's dispatch interface:
    ``handler(args_dict, **kwargs) -> str``
    r   r   c                    
 t                               d          t          k    rt                              d          }t	          j                    |z
  }|t          k     rRt          dt          t          |z
                      }t          j
        dd dt                     d| did	
          S t                    s+t                     t          j
        dd did	
          S j        st          t          dt!          pd                              rnVt                     t#                    rt          j
        dd did	
          S t          j
        dd did	
          S  fd

fd}	  |            }	 t          j        |          }d|v rt                     nt'                     n+# t          j        t*          f$ r t'                     Y nw xY w|S # t,          $ r t/                      cY S t0          $ r}t3          ||d           }	|	|	cY d }~S t5          ||d           }	|	|	cY d }~S t                     t6                              d|           t          j
        dt;          dt=          |          j         dtA          |                     id	
          cY d }~S d }~ww xY w)Nr   rg   r   r]   r  z' is unreachable after z0 consecutive failures. Auto-retry available in ~ue   s. Do NOT retry this tool yet — use alternative approaches or ask the user to check the MCP server.Fr  ' is not connectedr,  r@  ut   ' transport is down; reconnect requested. Do NOT retry this tool immediately — give it a few seconds to come back.c                    K   t                     j        4 d {V  t          j                    _        	 j                                       d {V } d _        n# d _        w xY w	 d d d           d {V  n# 1 d {V swxY w Y   t          dd           }|
 |             | j        rd}| j	        pg D ]R}t          |dd           r||j
        z  }t          t          |dd           dd           }|r|t          |          z  }St          j        dt          |pd          id	          S g }| j	        pg D ]}t          |d          r"|j
        r|                    |j
                   5t#          |          }|r|                    |           \t%          |          }|r|                    |           t'          |          }|r|                    |           t          |d
d           pt)          |          j        }	|	dv rt,                              d|	           t,                              d|	           |rd                    |          nd}
t          | dd           }|3|
rt          j        |
|dd	          S t          j        d|id	          S t          j        d|
id	          S )Nr   r  r   r   r-  r]   zMCP tool returned an errorFr  r>  >   r   audioimager-  z,MCP %s: content block type %r rendered emptyz2MCP %s: dropping unsupported content block type %rr  structuredContent)r   rw  r   )ro  r  contextvarscopy_contextrn  r~  	call_toolr   isErrorr  r   r   r  r  r   rF  r   r#  r<  rM  r>  rQ  r"   r#   r\   r   )r   _mark_proven
error_textr  res_textr   	image_tag	audio_tagresource_textrH  text_result
structuredr   r  r.   r   s               r,   _callz3_make_tool_handler.<locals>._handler.<locals>._call  s     %f---' 	8 	8 	8 	8 	8 	8 	8 	8
 0;/G/I/I,8#)>#;#;IQU#;#V#VVVVVVVF37F004F077777	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 	8 #6+A4HHL'~ '
$n2 	4 	4Eufd33 !"ej0
   'wuj$'G'GQUVVH 4"c(mm3
z_"B&B # !&	' ' ' '"  "E ..B " "5&)) ej LL,,,2599	 LL+++2599	 LL+++
 !;5+ N N  LL///
 %UFD99QT%[[=Q
!GGGLLF#Z   
 NNL#Z    /4;$))E***K !)<dCCJ% +:"--7' ' %*+ + + + z8Z"8uMMMM:x5EJJJJs(   B"A$B$	A--B
BBc                  &    t                     S Nr@  r  r  r  s   r,   
_call_oncez8_make_tool_handler.<locals>._handler.<locals>._call_oncee      #E<@@@@rT   ztools/call zMCP tool %s/%s call failed: %sMCP call failed: rT  )!r  r   r  r  r  r  _CIRCUIT_BREAKER_COOLDOWN_SECri   r  r  r  rm  r  r~  r  rH  r  r  r  r  r   rQ   r  r?  r!   r  r  r"   r]   r   r>  rQ  r   )r   rW  	opened_atager<  r  r   r  r+   r  r  r  r.   r   r  s   `         @@r,   r  z$_make_tool_handler.<locals>._handler  sP     ##K337QQQ155k3GGI.""Y.C2223'Ds'J#K#KLL	zO{ O O/<O O>GO O O# !&' ' ' ' 0<< 	#{+++:GGGG!# # # # ~  	' .C|/Bs)C)C D D   '  #;///$V,, +:S; S S S' %*+ + + + zKKKKK# %' ' ' 'b	K b	K b	K b	K b	K b	K b	K b	KH	A 	A 	A 	A 	A 	A,	#Z\\F1F++f$$&{3333'444()4 1 1 1#K000001M 	. 	. 	.+----- 	# 	# 	# 5S*)i)) I $       
 :S*)i)) I $       {+++LL0Y   :MS		(:MMhsmmMM  "	# # # # # # # # #5	#sa   
G> 7G G> %G96G> 8G99G> >K	K!K9K?KKA5KKKr   r   )r.   r   r  r  s   ``` r,   _make_tool_handlerr    sM    W#t W## W# W# W# W# W# W# W# W#r OrT   c                 4     dt           dt          f fd}|S )z>Return a sync handler that lists resources from an MCP server.r   r   c                 6   t                    rj        st          j        dd did          S fdfd}	  |            S # t          $ r t                      cY S t          $ r}t          ||d          }||cY d }~S t          ||d          }||cY d }~S t          
                    d	|           t          j        dt          d
t          |          j         dt          |                     id          cY d }~S d }~ww xY w)Nr]   r  rr  Fr  c                  <  K   t                     j        4 d {V  t          j        j        d           d {V } d d d           d {V  n# 1 d {V swxY w Y   g }| D ]}i }t          |d          rt          |j                  |d<   t          |d          r
|j        |d<   t          |d          r|j	        r
|j	        |d<   t          |d          r|j
        r
|j
        |d<   |                    |           t          j        d|id          S )N	resourcesr%  r  r   r  Fr  )ro  r  r   r~  list_resourcesrF  r   r%  r  r   r  r   r  r  )all_resourcesr  rentryr  r.   s       r,   r  z=_make_list_resources_handler.<locals>._handler.<locals>._call  s     %f---'        &9N1;' ' ! ! ! ! ! !                           I" 
( 
(1e$$ .#&qu::E%L1f%% +$%FE&M1m,, 9 9+,=E-(1j)) 3aj 3()
E*%  '''':{I6UKKKK   "A
A!Ac                  &    t                     S r  r  r  s   r,   r  zB_make_list_resources_handler.<locals>._handler.<locals>._call_once  r  rT   zresources/listz MCP %s/list_resources failed: %sr  rT  rm  r~  r  r  r  r?  r!   r  r  r"   r]   r   r>  rQ  r   	r   rW  r  r+   r  r  r  r.   r  s	        @@r,   r  z._make_list_resources_handler.<locals>._handler  s   /<< 	#V^ 	#:GGGG!# # # #	L 	L 	L 	L 	L 	L(	A 	A 	A 	A 	A 	A	#:<< 	. 	. 	.+----- 	# 	# 	#4S*.> I $       9S*.> I $       LL2K   :MS		(:MMhsmmMM  "	# # # # # # # # #	#=   	A D)	D2DDD"D(A%DDDr  r.   r  r  s   `` r,   _make_list_resources_handlerr    s=    4#t 4## 4# 4# 4# 4# 4# 4# 4#l OrT   c                 4     dt           dt          f fd}|S )zFReturn a sync handler that reads a resource by URI from an MCP server.r   r   c                    ddl m} t          	          rj        st	          j        dd	 did          S |                     d          s |d	          S 	fd

fd}	  |            S # t          $ r t                      cY S t          $ r}t          	||d          }||cY d }~S t          	||d          }||cY d }~S t                              d	|           t	          j        dt          dt          |          j         dt#          |                     id          cY d }~S d }~ww xY w)Nr   
tool_errorr]   r  rr  Fr  r%  z Missing required parameter 'uri'c                  x  K   t                     j        4 d {V  j                                       d {V } d d d           d {V  n# 1 d {V swxY w Y   g }t	          | d          r| j        ng }|D ]}t          |dd           |                    |j                   .t          |dd           Lt          t          d|                    }|                    |pdt          |j                   d           t          j        d|rd	                    |          nd
id          S )Ncontentsr   rB  r-  )r>  r-  z[binary data, r7  r   r  r   Fr  )ro  r  r~  r@  rF  r  r   r   r   rM  r   r   rB  r  r  r   )r   r   r  r  renderedr  r.   r%  s        r,   r  z<_make_read_resource_handler.<locals>._handler.<locals>._call  s     %f---' A A A A A A A A%~;;C@@@@@@@@A A A A A A A A A A A A A A A A A A A A A A A A A A A  "E*1&**E*EMv2H! X X5&$//;LL,,,,UFD11=  :'Z%HHH#   H LL!V-Vc%*oo-V-V-VWWW:xU)J5)9)9)9KZ_````s   !A
A Ac                  &    t                     S r  r  r  s   r,   r  zA_make_read_resource_handler.<locals>._handler.<locals>._call_once  r  rT   zresources/readzMCP %s/read_resource failed: %sr  rT  r  r  rm  r~  r  r  r   r  r?  r!   r  r  r"   r]   r   r>  rQ  r   )r   rW  r  r  r+   r  r  r  r%  r.   r  s         @@@r,   r  z-_make_read_resource_handler.<locals>._handler  s-   ------/<< 	#V^ 	#:GGGG!# # # # hhuoo 	B:@AAA	a 	a 	a 	a 	a 	a 	a*	A 	A 	A 	A 	A 	A	#:<< 	. 	. 	.+----- 	# 	# 	#4S*.> I $       9S*.> I $       LL1;   :MS		(:MMhsmmMM  "	# # # # # # # # #	#s=   /	A9 9E	ED=1E7D=EA%D=7E=Er  r  s   `` r,   _make_read_resource_handlerr    s=    ;#t ;## ;# ;# ;# ;# ;# ;# ;#z OrT   c                 4     dt           dt          f fd}|S )z<Return a sync handler that lists prompts from an MCP server.r   r   c                 6   t                    rj        st          j        dd did          S fdfd}	  |            S # t          $ r t                      cY S t          $ r}t          ||d          }||cY d }~S t          ||d          }||cY d }~S t          
                    d	|           t          j        dt          d
t          |          j         dt          |                     id          cY d }~S d }~ww xY w)Nr]   r  rr  Fr  c                    K   t                     j        4 d {V  t          j        j        d           d {V } d d d           d {V  n# 1 d {V swxY w Y   g }| D ]}i }t          |d          r
|j        |d<   t          |d          r|j        r
|j        |d<   t          |d          r|j        rd |j        D             |d<   |	                    |           t          j        d|id          S )Npromptsr  r   r   c                     g | ]H}d |j         it          |d          r|j        r	d|j        ini t          |d          r	d|j        ini IS )r  r   required)r  rF  r   r  )r   r   s     r,   r   zO_make_list_prompts_handler.<locals>._handler.<locals>._call.<locals>.<listcomp>0  s     * * * 	 #AFAHMAZAZt_`_ltq}==rt <C1j;Q;QY
AJ77WY* * *rT   Fr  )ro  r  r   r~  list_promptsrF  r  r   r   r   r  r  )all_promptsr  r  r  r  r.   s       r,   r  z;_make_list_prompts_handler.<locals>._handler.<locals>._call"  s     %f---'        $7N/K% %                                 G  & &1f%% +$%FE&M1m,, 9 9+,=E-(1k** q{ * * "#* * *E+& u%%%%:y'2GGGGr  c                  &    t                     S r  r  r  s   r,   r  z@_make_list_prompts_handler.<locals>._handler.<locals>._call_once;  r  rT   zprompts/listzMCP %s/list_prompts failed: %sr  rT  r  r  s	        @@r,   r  z,_make_list_prompts_handler.<locals>._handler  s   /<< 	#V^ 	#:GGGG!# # # #	H 	H 	H 	H 	H 	H2	A 	A 	A 	A 	A 	A	#:<< 	. 	. 	.+----- 	# 	# 	#4S*n I $       9S*n I $       LL0+s   :MS		(:MMhsmmMM  "	# # # # # # # # #	#r  r  r  s   `` r,   _make_list_prompts_handlerr    s=    9#t 9## 9# 9# 9# 9# 9# 9# 9#v OrT   c                 4     dt           dt          f fd}|S )zDReturn a sync handler that gets a prompt by name from an MCP server.r   r   c                   	 ddl m} t          
          		r	j        st	          j        dd
 did          S |                     d          s |d	          S |                     d
i           	fdfd}	  |            S # t          $ r t                      cY S t          $ r}t          
||d          }||cY d }~S t          
||d          }||cY d }~S t                              d
|           t	          j        dt          dt          |          j         dt#          |                     id          cY d }~S d }~ww xY w)Nr   r  r]   r  rr  Fr  r  z!Missing required parameter 'name'r   c                    K   t                     j        4 d {V  j                                       d {V } d d d           d {V  n# 1 d {V swxY w Y   g }t	          | d          r| j        ng D ]}i }t	          |d          r
|j        |d<   t	          |d          rO|j        }t	          |d          r|j        |d<   n-t          |t                    r||d<   nt          |          |d<   |                    |           d|i}t	          | d          r| j        r
| j        |d<   t          j        |d          S )	Nrt  r  r  r  r   r   Fr  )ro  r  r~  
get_promptrF  r  r  r  r   r   r   r   r   r  r  )	r   r  r   r  r  r?  r   r  r  s	         r,   r  z9_make_get_prompt_handler.<locals>._handler.<locals>._callj  s/     %f---' T T T T T T T T%~888SSSSSSSST T T T T T T T T T T T T T T T T T T T T T T T T T T H+26:+F+FNB ' '3'' -$'HE&M3	** 8!kGw// 8+2<i((#GS11 8+2i((+.w<<i(&&&&)Dv}-- 9&2D 9&,&8]#:d7777s   #A
A"Ac                  &    t                     S r  r  r  s   r,   r  z>_make_get_prompt_handler.<locals>._handler.<locals>._call_once  r  rT   zprompts/getzMCP %s/get_prompt failed: %sr  rT  r  )r   rW  r  r  r+   r  r  r   r  r  r.   r  s         @@@@r,   r  z*_make_get_prompt_handler.<locals>._handler\  s:   ------/<< 	#V^ 	#:GGGG!# # # # xx 	C:ABBBHH["--		8 	8 	8 	8 	8 	8 	80	A 	A 	A 	A 	A 	A	#:<< 	. 	. 	.+----- 	# 	# 	#4S*m I $       9S*m I $       LL.S   :MS		(:MMhsmmMM  "	# # # # # # # # #	#s=   	B E*	E3EEE#E)A%EEEr  r  s   `` r,   _make_get_prompt_handlerr  Y  s=    ?#t ?## ?# ?# ?# ?# ?# ?# ?#B OrT   c                 "     dt           f fd}|S )zBReturn a check function that verifies the MCP connection is alive.r   c                      t           5  t                                        } d d d            n# 1 swxY w Y   | d uo| j        d up|                                 S rd  )r  rx  r   r~  r  )r  r.   s    r,   _checkz_make_check_fn.<locals>._check  s     	/ 	/\\+..F	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ 	/ $ Lt+Jv/H/H/J/J	
s   044)rO  )r.   r  s   ` r,   _make_check_fnr    s/    
D 
 
 
 
 
 
 MrT   c                     | sdi dS fdd }fd |           } ||          } |          }t          |t                    sdi dS |                    d          dk    rd|vri |di i}|S )ad  Normalize MCP input schemas for LLM tool-calling compatibility.

    MCP servers can emit plain JSON Schema with ``definitions`` /
    ``#/definitions/...`` references.  Kimi / Moonshot rejects that form and
    requires local refs to point into ``#/$defs/...`` instead.  Normalize the
    common draft-07 shape here so MCP tool schemas remain portable across
    OpenAI-compatible providers.

    Additional MCP-server robustness repairs applied recursively:

    * Missing or ``null`` ``type`` on an object-shaped node is coerced to
      ``"object"`` (some servers omit it).  See PR #4897.
    * When an ``object`` node lacks ``properties``, an empty ``properties``
      dict is added so ``required`` entries don't dangle.
    * ``required`` arrays are pruned to only names that exist in
      ``properties``; otherwise Google AI Studio / Gemini 400s with
      ``property is not defined``.  See PR #4651.
    * MCP/Pydantic optional fields commonly arrive as
      ``anyOf: [{...}, {"type": "null"}], default: null``.  Anthropic rejects
      nullable branches in tool input schemas, so nullable unions are collapsed
      to the non-null branch and optionality remains represented solely by the
      parent object's ``required`` list.

    All repairs are provider-agnostic and ideally produce a schema valid on
    OpenAI, Anthropic, Gemini, and Moonshot in one pass.
    objectr>  rS  c                    t          | t                    ri }|                                 D ]Z\  }}|dv r9t          |t                    r$fd|                                D             ||<   B|dk    rdn|} |          ||<   [|                    d          }t          |t                    r2|                    d          rd|t          d          d         z   |d<   |S t          | t                    rfd	| D             S | S )
a  Walk the schema, promoting legacy ``definitions`` to ``$defs``.

        The promotion is contextual: ``definitions`` is renamed only when it
        appears as a JSON Schema *meta-keyword* (sibling of ``properties`` /
        ``$ref`` at a schema node), never when it appears as the *name of a
        property* (i.e., as a key inside a ``properties`` dict).

        Without this gate, MCP servers that legitimately expose a tool
        parameter named ``definitions`` (e.g. a CI/pipelines tool that uses
        ``definitions`` for an array of pipeline-definition IDs) would have
        that user-facing property name silently rewritten to ``$defs``.
        Anthropic and OpenAI both reject ``$`` in property names
        (``^[a-zA-Z0-9_.-]{1,64}$``), so the whole tool array gets a 400 and
        every conversation breaks.

        The gate works by treating ``properties`` and ``patternProperties``
        specially during descent: we iterate the property-name -> schema map
        directly, leaving the property names verbatim, then recurse into each
        property's schema where ordinary JSON Schema semantics resume (so any
        legitimately-nested ``definitions`` meta-keyword inside a property's
        schema is still promoted).
        )rS  patternPropertiesc                 .    i | ]\  }}| |          S rS   rS   )r   	prop_nameprop_schema_rewrite_local_refss      r,   rL  zL_normalize_mcp_input_schema.<locals>._rewrite_local_refs.<locals>.<dictcomp>  s;     ' ' '2I{ "#6#6{#C#C' ' 'rT   definitionsz$defsz$refz#/definitions/z#/$defs/Nc                 &    g | ]} |          S rS   rS   )r   r  r  s     r,   r   zL_normalize_mcp_input_schema.<locals>._rewrite_local_refs.<locals>.<listcomp>  s%    ???$''--???rT   )r   r   r   r   r   r   r   r  )r   r  r   r   out_keyr   r  s         r,   r  z8_normalize_mcp_input_schema.<locals>._rewrite_local_refs  sL   . dD!! 	J"jjll E E
U===*UTXBYBY=
' ' ' '6;kkmm' ' 'JsOO
 *-)=)=gg3G*=*=e*D*DJw''..((C#s## N7G(H(H N%/#c:J6K6K6L6L2M%M
6"dD!! 	@????$????rT   c                 (    ddl m}  || d          S )a  Collapse JSON Schema nullable unions to provider-safe non-null schemas.

        Delegates to ``tools.schema_sanitizer.strip_nullable_unions`` so MCP
        ingestion, the Anthropic guard, and the global sanitizer all share one
        implementation. Keeps the ``nullable: true`` hint so runtime argument
        coercion can still map a model-emitted ``"null"`` string to Python
        ``None`` for this optional field.
        r   )strip_nullable_unionsT)keep_nullable_hint)tools.schema_sanitizerr  )r   r  s     r,   _strip_nullable_unionz:_normalize_mcp_input_schema.<locals>._strip_nullable_union  s-     	A@@@@@$$TdCCCCrT   c                 "   t          | t                    rfd| D             S t          | t                    s| S fd|                                 D             }|                    d          sd|v sd|v rd|d<   |                    d          dk    rd|vs(t          |                    d          t                    s>d|vri n|d         |d<   t          |                    d          t                    si |d<   |                    d          }t          |t                    rc|                    d          pi fd|D             }t          |          t          |          k    r|r||d<   n|                    dd           |S )	zBRecursively repair object-shaped nodes: fill type, prune required.c                 &    g | ]} |          S rS   rS   )r   r  _repair_object_shapes     r,   r   zM_normalize_mcp_input_schema.<locals>._repair_object_shape.<locals>.<listcomp>  s%    @@@4((..@@@rT   c                 .    i | ]\  }}| |          S rS   rS   )r   rJ  rK  r  s      r,   rL  zM_normalize_mcp_input_schema.<locals>._repair_object_shape.<locals>.<dictcomp>  s+    HHH41aA++A..HHHrT   r>  rS  r  r  c                 F    g | ]}t          |t                    |v |S rS   )r   r   )r   r  rU  s     r,   r   zM_normalize_mcp_input_schema.<locals>._repair_object_shape.<locals>.<listcomp>(  s-    RRRq
1c0B0BRqEzzzzzrT   N)r   r  r   r   r   r   r  )r   repairedr  validrU  r  s       @r,   r  z9_normalize_mcp_input_schema.<locals>._repair_object_shape  s   dD!! 	A@@@@4@@@@$%% 	KHHHH4::<<HHH ||F## 	(H$$
h(>(>'HV<<8++8++:\**D4 4+ 0<8/K/KQYZfQg&!(,,|"<"<dCC 0-/H\*  ||J//H(D)) 7 \228bRRRRHRRRu::X.. 7/4,, Z666rT   r>  rS  )r   r   r   )rQ  r  r  r  r  s      @@r,   r:  r:    s    6  4 333, , , , ,\D D D$ $ $ $ $L %$V,,J&&z22J%%j11J j$'' 4 333~~f))l*.L.L5
5L"55
rT   r   c                 L    t          j        ddt          | pd                    S )a5  Return an MCP name component safe for tool and prefix generation.

    Preserves Hermes's historical behavior of converting hyphens to
    underscores, and also replaces any other character outside
    ``[A-Za-z0-9_]`` with ``_`` so generated tool names are compatible with
    provider validation rules.
    z[^A-Za-z0-9_]r   r   )r.  r   r   )r   s    r,   sanitize_mcp_name_componentr  >  s%     6"CU[b)9)9:::rT   mcp____c                 f    t          |           }t          |          }t           | t           | S )zmBuild the registry/wire name for an MCP tool.

    Produces ``mcp__<sanitizedServer>__<sanitizedTool>``.
    )r  MCP_TOOL_NAME_PREFIX_MCP_NAME_DELIM)r.   r   safe_server	safe_tools       r,   rG  rG  T  s9    
 .k::K+I66I"MKMM)MMMrT   c           	          t          | |j                  }||j        pd|j         d|  t          t	          |dd                    dS )aS  Convert an MCP tool listing to the Hermes registry schema format.

    Args:
        server_name: The logical server name for prefixing.
        mcp_tool:    An MCP ``Tool`` object with ``.name``, ``.description``,
                     and ``.inputSchema``.

    Returns:
        A dict suitable for ``registry.register(schema=...)``.
    z	MCP tool z from r7  Nr8  )rG  r  r   r:  r   )r.   mcp_toolprefixed_names      r,   _convert_mcp_schemar  ^  s^     +;FFM+]/]8=/]/]P[/]/]1'(MSW2X2XYY  rT   c                    t          | d          d|  ddi ddddt          | d          d	|  ddd
dddid
gddddt          | d          d|  ddi ddddt          | d          d|  dddddddi ddddgddddgS )zBuild schemas for the MCP utility tools (resources & prompts).

    Returns a list of (schema, handler_factory_name) tuples encoded as dicts
    with keys: schema, handler_key.
    r  z*List available resources from MCP server 'r  r  r  r8  )rQ  handler_keyr@  z(Read a resource by URI from MCP server 'r%  stringzURI of the resource to read)r>  r   )r>  rS  r  r  z(List available prompts from MCP server 'r  z&Get a prompt by name from MCP server 'zName of the prompt to retrievez(Optional arguments to pass to the promptT)r>  r   rS  additionalPropertiesr  r  )rG  r  s    r,   _build_utility_schemasr  q  sz    /{<LMMZKZZZ$"$   ,
	
 
	
 /{OLLX+XXX$$,+H   # "'	 	  +	
 	
& /{NKKX+XXX$"$   *
	
 
	
 /{LIIVVVV$ %-+K! !
 %-+U*,48	& &# # "(  ( (+	
 	
Q? ?rT   r  c                    | t                      S t          | t                    r| hS t          | t          t          t           f          rd | D             S t
                              d||            t                      S )z8Normalize include/exclude config to a set of tool names.Nc                 ,    h | ]}t          |          S rS   )r   r  s     r,   r  z)_normalize_name_filter.<locals>.<setcomp>  s    ,,,dD		,,,rT   z>MCP config %s must be a string or list of strings; ignoring %r)r  r   r   r  r  r"   r\   )r   r  s     r,   _normalize_name_filterr    sx    }uu% w%$s+,, -,,e,,,,
NNSUZ\abbb55LrT   r  c                    | |S t          | t                    r| S t          | t                    r2|                                                                 }|dv rdS |dv rdS t
                              d| |           |S )z2Parse a bool-like config value with safe fallback.N>   1onyestrueT>   0noofffalseFzAMCP config expected a boolean-ish value, got %r; using default=%s)r   rO  r   r   r   r"   r\   )r   r  lowereds      r,   _parse_boolishr    s    }% % ++--%%''00041115
NNVX]_fgggNrT   r   c                    |                      |          }|                      d          }|*t          |t                    r|                     |          }|dS 	 t          |          }n4# t          t
          f$ r  t                              d||           Y dS w xY w|dk    rdS |dk     rt                              d||           dS |S )zKReturn an optional positive lifecycle timeout from top-level/nested config.	lifecycleNz6MCP config %s must be a number of seconds; ignoring %rr   z+MCP config %s must be positive; ignoring %r)r   r   r   r  rQ   rR   r"   r\   )r  r   rawr  re   s        r,   r~  r~    s    
**S//C

;''I
{z)T22{mmC  
{t**z"   OQTVYZZZtt !||t{{Dc3OOOtNs   A, ,-BBr  r@  r  r  r  r@  r  r  r  r  c                 x    t          |          }t          5  |t          | <   ddd           dS # 1 swxY w Y   dS )z:Remember the exact MCP server that registered *tool_name*.N)r  r  r  )r   r.   safe_server_names      r,   _track_mcp_tool_serverr    s    2;??	 = =,<y)= = = = = = = = = = = = = = = = = =s   /33c                 |    t           5  t                              | d           ddd           dS # 1 swxY w Y   dS )z5Forget MCP server provenance for a deregistered tool.N)r  r  r  )r   s    r,   r  r  
  s    	 4 4""9d3334 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4s   155c                    |                     d          pi }t          |                     d          d          }t          |                     d          d          }d}t          |dd          }|t          |dd          }g }t          |           D ]}	|	d	         }
|
d
v r|st                              d| |
           -|
dv r|st                              d| |
           P|=t          |
         }t          ||d          t                              d| |
|           n@t          |
         }t          |j	        |          st                              d| |
|           |
                    |	           |S )z?Select utility schemas based on config and server capabilities.r+  r  Tr  r  Nr  r  r  >   r@  r  z;MCP server '%s': skipping utility '%s' (resources disabled)>   r  r  z9MCP server '%s': skipping utility '%s' (prompts disabled)zRMCP server '%s': skipping utility '%s' (server does not advertise '%s' capability)z9MCP server '%s': skipping utility '%s' (session lacks %s))r   r  r   r  r"   r#   _UTILITY_CAPABILITY_ATTRS_UTILITY_CAPABILITY_METHODSrF  r~  r   )r.   r  r  tools_filterresources_enabledprompts_enabledadvertised_capsr  selectedr  r  cap_attrrequired_methods                r,   _select_utility_schemasr	    s   ::g&&,"L&|'7'7'D'DdSSS$\%5%5i%@%@$OOOO O&"5t<<K!+~tDDH'44 $ $M*===FW=LLVXcepqqq8888LLTVacnooo &0=H$77?B    @ :+FO6>?;; O#	   OrT   c                     g } t                                           D ]j\  }}t          |d          r|                     |j                   0|j        D ]2}t          |j        |          }|                     |d                    3k| S )z6Return tool names for all currently connected servers.r  r  )	rx  r   rF  r   r  r  r  r  r   )names_snamer  r  rQ  s        r,   _existing_tool_namesr  I  s    E"..** ) )6344 	LL6777 	) 	)H(h??FLL((((	) LrT   c                    ddl m} g }d|  }|                    d          pi }t          |                    d          d|  d          t          |                    d          d|  d	          d
t          dt
          ffd}|j        D ]'} ||j                  s"t          	                    d| |j                   5t          | |j        |j        pd           t          | |          }	|	d         }
|                    |
          }|r9|                    d          s$t                              d| |j        |
|           |                    |
||	t#          | |j        |j                  t'          |           d|	d                    t)          |
|            |                    |
           )t,          t.          t0          t2          d}t'          |           }t5          | ||          D ]}|d         }	|d         } ||         | |j                  }|	d         }|                    |          }|r3|                    d          st                              d| ||           {|                    |||	||d|	d                    t)          ||            |                    |           |r|                    | |           |S )a  Register tools from an already-connected server into the registry.

    Handles include/exclude filtering and utility tools. Toolset resolution
    for ``mcp-{server}`` and raw server-name aliases is derived from the live
    registry, rather than mutating ``toolsets.TOOLSETS`` at runtime.

    Used by both initial discovery and dynamic refresh (list_changed).

    Returns:
        List of registered prefixed tool names.
    r   r  zmcp-r+  includezmcp_servers.z.tools.includeexcludez.tools.excluder   r   c                      r| v S r| vS dS )NTrS   )r   exclude_setinclude_sets    r,   _should_registerz0_register_server_tools.<locals>._should_registerq  s.     	,++ 	0K//trT   z8MCP server '%s': skipping tool '%s' (filtered by config)r   r  us   MCP server '%s': tool '%s' (→ '%s') collides with built-in tool in toolset '%s' — skipping to preserve built-inFr   )r  toolsetrQ  handlercheck_fnis_asyncr   r  rQ  r  up   MCP server '%s': utility tool '%s' collides with built-in tool in toolset '%s' — skipping to preserve built-in)r  r  r   r  r   rO  r  r  r"   r#   r   r   r  get_toolset_for_toolr   r\   registerr  r  r  r  r   r  r  r  r  r	  register_toolset_alias)r  r  r  r  registered_namestoolset_namer  r  r  rQ  tool_name_prefixedexisting_toolset_handler_factoriesr  r  r  r  	util_namer  r  s                     @@r,   r  r  V  s~    ('''''"$ $==L ::g&&,"L()9)9))D)DFiUYFiFiFijjK()9)9))D)DFiUYFiFiFijjKC D        M 4 4.. 	LLSUY[c[hiii 	dHM83G3M2NNN$T844#F^ $889KLL 	$4$?$?$G$G 	NNIhm%79I  
 # &tX]F<OPP#D))}- 	 	
 	
 	
 	14888 23333
 742.	  d##H(vv>> + +xM*1$[1$8KLL6N	 $88CC 	$4$?$?$G$G 	NNIi!1  
  }- 	 	
 	
 	
 	y$///	**** <''l;;;rT   c           	      &  K   |                     dt                    }t          j        t	          | |          |           d{V }t
          5  t                              |            t          	                    | d           |t          | <   ddd           n# 1 swxY w Y   t          | ||          }t          |          |_        d|v rdnd}t                              d| |t!          |          d                    |                     |S )	zsConnect to a single MCP server, discover tools, and register them.

    Returns list of registered tool names.
    r   r@  Nrw  HTTPstdioz/MCP server '%s' (%s): registered %d tool(s): %srE  )r   r  rg  rI  re  r  r  r  r  r  rx  r  r  r  r"   rZ   r   r   )r  r  r   r  r  transport_types         r,   _discover_and_register_serverr&    si     
 jj!24LMMO#f%%        F 
    ""4(((""4...                             
 .dFFCC$()9$:$:F!$VVGN
KK9nc"233		"##  
 s   A BB"Bc                    t           st                              d           g S t          |           } | st                              d           g S t          5  d |                                 D             d | D             }t                                         D ]}t          	                    |d           |                                 D ]y\  }}t          |                    dd          d          r(t                              t          |                     Rt                              t          |                     z	 ddd           n# 1 swxY w Y   |D ]}t!          |           st#                      S t%                       d	t&          d
t(          dt*          t&                   fdfd}ddlm}m}  |            }|r |d           	 t3          |d           |r |d           n# |r |d           w w xY wt          5  d D             }	t5          d |	D                       }
ddd           n# 1 swxY w Y   t7                    t7          |	          z
  }|
s|r;d|
 dt7          |	           d}|r	|d| dz  }t                              |           t#                      S )a[  Connect to explicit MCP servers and register their tools.

    Idempotent for already-connected server names. Servers with
    ``enabled: false`` are skipped without disconnecting existing sessions.

    Args:
        servers: Mapping of ``{server_name: server_config}``.

    Returns:
        List of all currently registered MCP tool names.
    z;MCP SDK not available -- skipping explicit MCP registrationz No explicit MCP servers providedc                     i | ]E\  }}|t           vt          |                    d d          d          3t          |          B||FS r{  Tr  )rx  r  r   r  rI  s      r,   rL  z(register_mcp_servers.<locals>.<dictcomp>  sh     
 
 
1  quuY55tDDD ! -Q// ! q   rT   c                 r    g | ]4}|t           v t          t           |         d d          't           |         5S )r~  N)rx  r   )r   rJ  s     r,   r   z(register_mcp_servers.<locals>.<listcomp>  sD     
 
 
H}}!i!F!F!N QK!N!N!NrT   Nsupports_parallel_tool_callsFr  r  rV  r   c                 2   K   t          | |           d{V S )z@Connect to a single server and return its registered tool names.N)r&  )r  rV  s     r,   _discover_onez+register_mcp_servers.<locals>._discover_one%  s(      24=========rT   c                  8  K   t                                                    } t          j        fd                                D             ddi d {V }t          | |          D ].\  }}t          |t                    r                    |i                               d          }t          |          }t          5  t                              |           |t          |<   t          |           d d d            n# 1 swxY w Y   t                              d||rd| dnd|           t          5  t                              |           t                              |d            t%          |           d d d            n# 1 swxY w Y   0d S )	Nc              3   6   K   | ]\  }} ||          V  d S rd  rS   )r   r  rV  r-  s      r,   re  z>register_mcp_servers.<locals>._discover_all.<locals>.<genexpr>-  s3      LL94mmD#&&LLLLLLrT   r  Tr   z*Failed to connect to MCP server '%s'%s: %sz
 (command=rz  r   )r  keysrg  r  r   zipr   r  r   r  r  r  r  r  r  r"   r\   r  r  )server_namesresultsr  r   r   r  r-  new_serverss         r,   _discover_allz+register_mcp_servers.<locals>._discover_all)  sB     K,,..//LLLL8I8I8K8KLLL
"
 
 
 
 
 
 
 
  g66 	1 	1LD&&-00 1%//$3377	BB/77 2 2&..t4443:*40
 ,D1112 2 2 2 2 2 2 2 2 2 2 2 2 2 2 @/6>+++++B	     1 1&..t444*..tT:::*40001 1 1 1 1 1 1 1 1 1 1 1 1 1 1'	1 	1s%   4DD	D	<AFF	F	r   )r,  set_interruptx   r@  Tc                 $    g | ]}|t           v |S rS   rx  r   r  s     r,   r   z(register_mcp_servers.<locals>.<listcomp>Z  s    ===1qH}}Q}}}rT   c              3   h   K   | ]-}t          t          t          |         d g                     V  .dS r  Nr   r   rx  r:  s     r,   re  z'register_mcp_servers.<locals>.<genexpr>[  sO       
 
 %=rBBCC
 
 
 
 
 
rT   zMCP: registered  tool(s) from 
 server(s)rD   failed))rM   r"   r#   rX  r  r   r  r   r  r  r  r   r  r  r  r  r  r  r  r   r   r	   r1  r,  r6  r  sumr   rZ   )rP  stale_cachedsrv_namesrv_cfgr  r5  _is_interrupted_set_interrupt_was_interrupted	connectednew_tool_countfailedsummaryr-  r4  s                @@r,   register_mcp_serversrL    s     RSSS	,W55G 7888	 
  V  V
 

 
 
$
 

 
 

 	!!+...# 	7 	7H"&&x6666!( 	V 	VHggkk*H%PPZ_``` V&**+Fx+P+PQQQQ&../J8/T/TUUUU		V9 V  V  V  V  V  V  V  V  V  V  V  V  V  V  VD   # &#%%% ># >D >T#Y > > > >1 1 1 1 1 1J cbbbbbbb&(( u!4444 	!N4     	!N4    	! 
 
 
=====	 
 

 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 I.F  ]^]]3y>>]]] 	-,F,,,,GG!!!s1   C3EE!E!H   H&IIIc                     t           st                              d           g S t                      } | st                              d           g S t          5  d |                                 D             }ddd           n# 1 swxY w Y   t          |           }|s|S t          5  d |D             }t          d |D                       }ddd           n# 1 swxY w Y   t          |          t          |          z
  }|s|r;d| dt          |           d	}|r	|d
| dz  }t          	                    |           |S )a  Entry point: load config, connect to MCP servers, register tools.

    Called from ``model_tools`` after ``discover_builtin_tools()``. Safe to call even when
    the ``mcp`` package is not installed (returns empty list).

    Idempotent for already-connected servers. If some servers failed on a
    previous call, only the missing ones are retried.

    Returns:
        List of all registered MCP tool names.
    z4MCP SDK not available -- skipping MCP tool discoveryzNo MCP servers configuredc                 t    g | ]5\  }}|t           vt          |                    d d          d          3|6S r)  )rx  r  r   )r   r  rV  s      r,   r   z&discover_mcp_tools.<locals>.<listcomp>  sR     
 
 
c8##swwy$7O7OY](^(^(^# ###rT   Nc                 $    g | ]}|t           v |S rS   r9  r   r  s     r,   r   z&discover_mcp_tools.<locals>.<listcomp>  s"    !X!X!X4txGWGW$GWGWGWrT   c              3   h   K   | ]-}t          t          t          |         d g                     V  .dS r<  r=  rP  s     r,   re  z%discover_mcp_tools.<locals>.<genexpr>  sO       
 
 (@"EEFF
 
 
 
 
 
rT   z  MCP: r>  r?  rD  r@  )
rM   r"   r#   rc  r  r   rL  rA  r   rZ   )rP  new_server_names
tool_namesconnected_server_namesrI  failed_countrK  s          r,   discover_mcp_toolsrV  i  s     KLLL	  G 0111		 
 

 
$]]__
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 &g..J 	 
 
!X!X3C!X!X!X 
 
.
 
 
 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 '((3/E+F+FFL  aNaa#>T:U:Uaaa 	32L2222GGs$   BB	B'&CC Cc                     |                      t                    sdS t          5  t                              |           }t          |o|t          v           cddd           S # 1 swxY w Y   dS )a  Check if an MCP tool belongs to a server that supports parallel tool calls.

    MCP tool names follow the pattern ``mcp__{server}__{tool}``, but that
    string shape is ambiguous when server names contain underscores. Use the
    exact server provenance captured at registration time rather than prefix
    matching, then check whether that server's config includes
    ``supports_parallel_tool_calls: true``.

    Returns False for non-MCP tools or tools from servers without the flag.
    FN)r   r  r  r  r   rO  r  )r   r.   s     r,   is_mcp_tool_parallel_saferX    s      455 u	 K K,00;;KIK3I$IJJK K K K K K K K K K K K K K K K K Ks   2A##A'*A'c                     g } t                      }|s| S t          5  t          t                    }t	          t
                    }t          t                    }ddd           n# 1 swxY w Y   |                                D ]c\  }}d|v r|                    dd          nd}t          |                    dd          d          }|                    |          }	|	r|	j
        x||t          |	d	          rt          |	j                  nt          |	j                  dd
dd}
|	j        rt          |	j        j                  |
d<   |                     |
           |s|                     ||dd
ddd           ||v r|                     ||dd
d
dd           ||v r%|                     ||dd
d
d||         d           G|                     ||dd
d
dd           e| S )a`  Return status of all configured MCP servers for banner display.

    Returns a list of dicts with keys: name, transport, tools, connected,
    disabled, and status. Includes connected servers, disabled servers,
    in-flight connection attempts, recorded failures, and servers that are
    configured but have not been started in this process yet.
    Nrw  rO  r{  r$  r{  Tr  r  FrH  )r  rO  r+  rH  disabledru  rz  r   rZ  
connectingrJ  )r  rO  r+  rH  rZ  ru  r]   
configured)rc  r  r   rx  r  r  r  r   r   r  r~  rF  r   r  r  r  r  r   )r   r\  active_serversr[  connect_errorsr  rV  rO  r{  r  r  s              r,   get_mcp_statusr_    s    F "##J 	 6 6h+,,
4556 6 6 6 6 6 6 6 6 6 6 6 6 6 6
  %%'' 7 7	c49SLLCGGK000g	 D!9!94HHH##D)) 3	fn0&?FvOg?h?h  AV:;;;nqrxr  oA  oA!!% E  C$()9)A$B$Bj!MM%     '	 MM&" $      ZMM&"!&      ^##MM&"!"'-      MM&"!&      Ms   =A%%A),A)c                     t           si S t                      } | si S d |                                 D             si S t                       i g fd}	 t	          |d           n2# t
          $ r%}t                              d|           Y d}~nd}~ww xY wt                       n# t                       w xY wS )u  Temporarily connect to configured MCP servers and list their tools.

    Designed for ``hermes tools`` interactive configuration — connects to each
    enabled server, grabs tool names and descriptions, then disconnects.
    Does NOT register tools in the Hermes registry.

    Returns:
        Dict mapping server name to list of (tool_name, description) tuples.
        Servers that fail to connect are omitted from the result.
    c                 d    i | ]-\  }}t          |                    d d          d          *||.S r)  )r  r   rI  s      r,   rL  z*probe_mcp_server_tools.<locals>.<dictcomp>  sO       A!%%	400$???	1  rT   c            	        K   t          
                                          } g }
                                D ]W\  }}|                    dt                    }|                    t          j        t          ||          |                     Xt          j	        |ddi d {V }t          | |          D ]\  }}t          |t                    rt                              d||           7                    |           g }|j        D ]1}t!          |dd          pd}	|                    |j        |	f           2||<   t          j	        d D             ddi d {V  d S )	Nr   r@  r  Tz$Probe: failed to connect to '%s': %sr   r   c              3   >   K   | ]}|                                 V  d S rd  r  )r   ss     r,   re  z=probe_mcp_server_tools.<locals>._probe_all.<locals>.<genexpr>0  s*      33qajjll333333rT   )r  r0  r   r   r  r   rg  rI  re  r  r1  r   r!   r"   r#   r  r   r  )r  corosr  rV  r@  outcomesoutcomer+  r  descr{  probed_serversr   s             r,   
_probe_allz*probe_mcp_server_tools.<locals>._probe_all  s     W\\^^$$  	S 	SID#*,DEEBLL)/$*D*DbQQQRRRR G$GGGGGGGG 11 		! 		!MD''9-- CT7SSS!!'***E^ - -q-44:afd^,,,, F4LL n33N333
"
 
 	
 	
 	
 	
 	
 	
 	
 	
 	
rT   r7  r@  zMCP probe failed: %sN)	rM   rc  r   r  r  r!   r"   r#   _stop_mcp_loop_if_idle)servers_configrk  r+   r{  rj  r   s      @@@r,   probe_mcp_server_toolsrn    s%     	%''N 	 '--//  G  	%'F*,N
 
 
 
 
 
 
4!S11111 2 2 2+S111111112 	        Ms0   A- ,B. -
B7BB. BB. .B>c                  l    t           5  t          t                    cddd           S # 1 swxY w Y   dS )u  True if any MCP server has actually registered tools into the registry.

    Cheap — checks the global MCP-tool→server name map under ``_lock``, no
    registry walk.  Used by the per-turn refresh hook so a session with no MCP
    tools (the common case, and also a connected-but-zero-tool/prompt-only
    server) skips the ``get_tool_definitions`` rebuild entirely.  Checks
    registered TOOLS, not connected servers, so a server that registers no tools
    doesn't keep the hook firing every turn.
    N)r  rO  r  rS   rT   r,   has_registered_mcp_toolsrp  F  sz     
 , ,*++, , , , , , , , , , , , , , , , , ,s   )--c                      t           5  t          t                                                    cddd           S # 1 swxY w Y   dS )a  Return the set of MCP server names that have actually registered at
    least one tool into the registry (post-connection, post check_fn/include-
    exclude filtering) -- i.e. the real, availability-filtered signal, not
    just what's present in config.yaml under ``mcp_servers``.

    Used by capability-aware prompt building (e.g. gateway/session.py's
    Slack platform note) to detect an MCP server that provides a given
    platform's capability regardless of what its config key is named.
    N)r  r  r  valuesrS   rT   r,   get_registered_mcp_server_namesrs  T  s     
 4 4)0022334 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4s   &;??)enabled_overridedisabled_override
quiet_moderv  c                   ddl m} ddlm} ||9||nt	          | dd          }||nt	          | dd          }|| _        || _        n"t	          | dd          }t	          | dd          }|j        }t           ||||          pg           }	d |	D             }
t          | |	|
          }t          5  t	          | d	d
          }t          |t                    r|nd
}||k     rt                      cddd           S d t	          | dd          pg D             }|
|k    r/t          ||          | _        t                      cddd           S |	| _        |
| _        t	          | dd          }t          |t                    r)|                                 |                    |           t          ||          | _        |
|z
  cddd           S # 1 swxY w Y   dS )u  Re-derive an already-built agent's tool snapshot from the live registry.

    The agent snapshots ``agent.tools`` once at build time and never re-reads
    the registry (see ``run_agent`` / ``agent_init``).  When MCP servers connect
    *after* that snapshot — a slow HTTP/OAuth server that misses the bounded
    startup wait, or a ``/reload-mcp`` — their tools are invisible until the
    snapshot is rebuilt.  This is the single shared rebuild used by every such
    caller (the TUI ``reload.mcp`` RPC, the gateway reload, the late-binding
    refresh thread, and the per-turn between-turns refresh) so they can't drift
    apart again.

    The rebuild respects the agent's own ``enabled_toolsets`` /
    ``disabled_toolsets`` (the same filtering it was built with) and diffs by
    tool **name** (not count — a count compare misses an equal-size add/remove
    swap).

    Crucially it is **additive-preserving**: ``get_tool_definitions`` returns
    only the registry-derived tools, but ``agent_init`` appends two further
    families directly onto ``agent.tools`` *after* that — external
    memory-provider tools (mem0/honcho/…) and context-engine tools
    (``lcm_*``).  A naive ``agent.tools = get_tool_definitions(...)`` would
    silently DELETE those.  So after rebuilding the registry set we re-run the
    same post-build injectors ``agent_init`` used, reconstructing the full
    surface.  The new ``(tools, valid_tool_names)`` pair is published together
    under ``_agent_tools_lock`` so a concurrent reader never sees a
    cross-attribute half-swap.

    Returns the set of newly-added tool names (empty when nothing changed), so
    callers can decide whether to notify the user / re-emit session info.  The
    caller owns the prompt-cache contract: this helper does NOT check turn state,
    because each caller has a different policy (``/reload-mcp`` rebuilds after
    explicit user consent; the late-binding and between-turns paths only rebuild
    at a turn boundary, before that turn's ``tools=`` prefix is assembled).
    r   )get_tool_definitionsr  Nenabled_toolsetsdisabled_toolsets)ry  rz  rv  c                 *    h | ]}|d          d         S r  r  rS   r;  s     r,   r  z*refresh_agent_mcp_tools.<locals>.<setcomp>  s!    9991:v&999rT   _tool_snapshot_generationr  c                 *    h | ]}|d          d         S r|  rS   r;  s     r,   r  z*refresh_agent_mcp_tools.<locals>.<setcomp>  s1     
 
 
 jM&!
 
 
rT   r+  _context_engine_tool_names)model_toolsrx  r  r  r   ry  rz  _generationr  _reinject_post_build_tools_agent_tools_lockr   r  r  ri   r}  r+  valid_tool_namesr  r   )agentrt  ru  rv  rx  r  r{  rZ  snapshot_generationnew_defs	new_namesstaged_engine_namespublished_gen_rawpublished_genr  engine_namess                   r,   refresh_agent_mcp_toolsr  c  s   R 100000'''''' #'8'D&6&B""PUWikoHpHp(9(E$$7SXZmosKtKt!("*%!3T::5"5t<< #. $&!	
 	
 	
 	
  H :9999I 5UHiPP
 
 # #
 $E+FKK-78I3-O-OW))UW..55# # # # # # # #
 
eWd339r
 
 
  /2-AT.U.UE+55%# # # # # # # #& !*u&BDIIlC(( 	5    3444*-m=P*Q*Q'7"7# # # # # # # # # # # # # # # # # #s    4>G?AGA6GGG
tools_listname_setc                    dt           dt          ffd}	 t          | dd          }|rt          |dd          nd}t          |          raddlm}  |t          | d	d          t          | d
d          dv           r- |            D ]"}t          |t                     r ||           #n,# t          $ r t          	                    dd           Y nw xY wt                      }	 t          | d	d          }	|	du pd|	v }
t          | dd          }|rt          |dd          nd}|
rjt          |          r[ |            D ]P}t          |t                     s|                    dd          } ||          r|r|                    |           Qn,# t          $ r t          	                    dd           Y nw xY w|S )u  Append memory-provider and context-engine tools onto staged locals.

    Mirrors the post-``get_tool_definitions`` injection in ``agent_init`` so a
    snapshot rebuild reconstructs the FULL tool surface, not just the
    registry-derived subset. Operates ONLY on the caller's staged ``tools_list``
    / ``name_set`` (never the live agent attributes) so the rebuild stays atomic.
    Idempotent (skips names already present) and fail-soft.

    Returns the set of context-engine routing names actually appended by THIS
    rebuild — matching ``agent_init``'s dedup behavior (a name already provided
    by a registry/plugin tool is NOT claimed for context-engine routing). The
    caller publishes this into ``agent._context_engine_tool_names`` atomically
    with the snapshot.
    rQ  r   c                     |                      dd          }|r|v rdS                     d| d                               |           dS )Nr  r   Fr  r9  T)r   r   r  )rQ  r  r  r  s     r,   _addz(_reinject_post_build_tools.<locals>._add  s`    zz&"%% 	tx''5:6BBCCCTtrT   _memory_managerNget_all_tool_schemasr   )memory_provider_tools_enabledry  rz  memory)memory_tool_presentz)Memory-provider tool re-injection skippedTrw  context_enginecontext_compressorget_tool_schemasr  r   z(Context-engine tool re-injection skipped)r   rO  r   r5  agent.memory_managerr  r   r!   r"   r#   r  r   r  )r  r  r  r  memory_managerget_mem_schemasr  rQ  r  r{  context_engine_allowed
compressorget_schemasr  s    ``           r,   r  r    ss   T d       Q (94@@Sak'.2H$OOOgkO$$ 
	%JJJJJJ,,14882D99$,$8   %
 .o// % %F!&$// %V Q Q Q@4PPPPPQ  #uuP%!3T::!(D!O4D4OU$8$??
GQ[gj*<dCCCW[! 		2h{&;&; 		2%+-- 2 2!&$// zz&"-- 4<< 2D 2'++D111 P P P?$OOOOOP s%   BB0 0&CC+B+F &G ?G c                  t   t           5  t          t                                                    ddd           n# 1 swxY w Y   sat           5  t                                           t                                           ddd           n# 1 swxY w Y   t                       dS fd} t           5  t          }ddd           n# 1 swxY w Y   ||	                                rnddl
m}  | |             |t          d          }|J	 |                    d           n2# t          $ r%}t                              d	|           Y d}~nd}~ww xY wt           5  t                                           t                                           ddd           n# 1 swxY w Y   t                       dS )
a  Close all MCP server connections and stop the background loop.

    Each server Task is signalled to exit its ``async with`` block so that
    the anyio cancel-scope cleanup happens in the same Task that opened it.
    All servers are shut down in parallel via ``asyncio.gather``.
    Nc                    K   t          j        d D             ddi d {V } t          |           D ];\  }}t          |t                    r!t
                              d|j        |           <t          5  t          
                                 t          
                                 t          
                                 d d d            d S # 1 swxY w Y   d S )Nc              3   >   K   | ]}|                                 V  d S rd  rd  )r   r  s     r,   re  z:shutdown_mcp_servers.<locals>._shutdown.<locals>.<genexpr>;  s,      ??Ffoo??????rT   r  Tz!Error closing MCP server '%s': %s)rg  r  r1  r   r!   r"   r#   r  r  rx  r  r  r  )r3  r  r   servers_snapshots      r,   	_shutdownz'shutdown_mcp_servers.<locals>._shutdown9  sD     ??.>???
"
 
 
 
 
 
 
 
 ""2G<< 	 	NFF&),, 7f    	- 	-NN (--///$**,,,	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	-s   8ACCCr   r-  z MCP shutdown: failed to scheduler/  r  r@  zError during MCP shutdown: %s)r  r  rx  rr  r  r  r  _stop_mcp_loopr  r  r2  r.  r"   r   r  r#   )r  r  r.  r9  r+   r  s        @r,   shutdown_mcp_serversr  "  s    
 3 3 1 1223 3 3 3 3 3 3 3 3 3 3 3 3 3 3   	- 	-'--///$**,,,	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- 	- - - - -$ 
                DOO-->>>>>>))IKK:
 
 

 Cb))))  C C C<cBBBBBBBBC 
 ) )#))+++ &&((() ) ) ) ) ) ) ) ) ) ) ) ) ) ) s]   '<A A 3BBB6C

CCD' '
E1EE 3FF#&F#include_activec                    ddl }t          5  i }t          D ]+}t                              |d          }|k    r&|||<   ,|D ]7}t                              |           t                              |d           8| rkt          t                    } fd|	                                D             }|
                    |           |D ]}t                              |d           d |D             D ]}t                              |d           	 ddd           n# 1 swxY w Y   |sdS 	 t          j                    n# t          t          f$ r dY nw xY wdt           dt           dt"          d	dffd
}|	                                D ]3\  } |||j                   t&                              d|           4t+          j        d           t/          |d|j                  }	ddlm}
 |	                                D ]:\  } |
|          s |||	           t&                              d|           ;dS )u^  Best-effort graceful shutdown of stdio MCP subprocesses to reap orphans.

    Orphans are PIDs that survived their session context exit (SDK teardown
    did not terminate the process — common on Linux when stdio children escape
    the parent cgroup on cancellation). By default only entries in
    ``_orphan_stdio_pids`` are reaped so concurrent cron jobs and live user
    sessions are not disrupted.

    Sends SIGTERM, waits 2 seconds, then escalates to SIGKILL for any
    survivors, avoiding shared-resource collisions when multiple hermes
    processes run on the same host (each has its own ``_stdio_pids`` dict).

    On POSIX, signals are sent via ``os.killpg`` to the spawn-time pgid when
    one is tracked, so reparented grandchildren in the same process group
    (e.g. ``claude mcp serve`` spawned by a stdio MCP wrapper that exited
    first) are reaped alongside the direct child.  Falls back to ``os.kill``
    on Windows and when no pgid is recorded.

    When ``server_name`` is set, only orphaned PIDs known to belong to that
    MCP server are reaped. This lets stdio reconnects clean up their previous
    transport without touching unrelated servers.

    With ``include_active=True`` also kills every PID in ``_stdio_pids`` —
    used only at final shutdown, after the MCP event loop has stopped and no
    sessions can still be in flight.
    r   Norphanc                 (    i | ]\  }}|k    ||S rS   rS   )r   r&  r_  r.   s      r,   rL  z/_kill_orphaned_mcp_children.<locals>.<dictcomp>  s4       "U++ +++rT   c                 <    i | ]}|t           v |t           |         S rS   )r  )r   r&  s     r,   rL  z/_kill_orphaned_mcp_children.<locals>.<dictcomp>  s*     _ _ _C3R^K^K^l3&7K^K^K^rT   r&  sigr.   r   c                                         |           }t          t          dd          }|x|v#|k    rt                              d||           nQ	  |||           dS # t
          t          t          f$ r(}t                              d||||           Y d}~nd}~ww xY w	 t          j	        | |           dS # t
          t          t          f$ r Y dS w xY w)z=SIGTERM/SIGKILL via pgroup on POSIX, fall back to pid signal.r  Nu   MCP server '%s' pgid %d matches gateway pgid; skipping killpg to avoid self-kill and using per-pid kill — any grandchildren in this group may not be reapedzHkillpg(%d, %d) failed for MCP server '%s': %s; falling back to kill(pid))
r   r   r$   r"   r\   r  r  rr  r#   kill)r&  r  r.   r)  r  r+   _my_pgidpgidss         r,   _send_signalz1_kill_orphaned_mcp_children.<locals>._send_signal  s-   yy~~Xt,, 2#(8(8 D  	   	F4%%%F*OWE    LLbc;       	GC"OW= 	 	 	DD	s*   A% %B$<BB$(B? ?CCz,Sent SIGTERM to orphaned MCP process %d (%s)r  SIGKILLr  z6Force-killed MCP process %d (%s) after SIGTERM timeout)signalr  r  r  r   r  r  r   r  r   r   r  r$   getpgrpr  rr  r  r   SIGTERMr"   r#   r  r  r   r  r  r\   )r  r.   _signalr  opidr_  activer&  r  _sigkillr  r  r  s    `         @@r,   r  r  e  s=   < 	 ( (!& 	 	D-11$AAE&5K+?+?DJJ 	6 	6D&&t,,,%))$5555 
	++&&F&   &,llnn  
 KK + +T**** !` _4 _ _ _ 	( 	(CS$''''	(1( ( ( ( ( ( ( ( ( ( ( ( ( ( (:  :<<G$   !# !C !c !d ! ! ! ! ! ! !H !JJLL W W[S'/;777CS+VVVV 	JqMMM w	7?;;H +***** JJLL 
 
[{3 	S(K000D	
 	
 	
 	
	
 
s$   D	D%%D),D)5E	 	EEc                  "    t          d          S )a  Stop the MCP loop only when no registered server still owns it.

    Probe paths create temporary MCPServerTask instances that are not placed in
    ``_servers``.  They should clean up an otherwise-idle loop, but must not
    tear down the process-global loop when live agent tools are registered on
    it.  Otherwise a dashboard/CLI probe can make later MCP tool calls fail
    with ``MCP event loop is not running``.
    Tonly_if_idle)r  rS   rT   r,   rl  rl    s     t,,,,rT   r  r  c                    t           5  | r6t          st          r(t                              d           	 ddd           dS t
          }t          }dadaddd           n# 1 swxY w Y   |h|                    |j                   ||	                    d           	 |
                                 n# t          $ r Y nw xY wt          d           dS )z3Stop the background event loop and join its thread.zKLeaving MCP event loop running; active servers are registered or connectingNFrc   r@  T)r  )r  rx  r  r"   r#   r  r  r  r  r   r4  r!   r  )r  r  threads      r,   r  r    sN    
   	X 	); 	LLfggg        	               !!$),,,KKK"""	JJLLLL 	 	 	D	
 	$488884s)   ,AAA#&A#B4 4
C C)r   )r  r   r   r   )r  )T)FN)rT  rg  rx  concurrent.futuresr6  rk  rN   r  r  r  r$   rj   r.  r   r&   r  r  r  r   typingr   r   r   r   r   r	   r
   r0  r   	getLoggerrQ  r"   r  r   __annotations__r  r   r-   r   r5   rM   ri  r  r  r  r  r;   r=  r6   r7   mcp.client.stdior8   mcp.client.streamable_httpr9   r  r:   rm  	mcp.typesr#   mcp.client.sser<   r=   r>   r?   r@   rA   rB   rC   rD   rE   rF   rG   rH   rI   rO  rU   rY   r  r  r  r  ERRORr  r  r  r  r  r  r  ri  rl   r  rm   r  r  	frozensetr   r   compile
IGNORECASEr   rO  r   r   r   r   r  r   r   r   r!   r   Ir   r   r   r   r   r  r   r  r
  r  r#  r:  r9  r5  r<  rM  rR   rO  ConnectionErrorrV  ra  r\  rv  r  r  r  r  r  r  r\  r^  r`  rx  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rp  r  r  r  r  r  r  r  r  AbstractEventLoopr  r  r  r  r  r  r  r	  r  r
  r  r  r#  r*  r  r?  rH  rX  rc  re  rk  rm  ro  r  r  r  r  r  r  r:  r  r  r  rG  r  r  r  r  r~  r  r   r  r  r	  r  r  r&  rL  rV  rX  r_  rn  r  rp  rs  r  r  r  r  rl  r  rS   rT   r,   <module>r     su  X X Xt               				  				  



      ! ! ! ! ! !             7 7 7 7 7 7 7 7 7 7 7 7 7 7 ! ! ! ! ! !		8	$	$  $ ( %) HSM ( ( (%y~''  "S  "  "  "  "F# $    &     !&  ' >K88888888------N$DDDDDD" $ $ $#$EEEEEE   k5555555 k k kijjjjjkZ------- Z Z Z
XYYYYYZN	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 # N N NLMMMMMNT????????!% T T TRSSSSST	`	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 #' ` ` `^_____` K K K
LLIJJJJJK     "@!?!A!A  `8 `
LL^___      #B"A"C"C 
 ]Ll]]	 	        
  "  Eu E E E E E "         #,) - - - # # B !bj		 M  " 2:.// 
s 
s 
 
 
 
"htn     B7# 7# 7 7 7 7	'- 	'C 	' 	' 	' 	' 'GGGGGGG ' ' ' &'M d    P RZ<bdCC?ARZ&--57RZDbdKKRZ "$''&(RZ3RT::!#RZ:BDAA RZ0"$77%'RZ2BD99 RZ&--!RZ924@@!#' 0s s  QUVYQZ    *t      "  &s & & & & &R,*C ,*d ,*uS$Y7G ,* ,* ,* ,*^) )D )U39=M ) ) ) )B;# ;# ; ; ; ;)!S )! )! )! )!d +  69Q>B              F%!S %! %! %! %!PH} H}3 H} H} H} H} H}`           /   # #= # # # #L3] 3t 3 3 3 3}     D1# 1C 1C 1 1 1 1hFc F4 F F F FR93} 93 93 93 93 93@ *-a    l9 l9 l9 l9 l9 l9 l9 l9ft # #    4Z. Z. Z. Z. Z. Z. Z. Z.BI I I I I I I I`4 &($sM!
" ' ' '"suu CH $ $ $)+ S#X + + +, 13 T#u*- 2 2 2+- $sCx. - - -"& !& J J J J J J$7 7 7 7 7 7@# @$ @ @ @ @, (* d38n ) ) ).0 4U
+ 0 0 0  $ 
BC 
BD 
B 
B 
B 
B5S 5T 5 5 5 5c d    0%c %d % % % % 	& & &	& & 	&
 
& & & &\ ( ( ((	( 	(
 ( 
( ( ( (`  5   'u ' ' ' 'T $    &gg	g 	g g g g\# %   , " F!= F!T F! F! F! F!RMM	M 	M M M Mf "cee  # # # *, S#X + + + 26	8G-. 5 5 5*.Xi&' . . . 		 !T#s(^       #%% C   ,. 4S> . . .  "d38n ! ! !c    @3 c3h    s  s        F, , ,      >  *I Iu I I I IX#      2DdO S$Y    6#$sDy/ # # # #T T m    ( m     D 8O    c d    `C `C `u ` ` ` `F9c 9 9 9 9 9x@S @ @ @ @ @F>C >u > > > >BD# DU D D D DN    $It I I I I IX;s ;s ; ; ; ;$  N N N N N N NS t    &E ET
 E E E EP	# 	c 	c#h 	 	 	 	 #       4 c huo    , '$"	  $ " 	  =c = = = = = =4s 4t 4 4 4 46 6m 6T 6VZ[_V` 6 6 6 6r
d3i 
 
 
 
k km kT kdSVi k k k k\c 4 DI    >C"$sDy/ C"d3i C" C" C" C"L.DI . . . .bK K K K K K$MT
 M M M M`?S$u+%5 6 ? ? ? ?N #IN$$ ,$ , , , ,4 4 4 4 4$ w# w# w#
 w# 	w# w# w# w#tB$ B# B# B B B BJ@ @ @H !!%}
 }
}
#}
 
}
 }
 }
 }
@	- 	- 	- 	- 	- ,1   D T      s   1G C 
G CG CG C" !G "C,)G +C,,G 0C7 6G 7DG DG D G D>;G =D>>G E G E41G 3E44G 8
F G F G F  G $F3 2G 3GG GG G10G1#K* *K43K4