+
    <WjhW              	         R t ^ RIHt ^ RIt^ RIt^ RIt^ RIHt ^ RIt^ RI	H
t
 ^ RIHt ^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt^ RIt]P.                  P1                  RR4      tR	] 2t]'       d6   ]P6                  ! ]P9                  4       4      P;                  4       R
,          MRt]
! RRR]! RR7      RR7      t]PA                  4       R R l4       t!]PA                  4       R R l4       t"]PA                  4       R R l4       t#]PA                  4       R R l4       t$]PA                  4       R R l4       t%]PA                  4       R R l4       t&]PA                  4       R R l4       t']PA                  4       R R  l4       t(]PA                  4       R! R" l4       t)]PA                  4       R# R$ l4       t*]PA                  4       R% R& l4       t+]PA                  4       RzR' R( ll4       t,]PA                  4       RzR) R* ll4       t-]PA                  4       RzR+ R, ll4       t.]PA                  4       R- R. l4       t/]PA                  4       RzR/ R0 ll4       t0]PA                  4       R{R1 R2 ll4       t1]PA                  4       R3 R4 l4       t2]PA                  4       RzR5 R6 ll4       t3]PA                  4       R{R7 R8 ll4       t4]PA                  4       R9 R: l4       t5]PA                  4       R|R; R< ll4       t6]PA                  4       R}R= R> ll4       t7]PA                  4       R? R@ l4       t8]PA                  4       RA RB l4       t9]PA                  4       RC RD l4       t:]PA                  4       RE RF l4       t;]PA                  4       RG RH l4       t<]PA                  4       RI RJ l4       t=]PA                  4       RK RL l4       t>]PA                  4       R~RM RN ll4       t?]PA                  4       RRO RP ll4       t@]PA                  4       RRQ RR ll4       tA]PA                  4       R}RS RT ll4       tB]PA                  4       RRU RV ll4       tC]PA                  4       RW RX l4       tD]PA                  4       RRY RZ ll4       tE]PA                  4       RR[ R\ ll4       tF]PA                  4       RR] R^ ll4       tG]PA                  4       R}R_ R` ll4       tH]PA                  4       Ra Rb l4       tI]PA                  4       Rc Rd l4       tJ]PA                  4       RRe Rf ll4       tK]PA                  4       RzRg Rh ll4       tL]PA                  4       Ri Rj l4       tM]PA                  4       R{Rk Rl ll4       tN]PA                  4       RRm Rn ll4       tO]PA                  4       RRo Rp ll4       tP]P                  4       tR]P                  tS]Rq 4       tTRr Rs ltURt Ru ltV ! Rv Rw4      tWRx Ry ltXR# )u  Shared jarvis-tools MCP endpoint (streamable-HTTP) for Railway.

Mirrors the memory service's proven pattern so the SAME claude.ai custom
connector setup works. Two doors, one handler, auth enforced BEFORE the MCP
handler runs:
  • POST /mcp          — Authorization: Bearer $JARVIS_TOOLS_KEY  (Claude Code --header)
  • POST /mcp/<token>  — token = first 32 hex of sha256(JARVIS_TOOLS_KEY)
                         (path-secret door for claude.ai custom connectors, whose
                         UI can't send custom headers; OAuth is overkill here).
If JARVIS_TOOLS_KEY is empty, BOTH doors are open.

Tools = the shared Google Drive folder (list/read/write/delete). Smartlead and
the Ultron DB can be added here later as more @mcp.tool()s on the same connector.
)annotationsN)asynccontextmanager)FastMCP)TransportSecuritySettings)RouteJARVIS_TOOLS_KEY zBearer :N    Nzjarvis-toolsTF)enable_dns_rebinding_protectionu  Ahmed's shared Jarvis storage — one Google Drive folder ('Jarvis Drive') any Claude on any device can read and write. drive_list to see what's there, drive_read to read a file (e.g. an Apollo CSV), drive_write to save one, drive_delete to remove one. It ONLY sees this one shared folder, never Ahmed's personal Drive. The asana_* tools are Ahmed's project management — everything he could do in the Asana web app: list/create/update/archive projects, invite people to them (asana_project_members) and post status updates; read a task in full (asana_task), list, create, update, move between projects/columns, complete, delete (recoverable 30 days) and break tasks into subtasks; set assignees, followers and tags; post and READ comment threads; and search. asana_api is the escape hatch for anything else in Asana's REST API. Prefer archiving a project over deleting it — archive is reversible.)stateless_httpjson_responsetransport_securityinstructionsc                   V ^8  d   QhRR/#    returnstr )formats   "mcp_server.py__annotate__r   E   s     < <# <    c                 x   "   \         P                  P                  \        P                  4      G Rj  xL
 #  L5i)z1List the files in the shared Jarvis Drive folder.N)anyio	to_threadrun_syncdrive
