
    $jE_              
       L   U d 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 ddlmZ ddlmZ dd	lmZmZ dd
lmZ ddlmZmZmZ ddlmZ ej                            dd                                          Zej        ej         ej!        ej"        ej#        ej#        dZ$ ej%        e$                    eej"                  d            ej&        e'          Z(ej                            dd          Z)ej                            d          Z* edd          Z+ee,dz           e-d<    edd          Z.ee,dz           e-d<    edd          Z/ee,dz           e-d<    edd          Z0ee,dz           e-d<    edd          Z1ee2         e-d <   d!e,dz  fd"Z3d!e,dz  fd#Z4d!e,dz  fd$Z5d!e,dz  fd%Z6d!e2fd&Z7d8d(e	d)e2d!efd*Z8d+ed!e9fd,Z:d+ed!dfd-Z;d.e9d/e,d0e<e,         d1e<e,         d!df
d2Z=d3e9d0e<e,         d1e<e,         d!e9fd4Z> G d5 d6          Z?d(e	fd7Z@dS )9zCHindsight MCP Server implementation using FastMCP (HTTP transport).    N)
ContextVar)FastMCP)MemoryEngine)__version__)_get_raw_config)_current_schema)MCPExtensionload_extension)AuthenticationError)
_ALL_TOOLSMCPToolsConfigregister_mcp_tools)RequestContextHINDSIGHT_API_LOG_LEVELinfo)criticalerrorwarningr   debugtracez4%(asctime)s - %(levelname)s - %(name)s - %(message)s)levelformatHINDSIGHT_MCP_BANK_IDdefaultHINDSIGHT_API_MCP_AUTH_TOKENcurrent_bank_id)r   _current_bank_idcurrent_api_key_current_api_keycurrent_tenant_id_current_tenant_idcurrent_api_key_id_current_api_key_idcurrent_mcp_authenticatedF_current_mcp_authenticatedreturnc                  4    t                                           S )z%Get the current bank_id from context.)r   get     ]/home/rurouni/.hermes/hermes-agent/venv/lib/python3.11/site-packages/hindsight_api/api/mcp.pyget_current_bank_idr,   8       !!!r*   c                  4    t                                           S )z%Get the current API key from context.)r   r(   r)   r*   r+   get_current_api_keyr/   =   r-   r*   c                  4    t                                           S )z'Get the current tenant_id from context.)r!   r(   r)   r*   r+   get_current_tenant_idr1   B   s    !!###r*   c                  4    t                                           S )z(Get the current api_key_id from context.)r#   r(   r)   r*   r+   get_current_api_key_idr3   G   s    ""$$$r*   c                  4    t                                           S )zDGet whether the request was pre-authenticated by MCP transport auth.)r%   r(   r)   r*   r+   get_current_mcp_authenticatedr5   L   s    %))+++r*   Tmemory
multi_bankc           	         t          dt                    }t                      }t          h d          }|rdn|}|j        "t          |j                  }||nt
          |z  }t          t          t          t          t          t          ||          }t          || |           t          dt                    }|r=t                              d|j        j                    |                    ||            t)          |           |S )aU  
    Create and configure the Hindsight MCP server.

    Args:
        memory: MemoryEngine instance (required)
        multi_bank: If True, expose all tools with bank_id parameters (default).
                   If False, only expose bank-scoped tools without bank_id parameters.

    Returns:
        Configured FastMCP server instance
    zhindsight-mcp-server)version>   recallretainreflectget_bank	list_tags
get_memorydelete_banksync_retainupdate_bankget_documentget_operationlist_memoriesclear_memorieslist_documentsdelete_documentlist_directiveslist_operationscancel_operationcreate_directivedelete_directiveget_mental_modellist_mental_modelscreate_mental_modeldelete_mental_modelupdate_mental_modelrefresh_mental_modelN)bank_id_resolverapi_key_resolvertenant_id_resolverapi_key_id_resolvermcp_authenticated_resolverinclude_bank_id_paramtoolsMCPzLoading MCP extension: )r   HINDSIGHT_VERSIONr   	frozensetmcp_enabled_toolsr   r   r,   r/   r1   r3   r5   r   r
   r	   loggerr   	__class____name__register_tools_make_tools_tolerant)	r6   r7   mcpglobal_config_SINGLE_BANK_TOOLS
