
    2UjU                    X   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ddl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j4                  ej7                               j9                         dd nd	Z e
ddd ed      d      Zej?                         dGd       Z ej?                         dHd       Z!ej?                         dId       Z"ej?                         dHd       Z#ej?                         dGd       Z$ej?                         dJd       Z%ej?                         dGd       Z&ej?                         dKd       Z'ej?                         dLd       Z(ej?                         dMd       Z)ej?                         dNd       Z*ej?                         dOdPd       Z+ej?                         dOdQd       Z,ej?                         dOdRd       Z-ej?                         dOdSd        Z.ej?                         dTdUd!       Z/ej?                         dVd"       Z0ej?                         dOdSd#       Z1ej?                         dTdUd$       Z2ej?                         dWd%       Z3ej?                         dXdYd&       Z4ej?                         	 	 dZ	 	 	 	 	 	 	 d[d'       Z5ej?                         d\d(       Z6ej?                         	 	 	 	 d]d)       Z7ej?                         dId*       Z8ej?                         dKd+       Z9ej?                         dGd,       Z:ej?                         dKd-       Z;ej?                         dGd.       Z<ej?                         d^d_d/       Z=ej?                         	 	 d`	 	 	 	 	 dad0       Z>ej?                         	 	 	 db	 	 	 	 	 	 	 dcd1       Z?ej?                         	 	 dZ	 	 	 ddd2       Z@ej?                         	 	 de	 	 	 	 	 dfd3       ZAej?                         dgd4       ZBej?                         	 	 dh	 	 	 	 	 did5       ZCej?                         	 	 	 dj	 	 	 	 	 	 	 	 	 dkd6       ZDej?                         	 	 	 	 dl	 	 	 	 	 	 	 	 	 dmd7       ZEej?                         	 	 dZ	 	 	 dnd8       ZFej?                         dgd9       ZGej?                         dgd:       ZHej?                         	 	 	 do	 	 	 	 	 	 	 	 	 dpd;       ZIej?                         dOdqd<       ZJej?                         drd=       ZKej?                         dTdsd>       ZLej?                         	 	 	 dl	 	 	 	 	 	 	 dtd?       ZMej?                         	 	 du	 	 	 dvd@       ZNej                         ZPej                  ZQedA        ZRdwdBZSdxdCZT G dD dE      ZUdxdFZVy)yu  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     z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                 x   K   t         j                  j                  t        j                         d{   S 7 w)z1List the files in the shared Jarvis Drive folder.N)anyio	to_threadrun_syncdrive
list_files     "jarvis-tools-service/mcp_server.py
drive_listr   C   )      ))%*:*:;;;;   1:8:c                d    K   t         j                  j                   fd       d{   S 7 w)zFRead a text/csv/json file from the shared folder. `file` = name or id.c                 .    t        j                         S N)r   	read_filefiles   r   <lambda>zdrive_read.<locals>.<lambda>L   s    %//$2Gr   Nr   r   r   r    s   `r   
drive_readr$   I   s%      ))*GHHHH   &0.0c                h    K   t         j                  j                   fd       d{   S 7 w)zHCreate or overwrite a text file in the shared folder from given content.c                 0    t        j                         S r   )r   
write_filecontentnames   r   r"   zdrive_write.<locals>.<lambda>R   s    %2B2B42Qr   Nr#   r+   r*   s   ``r   drive_writer-   O   s%      ))*QRRRR   '202c                d    K   t         j                  j                   fd       d{   S 7 w)zBRemove (trash) a file from the shared folder. `file` = name or id.c                 .    t        j                         S r   )r   delete_filer    s   r   r"   zdrive_delete.<locals>.<lambda>X       %2C2CD2Ir   Nr#   r    s   `r   drive_deleter3   U   s%      ))*IJJJJr%   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	campaignsr   r   r   smartlead_campaignsr7   \   s)      )))*=*=>>>>r   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 r   )r5   campaign_statscampaigns   r   r"   z*smartlead_campaign_stats.<locals>.<lambda>e   s    )2J2J82Tr   Nr#   r;   s   `r   smartlead_campaign_statsr=   b   s%      ))*TUUUUr%   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   r5   sendersr   r   r   smartlead_sendersr@   h   s)      )))*;*;<<<<r   c                d    K   t         j                  j                   fd       d{   S 7 w)z0Create a new (drafted) campaign; returns its id.c                 .    t        j                         S r   )r5   create_campaignr+   s   r   r"   z+smartlead_create_campaign.<locals>.<lambda>q   s    )2K2KD2Qr   Nr#   rD   s   `r   smartlead_create_campaignrE   n   s%      ))*QRRRRr%   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 r   )r5   set_sequencer<   sequence_jsons   r   r"   z(smartlead_set_sequence.<locals>.<lambda>y   s    	&&x?r   Nr#   rI   s   ``r   smartlead_set_sequencerK   t   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 r   )r5   	add_leadsr<   
leads_jsons   r   r"   z%smartlead_add_leads.<locals>.<lambda>   s    	##Hj9r   Nr#   rO   s   ``r   smartlead_add_leadsrQ   |   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 r   )r5   campaign_control)actionr<   s   r   r"   z,smartlead_campaign_control.<locals>.<lambda>   s    	**8V<r   Nr#   )r<   rU   s   ``r   smartlead_campaign_controlrV      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 r   )r5   raw)	body_jsonmethodpaths   r   r"   zsmartlead_api.<locals>.<lambda>   s    	fdI6r   Nr#   )r[   r\   rZ   s   ```r   smartlead_apir]      s-      ))68 8 8 8   (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 r   )	ultron_dbquery)confirmsqls   r   r"   zultron_query.<locals>.<lambda>   s    )//#w2Or   Nr#   )rd   rc   s   ``r   ultron_queryre      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 r   )ra   schematables   r   r"   zultron_schema.<locals>.<lambda>   s    )2B2B52Ir   Nr#   ri   s   `r   ultron_schemark      %      ))*IJJJJr%   c                p    K   t         j                  j                   fd       d{   S 7 w)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                 4    t        j                         S r   )emailboxsendbodyccsubjecttos   r   r"   zemail_send.<locals>.<lambda>   s    b'44r   Nr#   ru   rt   rr   rs   s   ````r   
email_sendrw      s-      ))46 6 6 6   )646c                h    K   t         j                  j                   fd       d{   S 7 w)zSearch the business inbox by phrase (sender+subject; empty = latest).
    Returns a list with a UID per message for email_read.c                 0    t        j                         S r   )ro   searchlimitrb   s   r   r"   zemail_search.<locals>.<lambda>   s    (//%2Or   Nr#   rb   r}   s   ``r   email_searchr      s%      ))*OPPPPr.   c                d    K   t         j                  j                   fd       d{   S 7 w)z<Read the full body of one business-inbox message by its UID.c                 .    t        j                         S r   )ro   readuids   r   r"   zemail_read.<locals>.<lambda>   s    (--2Dr   Nr#   r   s   `r   
email_readr      %      ))*DEEEEr%   c                p    K   t         j                  j                   fd       d{   S 7 w)zHSend an email from Ahmed's WORK Gmail (alrugaib). to/cc comma-separated.c                 4    t        j                         S r   )
gworkspace
gmail_sendrq   s   r   r"   zgmail_send.<locals>.<lambda>   s    
%%b'4<r   Nr#   rv   s   ````r   r   r      s-      ))<> > > >rx   c                h    K   t         j                  j                   fd       d{   S 7 w)zuSearch the WORK Gmail inbox (Gmail query syntax; empty = latest). Returns a
    message id per result for gmail_read.c                 0    t        j                         S r   )r   gmail_searchr|   s   r   r"   zgmail_search.<locals>.<lambda>   s    