list_filesr   r   r   
drive_listr   D   (      ))%*:*:;;;;   1:8:c                    V ^8  d   QhRRRR/# r   filer   r   r   )r   s   "r   r   r   K   s     I I3 I3 Ir   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)zFRead a text/csv/json file from the shared folder. `file` = name or id.c                 0   < \         P                  ! S 4      # N)r   	read_filer$   s   r   <lambda>drive_read.<locals>.<lambda>M   s    %//$2Gr   Nr   r   r   r)   s   fr   
drive_readr-   J   s$      ))*GHHHH   '1/1c               $    V ^8  d   QhRRRRRR/# r   namer   contentr   r   )r   s   "r   r   r   Q   s&     S SC S# S# Sr   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zHCreate or overwrite a text file in the shared folder from given content.c                 2   < \         P                  ! SS 4      # r'   )r   
write_filer2   r1   s   r   r*   drive_write.<locals>.<lambda>S   s    %2B2B42Qr   Nr,   r1   r2   s   ffr   drive_writer9   P   s$      ))*QRRRR   (313c                    V ^8  d   QhRRRR/# r#   r   )r   s   "r   r   r   W   s     K KS KS Kr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)zBRemove (trash) a file from the shared folder. `file` = name or id.c                 0   < \         P                  ! S 4      # r'   )r   delete_filer)   s   r   r*   drive_delete.<locals>.<lambda>Y       %2C2CD2Ir   Nr,   r)   s   fr   drive_deleterA   V   s$      ))*IJJJJr.   c                   V ^8  d   QhRR/# r   r   )r   s   "r   r   r   ^   s     ? ?3 ?r   c                 x   "   \         P                  P                  \        P                  4      G Rj  xL
 #  L5i)z=List Ahmed's Smartlead cold-email campaigns and their status.N)r   r   r   	smartlead	campaignsr   r   r   smartlead_campaignsrF   ]   s(      )))*=*=>>>>r!   c                    V ^8  d   QhRRRR/# )r   campaignr   r   r   )r   s   "r   r   r   d   s     V VS VS Vr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)zMAnalytics for one campaign (sent/open/reply/bounce). `campaign` = id or name.c                 0   < \         P                  ! S 4      # r'   )rD   campaign_statsrH   s   r   r*   *smartlead_campaign_stats.<locals>.<lambda>f   s    )2J2J82Tr   Nr,   rL   s   fr   smartlead_campaign_statsrN   c   s$      ))*TUUUUr.   c                   V ^8  d   QhRR/# r   r   )r   s   "r   r   r   j   s     = = =r   c                 x   "   \         P                  P                  \        P                  4      G Rj  xL
 #  L5i)zCList sender inboxes and their warmup reputation / daily send limit.N)r   r   r   rD   sendersr   r   r   smartlead_sendersrR   i   s(      )))*;*;<<<<r!   c                    V ^8  d   QhRRRR/# r   r1   r   r   r   )r   s   "r   r   r   p   s     S S# S# Sr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)z0Create a new (drafted) campaign; returns its id.c                 0   < \         P                  ! S 4      # r'   )rD   create_campaignr1   s   r   r*   +smartlead_create_campaign.<locals>.<lambda>r   s    )2K2KD2Qr   Nr,   rX   s   fr   smartlead_create_campaignrZ   o   s$      ))*QRRRRr.   c               $    V ^8  d   QhRRRRRR/# )r   rH   r   sequence_jsonr   r   )r   s   "r   r   r   v   s&     A A3 As As Ar   c                j   a a"   \         P                  P                  V V3R l4      G Rj  xL
 #  L5i)zSet a campaign's email sequence. sequence_json = JSON array of steps
{seq_number, seq_delay_details:{delay_in_days}, subject, email_body}.c                 2   < \         P                  ! S S4      # r'   )rD   set_sequencerH   r\   s   r   r*   (smartlead_set_sequence.<locals>.<lambda>z   s    	&&x?r   Nr,   r`   s   ffr   smartlead_set_sequencerb   u   s0      ))?A A A Ar:   c               $    V ^8  d   QhRRRRRR/# )r   rH   r   
leads_jsonr   r   )r   s   "r   r   r   ~   s!     ; ; ; ; ;r   c                j   a a"   \         P                  P                  V V3R l4      G Rj  xL
 #  L5i)zxAdd leads to a campaign. leads_json = JSON array of {email, first_name,
