
    TjY)                       d Z ddlmZ ddlZddlZddlZddlmZ ddlZddl	m
Z
 ddlmZ ddlmZ ddlZddlZddlZej$                  j'                  dd	      Zd
e Zer1 ej,                  ej/                               j1                         dd nd	Z e
ddd ed      d      Zej7                         d*d+d       Zej7                         	 	 d,	 	 	 d-d       Zej7                         	 d.	 	 	 d/d       Zej7                         d0d       Zej7                         d1d       Z ej7                         d2d       Z!ej7                         d3d       Z"ej7                         d2d       Z#ej7                         d1d       Z$ej7                         d4d       Z%ej7                         d1d       Z&ej7                         d5d       Z'ej7                         d6d       Z(ej7                         d7d       Z)ej7                         d8d        Z*ej7                         d9d:d!       Z+ej7                         d9d;d"       Z,ej7                         d9d<d#       Z-ej]                         Z/ej`                  Z0ed$        Z1d=d%Z2d>d&Z3 G d' d(      Z4d>d)Z5y)?ud  MCP (Model Context Protocol) endpoint for the Jarvis memory service.

Mounts a SINGLE streamable-HTTP MCP endpoint onto the same FastAPI app that
serves the REST API, so Claude Code and the claude.ai app can share the one
memory brain. The tools are THIN wrappers that call app.py's OWN functions in
process (never over HTTP to ourselves, no logic duplicated).

Two doors, same endpoint (auth enforced HERE, before the MCP handler runs):
  • POST /mcp            — Authorization: Bearer $MEMORY_API_KEY   (Claude Code --header)
  • POST /mcp/<token>    — token = first 32 hex of sha256(MEMORY_API_KEY)
                           (path-secret door for claude.ai custom connectors,
                           whose UI can't send custom headers; OAuth is overkill
                           for a personal service).
If MEMORY_API_KEY is empty, BOTH doors are open (matches app.py `_auth`).

Wiring (in app.py): `import mcp_server`, run `mcp_server.lifespan()` from the
FastAPI lifespan (the session manager needs its task group running), and call
`mcp_server.mount(app)` to attach the two routes. Nothing else changes.
    )annotationsN)asynccontextmanager)FastMCP)TransportSecuritySettings)RouteMEMORY_API_KEY zBearer     zjarvis-memoryTF)enable_dns_rebinding_protectionu   Ahmed's shared long-term memory (a fact graph). Search it before answering from scratch, save durable facts, and checkpoint projects so any Claude — Code or app — can pick up where the last one stopped.)stateless_httpjson_responsetransport_securityinstructionsc                t    K   ddl t        j                  j                   fd       d{   S 7 w)zISearch shared memory: salience-ranked facts plus their connected context.r   Nc                 :     j                  d ddt              S )N   F)qkgrouphopsinclude_staleauthorization)search_BEARER)appr   querys   G/Users/ahmed/devFolder/Ultron/claude-voice/memory-service/mcp_server.py<lambda>zmemory_search.<locals>.<lambda>G   s!    

Uat!).g  G    r   anyio	to_threadrun_sync)r   r   r   s   ``@r   memory_searchr$   B   s7      ))	GH H H Hs   ,868c                  K   ddl |xs g D cg c]   }t        |      j                         sd|i" }}j                  | |xs dd|xs d      t        j
                  j                  fd       d{   S c c}w 7 	w)zRSave one durable fact; `entities` are names (people/companies/projects) to attach.r   Nnamefactmcptextkindsourceentitiesc                 2     j                  t              S N)r   rememberr   r   bodys   r   r   z!memory_remember.<locals>.<lambda>T       T9r   )r   strstripRememberr!   r"   r#   )r*   r+   r-   eentsr   r3   s        @@r   memory_rememberr:   K   s      "*.b.D.ASV\\^VQK.DD<<T!%  /D))9; ; ; E;s!   BB B AB;B<
Bc                
  K   ddl d|  d| d| d}|r&|j                         r|d|j                          z  }j                  |dd	| d
dg      t        j                  j                  fd       d{   S 7 w)zPSave a 'where we stopped' checkpoint for a project: what's DONE and what's NEXT.r   NzPROJECT z checkpoint: DONE z. NEXT . 
checkpointr(   project)r&   typer)   c                 2     j                  t              S r/   r0   r2   s   r   r   z$project_checkpoint.<locals>.<lambda>b   r4   r   )r   r6   r7   r!   r"   r#   )r?   done
next_stepsdetailsr*   r   r3   s        @@r   project_checkpointrE   W   s      gY0gj\KD7==?!GMMO$%%<<TU+2I"F!G  ID))9; ; ; ;s   A8B<B=Bc                p    K   ddl t        j                  j                   fd       d{   S 7 w)zNPull up everything memory knows about one entity (person / company / project).r   Nc                 4     j                  d t              S )N)r&   r   r   )entityr   )r   r&   s   r   r   zentity_lookup.<locals>.<lambda>j   s    