''u5r   Nr#   r~   s   ``r   r   r      s-      ))57 7 7 7r.   c                d    K   t         j                  j                   fd       d{   S 7 w)z7Read the full body of one WORK Gmail message by its id.c                 .    t        j                         S r   )r   
gmail_read
message_ids   r   r"   zgmail_read.<locals>.<lambda>   s    *2G2G
2Sr   Nr#   r   s   `r   r   r      s%      ))*STTTTr%   c                d    K   t         j                  j                   fd       d{   S 7 w)zBList Ahmed's WORK calendar events for the next N days (default 7).c                 .    t        j                         S r   )r   calendar_listdayss   r   r"   zcalendar_list.<locals>.<lambda>   s    *2J2J42Pr   Nr#   r   s   `r   r   r      s%      ))*PQQQQr%   c                x    K   t         j                  j                   fd       d{   S 7 w)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                 8    t        j                         S r   )r   calendar_create_event)add_meet	attendeesdescriptionendstarttitles   r   r"   z'calendar_create_event.<locals>.<lambda>   s    
005#y+xAr   Nr#   )r   r   r   r   r   r   s   ``````r   r   r      s:      ))	A 	AB B B B   +:8:c                h    K   t         j                  j                   fd       d{   S 7 w)zORead a range from a Google Sheet (work account). a1_range e.g. 'Sheet1!A1:D20'.c                 0    t        j                         S r   )r   sheets_read)a1_rangespreadsheet_ids   r   r"   zsheets_read.<locals>.<lambda>   s    
&&~x@r   Nr#   )r   r   s   ``r   r   r      s1      ))@B B B Br.   c                l    K   t         j                  j                   fd       d{   S 7 w)zoWrite to a Google Sheet range (work account). values_json = JSON array of
    rows, e.g. [["a","b"],["c","d"]].c                 2    t        j                         S r   )r   sheets_write)r   r   values_jsons   r   r"   zsheets_write.<locals>.<lambda>   s    