last_name, company_name, ...}. Max 100 per call.c                 2   < \         P                  ! S S4      # r'   )rD   	add_leadsrH   rd   s   r   r*   %smartlead_add_leads.<locals>.<lambda>   s    	##Hj9r   Nr,   rh   s   ffr   smartlead_add_leadsrj   }   s,      ))9; ; ; ;r:   c               $    V ^8  d   QhRRRRRR/# )r   rH   r   actionr   r   )r   s   "r   r   r      s!     > >s >C >C >r   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zEStart / pause / stop a campaign. action = 'start' | 'pause' | 'stop'.c                 2   < \         P                  ! SS 4      # r'   )rD   campaign_control)rl   rH   s   r   r*   ,smartlead_campaign_control.<locals>.<lambda>   s    	**8V<r   Nr,   )rH   rl   s   ffr   smartlead_campaign_controlrq      s,      ))<> > > >r:   c               (    V ^8  d   QhRRRRRRRR/# )r   methodr   path	body_jsonr   r   )r   s   "r   r   r      s(     8 8 83 83 8 8r   c                n   a aa"   \         P                  P                  VV V3R l4      G Rj  xL
 #  L5i)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                 4   < \         P                  ! SSS 4      # r'   )rD   raw)ru   rs   rt   s   r   r*   smartlead_api.<locals>.<lambda>   s    	fdI6r   Nr,   )rs   rt   ru   s   fffr   smartlead_apirz      s,      ))68 8 8 8   )535c               $    V ^8  d   QhRRRRRR/# )r   sqlr   confirmr   r   )r   s   "r   r   r      s&     Q QC Q# Qs Qr   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)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                 2   < \         P                  ! SS 4      # r'   )	ultron_dbquery)r~   r}   s   r   r*   ultron_query.<locals>.<lambda>   s    )//#w2Or   Nr,   )r}   r~   s   ffr   ultron_queryr      s$     
 ))*OPPPPr:   c                    V ^8  d   QhRRRR/# )r   tabler   r   r   )r   s   "r   r   r      s     K Ks KC Kr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)ut   Inspect the DB: no table → all tables with row counts; with a table → its
columns. Use before composing a query.c                 0   < \         P                  ! S 4      # r'   )r   schemar   s   r   r*   ultron_schema.<locals>.<lambda>   s    )2B2B52Ir   Nr,   r   s   fr   ultron_schemar      $      ))*IJJJJr.   c                    V ^8  d   QhRRRR/# )r   r   r   r   r   )r   s   "r   r   r      s     O Os Os Or   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)zSearch EVERYWHERE Ahmed keeps people/leads (memory graph, Ultron leads DB,
Jarvis Drive sheets) by phone number or name. Use whenever Ahmed asks who
someone is or gives an unknown number.c                 0   < \         P                  ! S 4      # r'   )lookupperson_lookupr   s   r   r*   person_lookup.<locals>.<lambda>   s    &2F2Fu2Mr   Nr,   r   s   fr   r   r      s$     
 ))*MNNNNr.   c          
     ,    V ^8  d   QhRRRRRRRRRR/# r   tor   subjectbodyccr   r   )r   s   "r   r   r      s/     6 6 6s 6# 63 6 6r   c                r   a aaa"   \         P                  P                  VVVV 3R l4      G Rj  xL
 #  L5i)zSend an email from Ahmed's business mailbox (ahmad@revalstudio.com). `to`
and `cc` are comma-separated; cc optional. Namecheap takes ~15-30s to deliver.c                 6   < \         P                  ! SSS S4      # r'   )emailboxsendr   r   r   r   s   r   r*   email_send.<locals>.<lambda>   s    b'44r   Nr,   r   r   r   r   s   ffffr   
email_sendr      s,      ))46 6 6 6   *757c               $    V ^8  d   QhRRRRRR/# r   r   r   limitintr   r   )r   s   "r   r   r      s&     Q Qc Q# Qs Qr   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)z{Search the business inbox by phrase (sender+subject; empty = latest).
Returns a list with a UID per message for email_read.c                 2   < \         P                  ! SS 4      # r'   )r   searchr   r   s   r   r*   email_search.<locals>.<lambda>   s    (//%2Or   Nr,   r   r   s   ffr   email_searchr      s$      ))*OPPPPr:   c                    V ^8  d   QhRRRR/# )r   uidr   r   r   )r   s   "r   r   r      s     F F# F# Fr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)z<Read the full body of one business-inbox message by its UID.c                 0   < \         P                  ! S 4      # r'   )r   readr   s   r   r*   email_read.<locals>.<lambda>   s    (--2Dr   Nr,   r   s   fr   
email_readr      $      ))*DEEEEr.   c          
     ,    V ^8  d   QhRRRRRRRRRR/# r   r   )r   s   "r   r   r      s/     > > >s ># >3 > >r   c                r   a aaa"   \         P                  P                  VVVV 3R l4      G Rj  xL
 #  L5i)zHSend an email from Ahmed's WORK Gmail (alrugaib). to/cc comma-separated.c                 6   < \         P                  ! SSS S4      # r'   )
