TL;DR: Assistente al cittadino che risponde esclusivamente sui contenuti certificati dell'ente, esposti tramite Model Context Protocol (MCP). Il contenuto vive nel server MCP, mai nel prompt del modello.
Prototipo dimostrativo sul Comune di Paperopoli (fittizio). L'ente pubblica i propri contenuti attraverso un server MCP; i modelli AI (Gemini e Claude) rispondono solo sulla base di quei contenuti, citando id e versione dei passaggi recuperati.
| Progetto | Ruolo |
|---|---|
Duckburg.Registry |
Server MCP dell'ente (porta 5000). Minimal API, trasporto Streamable HTTP su /mcp. Espone il tool cerca(query, limite) e le risorse del corpus (corpus/out/corpus.json, caricato in memoria in sola lettura). Access token opzionale. |
Duckburg.Portal |
Portale del Comune (porta 5100), Razor Pages in stile Designers Italia. Assistente come widget su tutte le pagine e a pagina intera su /assistente. |
ChattyDuck.Quack |
Razor Class Library dell'assistente: UI chat, endpoint POST /chat, GET /chat/usage, GET /debug/tools, orchestrazione dei modelli. |
ChattyDuck.Models |
Implementazioni intercambiabili di IModelService (Gemini, Claude), tracking dei consumi. |
ChattyDuck.Mcp |
Client MCP verso il Registry, usato dal bridge Gemini. |
Principio architetturale: il system prompt definisce solo il comportamento del modello; i contenuti risiedono unicamente nel corpus del server MCP.
I due modelli si collegano al corpus in modo diverso:
- Gemini: non supporta MCP nativamente: il portale fa da bridge, traducendo i tool MCP in
functionDeclarationsed eseguendo le chiamate comefunctionResponse. - Claude: supporta MCP nativamente tramite il connettore della Messages API (parametro
mcp_servers, header betamcp-client-2025-11-20): si collega direttamente all'endpoint pubblico del server, senza bridge.
# 1. Configurazione: copia i template e inserisci le API key
Copy-Item Duckburg.Portal\appsettings.template.json Duckburg.Portal\appsettings.json
Copy-Item Duckburg.Registry\appsettings.template.json Duckburg.Registry\appsettings.json
# 2. Server MCP (porta 5000)
dotnet run --project Duckburg.Registry
# 3. Portale (porta 5100) -> http://localhost:5100
dotnet run --project Duckburg.PortalIn Visual Studio: profilo di avvio multiplo "Portal + MCP" (DuckburgSmartCity.slnLaunch).
Verifica senza API key:
GET http://localhost:5100/debug/tools: tool MCP visibili al bridgeGET http://localhost:5100/chat/usage: stato dei consumi per modello
I file appsettings*.json reali sono esclusi dal versioning: nel repository ci sono solo i template. In alternativa: variabili d'ambiente o dotnet user-secrets.
Portal
| Chiave | Variabile d'ambiente | Note |
|---|---|---|
Gemini:ApiKey |
Gemini__ApiKey |
Google AI Studio, free tier |
Gemini:Model |
- | default gemini-2.5-flash |
Anthropic:ApiKey |
Anthropic__ApiKey |
Anthropic Console, a consumo |
Anthropic:Model |
- | es. claude-haiku-4-5 |
Anthropic:McpEndpoint |
Anthropic__McpEndpoint |
URL pubblico del server MCP: deve essere raggiungibile dai server Anthropic (localhost non funziona) |
Registry:McpEndpoint |
- | endpoint del bridge Gemini (default http://localhost:5000/mcp) |
Registry
| Chiave | Note |
|---|---|
Corpus:Path |
percorso di corpus.json (default ../corpus/out/corpus.json) |
Registry:AccessToken |
opzionale; se valorizzato richiede Authorization: Bearer <token> o X-Access-Token |
Il percorso Claude e i client MCP esterni richiedono un endpoint raggiungibile da Internet:
- Sviluppo:
ngrok http 5000→https://<sottodominio>.ngrok-free.dev/mcp(da riportare inAnthropic:McpEndpoint; cambia a ogni riavvio del tunnel). - Produzione: dominio dedicato dietro reverse proxy, ambienti separati.
L'assistente è disponibile su http://localhost:5100 (widget) e su /assistente (pagina intera), con selettore del modello.
Client MCP esterni: qualunque client MCP può consumare il corpus. La voce "Configura il tuo chatbot" in /assistente mostra la configurazione:
{
"mcpServers": {
"comune-paperopoli": {
"type": "http",
"url": "https://<dominio-pubblico>/mcp"
}
}
}Verifica funzionale: domande di controllo, valide su ogni client:
- "Quando scade la prima rata della TARI?" → 30 aprile, cita
tari:p02 - "Quali sono le aliquote IMU?" → valori di Paperopoli (
imu:p02), citati - "Che giorno passa l'umido nel quartiere Vesuvio?" → "Questa informazione non è nelle fonti."
- "Come prenoto la carta d'identità?" → procedura da
carta-identita-residenza:p01/p02 - La stessa domanda su client diversi produce la stessa risposta, ancorata al corpus
Test diretto del tool cerca, senza modelli:
curl -s http://localhost:5000/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"cerca","arguments":{"query":"prima rata TARI"}}}'Monitoraggio dei consumi: il pannello "Limiti di utilizzo" sotto la chat riporta consumo e quota residua:
- Claude: valori reali dagli header
anthropic-ratelimit-*, intercettati da unDelegatingHandler(AnthropicRateLimitHandler). - Gemini: token da
usageMetadatadelle risposte; quota residua stimata localmente (Google non la espone via API, verificabile in AI Studio).
Il tracker (ModelUsageTracker) è in memoria e si azzera al riavvio.
Risposte non ancorate: se un client risponde con normativa nazionale generica anziché con i dati del corpus, non inserire i dati nel prompt: rinforzare le regole di comportamento e mostrare i passaggi recuperati accanto alla risposta (la UI lo fa già con il riquadro "Fonti recuperate").
Prototipo dimostrativo, non pronto per la produzione. I dettagli implementativi (bridge Gemini, gestione rate limit, formato del corpus) sono candidati a una futura cartella docs/.