base_toolsallowedconfigmcp_extensions	            r+   create_mcp_serverrk   Q   s'    (2C
D
D
DC#%%M *3	
 	
 	
* *< 1;(R@RJ &2M;<<$.$:jj
gU
 ,,02#@(  F sFF+++ #5,77M 2Pm.E.NPPQQQ$$S&111 Jr*   rd   c                     t          | d          r| j        j        S t          | d          r(d | j        j                                        D             S d}t          |          )uD   Get tool name→object mapping, compatible with FastMCP 2.x and 3.x._tool_manager_local_providerc                     i | ]O\  }}|                     d           |                    d          d                             d          d         |PS )ztool::   @r   )
startswithsplit.0kvs      r+   
<dictcomp>z"_get_mcp_tools.<locals>.<dictcomp>   sa     
 
 
1||G$$
GGCLLO!!#&&q)1
 
 
r*   z'Cannot locate tools on FastMCP instance)hasattrrm   _toolsrn   _componentsitemsAttributeError)rd   msgs     r+   _get_mcp_toolsr      s}     sO$$ ( ''s%&& 

 
+7==??
 
 
 	

 4C


r*   c                 ^   	 t          |           }|                                D ]\  }}t          |d          r|j        r|j                            di           }t          |                                          }t                      }t                      }|                                D ]\  }}	t          |	|||           |j        }
||
||fd}t          
                    |d|           dS # t          t          f$ r(}t                              d|            Y d}~dS d}~ww xY w)a  Wrap all tool run methods to strip unknown arguments and coerce string-encoded JSON.

    LLMs frequently add extra fields like "explanation" or "reasoning" to tool calls.
    FastMCP's Pydantic TypeAdapter rejects these with "Unexpected keyword argument".

    LLMs also frequently serialize list/dict arguments as JSON strings instead of native
    types (e.g., tags='["a","b"]' instead of tags=["a","b"]). This auto-coerces them.

    This wraps each tool's run() to apply both fixes before validation.
    
parameters
propertiesc                   K   t          |                                           z
  }|r=t                              d|            fd|                                 D             } t          | ||          }  ||            d {V S )Nz,Stripping unknown arguments from tool call: c                 $    i | ]\  }}|v 	||S r)   r)   )rv   rw   rx   _alloweds      r+   ry   z?_make_tools_tolerant.<locals>._tolerant_run.<locals>.<dictcomp>   s%    $Y$Y$Yda1PX==Q===r*   )setkeysr_   r   r}   _coerce_string_json)	argumentsr   _orig_array_params_object_params
extra_keyss    `    r+   _tolerant_runz+_make_tools_tolerant.<locals>._tolerant_run   s       "%Y^^%5%5!6!6!AJ! Z%`T^%`%`aaa$Y$Y$Y$Yioo6G6G$Y$Y$Y	 !4I}n ] ]I!&y!1!11111111r*   runz2Could not make tools tolerant of extra arguments: N)r   r}   rz   r   r(   r   r   _collect_coercible_typesr   object__setattr__r~   KeyErrorr_   r   )rd   rZ   nametoolr   rh   array_paramsobject_params
param_nameparam_schemaoriginal_runr   es                r+   rc   rc      sn   &Qs##++-- "	? "	?JD$t\** !?t !?!_00rBB
joo//00
 *-*-%%0:0@0@0B0B d d,J,\:|Ubcccc#x %&".#02 2 2 2& ""4>>>E"	? "	?F H% Q Q QOAOOPPPPPPPPPQs   C/C3 3D,D''D,schemar   r   r   c                    |                      d          }|dk    r|                    |           dS |dk    r|                    |           dS |                      dg           |                      dg           z   D ]S}|                     d          }|dk    r|                    |            dS |dk    r|                    |            dS TdS )z\Check a JSON Schema property and add param_name to array_params/object_params if applicable.typearrayNr   anyOfoneOf)r(   add)r   r   r   r   schema_typevariantvariant_types          r+   r   r      s     **V$$Kg$$$h*%%% ::gr**VZZ-D-DD  {{6**7""Z(((FF8##j)))FF $ r*   r   c                 v   |D ]}|                      |          }t          |t                    rm	 t          j        |          }t          |t
                    r&i | ||i} t                              d| d           }# t          j        t          f$ r Y w xY w|D ]}|                      |          }t          |t                    rm	 t          j        |          }t          |t                    r&i | ||i} t                              d| d           }# t          j        t          f$ r Y w xY w| S )zAuto-coerce string-encoded JSON arrays/objects to native types.

    LLM agents frequently serialize list and dict tool arguments as JSON strings.
    This is backward-compatible: native arrays/objects pass through unchanged.
    z&Coerced string to list for parameter ''z&Coerced string to dict for parameter ')r(   