D
Hr   r    )r&   r   s   `@r   entity_lookuprI   e   s4      ))HJ J J Js   +646c                 x   K   t         j                  j                  t        j                         d{   S 7 w)z1List the files in the shared Jarvis Drive folder.N)r!   r"   r#   drive
list_files r   r   
drive_listrN   n   s)      ))%*:*:;;;;   1:8:c                d    K   t         j                  j                   fd       d{   S 7 w)zSRead a text/csv/json file from the shared Jarvis Drive folder. `file` = name or id.c                 .    t        j                         S N)rK   	read_filefiles   r   r   zdrive_read.<locals>.<lambda>w   s    %//$2Gr   Nr!   r"   r#   rT   s   `r   
drive_readrW   t   s%      ))*GHHHH   &0.0c                h    K   t         j                  j                   fd       d{   S 7 w)zBCreate or overwrite a text file in the shared Jarvis Drive folder.c                 0    t        j                         S rR   )rK   
write_file)contentr&   s   r   r   zdrive_write.<locals>.<lambda>}   s    %2B2B42Qr   NrV   )r&   r\   s   ``r   drive_writer]   z   s%      ))*QRRRR   '202c                d    K   t         j                  j                   fd       d{   S 7 w)zORemove (trash) a file from the shared Jarvis Drive folder. `file` = name or id.c                 .    t        j                         S rR   )rK   delete_filerT   s   r   r   zdrive_delete.<locals>.<lambda>   s    %2C2CD2Ir   NrV   rT   s   `r   drive_deleterb      s%      ))*IJJJJrX   c                 x   K   t         j                  j                  t        j                         d{   S 7 w)z=List Ahmed's Smartlead cold-email campaigns and their status.N)r!   r"   r#   	smartlead	campaignsrM   r   r   smartlead_campaignsrf      s)      )))*=*=>>>>rO   c                d    K   t         j                  j                   fd       d{   S 7 w)zMAnalytics for one campaign (sent/open/reply/bounce). `campaign` = id or name.c                 .    t        j                         S rR   )rd   campaign_statscampaigns   r   r   z*smartlead_campaign_stats.<locals>.<lambda>   s    )2J2J82Tr   NrV   rj   s   `r   smartlead_campaign_statsrl      s%      ))*TUUUUrX   c                 x   K   t         j                  j                  t        j                         d{   S 7 w)zCList sender inboxes and their warmup reputation / daily send limit.N)r!   r"   r#   rd   sendersrM   r   r   smartlead_sendersro      s)      )))*;*;<<<<rO   c                d    K   t         j                  j                   fd       d{   S 7 w)z0Create a new (drafted) campaign; returns its id.c                 .    t        j                         S rR   )rd   create_campaignr&   s   r   r   z+smartlead_create_campaign.<locals>.<lambda>   s    )2K2KD2Qr   NrV   rs   s   `r   smartlead_create_campaignrt      s%      ))*QRRRRrX   c                h    K   t         j                  j                   fd       d{   S 7 w)zSet a campaign's email sequence. sequence_json = JSON array of steps
    {seq_number, seq_delay_details:{delay_in_days}, subject, email_body}.c                 0    t        j                         S rR   )rd   set_sequencerk   sequence_jsons   r   r   z(smartlead_set_sequence.<locals>.<lambda>   s    	&&x?r   NrV   rx   s   ``r   smartlead_set_sequencerz      s1      ))?A A A Ar^   c                h    K   t         j                  j                   fd       d{   S 7 w)z|Add leads to a campaign. leads_json = JSON array of {email, first_name,
    last_name, company_name, ...}. Max 100 per call.c                 0    t        j                         S rR   )rd   	add_leadsrk   