gworkspace
gmail_sendr   s   r   r*   gmail_send.<locals>.<lambda>   s    
%%b'4<r   Nr,   r   s   ffffr   r   r      s,      ))<> > > >r   c               $    V ^8  d   QhRRRRRR/# r   r   )r   s   "r   r   r      s!     7 7c 7# 7s 7r   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zqSearch the WORK Gmail inbox (Gmail query syntax; empty = latest). Returns a
message id per result for gmail_read.c                 2   < \         P                  ! SS 4      # r'   )r   gmail_searchr   s   r   r*   gmail_search.<locals>.<lambda>   s    
''u5r   Nr,   r   s   ffr   r   r      s,      ))57 7 7 7r:   c                    V ^8  d   QhRRRR/# )r   
message_idr   r   r   )r   s   "r   r   r      s     U U U Ur   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)z7Read the full body of one WORK Gmail message by its id.c                 0   < \         P                  ! S 4      # r'   )r   
gmail_readr   s   r   r*   gmail_read.<locals>.<lambda>   s    *2G2G
2Sr   Nr,   r   s   fr   r   r      s$      ))*STTTTr.   c                    V ^8  d   QhRRRR/# )r   daysr   r   r   r   )r   s   "r   r   r      s     R Rc R# Rr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)zBList Ahmed's WORK calendar events for the next N days (default 7).c                 0   < \         P                  ! S 4      # r'   )r   calendar_listr   s   r   r*   calendar_list.<locals>.<lambda>   s    *2J2J42Pr   Nr,   r   s   fr   r   r      s$      ))*PQQQQr.   c               4    V ^8  d   QhRRRRRRRRRRRRRR/# )	r   titler   startend	attendeesdescriptionadd_meetr   r   )r   s   "r   r   r      sJ     B Bs B3 BS B+.BBEB*-B7:Br   c                z   a aaaaa"   \         P                  P                  VVVVVV 3R l4      G Rj  xL
 #  L5i)zCreate a WORK calendar event. start/end = ISO datetime with timezone
(2026-07-15T14:00:00+03:00). attendees = comma-separated emails (invited).
add_meet='yes' attaches a Google Meet link.c                 :   < \         P                  ! SSSSSS 4      # r'   )r   calendar_create_event)r   r   r   r   r   r   s   r   r*   'calendar_create_event.<locals>.<lambda>   s    
005#y+xAr   Nr,   )r   r   r   r   r   r   s   ffffffr   r   r      s9      ))	A 	AB B B B   ,;9;c               $    V ^8  d   QhRRRRRR/# )r   spreadsheet_idr   a1_ranger   r   )r   s   "r   r   r      s&     B Bc BS BS Br   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zORead a range from a Google Sheet (work account). a1_range e.g. 'Sheet1!A1:D20'.c                 2   < \         P                  ! SS 4      # r'   )r   sheets_read)r   r   s   r   r*   sheets_read.<locals>.<lambda>   s    
&&~x@r   Nr,   )r   r   s   ffr   r   r      s0      ))@B B B Br:   c               (    V ^8  d   QhRRRRRRRR/# )r   r   r   r   values_jsonr   r   )r   s   "r   r   r      s0     P Ps Pc P$'P,/Pr   c                n   a aa"   \         P                  P                  VV V3R l4      G Rj  xL
 #  L5i)zkWrite to a Google Sheet range (work account). values_json = JSON array of
rows, e.g. [["a","b"],["c","d"]].c                 4   < \         P                  ! SS S4      # r'   )r   sheets_write)r   r   r   s   r   r*   sheets_write.<locals>.<lambda>   s    
''+Nr   Nr,   )r   r   r   s   fffr   r   r      s0     
 ))NP P P Pr{   c               $    V ^8  d   QhRRRRRR/# r0   r   )r   s   "r   r   r      s&     P Pc PC PC Pr   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zSave a text file to the shared writable storage (syncs across all devices).
`name` = file name/path (e.g. 'crm/riyadh.csv'), `content` = the full text.
This is where Jarvis/AIs put files for each other (drive_* is read-only).c                 2   < \         P                  ! SS 4      # r'   )storagewriter6   s   r   r*   storage_write.<locals>.<lambda>  s    '--g2Nr   Nr,   r8   s   ffr   storage_writer      s$     
 ))*NOOOOr:   c                    V ^8  d   QhRRRR/# rT   r   )r   s   "r   r   r     s     F FS FS Fr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)z?Read a file back from the shared writable storage by name/path.c                 0   < \         P                  ! S 4      # r'   )r   r   rX   s   r   r*   storage_read.<locals>.<lambda>  s    ',,t2Dr   Nr,   rX   s   fr   storage_readr     r   r.   c                   V ^8  d   QhRR/# r   r   )r   s   "r   r   r     s     > >C >r   c                 x   "   \         P                  P                  \        P                  4      G Rj  xL
 #  L5i)z.List the files in the shared writable storage.N)r   r   r   r   r   r   r   r   storage_listr   
  s(      ))'*<*<====r!   c                    V ^8  d   QhRRRR/# rT   r   )r   s   "r   r   r     s     H Hs Hs Hr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)z<Delete a file from the shared writable storage by name/path.c                 0   < \         P                  ! S 4      # r'   )r   deleterX   s   r   r*    storage_delete.<locals>.<lambda>  s    '..2Fr   Nr,   rX   s   fr   storage_deleter     s$      ))*FGGGGr.   c                   V ^8  d   QhRR/# r   r   )r   s   "r   r   r     s     < < <r   c                 x   "   \         P                  P                  \        P                  4      G Rj  xL
 #  L5i)zAList Ahmed's Asana workspaces and which one the tools default to.N)r   r   r   asana