isinstancestrjsonloadslistr_   r   JSONDecodeError	TypeErrordict)r   r   r   r   valparseds         r+   r   r      s|    # 	 	
mmJ''c3 	Cfd++ Y A9 Aj& A AILL!W*!W!W!WXXX()4   	 $ 	 	
mmJ''c3 	Cfd++ Y A9 Aj& A AILL!W*!W!W!WXXX()4   	 s%   AB  BBADD54D5c            	           e Zd ZdZ	 	 	 	 	 ddedefdZdeded	edz  fd
Zd Z	d Z
ddededeeef         dz  fdZdS )MCPMiddlewareu.  ASGI middleware that intercepts MCP requests and routes to appropriate MCP server.

    This middleware wraps the main FastAPI app and intercepts requests matching the
    configured prefix (default: /mcp). Non-MCP requests pass through to the inner app.

    Authentication:
        1. If HINDSIGHT_API_MCP_AUTH_TOKEN is set (legacy), validates against that token
        2. Otherwise, uses TenantExtension.authenticate_mcp() from the MemoryEngine
           - DefaultTenantExtension: no auth required (local dev)
           - ApiKeyTenantExtension: validates against env var

    Two modes based on URL structure:

    1. Multi-bank mode (for /mcp/ root endpoint):
       - Exposes all tools: retain, recall, reflect, list_banks, create_bank
       - All tools include optional bank_id parameter for cross-bank operations
       - Bank ID from: X-Bank-Id header or HINDSIGHT_MCP_BANK_ID env var

    2. Single-bank mode (for /mcp/{bank_id}/ endpoints):
       - Exposes bank-scoped tools only: retain, recall, reflect
       - No bank_id parameter (comes from URL)
       - No bank management tools (list_banks, create_bank)
       - Recommended for agent isolation

    Bank ID resolution priority:
        1. URL path (e.g., /mcp/{bank_id}/) → single-bank mode
        2. X-Bank-Id header → multi-bank mode
        3. HINDSIGHT_MCP_BANK_ID env var → multi-bank mode (default: "default")

    Examples:
        # Single-bank mode (recommended for agent isolation)
        claude mcp add --transport http my-agent http://localhost:8888/mcp/my-agent-bank/ \
            --header "Authorization: Bearer <token>"

        # Multi-bank mode (for cross-bank operations)
        claude mcp add --transport http hindsight http://localhost:8888/mcp \
            --header "X-Bank-Id: my-bank" --header "Authorization: Bearer <token>"
    /mcpNr6   prefixc                    || _         || _        || _        |j        | _        |r |r|| _        || _        || _        || _        d S t                      }|j
        }	t          |d          | _        | j                            d|	          | _        t          |d          | _        | j                            d|	          | _        d S )NTr7   /pathstateless_httpF)appr   r6   _tenant_extensiontenant_extensionmulti_bank_appsingle_bank_appmulti_bank_serversingle_bank_serverr   mcp_statelessrk   http_app)
selfr   r6   r   r   r   r   r   re   	statelesss
             r+   __init__zMCPMiddleware.__init__A  s      & 8 	ho 	h"0D#2D %6D"&8D### ,--M%3I%6v$%O%O%OD""&"8"A"As[d"A"e"eD&75&Q&Q&QD##'#:#C#C]f#C#g#gD   r*   scoper   r&   c                     |                                                                 }|                    dg           D ]3\  }}|                                 |k    r|                                c S 4dS )z'Extract a header value from ASGI scope.headersN)lowerencoder(   decode)r   r   r   
name_lowerheader_nameheader_values         r+   _get_headerzMCPMiddleware._get_header_  sv    ZZ\\((**
).9b)A)A 	- 	-%K  ""j00#**,,,,, 1tr*   c                 j  K   |d         dk    r|                      ||           d {V  d S |                    dd          }|| j        k    s<|                    | j        dz             s|                      ||           d {V  d S |                    dd          }|dk    rO|                     |d          }|s7t
                              d	           |                                d {V  d S |t          | j                  d          pd}|                     |d
          }d }|rE|                    d          r|dd          	                                n|	                                }d }	d }