''+Nr   Nr#   )r   r   r   s   ```r   r   r      s1     
 ))NP P P Pr^   c                h    K   t         j                  j                   fd       d{   S 7 w)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                 0    t        j                         S r   )storagewriter)   s   r   r"   zstorage_write.<locals>.<lambda>   s    '--g2Nr   Nr#   r,   s   ``r   storage_writer      s%     
 ))*NOOOOr.   c                d    K   t         j                  j                   fd       d{   S 7 w)z?Read a file back from the shared writable storage by name/path.c                 .    t        j                         S r   )r   r   rD   s   r   r"   zstorage_read.<locals>.<lambda>   s    ',,t2Dr   Nr#   rD   s   `r   storage_readr      r   r%   c                 x   K   t         j                  j                  t        j                         d{   S 7 w)z.List the files in the shared writable storage.N)r   r   r   r   r   r   r   r   storage_listr      s)      ))'*<*<====r   c                d    K   t         j                  j                   fd       d{   S 7 w)z<Delete a file from the shared writable storage by name/path.c                 .    t        j                         S r   )r   deleterD   s   r   r"   z storage_delete.<locals>.<lambda>	  s    '..2Fr   Nr#   rD   s   `r   storage_deleter     s%      ))*FGGGGr%   c                 x   K   t         j                  j                  t        j                         d{   S 7 w)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                h    K   t         j                  j                   fd       d{   S 7 w)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                 0    t        j                         S r   )r   projects)archivedrb   s   r   r"   z asana_projects.<locals>.<lambda>  s    %..2Qr   Nr#   )rb   r   s   ``r   asana_projectsr     s%      ))*QRRRRr.   c                t    K   t         j                  j                   fd       d{   S 7 w)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                 6    t        j                         S r   )r   project_create)due_onr+   notesprivacyteams   r   r"   z&asana_project_create.<locals>.<lambda>#  s    $$T4Hr   Nr#   )r+   r   r   r   r   s   `````r   asana_project_creater     s1      ))HJ J J J   *868c                x    K   t         j                  j                   fd       d{   S 7 w)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                 8    t        j                         S r   )r   project_update)archiver   r+   r   ownerprojects   r   r"   z&asana_project_update.<locals>.<lambda>/  s    $$WdE65'Rr   Nr#   )r   r+   r   r   r   r   s   ``````r   asana_project_updater   &  s4      ))RRT T T Tr   c                p    K   t         j                  j                   fd       d{   S 7 w)aZ  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                 4    t        j                         S r   )r   project_members)access_leveladdr   removes   r   r"   z'asana_project_members.<locals>.<lambda>;  s    %%gsFLIr   Nr#   )r   r   r   r   s   ````r   asana_project_membersr   2  s1      ))IK K K Krx   c                p    K   t         j                  j                   fd       d{   S 7 w)zdPost a project status update. status_type = on_track | at_risk | off_track |
    on_hold | complete.c                 4    t        j                         S r   )r   project_status)r   status_typetextr   s   r   r"   z&asana_project_status.<locals>.<lambda>E  s    $$WdKGr   Nr#   )r   r   r   r   s   ````r   asana_project_statusr   >  s1      ))GI I I Irx   c                d    K   t         j                  j                   fd       d{   S 7 w)zFULL detail of ONE task (`task` = name or id): notes, assignee, due date,
    project, section, tags, followers, subtasks and the latest comments.c                 .    t        j                         S r   )r   taskr   s   r   r"   zasana_task.<locals>.<lambda>L  s    %**T2Br   Nr#   r   s   `r   