workspacesr   r   r   asana_workspacesr    r    r!   c               $    V ^8  d   QhRRRRRR/# )r   r   r   archivedboolr   r   )r   s   "r   r   r      s&     S S SD SS Sr   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zList Asana projects; `query` filters by name, archived=True lists archived
ones. Use it to find the right project before asking for its tasks.c                 2   < \         P                  ! SS 4      # r'   )r  projects)r  r   s   r   r*    asana_projects.<locals>.<lambda>#  s    %..2Qr   Nr,   )r   r  s   ffr   asana_projectsr    s$      ))*QRRRRr:   c               0    V ^8  d   QhRRRRRRRRRRRR/# )r   r1   r   teamnotesdue_onprivacyr   r   )r   s   "r   r   r   '  sA     J JS J J J'*J:=JGJJr   c                v   a aaaa"   \         P                  P                  VV VVV3R l4      G Rj  xL
 #  L5i)zCreate an Asana project. `team` is required only if the workspace is an
ORGANIZATION (the tool says so, and names the teams, if it is). `privacy` =
public_to_workspace | private_to_team | private.c                 8   < \         P                  ! SSSS S4      # r'   )r  project_create)r  r1   r  r  r  s   r   r*   &asana_project_create.<locals>.<lambda>-  s    $$T4Hr   Nr,   )r1   r  r  r  r  s   fffffr   asana_project_creater  &  s3      ))HHJ J J J   +979c               4    V ^8  d   QhRRRRRRRRRRRRR	R/# )
r   projectr   r1   r  r  ownerarchivebool | Noner   r   )r   s   "r   r   r   1  sJ     T T T3 TC T'*T8;T(3T?BTr   c                z   a aaaaa"   \         P                  P                  VVVVVV 3R l4      G Rj  xL
 #  L5i)u  Change a project (`project` = name or id): rename, notes, due_on, owner, or
archive=True to ARCHIVE it (reversible, keeps everything — this is how you
retire a project; deleting one is not offered here because it can't be undone,
use asana_api if you truly mean DELETE).c                 :   < \         P                  ! SSSSSS 4      # r'   )r  project_update)r  r  r1   r  r  r  s   r   r*   &asana_project_update.<locals>.<lambda>9  s    $$WdE65'Rr   Nr,   )r  r1   r  r  r  r  s   ffffffr   asana_project_updater   0  s3      ))RRT T T Tr   c          
     ,    V ^8  d   QhRRRRRRRRRR/# )r   r  r   addremoveaccess_levelr   r   )r   s   "r   r   r   =  s8     K K K3 KS K.1K;>Kr   c                r   a aaa"   \         P                  P                  VVV V3R l4      G Rj  xL
 #  L5i)aJ  Who's on a project, and invite/remove them. `add`/`remove` = comma-separated
emails, names or ids ('me' works). `access_level` = admin|editor|commenter|viewer.
No add/remove = list them. This shares a project with someone already in the
workspace; inviting a NEW person to Asana itself is asana_api POST
/workspaces/{gid}/addUser.c                 6   < \         P                  ! SSSS 4      # r'   )r  project_members)r$  r"  r  r#  s   r   r*   'asana_project_members.<locals>.<lambda>E  s    %%gsFLIr   Nr,   )r  r"  r#  r$  s   ffffr   asana_project_membersr)  <  s0      ))IK K K Kr   c          
     ,    V ^8  d   QhRRRRRRRRRR/# )r   r  r   textstatus_typer   r   r   )r   s   "r   r   r   I  s9     I I I3 I,/I&)I36Ir   c                r   a aaa"   \         P                  P                  V VVV3R l4      G Rj  xL
 #  L5i)z`Post a project status update. status_type = on_track | at_risk | off_track |
