MCP Server Workshop: da zero a produzione
Costruisci un MCP server remoto da zero con Fastify, Keycloak OAuth e MinIO storage.
Workshop di 90 minuti in live-coding che attraversa il Model Context Protocol step-by-step: da un endpoint Fastify vuoto a un server completamente autenticato con tool, resource e prompt.
Perché MCP
MCP è lo standard de facto per connettere AI client a servizi esterni. Figma, Notion, GitHub, AWS: tutti espongono MCP server. È il punto di contatto con gli agent AI: se il vostro servizio non espone un endpoint MCP, per un AI agent semplicemente non esiste.
- MCP non è una libreria, è un protocollo (come HTTP, come gRPC)
- È l'interfaccia tra il reasoning del modello AI e le azioni nel mondo reale
- Il modello decide cosa fare (reasoning), MCP gli dice come farlo (protocol)
- Se il vostro servizio non espone un endpoint MCP, per un AI agent non esiste
Cos'è un MCP server
Un MCP server è un processo che espone capability in un formato che un AI client può scoprire e utilizzare autonomamente.
Il modello AI non ha bisogno di documentazione, non ha bisogno che qualcuno gli spieghi come usare la vostra API. Si connette, chiede "cosa sai fare?", riceve la lista di tool con description e schema dei parametri, e da quel momento decide autonomamente quando e come invocarli durante il suo reasoning.
È un ribaltamento di prospettiva rispetto a un'API REST tradizionale:
| API REST | MCP Server |
|---|---|
| La usa uno sviluppatore che legge la doc e scrive codice | Lo usa un modello AI che legge le description e decide a runtime |
Il server non cambia: è sempre il vostro backend, le vostre business logic. Quello che cambia e chi lo consuma e come lo scopre.
Lo stack del workshop
Non usiamo un framework opinionated tipo create-mcp-app. Nella maggior parte dei casi reali non state costruendo un MCP server standalone da zero: avete gia un backend in produzione e dovete estenderlo con le capability MCP. Un tempo l'avanguardia era dare un'API pubblica agli sviluppatori. Oggi anche i non tecnici usano l'AI nei loro workflow quotidiani e chiedono di integrare il prodotto nel loro agent.
- SDK ufficiale TypeScript (
@modelcontextprotocol/sdk): vi dà la flessibilita di montare/mcpsu un server che ha gia le sue route e il suo middleware - Fastify come web server: il vostro backend esistente, esteso con
/mcp - jose per JWT/JWKS, Zod per schema validation
- Keycloak come Authorization Server (OIDC)
- MinIO come object storage S3-compatible (per le resource)
- Node.js 24, TypeScript
L'SDK ci da esattamente due cose: registrazione delle primitive (tool, resource, prompt) e trasporto JSON-RPC su Streamable HTTP. Tutto il resto lo costruiamo noi sopra, pezzo per pezzo.
JSON-RPC 2.0
Il formato dei messaggi tra client e server è JSON-RPC 2.0: request/response minimale e standardizzato. È lo stesso protocollo usato da Ethereum (tutte le interazioni con la blockchain), dal Language Server Protocol (l'autocompletamento nel vostro IDE) e dai Jupyter Notebook. Intenzionalmente minimale: un method, dei params, un id.
Input validation via JSON Schema.
Transport: Streamable HTTP
Il transport definisce come i messaggi JSON-RPC vengono incapsulati e consegnati. La spec definisce due transport standard:
stdio: per server locali (sottoprocesso, stdin/stdout)
Streamable HTTP: per server remoti (quello che implementiamo)
Caratteristiche: singolo endpoint, stateless by design, infrastruttura HTTP standard, streaming opzionale. Funziona su qualsiasi proxy, CDN, API gateway, Cloudflare Workers, Lambda, senza configurazione speciale.
Streamable HTTP vs HTTP+SSE (deprecato)
| Aspetto | HTTP+SSE (vecchio) | Streamable HTTP |
|---|---|---|
| Connessioni per client | 2 (GET SSE + POST) | 1 (POST) |
| Sticky sessions | Obbligatorie | Non necessarie |
| Serverless / autoscaling | Incompatibile | Nativo |
| Caching HTTP | Impossibile | Standard |
| Deploy blue-green | Problematico | Trasparente |
| Debugging | Stream continuo | Coppie request/response |
Da stateful a stateless: evoluzione
Lo stato applicativo (se serve) si gestisce con handle espliciti ritornati dai tool, non nel protocollo di trasporto.
Il pattern: un server per ogni request
- Factory
createMcpServer(): ogni request ha la sua istanza isolata. L'McpServer dell'SDK mantiene stato interno: se lo condividessimo tra request concorrenti, avremmo race condition e stato che leaka tra utenti diversi sessionIdGenerator: undefined: opt-in esplicito per stateless. Il server non ha memoria tra una request e l'altra, il load balancer non deve fare sticky routing, e potete scalare orizzontalmente senza vincoli
Quando la spec 2026-07-28 sarà final, questo diventerà l'unico pattern supportato dal core protocol.
Le tre primitive MCP
| Primitiva | Analogia | Chi la invoca | Side effects? |
|---|---|---|---|
| Tool | Una funzione che fa qualcosa | Il modello AI (function calling) | Si |
| Resource | Un documento da leggere | Il client (per contesto) | No |
| Prompt | Un template di conversazione | L'utente/client | No |
Tool: random-number
I tool sono azioni che il modello decide di eseguire durante il reasoning. La description è il contratto tra voi e il modello: se è vaga, il modello non saprà quando usare il tool. Se è precisa, lo userà bene. L'inputSchema con Zod genera JSON Schema automaticamente, e il client valida i parametri prima di chiamare.
Resource: contesto read-only
Le resource sono dati che il client fetcha per arricchire il contesto del modello. Nessun side effect, pure letture. Il client fa resources/list, vede tutte le resource disponibili, ne fetcha una e il contenuto finisce nel contesto come informazione di background.
Use case: documentazione interna, schema del database, configurazione corrente, file di un repository. Il modello li legge e poi scrive query corrette, genera codice coerente, o risponde con informazioni aggiornate.
Prompt: template di conversazione
I prompt sono template predefiniti offerti al client. La differenza fondamentale: un tool lo chiama il modello durante il reasoning ("ho bisogno di cercare X"); un prompt lo attiva l'utente ("voglio usare il template Y"). Pensateli come gli slash command di Slack, ma per AI.
Autenticazione in MCP: la storia
Prima della spec 2025-03-26: il Far West. Non c'era uno standard. Alcuni server si generavano API key, altri usavano basic auth, altri non avevano auth per niente. Il problema: zero interoperabilità. Ogni client doveva implementare la logica auth specifica per ogni server. Nessun flow automatizzato, nessun consent, nessuna revoca granulare.
Dalla spec 2025-11-25: OAuth 2.1 formalizzato. L'MCP server è un Resource Server: verifica token, non autentica. L'autenticazione la fa un Authorization Server separato (Keycloak, Auth0, Entra ID, Google). Un client MCP che implementa il flow OAuth della spec può connettersi a qualsiasi MCP server protetto senza configurazione manuale.
Il flow è standardizzato, automatizzabile, revocabile.
Il flow di discovery (RFC 9728)
Il server dichiara Protected Resource Metadata (RFC 9728). Il client scopre dove autenticarsi senza configurazione manuale.
Protected Resource Metadata
La 401 include l'header WWW-Authenticate: Bearer resource_metadata=" che punta al metadata.
JWT + JWKS: verifica del token
I JWT sono firmati dall'Authorization Server con una chiave privata. Il nostro server verifica con la chiave pubblica corrispondente: crittografia asimmetrica. Chi firma e chi verifica usano chiavi diverse. La verifica è locale, zero network per request: la latenza aggiunta e nell'ordine dei microsecondi.
Middleware: validazione con jose
Tool autenticato: greet
Il tool non chiede "chi sei?" come parametro. Lo sa dal token.
Stesso meccanismo di Notion quando chiede "mostrami le mie pagine": il server estrae l'identità dal JWT.
Client registration: il problema
Come fa l'Authorization Server a sapere chi è il client che chiede un token?
| Approccio | Limiti |
|---|---|
| Pre-registered ID | Non scala a N client, consent generico, niente revoca granulare |
| DCR (RFC 7591) | DoS, nomi self-declared, UUID illeggibili nel consent, supporto scarso |
| CIMD | Identita verificabile via dominio, zero registrazione, policy granulari |
CIMD: Client ID Metadata Documents
Il client_id è un URL: https://kiro.dev/.well-known/oauth-client
Domain ownership = identità del client. Il certificato TLS lo garantisce.
CIMD vs alternative: confronto
| Problema | Shared ID + PKCE | DCR | CIMD |
|---|---|---|---|
| Consent leggibile | Nome generico | UUID | Nome + dominio verificato |
| Revoca granulare | Tutto o niente | Per client | Per client |
| Policy per client | Tutti uguali | Nessuna pre-reg | Policy su dominio |
| DoS/pollution | N/A | Registrazione aperta | Nessun endpoint |
| Identita verificabile | No | Self-declared | TLS + dominio |
| Supporto AS | Tutti | Pochi | In crescita |
Spec 2026-07-28: dove sta andando MCP
La prossima spec (RC disponibile, final a luglio 2026) conferma la direzione stateless:
initialize/initializedrimosso: niente handshake, la prima request e direttamente untools/callMcp-Session-Idrimosso: zero sessioni a livello di protocolloMcp-Method+Mcp-Nameheaders: routing senza deep packet inspectionttlMs+cacheScope: cache dichiarativa sulle list response- Extensions framework: Tasks (long-running), MCP Apps (UI in iframe), opt-in
- Roots, Sampling, Logging deprecati: sostituiti da tool params, LLM API dirette, OpenTelemetry
Vuoi approfondire qualche aspetto del workshop?
Chiedimi dei dettagli implementativi, della spec MCP, o dell'architettura OAuth