Γj@dZddlmZddlZddlZddlZddlZddlZddlm Z ddl Z ddl m Z ddl mZmZmZmZmZmZddlmZmZmZddlmZdd lmZdd lmZmZdd lm Z m!Z!e e"jGjHZ%e e%d z e%jHjHd z dz dz dz Z&hdZ'dZ(hdZ)GddeZ*ejVjYddZ-ejVdZ.ejVdZ/dZ0e%dz Z1GddeZ2d.dZ3e3d Z4d/d!Z5d0d"Z6e4jod1d#Z8e4joe(fd2d$Z9e:d%k(rddl;Z;d&e;jxvrlddl=Z=e3d' Z>e>joe8e>joe9e>jegd(dg)*Z@e=je@d+d,-ye4jyy)3userver.py — MCP per query sui dati di business ICR (Emesso/Anagrafica/Diritti...). Nato da una discussione con Mauro (09/08/2026): Claude.ai dovrebbe poter interrogare i dati di business direttamente, con query generate da lui stesso — questo server resta puramente meccanico ("agnostico", stesso principio di gate-ufficio): esegue la query, non interpreta il dominio. La conoscenza di schema/business-logic non vive qui — Claude.ai la legge da archivio/Adrian/agenti/data-expert.md, già nel mirror Drive "SecondBrain", stessa fonte usata dal subagente data-expert. Autenticazione OAuth: recuperata da git (vault-secondbrain/server.py, commit 5b461f6~1, cancellato il 29/07/2026 con lo smantellamento del vecchio vault) — stesso pattern collaudato con Elon/Claude.ai, adattato al nuovo SDK (mcp 2.0.0, MCPServer al posto di FastMCP — API di auth invariata, verificato compatibile 09/08/2026). Nessuna logica di dominio recuperata insieme, solo l'infrastruttura OAuth (generica, non legata al contenuto del vault). Sicurezza query: stesse regole del Frank-relay (readonly_actions.py in PYTHON/gate-ufficio/) — solo SELECT/WITH, nessuna keyword di scrittura, nessun punto e virgola interno (niente query multiple). ) annotationsN)Path) load_dotenv) AccessTokenAuthorizationCodeAuthorizationParams OAuthAuthorizationServerProvider RefreshTokenconstruct_redirect_uri) AuthSettingsClientRegistrationOptionsRevocationOptions) MCPServer)TransportSecuritySettings)OAuthClientInformationFull OAuthToken) AnyHttpUrlAnyUrlz.envPYTHON MyICR_Suitelocal_dbparquet>CALLCOPYDROPALTERMERGEATTACHCREATEDELETEDETACHEXPORTINSERTPRAGMAUPDATEREPLACETRUNCATEi> 127.0.0.1 claude.ai claude.com localhostc$eZdZdZdfd ZxZS) TrustedClientuClient il cui redirect_uri non è fissato a una stringa esatta: viene accettato qualsiasi URI con host in TRUSTED_REDIRECT_HOSTS. Il client_secret resta obbligatorio allo scambio del token, quindi l'autenticità non dipende da questo.cN||jtvr|St| |SN)hostTRUSTED_REDIRECT_HOSTSsupervalidate_redirect_uri)self redirect_uri __class__s 8/mnt/ssd/data/Dropbox/adrian/sandbox/mcp-query/server.pyr3z#TrustedClient.validate_redirect_uriHs-  # (9(9=S(S w,\::)r5z AnyUrl | Nonereturnr)__name__ __module__ __qualname____doc__r3 __classcell__)r6s@r7r-r-CsY;;r8r- ISSUER_URLzhttps://query.privcloud.devMCP_QUERY_OAUTH_CLIENT_IDMCP_QUERY_OAUTH_CLIENT_SECRETi'zoauth_tokens.jsonceZdZdZddZddZddZddZddZddZ ddZ dd Z dd Z dd Z dd Zdd Zy)McpQueryOAuthProviderzProvider OAuth con persistenza token su disco. Client pre-registrato per Claude.ai + registrazione dinamica per Claude Code (RFC 7591).c i|_i|_i|_|jtt tt tdgddgdgdi|_y)Nz'https://claude.ai/api/mcp/auth_callbackauthorization_code refresh_tokencodeclient_secret_post) client_id client_secret redirect_uris grant_typesresponse_typestoken_endpoint_auth_method) _auth_codes_access_tokens_refresh_tokens _load_tokens CLIENT_IDr- CLIENT_SECRETr_clients)r4s r7__init__zMcpQueryOAuthProvider.__init__Zs\9;688:  }#+%&OPQ1?C &x+?  @  r8ctjsy tjtj }t j }|j dijD]3\}}|j dd|kDstdi||j|<5|j dijD]\}}tdi||j|<y#t$rYywxYw)N access_tokens expires_atrrefresh_tokens) TOKENS_PATHexistsjsonloads read_texttimegetitemsrrPr rQ Exception)r4datanowtokds r7rRz"McpQueryOAuthProvider._load_tokensjs!!#  ::k3356D))+C((?B7==? @Q55q)C//:/?Q/?D'', @((#3R8>>@ >Q,8,=1,=$$S) >   sA6C)AC)) C54C5c~ |jjDcic]\}}||jc}}|jjDcic]\}}||jc}}d}tj t j|ycc}}wcc}}w#t$rYywxYw)N)rXrZ) rPrc model_dumprQr\ write_textr^dumpsrd)r4tvres r7 _save_tokensz"McpQueryOAuthProvider._save_tokensxs @D@S@S@Y@Y@[!\1!Q\\^"3!\AEAUAUA[A[A]"^A1alln#4"^D  " "4::d#3 4"]"^   s-B0B$ B0B*5.B0$ B00 B<;B<c@K|jj|Swr/)rUrb)r4rIs r7 get_clientz McpQueryOAuthProvider.get_clients}}  ++c Ktdw)Nz6Registrazione dinamica client disattivata su mcp-query)NotImplementedError)r4 client_infos r7register_clientz%McpQueryOAuthProvider.register_clients""Z[[s c K|jtk7rtd|jtjd}t ||j xsgtjdz|j|j|j|j|j|j|<tt|j||jSw)NzClient non autorizzato: iX)rGscopesrYrIcode_challenger5 redirect_uri_provided_explicitlyresource)rGstate)rIrS ValueErrorsecrets token_urlsaferryrarzr5r{r|rOr strr})r4clientparamsrGs r7 authorizezMcpQueryOAuthProvider.authorizes   y (78H8H7IJK K$$R(!2==&Byy{S(&&!00,,-3-T-T__ " &c&*=*=&>TQWQ]Q]^^sC!C#c@K|jj|Swr/)rOrb)r4rrEs r7load_authorization_codez-McpQueryOAuthProvider.load_authorization_codes ##$677rrc K|j|j=tjd}tjd}t ||j |j ttjtz|j|j|<t||j |j |j|<|jt|dt|dj!|j Sw)Nrx)tokenrIryrYr|rrIrybearer  access_token token_type expires_inrFscope)rOrGrrrrIryintraACCESS_TOKEN_TTLr|rPr rQrorjoin)r4rrErrFs r7exchange_authorization_codez1McpQueryOAuthProvider.exchange_authorization_codes   /44 5,,R0 --b1 ,7&&%,,499;)99:'00 - L)/;&&%,,/ ]+ %''((-445   sD D c@K|jj|Swr/)rQrb)r4rrFs r7load_refresh_tokenz(McpQueryOAuthProvider.load_refresh_tokens##'' 66rrc  K|j|j=tjd}tjd}t ||j |xs |j ttjtz|j|<t||j |xs |j |j|<|jt|dt|dj|xs |j Sw)Nrx)rrIryrYrrrr)rQrrrrrIryrrarrPr rorr)r4rrFryr new_refreshs r7exchange_refresh_tokenz,McpQueryOAuthProvider.exchange_refresh_tokens  !4!4 5,,R0 ++B/ ,7&&1]11499;)99: - L) -9&&1]11- [) %'%((69]%9%9:   sD DcK|jj|}|r;|jr/|jtjkr|j|=y|Swr/)rPrbrYra)r4rats r7load_access_tokenz'McpQueryOAuthProvider.load_access_tokensL  $ $U + "--BMMDIIK$?##E* sAAcK|jjt|d|d|jjt|d|d|j yw)Nr)rPpopgetattrrQro)r4rs r7 revoke_tokenz"McpQueryOAuthProvider.revoke_tokensP w >E   !?F sA A"N)r9None)rIrr9z!OAuthClientInformationFull | None)rurr9r)rrrrr9r)rrrErr9zAuthorizationCode | None)rrrErr9r)rrrFrr9zRefreshToken | None)rrrFr ryz list[str]r9r)rrr9zAccessToken | None)r:r;r<r=rVrRrorqrvrrrrrrrr[r8r7rCrCVs` , \_&808FI8 !8  0 FW  67 * $     :r8rCci}|rPt|d<tttttdt dt d|d<t d dd i|S) uwith_auth=False per i test locali via stdio (test_client.py) — l'auth OAuth ha senso solo sul transport HTTP esposto pubblicamente, e costruire il provider a vuoto ogni test rallenta inutilmente senza aggiungere nulla.auth_server_providerz/mcpF)enabledT) issuer_urlresource_server_urlclient_registration_optionsrevocation_optionsauth descriptionu|Query di sola lettura sui dati di business ICR (diritti, anagrafica prodotti, emesso/palinsesto, boxoffice, cast, IMDb) — mirror parquet sincronizzati sulla Nave. Prima di interrogare, leggi lo schema/le regole di business in archivio/Adrian/agenti/data-expert.md (mirror Drive 'SecondBrain') — questo server esegue la query così com'è, senza conoscenza propria del dominio.)z mcp-query)rCr rr?r rr) with_authkwargss r7 _build_serverrsxF)>)@%&%!*- *j\+> ?)B%(P0>  v   4    r8F)rctjd}ttj dD]A}|j }t |jdd}|jd|d|dC|S)uApre una connessione DuckDB in-memory con una view per ogni parquet disponibile — così le query usano nomi tabella semplici (es. "emesso") invece del path completo al file.z:memory: *.parquet'z''z CREATE VIEW "z!" AS SELECT * FROM read_parquet('')) duckdbconnectsorted PARQUET_DIRglobstemrreplaceexecute)conpath view_name escaped_paths r7_connectrsz .. $C{'' 45dII 4y((d3  nYK/QR^Q__abc d Jr8c~|j}|r!|jjdnd}|dvrtd|d|j j d}d|vr td|j}t D]-}t jd|zdz|s td |d |S) uStessa validazione di readonly_actions.py::execute_readonly (azione duckdb_query) — solo SELECT/WITH, niente keyword di scrittura, niente query multiple. Ritorna la query "pulita" (senza whitespace/`;` finali) o solleva ValueError.r)SELECTWITHz6Solo SELECT/WITH ammessi come inizio query (trovato: 'r;z7Query multiple non ammesse (';' interno non consentito)z\bzKeyword non ammessa: 'r)stripuppersplitr~rstrip_WRITE_KEYWORDSresearch)sql sql_stripped first_word sql_upperkws r7_validate_readonlyr s 99;L4@##%++-a0bJ++QR\Q]]_`aa&&(//4L lRSS""$I= 99URZ%' 35bT;< <= r8c<t} g}ttjdD]H}|j}|j d|dj d}|j||dJ||jS#|jwxYw)uElenca le tabelle (view) disponibili, con conteggio righe. Usalo per sapere cosa esiste — per capire COSA contiene ciascuna tabella e come interrogarla bene, leggi archivio/Adrian/agenti/data-expert.md su Drive prima di scrivere query complesse.rzSELECT COUNT(*) FROM ""r)tablerows) rrrrrrfetchoneappendclose)rtablesrrcounts r7 list_tablesr6s *C;++K89 ?D IKK"8 1 EFOOQRSTE MMIu= > ?  s A,B BcZt|}t} |j|}|jDcgc]}|d }}|j |}||Dcgc] }t |c}t ||k(d|jScc}wcc}w#|jwxYw)usEsegue una query SQL di sola lettura (SELECT/WITH) sui dati di business ICR. Le tabelle disponibili si vedono con list_tables(); nomi colonne/join/regole di business vanno letti da archivio/Adrian/agenti/data-expert.md (mirror Drive "SecondBrain") PRIMA di scrivere la query — questo tool la esegue così com'è, senza correggerla né interpretarla.r)columnsr truncated)rrrr fetchmanylistlenr) rlimit sql_cleanrrelrhcolsrrs r7query_business_datarHs#3'I *C kk)$!oo.!..}}U#&*+T!W+Te+ /, s(B BBB,B BB*__main__z--realT)zquery.privcloud.devz 10.0.0.2:8765zlocalhost:8765) allowed_hostsallowed_origins)transport_securityz10.0.0.2i=")r0port)rboolr9r)r9zduckdb.DuckDBPyConnection)rrr9r)r9z list[dict])rrrrr9dict)Br= __future__rr^osrrrapathlibrrdotenvrmcp.server.auth.providerrrrr r r mcp.server.auth.settingsr r rmcp.server.mcpserverrmcp.server.transport_securityrmcp.shared.authrrpydanticrr__file__resolveparentHERErr DEFAULT_LIMITr1r-environrbr?rSrTrr\rCrserverrrtoolrrr:sysargvuvicorn prod_serverstreamable_http_appapprunr[r8r7rs*#   `_*CB' H~&& D6M KK!M1J>J   O;.;ZZ^^L*G H JJ2 3  :; !(( W<Wt>  ' ,"/<, z388 #d3  ;' ./--8X!> ? .   Cjt4  7r8