d }d}t          rP|s|                     dd           d {V  d S |t          k    r|                     dd           d {V  d S d }	d}n	 t          |          }| j                            |           d {V }	|j        }
|j        }nI# t"          $ r<}|                     dt%          |          |j                   d {V  Y d }~d S d }~ww xY w|	r |	j        rt+          j        |	j                  nd }d d|}|                    d          rct          |          dk    rP|dd                              dd          }|d         r*|d         dt          |          dk    rd|d         z   nd}s|                     |d          s$t0          t
                              d            r| j        n| j        }t6                                        }|rt8                              |          nd }|
rt:                              |
          nd }|rt<                              |          nd }t>                              |          }	 |                                 }||d<   d|d<   |                     |d          }|rd|vr:d |                    dg           D             }|!                    d           ||d<   dfd} ||||           d {V  t6          "                    |           |t8          "                    |           |t:          "                    |           |t<          "                    |           t>          "                    |           |t+          j"        |           d S d S # t6          "                    |           |t8          "                    |           |t:          "                    |           |t<          "                    |           t>          "                    |           |t+          j"        |           w w xY w)Nr   httpr    r   methodGETzMcp-Session-Idu>   MCP GET without session ID (client probe) — returning 200 OKAuthorizationzBearer    Fi  zAuthorization header requiredzInvalid authentication tokenT)api_key)extra_headersrq   r   z	X-Bank-IdzUsing default bank_id: 	root_pathacceptztext/event-streamc                 L    g | ]!\  }}|                                 d k    ||f"S )   accept)r   ru   s      r+   
<listcomp>z*MCPMiddleware.__call__.<locals>.<listcomp>  s3    eeedaaggii[dNdNdAq6NdNdNdr*   r   )r   s#   application/json, text/event-streamc                 Z  K   | d         dk    r*|                      dg           D ]\  }}|dk    rd|v rd n| d         dk    rTrRrP|                      dd	          }|r8d
|v r4|                    dd d                                          }i | d|i}  |            d {V  d S )Nr   http.response.startr      content-types   text/event-streamThttp.response.bodybodyr*   s	   /messagess   data: /messageszdata: /z	/messages)r(   replacer   )messager   r   r   bank_idbank_id_from_pathis_sse_responsesends       r+   send_wrapperz,MCPMiddleware.__call__.<locals>.send_wrapper  s     6?&;;;5<[[B5O5O " "1\&/99>RVb>b>b.2O!E6?&:::?P:Ud:";;vs33D < 4 4#||,>@\'@\@\@\@c@c@e@eff";W";fd";";d7mm#########r*   )#r   r(   r   rs   r   r_   r   _send_oklenstripMCP_AUTH_TOKEN_send_errorr   r   authenticate_mcp	tenant_id
api_key_idr   r   r   schema_namer   r   rt   DEFAULT_BANK_IDr   r   r   r   r!   r#   r%   copyappendreset)r   r   receiver   r   r   
session_idauth_header
auth_tokentenant_contextauth_tenant_idauth_api_key_idmcp_pre_authenticatedauth_contextr   schema_tokennew_pathparts
target_appbank_id_tokenapi_key_tokentenant_id_tokenapi_key_id_tokenmcp_auth_token	new_scopeaccept_headerr   r   r   r   r   s      `                        @@@r+   __call__zMCPMiddleware.__call__g  s     =F""((5'4000000000Fyy$$ ##tt{S7H'I'I#((5'4000000000F
 8R((U??))%1ABBJ ]^^^mmD))))))))) C$$&&'.3 &&uo>>!%
 	o4?4J4J94U4UnQRR..000[f[l[l[n[nJ %)&* % 	 &&tS2QRRRRRRRRR^++&&tS2PQQQQQQQQQ!N$(!!-jAAA'+'<'M'Ml'['[![![![![![![!-!7"."9&   &&tS#a&&	&RRRRRRRRR @NvR`RlvO :;;;rv 	 ! ??3 	ECIIMMHNN3**EQx E($(!-0ZZ!^^3q>>  	;&&uk::G  	>%GLL<7<<===
 .?WT))DDW
 ),,W55<FP(,,Z888DDR\,00@@@X\GV`.22?CCC\`3778MNN.	4

I (If%'Ik"
 !,,YAAM  /$7}$L$LeeimmIr.J.JeeeRSSS'.	)$
 $O$ $ $ $ $ $ $ $ *Y>>>>>>>>>""=111( &&}555*"((999+#))*:;;;&,,^<<<'%l33333 (' ""=111( &&}555*"((999+#))*:;;;&,,^<<<'%l3333 (s&   >H 
I 1IIBT B!V2c                 ^   K    |dddgd           d{V   |ddd           d{V  dS )	zMSend a 200 OK response with empty body (used for GET probes without session).r      r   s   application/jsonr   statusr   Nr   s   {}r   r   r)   )r   r   s     r+   r   zMCPMiddleware._send_ok   s      d-BC 
 
 	
 	
 	
 	
 	
 	
 	
 d, 
 
 	
 	
 	
 	
 	
 	
 	
 	
 	
r*   r  r   r   c                 `  K   t          j        d|i                                          }dg}|pi                                 D ]@\  }}|                    |                                |                                f           A |d||d           d{V   |d|d           d{V  dS )zSend an error response.r   r  r   r  Nr   r  )r   dumpsr   r}   r   )	r   r   r  r   r   r   r   keyvalues	            r+   r   zMCPMiddleware._send_error  s     z7G,--44669:(.B5577 	; 	;JCNNCJJLL%,,..9::::d- " 
 
 	
 	
 	
 	
 	
 	
 	
 d, 
 
 	
 	
 	
 	
 	
 	
 	
 	
 	
r*   )r   NNNN)N)ra   
__module____qualname____doc__r   r   r   r   r   r  r   intr   r)   r*   r+   r   r     s        % %V h h h 	h h h h< S S4Z    W4 W4 W4r
 
 
 
 
c 
C 
PTUXZ]U]P^aePe 
 
 
 
 
 
