Vai al contenuto

Connettore MCP

Il connettore MCP (Model Context Protocol) collega GesTask a Claude: claude.ai, l'app mobile, Claude Desktop e Claude Code. Claude riceve un tool per ogni operazione dell'API HTTP e agisce a nome dell'utente che ha dato il consenso, con i suoi permessi.

Principio: l'MCP è l'API. Un tool non contiene logica propria: esegue l'action v1 corrispondente, quindi permessi, validazione, notifiche, webhook e registro operazioni sono gli stessi delle chiamate HTTP.

Pezzi

Cosa Dove
Server MCP (POST /mcp) api/controllers/McpController.php
Catalogo dei tool, generato api/mcp/tools.php
Prompt, scritti a mano api/mcp/prompts.php
Approvazioni delle operazioni pesanti common/models/McpApproval.php, frontend/controllers/McpApprovalController.php
Client ID Metadata Documents common/services/OAuthClientMetadata.php
Plugin per Claude Code claude-plugin/gestask/, marketplace in .claude-plugin/marketplace.json
Generatore del catalogo api/components/McpCatalog.php, comando php yii mcp/tools
Endpoint OAuth pubblici api/controllers/OauthController.php
Pagina di consenso frontend/controllers/OauthController.php, viste in frontend/views/oauth/
Server di autorizzazione common/services/OAuthServer.php, modelli OAuthClient e OAuthToken
App collegate e revoca Profilo › App collegate (ProfileController::actionRevokeApp)

Due connettori

Indirizzo Nome Tool Per chi
POST /mcp GesTask il lavoro di tutti i giorni (147 tool), più prompt e risorse ogni utente
POST /mcp/admin GesTask amministrazione i soli strumenti del pannello (31 tool, tag Amministrazione) chi ha admin, superAdmin o admin.panel; gli altri ricevono 403

Sono lo stesso McpController: actionAdmin() verifica l'accesso al pannello, accende $admin e prosegue come actionIndex(). Ognuno elenca ed esegue solo i propri tool, quindi un amministratore che usa Claude per le sue task non si porta dietro gli strumenti del pannello. Il connettore di amministrazione ha istruzioni proprie (conferme esplicite, nome esatto per il cestino) e non offre prompt né risorse. Il token OAuth è lo stesso; i metadati della risorsa protetta sono due (/.well-known/oauth-protected-resource/mcp e …/mcp/admin).

La pagina «Collega Claude»

frontend/controllers/ClaudeController (indirizzo /claude, voce nel menu laterale) è la guida per chi non è tecnico:

  • indirizzo da copiare, per il connettore utente e, a chi accede al pannello, per quello di amministrazione (McpConnector::url(): da MCP_API_URL, oppure ricavato dall'indirizzo del frontend quando le due app stanno affiancate come in XAMPP; altrimenti un messaggio che rimanda all'amministratore);
  • passi per claude.ai e l'app per computer, il telefono e Claude Code;
  • esempi di richieste, i flussi pronti (letti da api/mcp/prompts.php) e come allegare una task;
  • l'elenco completo di ciò che Claude può fare, per area, letto da api/mcp/tools.php (McpConnector::capabilities()), con l'indicazione di cosa legge, modifica o elimina: la pagina resta allineata al catalogo senza doverla aggiornare a mano;
  • sicurezza, revoca e problemi frequenti.

La guida in-app (argomento claude), il tour di benvenuto e la sezione «App collegate» del profilo rimandano a questa pagina.

Il server MCP

Trasporto Streamable HTTP senza stato: ogni POST /mcp porta un messaggio JSON-RPC e riceve la risposta in JSON. Non ci sono sessioni né flusso SSE: GET e DELETE /mcp rispondono 405.