asana_taskr   H  s%      ))*BCCCCr%   c                t    K   t         j                  j                   fd       d{   S 7 w)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                 6    t        j                         S r   )r   tasks)	completedr}   miner   sections   r   r"   zasana_tasks.<locals>.<lambda>V  s    GT7IuEr   Nr#   )r   r   r   r   r}   s   `````r   asana_tasksr   O  s1      ))EG G G Gr   c           	     |    K   t         j                  j                   fd       d{   S 7 w)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            	     :    t        j                         S r   )r   task_create)assigneer   r+   r   parentr   r   s   r   r"   z#asana_task_create.<locals>.<lambda>a  s    !!$w&")63r   Nr#   )r+   r   r   r   r   r   r   s   ```````r   asana_task_creater   Y  s4      ))	3 	34 4 4 4   ,<:<c           	     |    K   t         j                  j                   fd       d{   S 7 w)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            	     :    t        j                         S r   )r   task_update)r   completer   r+   r   start_onr   s   r   r"   z#asana_task_update.<locals>.<lambda>n  s    !!$eVXx"*,r   Nr#   )r   r+   r   r   r   r   r   s   ```````r   asana_task_updater   e  s4      ))	, 	,- - - -r   c                p    K   t         j                  j                   fd       d{   S 7 w)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                 4    t        j                         S r   )r   	task_move)r   remove_fromr   r   s   r   r"   z!asana_task_move.<locals>.<lambda>y  s    gwDr   Nr#   )r   r   r   r   s   ````r   asana_task_mover   r  s1      ))DF F F Frx   c                d    K   t         j                  j                   fd       d{   S 7 w)z1Mark an Asana task done. `task` = its name or id.c                 .    t        j                         S r   )r   task_completer   s   r   r"   z%asana_task_complete.<locals>.<lambda>  s    %2E2Ed2Kr   Nr#   r   s   `r   asana_task_completer   |  s%      ))*KLLLLr%   c                d    K   t         j                  j                   fd       d{   S 7 w)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                 .    t        j                         S r   )r   task_deleter   s   r   r"   z#asana_task_delete.<locals>.<lambda>  r2   r   Nr#   r   s   `r   asana_task_deleter    rl   r%   c                x    K   t         j                  j                   fd       d{   S 7 w)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                 8    t        j                         S r   )r   task_people)add_followersadd_tagr   remove_followers
remove_tagr   s   r   r"   z#asana_task_people.<locals>.<lambda>  s    !!$-AQ"):7r   Nr#   )r   r   r  r
  r	  r  s   ``````r   asana_task_peopler    s4      ))	7 	78 8 8 8r   c                h    K   t         j                  j                   fd       d{   S 7 w)zNList a task's subtasks, or create them: `add` = comma-separated subtask names.c                 0    t        j                         S r   )r   subtasks)r   r   s   r   r"   z asana_subtasks.<locals>.<lambda>  s    %..s2Kr   Nr#   )r   r   s   ``r   asana_subtasksr    %      ))*KLLLLr.   c                h    K   t         j                  j                   fd       d{   S 7 w)uO   Post a comment on an Asana task (`task` = name or id) — e.g. a status update.c                 0    t        j                         S r   )r   commentr   r   s   r   r"   zasana_comment.<locals>.<lambda>  s    %--d2Kr   Nr#   r  s   ``r   asana_commentr    r  r.   c                h    K   t         j                  j                   fd       d{   S 7 w)zhREAD a task's comment thread (system events skipped). `limit` = how many of
    the latest (default 10).c                 0    t        j                         S r   )r   comments)r}   r   s   r   r"   z asana_comments.<locals>.<lambda>  s    %..u2Mr   Nr#   )r   r}   s   ``r   asana_commentsr    s%      ))*MNNNNr.   c                x    K   t         j                  j                   fd       d{   S 7 w)uB  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                 8    t        j                         S r   )r   r{   )r   r   	due_after