leads_jsons   r   r   z%smartlead_add_leads.<locals>.<lambda>   s    	##Hj9r   NrV   r~   s   ``r   smartlead_add_leadsr      s-      ))9; ; ; ;r^   c                h    K   t         j                  j                   fd       d{   S 7 w)zEStart / pause / stop a campaign. action = 'start' | 'pause' | 'stop'.c                 0    t        j                         S rR   )rd   campaign_control)actionrk   s   r   r   z,smartlead_campaign_control.<locals>.<lambda>   s    	**8V<r   NrV   )rk   r   s   ``r   smartlead_campaign_controlr      s-      ))<> > > >r^   c                l    K   t         j                  j                   fd       d{   S 7 w)u   Escape hatch — call ANY Smartlead REST endpoint. method=GET/POST/DELETE,
    path after /api/v1 (api_key added for you), body_json = JSON body for writes.c                 2    t        j                         S rR   )rd   raw)	body_jsonmethodpaths   r   r   zsmartlead_api.<locals>.<lambda>   s    	fdI6r   NrV   )r   r   r   s   ```r   smartlead_apir      s-      ))68 8 8 8s   (424c                h    K   t         j                  j                   fd       d{   S 7 w)u   Run SQL against the Ultron master DB (18k+ leads + CRM: clients, contacts,
    users…). SELECT to read/aggregate, INSERT/UPDATE to write. Irreversible
    statements (drop/truncate, WHERE-less delete/update) need confirm='yes'.c                 0    t        j                         S rR   )	ultron_dbr   )confirmsqls   r   r   zultron_query.<locals>.<lambda>   s    )//#w2Or   NrV   )r   r   s   ``r   ultron_queryr      s%     
 ))*OPPPPr^   c                d    K   t         j                  j                   fd       d{   S 7 w)ux   Inspect the DB: no table → all tables with row counts; with a table → its
    columns. Use before composing a query.c                 .    t        j                         S rR   )r   schematables   r   r   zultron_schema.<locals>.<lambda>   s    )2B2B52Ir   NrV   r   s   `r   ultron_schemar      s%      ))*IJJJJrX   c                   K   t         j                         4 d{    d ddd      d{    y7 7 # 1 d{  7  sw Y   yxY ww)zIEnter from the FastAPI lifespan so the session manager's task group runs.N)session_managerrunrM   r   r   lifespanr      s3      ""$$ %$$$$$$s<   A6A:A8AAA AAAc                F   t         sy|t        j                  |t              S | j	                  dg       D ci c]2  \  }}|j                         j                         |j                         4 }}}t        j                  |j	                  dd      t              S c c}}w )NTheadersr   r	   )API_KEYhmaccompare_digest
PATH_TOKENgetdecodelowerr   )scopetokenr   vr   s        r   _authorizedr      s    ""5*55 99Y35341a xxz!188:-3  5w{{?B?II5s   7Bc                d   K    | dddgd       d {     | ddd       d {    y 7 7 w)Nzhttp.response.starti  )s   content-types   application/json)r@   statusr   zhttp.response.bodys   {"error":"unauthorized"})r@   r3   rM   )sends    r   _rejectr      sK     
-BCE F F F
,6QR
SSSFSs   0,0.00c                      e Zd ZdZddZd Zy)_DoorzHASGI gate in front of the shared MCP handler. mode: 'header' or 'token'.c                    || _         y rR   )mode)selfr   s     r   __init__z_Door.__init__   s	    	r   c                2  K   |d   dk7  rt        |       d {    y | j                  dk(  r!|j                  di       j                  d      nd }t        ||      st        |       d {    y t        j                  |||       d {    y 7 w7 '7 	w)Nr@   httpr   path_params)r   r   r   r   r   handle_request)r   r   receiver   r   s        r   __call__z_Door.__call__   s     =F"$-II( ="-11':.2 	5%($-,,UGTBBB  
  Bs4   BBAB+B,BBBBBN)r   r5   )__name__
__module____qualname____doc__r   r   rM   r   r   r   r      s    R	Cr   r   c                    | j                   j                  j                  t        dt	        d                   | j                   j                  j                  t        dt	        d                   y)z=Attach both MCP doors to an existing FastAPI / Starlette app.z/mcpheaderz/mcp/{token}r   N)routerroutesappendr   r   )r   s    r   mountr      sH    JJU65?;<JJU>5>BCr   )   )r   r5   r   intreturndict)r'   N)r*   r5   r+   r5   r-   zlist[str] | Noner   r   rR   )
r?   r5   rB   r5   rC   r5   rD   
str | Noner   r   )r&   r5   r   r   )r   r5   )rU   r5   r   r5   )r&   r5   r\   r5   r   r5   )rk   r5   r   r5   )r&   r5   r   r5   )rk   r5   ry   r5   r   r5   )rk   r5   r   r5   r   r5   )rk   r5   r   r5   r   r5   )r	   )r   r5   r   r5   r   r5   r   r5   )r   r5   r   r5   r   r5   )r   r5   r   r5   )r   r   r   bool)r   None)6r   
__future__r   hashlibr   os
contextlibr   r!   mcp.server.fastmcpr   mcp.server.transport_securityr   starlette.routingr   rK   rd   r   environr   r   r   sha256encode	hexdigestr   r(   toolr$   r:   rE   rI   rN   rW   r]   rb   rf   rl   ro   rt   rz   r   r   r   r   r   streamable_http_app_r   r   r   r   r   r   rM   r   r   <module>r      s  & #   	 *  & C #   
**..)2
.G9
 CJ^W^^GNN,-779#2>r
 1(-/	Q. H H 177;;$4;@D; ; 37
;&0
;<@
; 
; J J < <
 I I
 S S
 K K ? ?
 V V
 = =
 S S
 A A ; ; > > 8 8 Q Q K K %%  JTC C$Dr   