La necessità di questo sviluppo si è palesata lavorando in un contesto fortemente regolamentato come quello dell’AI nella PA, dove è importante fornire a ciascun assistente il set minimo di dati e documenti necessari alla risposta. Questo faceva sì che ogni nuovo assistente agentico per un cliente richiedesse tool dedicati, specialmente svariate tipologie di RAG che implementassero i filtri desiderati da ciascuno.

Le alternative scartate

Ne conseguivano sviluppo e manutenzione moltiplicati e superficie di vulnerabilità crescente. L’alternativa diametralmente opposta, ovvero conferire un accesso pienamente agentico a sistemi e dati con il solo prompt a fare da confine, non regge in un contesto regolamentato su auditabilità e sicurezza. Anche l’utilizzo di MCP esterni non risolveva del tutto il problema, perché a parità di risorsa (ad esempio una collezione Weaviate) utenti diversi del sistema dovevano vedere parti diverse della documentazione, e non altre. I filtri e le tipologie di accesso alle risorse erano troppi per riuscire a tramutarli in una specifica di MCP associata al sistema da fruire.

4–5g → ½g sviluppo di un agente
~10 righe di YAML per tool
0 codice per rilasciare un tool

Primitive e composizione in YAML

Perciò, ho consolidato i tool frammentati in un unico server MCP interno (STDIO) che separa le primitive, mantenute centralmente (ricerca semantica, filtro esatto sui metadati, individuazione dei metadati, memoria conversazionale, chiamate API), dalla loro composizione in tool veri e propri, dichiarata in un file YAML: una decina di righe di configurazione per tool, senza scrivere codice. Il server consuma la configurazione e produce tool già calibrati sui confini del singolo agente.

Nel catalogo del runtime convivono tre famiglie di tool: quelli statici tipizzati in Python nel repository (l’eredità dell’approccio precedente), quelli instradati verso server MCP esterni e quelli del server MCP interno. Quando l’agente invoca un tool, il runtime lo cerca nel catalogo e capisce a chi passare la chiamata:

              agente (tool call, formato OpenAI)
                            |
                            v
                 catalogo tool del runtime
                            |  lookup: di che tipo è?
          +-----------------+-----------------+
          v                 v                 v
    tool statici       server MCP        server MCP
    (classi Python      esterni          interno (STDIO)
     nel repo)                                |
                                              v
                                     binding dal YAML:
                                     primitive + filtri

Per l’agente le tre famiglie sono indistinguibili: l’instradamento e i confini vivono sotto il catalogo.

Cosa vede il modello

Il modello vede solo nome, descrizione e argomenti di ciascun tool: la sezione di binding, quella che dichiara la primitiva da usare e i filtri da applicare, gli è invisibile. Ed è lì che sta il valore in un contesto regolamentato. Un filtro può prendere il valore da un argomento della tool call, ma anche da una costante fissata nella configurazione o dal runtime che ospita l’assistente (per esempio l’ente dell’utente autenticato): in questi due casi il confine vale per costruzione, senza doversi fidare che il modello passi il parametro giusto, perché il modello non può né vedere il filtro né alterarlo. Il principio è trasferire in artefatti statici e versionabili tutti i confini noti in anticipo, riducendo l’autonomia del modello dove non serve.

Tre tool, una primitiva

Un esempio rende l’idea: tre tool con nomi, argomenti e confini diversi per tre assistenti, la stessa primitiva sotto (i nomi sono di fantasia, la struttura invece è concettualmente quella giusta):

cerca_documenti_bando   cerca_atti_ente       cerca_faq
  (bando_id, query)         (query)            (query)
          |                    |                  |
          v binding            v binding          v binding
  filtro: id bando       filtro: ente     filtro: tipo doc
 valore <- argomento   valore <- runtime   valore <- "faq"
     (bando_id)       (ente dell'utente)     (costante)
          |                    |                  |
          +--------------------+------------------+
                              v
                primitiva: ricerca semantica
          (un'unica implementazione centralizzata)

Mentre i server MCP esterni usano il protocollo per l’interoperabilità, qui il punto è la riusabilità: tanti tool agli occhi degli agenti, un’unica implementazione da mantenere e revisionare.

L’effetto sullo sviluppo

Lo sviluppo di un agente per un nuovo cliente è passato da 4-5 giorni a mezza giornata, spesa sulla comprensione del dominio invece che sull’implementazione. La revisione di sicurezza si applica a un insieme ristretto di primitive centralizzate, le correzioni si propagano automaticamente a tutti i tool che le usano, e il collo di bottiglia si sposta dagli sviluppatori agli esperti di dominio, con una UI di back-office in arrivo per la composizione autonoma dei tool.

Indipendenza dall’orchestratore

L’utilizzo del protocollo MCP per sviluppare questo layer di configurabilità dei tool ci permette di renderlo indipendente dall’orchestratore che lo avvolge, che può essere un loop agentico puro, un sistema a grafi come LangGraph e così via. L’importante è che il sistema di orchestrazione abbia un connettore per MCP, che è un requisito sempre più standard visto il successo che sta riscuotendo il protocollo proposto da Anthropic.

Il dettaglio dell’architettura e delle alternative scartate è nell’articolo su Agenda Digitale.