on_hold | complete.c                 6   < \         P                  ! S SSS4      # r'   )r  project_status)r  r,  r+  r   s   r   r*   &asana_project_status.<locals>.<lambda>O  s    $$WdKGr   Nr,   )r  r+  r,  r   s   ffffr   asana_project_statusr1  H  s0      ))GI I I Ir   c                    V ^8  d   QhRRRR/# r   taskr   r   r   )r   s   "r   r   r   S  s     D D3 D3 Dr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)zFULL detail of ONE task (`task` = name or id): notes, assignee, due date,
project, section, tags, followers, subtasks and the latest comments.c                 0   < \         P                  ! S 4      # r'   )r  r4  r4  s   r   r*   asana_task.<locals>.<lambda>V  s    %**T2Br   Nr,   r7  s   fr   
asana_taskr9  R  s$      ))*BCCCCr.   c               0    V ^8  d   QhRRRRRRRRRRR	R/# )
r   r  r   miner  section	completedr   r   r   r   )r   s   "r   r   r   Z  sA     G Gs Gt GS G!%G69GCFGr   c                v   a aaaa"   \         P                  P                  VVVV V3R l4      G Rj  xL
 #  L5i)zList Asana tasks. `project` (name or id) = that project's; add `section` for
one board column; empty project with mine=True = 'what's on my plate'. Open
tasks only unless completed=True.c                 8   < \         P                  ! SSSS S4      # r'   )r  tasks)r=  r   r;  r  r<  s   r   r*   asana_tasks.<locals>.<lambda>`  s    GT7IuEr   Nr,   )r  r;  r<  r=  r   s   fffffr   asana_tasksrB  Y  s3      ))EEG G G Gr  c               8    V ^8  d   QhRRRRRRRRRRRRRRR	R/# )
r   r1   r   r  r  assigneer  r<  parentr   r   )r   s   "r   r   r   d  sI     4 4# 4c 4 4&)4:=4%(47:4DG4r   c           	     ~   a aaaaaa"   \         P                  P                  VVV VVVV3R l4      G Rj  xL
 #  L5i)zCreate an Asana task. `project` = project name or id (optional), `section` =
a board column in it, `parent` = another task to make this a SUBTASK of,
`due_on` = YYYY-MM-DD, `assignee` = 'me' or a user id/email/name.c            	     <   < \         P                  ! SSSS SSS4      # r'   )r  task_create)rD  r  r1   r  rE  r  r<  s   r   r*   #asana_task_create.<locals>.<lambda>k  s    !!$w&")63r   Nr,   )r1   r  r  rD  r  r<  rE  s   fffffffr   asana_task_createrJ  c  s3      ))	3 	34 4 4 4   -=;=c               8    V ^8  d   QhRRRRRRRRRRRRRR	R
R/# )r   r4  r   r1   r  r  start_onrD  completer  r   r   )r   s   "r   r   r   p  sN     	- 	-# 	-S 	-c 	-$'	-8;	-&)	- '2	- >A	-r   c           	     ~   a aaaaaa"   \         P                  P                  VVVVVVV 3R l4      G Rj  xL
 #  L5i)zChange a task (`task` = name or id): rename, edit notes, set due_on
(YYYY-MM-DD), reassign, or set complete true/false. `start_on` needs a due date
too AND a paid Asana plan (it 402s on a free workspace).c            	     <   < \         P                  ! SSSSSS S4      # r'   )r  task_update)rD  rN  r  r1   r  rM  r4  s   r   r*   #asana_task_update.<locals>.<lambda>x  s    !!$eVXx"*,r   Nr,   )r4  r1   r  r  rM  rD  rN  s   fffffffr   asana_task_updaterS  o  s3      ))	, 	,- - - -rK  c          
     ,    V ^8  d   QhRRRRRRRRRR/# )r   r4  r   r  r<  remove_fromr   r   )r   s   "r   r   r   }  s8     F F Fc F F'*F47Fr   c                r   a aaa"   \         P                  P                  VVVV 3R l4      G Rj  xL
 #  L5i)zMove a task to another `project` and/or `section`. A REAL move: it is removed
from the project it was in (Asana's addProject alone would leave it in both).
`section` alone moves it between columns of its current project.c                 6   < \         P                  ! SS SS4      # r'   )r  	task_move)r  rU  r<  r4  s   r   r*   !asana_task_move.<locals>.<lambda>  s    gwDr   Nr,   )r4  r  r<  rU  s   ffffr   asana_task_moverZ  |  s0      ))DF F F Fr   c                    V ^8  d   QhRRRR/# r3  r   )r   s   "r   r   r     s     M MC MC Mr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)z1Mark an Asana task done. `task` = its name or id.c                 0   < \         P                  ! S 4      # r'   )r  task_completer7  s   r   r*   %asana_task_complete.<locals>.<lambda>  s    %2E2Ed2Kr   Nr,   r7  s   fr   asana_task_completer`    s$      ))*KLLLLr.   c                    V ^8  d   QhRRRR/# r3  r   )r   s   "r   r   r     s     K K# K# Kr   c                f   a "   \         P                  P                  V 3R l4      G Rj  xL
 #  L5i)u   Delete an Asana task. It goes to Deleted Items and is recoverable for 30
days. Only when he means DELETE — 'it's done' is asana_task_complete.c                 0   < \         P                  ! S 4      # r'   )r  task_deleter7  s   r   r*   #asana_task_delete.<locals>.<lambda>  r@   r   Nr,   r7  s   fr   asana_task_deleterf    r   r.   c               4    V ^8  d   QhRRRRRRRRRRRRRR/# )	r   r4  r   rD  add_followersremove_followersadd_tag
remove_tagr   r   )r   s   "r   r   r     sB     8 8# 8 8+.8GJ8%(8;>8HK8r   c                z   a aaaaa"   \         P                  P                  VVVVVV 3R l4      G Rj  xL
 #  L5i)zWho/what is on a task: set the assignee, add/remove followers (comma-separated