Metodo Risposta
initialize versione del protocollo (2025-06-18 o 2025-03-26), capabilities.tools, serverInfo con la versione dal changelog, instructions sul dominio
notifications/* 202 senza corpo
ping oggetto vuoto
tools/list i tool del connettore: quelli utente su /mcp, quelli col tag Amministrazione su /mcp/admin
tools/call esecuzione del tool
prompts/list, prompts/get i flussi di lavoro pronti di api/mcp/prompts.php
resources/list, resources/templates/list, resources/read task e allegati da allegare alla conversazione
altro errore -32601

Gli errori dell'API (403, 404, 422 con i fields, regole di dominio) tornano come risultato del tool con isError: true e lo stesso JSON delle risposte HTTP: Claude li legge e corregge la richiesta. Gli errori JSON-RPC sono solo quelli di protocollo.

Esecuzione di un tool

McpController::execute() imposta sulla request corrente parametri di query, corpo e verbo HTTP del tool ($_SERVER['REQUEST_METHOD'], perché alcune action scelgono in base al verbo, es. task/set-tag), esegue Yii::$app->runAction('v1/<route>') e rimette tutto com'era. Request e response restano gli stessi oggetti: sostituirli con set() cambierebbe anche la definizione del componente.

L'autenticazione passa di nuovo dall'authenticator della v1, con lo stesso header Authorization; il limite di richieste lo conta ogni esecuzione di un tool, non il messaggio MCP.

  • File in ingresso (allegati, import CSV, avatar): il tool riceve filename e content_base64. Il contenuto si scrive in un file temporaneo esposto in $_FILES['file'] con tmp_resource, così UploadedFile::saveAs() lo copia invece di usare move_uploaded_file.
  • File in uscita (download di un allegato, export CSV): le immagini tornano come contenuto image, i testi e i CSV come text, il resto come resource in base64.
  • Allegati oltre 5 MB non entrano nella conversazione: il tool restituisce download_url, un link GET /mcp/files/<token> valido 15 minuti. Il token è casuale e sta solo nella cache, legato all'utente e all'allegato; aprendo il link il permesso board.view si ricontrolla con quell'utente. Per altri file grandi (un export CSV enorme) escono solo i metadati.
  • Riferimenti per nome. Gli argomenti uuid che indicano una task, una bacheca, un team o una persona accettano anche il nome con cui li chiamano le persone: la sigla della task (ABC-12), la sigla della bacheca (ABC), il nome del team, lo username; personale indica la bacheca o il team personale dell'utente. McpCatalog::refKind() li riconosce dal percorso ({uuid} sotto /tasks, /boards, /teams, /users) o dal nome dell'argomento (taskUuid, board, team, assignees, presenter…) e li mette in refs; McpController::refUuid() li risolve prima di chiamare l'action, cercando task, bacheche e team solo fra quelli visibili all'utente. Due team con lo stesso nome danno un errore che chiede l'uuid.
  • Date leggibili. L'API dà le scadenze come 2026-09-12 00:00:00 e creazione e modifica come timestamp unix. Nella risposta dei tool ogni *_at intero diventa una data nello stesso formato, nel fuso dell'applicazione, così Claude non deve convertirla. L'API resta com'è.
  • Esito senza corpo. Un'action che risponde 204 diventa {"done": true, "message": …}; per le eliminazioni il messaggio ricorda come ripristinare dal cestino.
  • Elenchi compatti. Gli elenchi di task (list_my_tasks, list_board_tasks, list_subtasks, search) escono di default con i soli campi essenziali (uuid, key, title, status, priority, due_at, list, board, assignees, is_blocked, parent), tramite il parametro fields dell'API; fields=all li restituisce tutti. Il dettaglio sta in get_task.
  • Registro operazioni. McpController segna la richiesta (params['mcpRequest']): le operazioni dei tool hanno origine mcp («Claude (MCP)») e l'autore diventa «mario via Claude» con OAuth (il nome dell'app registrata) o «mario via connettore MCP» con il token personale.

Prompt

I prompt sono flussi di lavoro pronti che l'utente sceglie dal client (in Claude compaiono fra i comandi). Stanno in api/mcp/prompts.php, scritti a mano: nome, descrizione, argomenti e un testo che dice a Claude quali tool usare e in che ordine, chiedendo conferma prima di modificare.

Prompt Argomenti Cosa fa
lavora-sulla-task task (sigla o uuid) carica la task con tutto il contesto, propone un piano, la porta avanti (stato, checklist, sottotask) e alla fine commenta, registra le ore e la completa
pianifica-settimana — ritardi, scadenze della settimana, riunioni e assenze, e un piano giorno per giorno
riepilogo-giornata — notifiche non lette, task di oggi e in ritardo, riunioni del giorno
prepara-riunione team punti da discutere dalle bacheche del team e ordine del giorno della riunione; ogni punto su una task lo presenta il suo assegnatario
chiudi-giornata — chiede su cosa si è lavorato, registra le ore non contate dal timer, spunta la checklist e commenta l'avanzamento
riordina-backlog bacheca (sigla) propone priorità, date e stime per le task aperte senza scadenza, e segnala quelle senza descrizione, senza assegnatario o doppie

Il commento di riepilogo di lavora-sulla-task ha un modello fisso, in Markdown: Cosa è stato fatto, Come funziona, Cosa è cambiato, Sezioni toccate, Cosa non funziona più. È scritto per chi usa GesTask: niente hash di commit né percorsi di file. Non indica una versione e non aspetta un rilascio, perché il prompt lo usa chiunque, non solo chi sviluppa GesTask. Le istruzioni del server ripetono le cinque voci, così valgono anche quando il prompt non è stato richiamato.

Il timer conta solo l'orario lavorativo (params['workingHours'], di norma lun–ven 9–18), quindi una task lavorata fuori orario resta a zero minuti. Per questo lavora-sulla-task, appena spostata la task in corso, controlla l'ora: fuori orario lo segnala all'utente e, se conferma, annota l'ora di inizio e a fine lavoro registra i minuti con create_task_worklog. Anche questa regola è ripetuta nelle istruzioni del server. L'orario è scritto nel testo del prompt: se cambia workingHours, va aggiornato anche lì.

Risorse

Le risorse si allegano alla conversazione (in Claude con @) invece di farle leggere con un tool:

  • resources/list propone le task aperte assegnate all'utente (al massimo 50);
  • gestask://task/{task} (sigla o uuid): la task in JSON con sottotask, checklist, commenti, allegati e relazioni (get_task con expand);
  • gestask://attachment/{uuid}: il file allegato, come testo o in base64; oltre 5 MB il link temporaneo.

Una risorsa inesistente o non accessibile risponde con l'errore -32002 e il messaggio dell'API.

Descrizioni per Claude

Il campo x-mcp-description di un'operazione dello spec è il testo che Claude legge per primo: quando usare il tool e cosa chiamare dopo. Va scritto per le operazioni più usate; per le altre bastano summary e description.

Catalogo dei tool

Il catalogo nasce dallo spec api/web/openapi-v1.yaml:

  • un tool per operazione, con nome dall'operationId in snake_case (listMyTasks → list_my_tasks);
  • descrizione da x-mcp-description (se c'è), summary e description;
  • argomenti dai parametri di percorso e query e dal corpo, con i $ref risolti; un {uuid} senza descrizione dice in quale percorso sta;
  • annotations: GET in sola lettura, DELETE distruttivo, PUT idempotente;
  • action dalla rotta con lo stesso verbo e percorso in routes-v1.php.

Il generatore fallisce se una rotta non ha l'operazione nello spec o viceversa. McpCest::everyRouteIsATool verifica che api/mcp/tools.php sia allineato.

Aggiungere una capacità

  1. Action v1 sottile sopra un servizio di common/services.
  2. Rotta in api/config/routes-v1.php e controller in ApiMasterToken::MODULES, se nuovo.
  3. Operazione nello spec, con operationId, summary, description (il permesso richiesto) e gli schemi. Le operazioni del pannello vanno sotto il tag Amministrazione.
  4. php yii mcp/tools, poi i test.

mcp/tools usa symfony/yaml, presente solo fra le dipendenze di sviluppo: a runtime il catalogo è un file PHP.

OAuth 2.1

claude.ai e Claude Desktop accettano solo connettori con OAuth. Il server supporta client pubblici con codice di autorizzazione e PKCE S256.

  1. Claude chiama POST /mcp senza token e riceve 401 con WWW-Authenticate: Bearer resource_metadata="<api>/.well-known/oauth-protected-resource/mcp".
  2. Legge i metadati della risorsa e quelli del server di autorizzazione (/.well-known/oauth-authorization-server, sull'app api).
  3. Si registra con POST /oauth/register.
  4. Apre /oauth/authorize sul frontend: l'utente accede (con la verifica in due passaggi, se attiva) e acconsente.
  5. Il frontend rimanda a Claude con code e state.
  6. Claude scambia il codice con POST /oauth/token, inviando il code_verifier, e riceve access token (1 ora) e refresh token (30 giorni).

  7. Registrazione aperta (RFC 7591): non dà accesso, che arriva solo dal consenso. Sono ammessi solo indirizzi di ritorno https: o http verso localhost; al massimo 20 registrazioni l'ora per indirizzo IP.

  8. Consenso: dietro il login normale, compresa la verifica in due passaggi; dopo l'accesso si torna alla pagina richiesta (goBack). Il modulo invia alla stessa query string, che si rivalida: nessun parametro passa da campi nascosti.
  9. Token: gto_ (access, 1 ora) e gtr_ (refresh, 30 giorni, ruota a ogni uso). Si salvano solo le impronte sha256. Codice e refresh valgono una volta: il rinnovo aggiorna la riga solo se il segreto usato è ancora quello in tabella, così due richieste concorrenti non ottengono entrambe un token.
  10. Un'autorizzazione è una riga di oauth_token, dal codice alla revoca.
  11. Autenticazione: MasterTokenAuth riconosce il prefisso gto_ e imposta l'utente. Un utente disattivato perde subito l'accesso.
  12. Revoca: da Profilo › App collegate, o POST /oauth/revoke (RFC 7009).
  13. Riuso del refresh: chi ripresenta un refresh già sostituito (previous_refresh_hash) ne possiede una copia; l'autorizzazione intera si revoca e l'utente deve ricollegare l'app.
  14. Client ID Metadata Documents: un client_id che è un indirizzo https (con percorso) non va registrato. OAuthClientMetadata scarica il documento con OutboundUrlGuard (niente reti interne né redirect, 5 secondi, 10 KB), controlla che dichiari lo stesso client_id, crea o aggiorna il client (oauth_client.metadata_url) e tiene il documento in cache un'ora. I metadati annunciano client_id_metadata_document_supported. La registrazione aperta resta per i client che non li usano.

Cosa concede l'utente (scope)

Nel consenso l'utente sceglie, per ogni app, fra tre livelli (OAuthToken::SCOPES); lo scope chiesto dal client è solo la scelta proposta:

Scope Cosa permette
read (Solo lettura) ogni GET, tranne l'amministrazione
work (Lavoro quotidiano) ogni GET e le scritture dei controller task, task-part, leave, meeting, me, stats, info, search
full (Tutto) tutto, compresa l'amministrazione

OAuthToken::allows(route, verbo) decide. Il server MCP nasconde da tools/list e da tools/call i tool non concessi, e MasterTokenAuth rifiuta con 403 le action fuori scope anche nelle sotto-richieste. Il connettore di amministrazione vuole full, e il consenso per …/mcp/admin non offre altra scelta. Le autorizzazioni nate prima degli scope valgono full.

Token legato al connettore

Il client indica nel consenso il connettore per cui chiede il token (resource, RFC 8707): si salva in oauth_token.resource e vale da lì in poi.

  • il token di /mcp non apre /mcp/admin, e viceversa;
  • da solo non vale sull'API v1: le sotto-richieste dei tool passano perché il server MCP le ha segnate come sue (params['mcpRequest']);
  • al rinnovo, una resource diversa da quella concessa è un invalid_target.

Un token senza resource (client che non la manda) vale come prima.

Operazioni da approvare

Le operazioni pesanti non le decide Claude:

  • quali: tutte le scritture del connettore di amministrazione e, nel connettore utente, empty_trash, purge_trash_item, delete_board_list, remove_team_member, delete_team_role, replace_team_permissions, delete_webhook (McpController::APPROVAL_TOOLS);
  • come: alla prima chiamata il tool non parte. Nasce una richiesta in mcp_approval (utente, tool, impronta degli argomenti, riepilogo leggibile, dieci minuti di validità) e Claude riceve approval_required con il link …/mcp-approval/view/<uuid>. L'utente lo apre in GesTask, dove è entrato con le sue credenziali, e approva o rifiuta (McpApprovalController);
  • dopo: la stessa chiamata, con gli stessi argomenti, trova l'approvazione e la consuma in modo atomico. Ogni approvazione vale una volta sola.

L'elicitation di MCP (il server che chiede conferma al client a metà di una chiamata) richiederebbe sessioni e uno stream SSE aperti, che il server senza stato non ha. Il link funziona con ogni client e mette la decisione nel posto in cui l'utente è autenticato, non nella conversazione che Claude potrebbe compilare da solo.

claude.ai e Claude Desktop mettono l'intero elenco dei tool nel contesto di ogni conversazione, quindi il catalogo è tenuto leggero (circa 92 KB per i 144 tool dell'area utente):

  • niente outputSchema né structuredContent: Claude legge comunque il testo JSON, e gli schemi di uscita pesavano quanto quelli di ingresso;
  • niente example negli schemi di ingresso (McpCatalog::compact());
  • la descrizione usa x-mcp-description al posto del summary, che è già il titolo del tool, e annotations non ripete il titolo.

Prima di aggiungere testo a molte operazioni dello spec conviene misurare l'elenco con tools/list.

Prestazioni

Il server MCP autentica una volta per messaggio: le sotto-richieste dei tool trovano in params['mcpAuth'] l'identità già verificata (stesso token) e non tornano al database. I limiti del token (scope, operazioni del token master) si controllano comunque, rotta per rotta. Lo stato del messaggio si azzera alla fine della richiesta (McpController::runAction).

Plugin per Claude Code

claude-plugin/gestask/ è il plugin, con il marketplace in .claude-plugin/marketplace.json alla radice. Le istruzioni di installazione arriveranno quando il plugin avrà il suo repository definitivo.

  • .mcp.json collega il server gestask all'indirizzo in GESTASK_MCP_URL;
  • commands/ contiene un comando per prompt (/gestask:lavora-sulla-task ABC-12, …), generato da api/mcp/prompts.php con php yii mcp/plugin. McpCest::thePluginCommandsFollowThePrompts fallisce se non è allineato;
  • dopo un cambio si alza version in plugin.json. claude plugin validate ./claude-plugin/gestask controlla il manifest.

Configurazione

Parametro Variabile Uso
params['mcp']['frontendUrl'] MCP_FRONTEND_URL indirizzo pubblico del frontend, per la pagina di consenso e i link del feed iCal. Obbligatorio
params['mcp']['apiUrl'] MCP_API_URL indirizzo pubblico dell'API (es. dietro un proxy); vuoto = quello della richiesta
params['mcp']['accessTokenTtl'] durata dell'access token, default 3600 s
params['mcp']['refreshTokenTtl'] durata del refresh token, default 30 giorni

Le variabili arrivano dall'ambiente: in Docker da .env, in Apache con SetEnv. Gli indirizzi vanno codificati (%20 per gli spazi).

La scoperta OAuth cerca /.well-known/... alla radice dell'host: con l'app in una sottocartella (XAMPP) non funziona. In quel caso si collega il connettore con il token personale:

claude mcp add --transport http gestask "<api>/mcp" \
  --header "Authorization: Bearer <token>"

claude.ai e Claude Desktop raggiungono il server dai server di Anthropic: serve un indirizzo HTTPS pubblico.

Funzioni arrivate nell'API con il connettore

Per dare a Claude tutto ciò che un utente può fare, l'API ha ricevuto le funzioni del frontend e del pannello che mancavano. La logica è stata spostata in common e le pagine usano gli stessi servizi:

Area Endpoint Servizio
Le mie task me/tasks (filtri), me/tasks/stats, me/tasks/order, me/tasks/{uuid}/today MyTasks
Dashboard me/dashboard, users/{uuid}/dashboard, teams/{uuid}/dashboard, dashboard/global DashboardStats
Ore timesheet TimesheetService::forViewer()
Modelli board-templates/{uuid}, boards/{uuid}/template, predefinito BoardTemplateService
Reazioni comments/{uuid}/reactions TaskParts::react()
Ordine bacheche teams/{uuid}/board-order BoardSettings::orderBoards()
Profilo me/calendar-feed, me/avatar User, AvatarForm
Novità e guida releases, guide ReleaseNotes, Guide
Amministrazione admin/* AdminScope, AdminStats, UserAdministration, GlobalRoleManager, UserSearch, AuditLogSearch

Restano fuori, di proposito, le credenziali: token personale e master, password e verifica in due passaggi (propri e altrui), log grezzi di sistema.