r*   r   c                    t                      }|j        }t          | d          }|                    d|          }t          | d          }|                    d|          }t                              d| d           ||||fS )a+  Create multi-bank and single-bank MCP servers and their Starlette apps.

    Returns the servers and apps separately so lifespans can be chained before
    the middleware wraps the main app.

    Returns:
        Tuple of (multi_bank_server, single_bank_server, multi_bank_app, single_bank_app)
    Tr   r   r   Fz$MCP servers created (stateless_http=))r   r   rk   r   r_   r   )r6   re   r   r   r   r   r   s          r+   create_mcp_serversr  %  s     $%%M+I)&TBBB&//S/SSN*6eDDD(11s91UUO
KKCyCCCDDD0./QQr*   )T)Ar  r   loggingoscontextvarsr   fastmcpr   hindsight_apir   r   r\   hindsight_api.configr   "hindsight_api.engine.memory_enginer   hindsight_api.extensionsr	   r
   hindsight_api.extensions.tenantr   hindsight_api.mcp_toolsr   r   r   hindsight_api.modelsr   environr(   r   _log_level_strCRITICALERRORWARNINGINFODEBUG_log_level_mapbasicConfig	getLoggerra   r_   r   r   r   r   __annotations__r   r!   r#   r%   boolr,   r/   r1   r3   r5   rk   r   r   rc   r   r   r   r   r  r)   r*   r+   <module>r7     s   I I I   				 " " " " " "       & & & & & & : : : : : : 0 0 0 0 0 0 > > > > > > A A A A A A A A ? ? ? ? ? ? R R R R R R R R R R / / / / / /  96BBHHJJ ]L]]   


^W\
:
:A    
	8	$	$ *..!8)DD  >?? ,6:6GQU+V+V+V *S4Z( V V V ,6:6GQU+V+V+V *S4Z( V V V .8Z8KUY-Z-Z-Z JsTz* Z Z Z.8j9MW[.\.\.\ Zd
+ \ \ \ 0:z:U_d/e/e/e Jt, e e e"S4Z " " " "
"S4Z " " " "
$sTz $ $ $ $
%d
 % % % %
,t , , , ,
L Ll L L L L L L^ D     1Qg 1Q$ 1Q 1Q 1Q 1QhT s #c( cfgjck pt    ,4 s3x PSTWPX ]a    >I
 I
 I
 I
 I
 I
 I
 I
XR| R R R R R Rr*   