names, emails or 'me'), add/remove tags (a new tag is created if needed). With no
changes it just reports.c                 :   < \         P                  ! SSS SSS4      # r'   )r  task_people)rh  rj  rD  ri  rk  r4  s   r   r*   #asana_task_people.<locals>.<lambda>  s    !!$-AQ"):7r   Nr,   )r4  rD  rh  ri  rj  rk  s   ffffffr   asana_task_peoplerp    s3      ))	7 	78 8 8 8r   c               $    V ^8  d   QhRRRRRR/# )r   r4  r   r"  r   r   )r   s   "r   r   r     s&     M Ms M Mc Mr   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zNList a task's subtasks, or create them: `add` = comma-separated subtask names.c                 2   < \         P                  ! SS 4      # r'   )r  subtasks)r"  r4  s   r   r*    asana_subtasks.<locals>.<lambda>  s    %..s2Kr   Nr,   )r4  r"  s   ffr   asana_subtasksrv    $      ))*KLLLLr:   c               $    V ^8  d   QhRRRRRR/# )r   r4  r   r+  r   r   )r   s   "r   r   r     s&     M Mc M M Mr   c                j   a a"   \         P                  P                  V V3R l4      G Rj  xL
 #  L5i)uO   Post a comment on an Asana task (`task` = name or id) — e.g. a status update.c                 2   < \         P                  ! S S4      # r'   )r  commentr4  r+  s   r   r*   asana_comment.<locals>.<lambda>  s    %--d2Kr   Nr,   r|  s   ffr   asana_commentr~    rw  r:   c               $    V ^8  d   QhRRRRRR/# )r   r4  r   r   r   r   r   )r   s   "r   r   r     s&     O Os O3 O Or   c                j   a a"   \         P                  P                  VV 3R l4      G Rj  xL
 #  L5i)zdREAD a task's comment thread (system events skipped). `limit` = how many of
the latest (default 10).c                 2   < \         P                  ! SS 4      # r'   )r  comments)r   r4  s   r   r*    asana_comments.<locals>.<lambda>  s    %..u2Mr   Nr,   )r4  r   s   ffr   asana_commentsr    s$      ))*MNNNNr:   c               4    V ^8  d   QhRRRRRRRRRRRRR	R/# )
r   r   r   r  rD  
due_before	due_afterr=  r  r   r   )r   s   "r   r   r     sA     	) 	)c 	) 	)S 	)#&	)8;	)"-	)9<	)r   c                z   a aaaaa"   \         P                  P                  VVVVVV 3R l4      G Rj  xL
 #  L5i)u6  Find tasks by phrase, with optional filters (project, assignee='me' or a name,
due_before/due_after as YYYY-MM-DD, completed). Uses Asana's premium search when
the plan has it, else name-matching typeahead. The search index lags up to a
minute — for a task just created, use asana_task / asana_tasks instead.c                 :   < \         P                  ! SSS SSS4      # r'   )r  r   )rD  r=  r  r  r  r   s   r   r*   asana_search.<locals>.<lambda>  s    UGXz9&(r   Nr,   )r   r  rD  r  r  r=  s   ffffffr   asana_searchr    s3      ))	( 	() ) ) )r   c          
     ,    V ^8  d   QhRRRRRRRRRR/# )r   rs   r   rt   ru   params_jsonr   r   )r   s   "r   r   r     s8     A AC As As A!$A.1Ar   c                r   a aaa"   \         P                  P                  VV VV3R l4      G Rj  xL
 #  L5i)u  Escape hatch — call ANY Asana REST endpoint (base https://app.asana.com/api/1.0)
for what the curated asana_* tools don't cover: sections, tags, teams,
attachments, dependencies, templates, goals, portfolios, custom fields, webhooks,
batch, inviting a NEW person to the workspace, deleting a project.

method = GET/POST/PUT/DELETE. path = everything after /api/1.0, e.g.
'/projects/12345/sections'.

ENVELOPE: do NOT wrap body_json in {"data": ...} — that's added for you. Just
pass the inner object: {"name": "Q3 launch", "workspace": "12345"}. The response
is unwrapped from `data` for you too.

OPT_FIELDS (important): Asana returns ONLY gid/resource_type/name by default. To
get real fields, pass them in params_json:
  params_json = {"opt_fields": "name,due_on,completed,assignee.name", "limit": 50}