due_beforer   rb   s   r   r"   zasana_search.<locals>.<lambda>  s    UGXz9&(r   Nr#   )rb   r   r   r  r  r   s   ``````r   asana_searchr    s4      ))	( 	() ) ) )r   c                p    K   t         j                  j                   fd       d{   S 7 w)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                 4    t        j                         S r   )r   rY   )rZ   r[   params_jsonr\   s   r   r"   zasana_api.<locals>.<lambda>  s    		&$	;?r   Nr#   )r[   r\   rZ   r"  s   ````r   	asana_apir#    s1     ( ))?A A A Arx   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runr   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headersauthorizationr	   )API_KEYhmaccompare_digest
PATH_TOKENgetdecodelower_BEARER)scopetokenkvr)  s        r   _authorizedr7    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)typestatusr)  zhttp.response.bodys   {"error":"unauthorized"})r9  rr   r   )rp   s    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 r   )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)Nr9  httpr4  path_params)r;  r?  r/  r7  r%  handle_request)r@  r3  receiverp   r4  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?  str)__name__
__module____qualname____doc__rA  rG  r   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}r4  N)routerroutesappendr   r=  )apps    r   mountrS    sH    JJU65?;<JJU>5>BCr   )returnrH  )r!   rH  rT  rH  )r+   rH  r*   rH  rT  rH  )r<   rH  rT  rH  )r+   rH  rT  rH  )r<   rH  rJ   rH  rT  rH  )r<   rH  rP   rH  rT  rH  )r<   rH  rU   rH  rT  rH  )r	   )r[   rH  r\   rH  rZ   rH  rT  rH  )rd   rH  rc   rH  rT  rH  )rj   rH  rT  rH  )
ru   rH  rt   rH  rr   rH  rs   rH  rT  rH  )
   )rb   rH  r}   intrT  rH  )r   rH  rT  rH  )r   rH  rT  rH  )   )r   rV  rT  rH  )r	   r	   r	   )r   rH  r   rH  r   rH  r   rH  r   rH  r   rH  rT  rH  )r   rH  r   rH  rT  rH  )r   rH  r   rH  r   rH  rT  rH  )r	   F)rb   rH  r   boolrT  rH  )r	   r	   r	   r	   )r+   rH  r   rH  r   rH  r   rH  r   rH  rT  rH  )r	   r	   r	   r	   N)r   rH  r+   rH  r   rH  r   rH  r   rH  r   bool | NonerT  rH  )
r   rH  r   rH  r   rH  r   rH  rT  rH  )on_trackr	   )
r   rH  r   rH  r   rH  r   rH  rT  rH  )r   rH  rT  rH  )r	   Tr	   F2   )r   rH  r   rX  r   rH  r   rX  r}   rV  rT  rH  )r	   r	   mer	   r	   r	   )r+   rH  r   rH  r   rH  r   rH  r   rH  r   rH  r   rH  rT  rH  )r	   r	   r	   r	   r	   N)r   rH  r+   rH  r   rH  r   rH  r   rH  r   rH  r   rY  rT  rH  )
r   rH  r   rH  r   rH  r   rH  rT  rH  )r	   r	   r	   r	   r	   )r   rH  r   rH  r  rH  r
  rH  r	  rH  r  rH  rT  rH  )r   rH  r   rH  rT  rH  )r   rH  r   rH  rT  rH  )r   rH  r}   rV  rT  rH  )rb   rH  r   rH  r   rH  r  rH  r  rH  r   rY  rT  rH  )r	   r	   )
r[   rH  r\   rH  rZ   rH  r"  rH  rT  rH  )r4  z
str | NonerT  rX  )rT  None)WrL  
__future__r   hashlibr,  os
contextlibr   r   mcp.server.fastmcpr   mcp.server.transport_securityr   starlette.routingr   r   r   ro   r   r5   r   ra   environr/  r+  r2  sha256encode	hexdigestr.  mcptoolr   r$   r-   r3   r7   r=   r@   rE   rK   rQ   rV   r]   re   rk   rw   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r  r  r#  streamable_http_app_r%  r'  r7  r;  r=  rS  r   r   r   <module>rm     s   #   	 *  & C #       
**..+R
0G9
 CJ^W^^GNN,-779#2>r
 1(-/	Q2 < <
 I I
 S S
 K K ? ?
 V V
 = =
 S S
 A A ; ; > > 8 8 Q Q K K 6 6 Q Q F F > > 7 7 U U
 R R
 HJ02B+.BBEB*-B7:B B B B P$'P,/P P P P F F
 > >
 H H < <
 S S GI@BJ'*J:=JGJJ J JL>@6:T'*T8;T(3T?BT T KM46K.1K;>K K 2<,.I,/I&)I36I I D D KM<>G!%G69GCFG G GI@B=?4&)4:=4%(47:4DG4 4 DF>@,.48	-$'	-8;	-&)	- '2	- >A	- 	- GI-/F'*F47F F M M
 K K 79MOAC8+.8GJ8%(8;>8HK8 8 M M
 M M
 O O KM>@04	)#&	)8;	)"-	)9<	) 	) =?')A!$A.1A A. %%  JTC C$Dr   