Dot-notation reaches into related objects (assignee.name, projects.name).
Returns raw JSON.c                 6   < \         P                  ! SSS S4      # r'   )r  rx   )ru   rs   r  rt   s   r   r*   asana_api.<locals>.<lambda>  s    		&$	;?r   Nr,   )rs   rt   ru   r  s   ffffr   	asana_apir    s0     ( ))?A A A Ar   c                   "   \         P                  4       ;_uu_4       GRj  xL
  R5x  RRR4      GRj  xL
  R#  L L  + GRj  xL 
 '       g   i     R# ; i5i)zIEnter from the FastAPI lifespan so the session manager's task group runs.N)session_managerrunr   r   r   lifespanr    s0      ""$$$ %$$$$$sF   !A ?A AA AA A A		A

A	A		A c                    V ^8  d   QhRRRR/# )r   tokenz
str | Noner   r  r   )r   s   "r   r   r     s     J Jj JT Jr   c                ^   \         '       g   R # Ve   \        P                  ! V\        4      # V P	                  R. 4       UUu/ uF2  w  r#VP                  4       P                  4       VP                  4       bK4  	  ppp\        P                  ! VP	                  RR4      \        4      # u uppi )Theadersauthorizationr   )API_KEYhmaccompare_digest
PATH_TOKENgetdecodelower_BEARER)scoper  kvr  s   &&   r   _authorizedr    s    7""5*55 99Y35341 xxz!188:-3  5w{{?B?II5s   8B)c                   V ^8  d   QhRR/# r   r   Noner   )r   s   "r   r   r     s     T T4 Tr   c                l   "   V ! R RRRRR	./4      G Rj  xL
  V ! R RRR/4      G Rj  xL
  R#  L L5i)
typezhttp.response.startstatusi  r  Nzhttp.response.bodyr   s   {"error":"unauthorized"})s   content-types   application/jsonr   )r   s   &r   _rejectr    sT     
-xBCE F F F
,f6QR
SSSFSs   404244c                  ,    ] tR tRtRtR R ltR tRtR# )_Doori  zHASGI gate in front of the shared MCP handler. mode: 'header' or 'token'.c                   V ^8  d   QhRR/# )r   moder   r   )r   s   "r   r   _Door.__annotate__  s      S r   c                	    Wn         R # r'   r  )selfr  s   &&r   __init___Door.__init__  s    	r   c                	P  "   VR ,          R8w  d   \        V4      G Rj  xL
  R# V P                  R8X  d"   VP                  R/ 4      P                  R4      MRp\        W4      '       g   \        V4      G Rj  xL
  R# \        P                  WV4      G Rj  xL
  R#  L L( L
5i)r  httpNr  path_params)r  r  r  r  r  handle_request)r  r  receiver   r  s   &&&& r   __call___Door.__call__  s     =F"$-II( ="-11':.2 	5(($-,,UTBBB  
  Bs4   B&B AB&9B":B&B$B&"B&$B&r  N)__name__
__module____qualname____firstlineno____doc__r  r  __static_attributes__r   r   r   r  r    s    R	Cr   r  c                   V ^8  d   QhRR/# r  r   )r   s   "r   r   r     s     D D$ Dr   c                    V P                   P                  P                  \        R\	        R4      4      4       V P                   P                  P                  \        R\	        R4      4      4       R# )z=Attach both MCP doors to an existing FastAPI / Starlette app.z/mcpheaderz/mcp/{token}r  N)routerroutesappendr   r  )apps   &r   mountr    sH    JJU65?;<JJU>5>BCr   )r   )
   )   )r   r   r   )r   F)r   r   r   r   )r   r   r   r   N)on_trackr   )r   Tr   F2   )r   r   mer   r   r   )r   r   r   r   r   N)r   r   r   r   r   )r   r   )Yr  
__future__r   hashlibr  os
contextlibr   r   mcp.server.fastmcpr   mcp.server.transport_securityr   starlette.routingr   r  r   r   r   r   rD   r   r   environr  r  r  sha256encode	hexdigestr  mcptoolr   r-   r9   rA   rF   rN   rR   rZ   rb   rj   rq   rz   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r   r)  r1  r9  rB  rJ  rS  rZ  r`  rf  rp  rv  r~  r  r  r  streamable_http_app_r  r  r  r  r  r  r   r   r   <module>r     s    #   	 *  & C #        
**..+R
0G9
 CJW^^GNN,-779#>r
 1(-/	Q2 < <
 I I
 S S
 K K ? ?
 V V
 = =
 S S
 A A ; ; > > 8 8 Q Q K K O O 6 6 Q Q F F > > 7 7 U U
 R R
 B B B B P P P P F F
 > >
 H H < <
 S S J J T T K K I I D D G G 4 4 	- 	- F F M M
 K K 8 8 M M
 M M
 O O 	) 	) A A. %%  JTC C$Dr   