Introduzione
Se state utilizzando Claude, ChatGPT, Cursor o qualsiasi altra AI moderna, è molto probabile che stiate già interagendo con il Model Context Protocol (MCP) senza rendervene conto. MCP è il protocollo che trasforma un Large Language Model da semplice chatbot a sistema realmente integrato con i vostri dati e strumenti.
Non si tratta di teoria astratta o di un concetto futuristico. MCP è già in produzione e permette alle AI di:
- Leggere e scrivere file sul filesystem
- Interrogare database relazionali e NoSQL
- Chiamare API esterne
- Accedere a knowledge base proprietarie
- Eseguire operazioni complesse sui vostri sistemi
Questa guida vi accompagnerà attraverso l'architettura, il funzionamento e l'implementazione pratica di MCP, fornendo le conoscenze necessarie per integrare le AI moderne nel vostro stack tecnologico.
Il Problema: Integration Hell
La Frammentazione delle Integrazioni AI
Prima dell'introduzione di MCP, il panorama delle integrazioni AI era caratterizzato da una frammentazione estrema. Ogni provider aveva il proprio sistema proprietario:
- Anthropic con le sue API specifiche per Claude
- OpenAI con il proprio ecosistema per GPT
- Google con le integrazioni proprietarie per Gemini
A prima vista, questo potrebbe sembrare semplicemente un problema di duplicazione del codice. Ma la realtà è molto più complessa e problematica.
I Tre Volti della Complessità
1. Adattatori Custom Hard-Coded
Senza uno standard comune, ogni integrazione richiede un adattatore scritto su misura. Questi adattatori non sono semplici wrapper: contengono logica di business complessa per:
- Gestire il contesto: Come passare informazioni da una chiamata all'altra
- Normalizzare i formati: Convertire tra schemi dati diversi
- Gestire gli errori: Ogni API ha le sue convenzioni per gli errori
- Controllare il rate limiting: Implementazioni diverse per ogni provider
Il risultato? Codice fragile, difficile da testare e quasi impossibile da mantenere.
2. Fragilità Sistemica
Ogni modifica al sistema diventa un potenziale punto di rottura:
Cambio ambiente (Test → Produzione):
- Endpoint diversi
- Credenziali diverse
- Configurazioni diverse
→ Pipeline di integrazione da riscrivereAggiornamento API del provider:
- Schema cambiato
- Nuovi campi obbligatori
- Deprecazione di endpoint
→ Metà della logica di integrazione obsoletaNuova fonte dati da integrare:
- Formato diverso
- Autenticazione diversa
- Semantica diversa
→ Nuovo adattatore custom da zero3. Complessità Esponenziale
Il vero problema emerge quando si considera l'intera organizzazione:
- Backend Engineers parlano in JSON strutturato e REST API
- Data Scientists parlano in embedding, vettori e indici semantici
- DevOps parlano in container, orchestrazione e deployment
Senza uno standard condiviso, questi team non condividono nemmeno un linguaggio comune per l'integrazione. Ogni team implementa le proprie integrazioni, duplicando sforzi e creando debito tecnico.
L'Analogia: Prima di USB-C
Per comprendere meglio il problema, pensate all'ecosistema di ricarica prima dello standard USB-C:
- Apple aveva Lightning
- MacBook aveva MagSafe
- Android aveva Micro-USB
- Altri dispositivi avevano mini-USB
Il risultato? Tre o quattro cavi diversi nella borsa, nessuna interoperabilità, e la continua frustrazione di non avere mai il cavo giusto al momento giusto.
MCP risolve lo stesso problema per le integrazioni AI: un solo protocollo standard per tutte le integrazioni.
La Soluzione: Model Context Protocol
Che Cos'è MCP?
Model Context Protocol (MCP) è uno standard aperto che definisce come i Large Language Model comunicano con sistemi esterni. È il layer di astrazione che si posiziona tra:
- Le AI (Claude, GPT, altri LLM)
- I vostri sistemi (database, filesystem, API, tool proprietari)
I Principi Fondamentali
MCP si basa su tre principi chiave:
1. Standardizzazione Completa
MCP non standardizza solo i messaggi o il formato dei dati. Standardizza l'intero modello di integrazione:
- Come si stabilisce una connessione
- Come si negoziano le capabilities
- Come si scoprono i tools disponibili
- Come si invocano le funzioni
- Come si gestiscono gli errori
Questo significa che scrivendo un server MCP una volta sola, ottieni automaticamente compatibilità con tutti i client MCP.
2. Write Once, Run Everywhere
Un server MCP scritto per Claude Desktop funziona immediatamente con:
- Cursor (IDE AI-powered)
- Zed (editor di testo moderno)
- Qualsiasi altro client compatibile MCP
Non servono modifiche. Non servono adattatori. Lo stesso codice, ovunque.
3. Nessun Vendor Lock-In
MCP è uno standard aperto. Non è proprietario di Anthropic, OpenAI o Google. È un protocollo aperto che chiunque può implementare.
Questo significa:
- Libertà di cambiare provider AI senza riscrivere integrazioni
- Possibilità di supportare multiple AI contemporaneamente
- Controllo completo del vostro stack tecnologico
Il Risultato
Con MCP, state implementando uno standard, non integrando una singola AI. E questo cambia completamente il paradigma di integrazione.
Architettura MCP: I Tre Componenti
MCP definisce un'architettura chiara basata su tre componenti principali. Comprendere questa architettura è fondamentale per implementare integrazioni corrette e robuste.
1. Host
L'Host è l'applicazione che l'utente finale utilizza. È l'ambiente che contiene tutto:
Componenti dell'Host:
- User Interface: L'interfaccia con cui l'utente interagisce
- Model (LLM): Il Large Language Model che genera risposte
- Client MCP: Il componente che implementa il protocollo MCP
Esempi di Host:
- Claude Desktop
- Cursor
- IDE con supporto AI
- Chat application custom
Ruolo: L'Host è il punto di orchestrazione. Gestisce l'interazione utente, coordina le chiamate al Model, e orchestra la comunicazione con i Server MCP tramite il Client.
2. Client
Il Client è il componente all'interno dell'Host che parla il protocollo MCP.
Responsabilità del Client:
- Stabilire e mantenere connessioni con i Server MCP
- Tradurre le decisioni del Model in messaggi MCP
- Inviare richieste JSON-RPC ai Server
- Ricevere e processare le risposte
- Gestire errori e timeout
Importante: Il Client è un traduttore, non un decisore. Non contiene logica di business. Non decide quando invocare i tools. Traduce semplicemente le decisioni del Model in chiamate protocollari.
3. Server
Il Server è il componente che voi implementate. Questo è il vostro codice.
Responsabilità del Server:
- Esporre Tools (funzioni eseguibili)
- Esporre Resources (dati leggibili)
- Esporre Prompts (template riutilizzabili)
- Eseguire le operazioni richieste
- Validare gli input
- Gestire errori e edge cases
Il Server è dove vivono:
- L'accesso ai vostri database
- La logica di business
- Le chiamate alle vostre API
- L'indicizzazione delle vostre knowledge base
Il Model: Dov'è?
Punto fondamentale da comprendere: il Model non è parte del protocollo MCP.
Il Model (l'LLM) vive dentro l'Host. Non comunica direttamente con il Server. La comunicazione avviene sempre attraverso il Client.
Il flusso è sempre:
Model → Client → ServerMai:
Model → Server (diretto)Il Principio Chiave: Separazione delle Responsabilità
Model decide → "Ho bisogno di cercare nella KB"
Client comunica → Traduce in tools/call JSON-RPC
Server esegue → Esegue la ricerca e ritorna risultatiQuesta separazione garantisce:
- Sicurezza: Il Model non ha accesso diretto ai vostri sistemi
- Testabilità: Ogni componente può essere testato indipendentemente
- Manutenibilità: Modifiche a un componente non impattano gli altri
Modello di Comunicazione
Il modello di comunicazione primario in MCP è il request-response:
✅ Il pattern fondamentale: Request-Response
Client → richiesta → Server
Server → risposta → ClientQuesto è il flusso che userete nel 99% dei casi: il Client chiede, il Server risponde. Semplice, prevedibile, debuggabile.
✅ Supportato anche: Notifications
Oltre al request-response, MCP supporta le notifications: messaggi unidirezionali che non richiedono risposta. Sia il Client che il Server possono inviare notifications.
Esempi pratici:
- Il Server notifica che la lista dei tools è cambiata (
notifications/tools/list_changed) - Il Server notifica che una resource è stata aggiornata (
notifications/resources/updated) - Il Client notifica che i root paths sono cambiati (
notifications/roots/list_changed)
Le notifications sono un meccanismo di segnalazione leggero: dicono "è successo qualcosa", ma non richiedono una risposta. Se il Client riceve una notifica di cambio tools, potrà poi fare una nuova tools/list per ottenere la lista aggiornata — sempre con il pattern request-response.
❌ Non Supportato:
- Il Server non può invocare tools sul Client
- Nessuno streaming bidirezionale arbitrario
- Nessun callback asincrono complesso
Perché questo modello?
1. Sicurezza: Il Server non può "attaccare" il Client. Non può inondarlo di richieste. Non può tentare di prendere controllo. Può rispondere a richieste e inviare segnalazioni leggere, nulla di più.
2. Semplicità: Request-response con notifications è un modello semplice da implementare, debuggare e monitorare. Non ci sono:
- Race condition complesse
- Sincronizzazione bidirezionale
- Code di messaggi da gestire
Il risultato è un protocollo prevedibile e affidabile, che bilancia flessibilità e sicurezza.
I Quattro Pilastri di MCP
MCP espone quattro primitive fondamentali che definiscono come il Client può interagire con il Server. Comprendere a fondo questi quattro elementi è essenziale per progettare server MCP efficaci.
1. Resources
Definizione: Le Resources sono dati esposti dal Server che il Model può leggere.
Caratteristiche:
- Passive: Non eseguono codice
- Read-only: Il Model può leggerle ma non modificarle
- Context: Forniscono informazioni di contesto al Model
Esempi concreti:
docs://api-reference.mdUn file di documentazione API che il Server rende disponibile. Quando il Model ha bisogno di rispondere a domande sulle API, il Client può richiedere questa Resource.
db://config/productionRecord di configurazione del database di produzione esposti come Resource.
logs://system/errors/2024-02-15Log di sistema per una data specifica.
Caso d'uso tipico:
Immaginate di avere una knowledge base di documentazione tecnica interna. Il vostro Server MCP espone ogni documento come Resource:
Resource: kb://architecture/microservices-design
Contenuto: Documento di 5000 parole sulla vostra architetturaQuando un utente chiede "Come è strutturata la nostra architettura a microservizi?", il Model:
- Identifica che serve il documento
- Il Client richiede la Resource
- Il Server legge il file dal filesystem
- Ritorna il contenuto
- Il Model lo usa per generare una risposta accurata
Punto chiave: Le Resources non eseguono mai codice. Sono dati puri.
2. Tools
Definizione: I Tools sono azioni eseguibili che il Server espone.
Caratteristiche:
- Active: Eseguono codice sul Server
- Parametrizzabili: Accettano input strutturati
- Side effects: Possono modificare stato
Differenza fondamentale con Resources:
Resources → Lettura passiva di dati
Tools → Esecuzione attiva di operazioniEsempi concreti:
Tool: search_kb
{
name: "search_kb",
description: "Cerca documenti nella knowledge base usando ricerca semantica",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Query di ricerca in linguaggio naturale"
},
maxResults: {
type: "number",
description: "Numero massimo di risultati (default: 5)"
}
},
required: ["query"]
}
}Implementazione sul Server:
async function search_kb(query, maxResults = 5) {
// 1. Tokenizza la query
const tokens = tokenize(query);
// 2. Genera embedding
const embedding = await generateEmbedding(tokens);
// 3. Cerca nell'indice vettoriale
const results = await vectorIndex.search(embedding, maxResults);
// 4. Rankizza per rilevanza
const ranked = rankByRelevance(results);
return ranked;
}Tool: create_file
{
name: "create_file",
description: "Crea un nuovo file nel workspace",
inputSchema: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" },
overwrite: { type: "boolean", default: false }
},
required: ["path", "content"]
}
}Tool: query_database
{
name: "query_database",
description: "Esegue query SQL safe sul database di produzione",
inputSchema: {
type: "object",
properties: {
query: { type: "string" },
params: { type: "array" }
},
required: ["query"]
}
}Punto critico: La quality delle description è fondamentale.
Il Model vede SOLO:
- Il nome del tool
- La description
- L'input schema
Non vede il vostro codice. Non vede l'implementazione. Basa le sue decisioni esclusivamente su queste informazioni.
Description migliori → Decisioni migliori del Model.
Esempio di description debole vs forte:
❌ Debole:
"Cerca documenti"✅ Forte:
"Cerca documenti nella knowledge base usando ricerca semantica.
Supporta query in linguaggio naturale. Ritorna i documenti più
rilevanti con score di similarità. Usa questo tool quando l'utente
chiede informazioni presenti nella documentazione aziendale."3. Prompts
Definizione: I Prompts sono template riutilizzabili che il Server espone per guidare il Model in task specifici.
Caratteristiche:
- Template-based: Strutture di prompt predefinite con placeholders
- Parametrizzabili: Accettano variabili che vengono sostituite a runtime
- Reusable: Possono essere invocati ripetutamente con input diversi
- Server-side: Vivono sul Server, non nel Client
Il problema che risolvono: Senza Prompts, ogni volta che un utente chiede "fammi una code review" o "analizza questi log", il Model deve costruire da zero le istruzioni su come strutturare l'analisi. Il risultato è inconsistente: a volte l'analisi è approfondita, a volte superficiale, e il formato cambia ogni volta.
Con i Prompts, voi codificate il vostro know-how in un template. Il Model lo usa come guida strutturata, producendo output consistenti e di qualità prevedibile.
Come funzionano nel protocollo: Il Client chiede al Server quali Prompts sono disponibili (prompts/list), poi può richiedere un Prompt specifico con i suoi argomenti (prompts/get). Il Server restituisce una sequenza di messaggi già strutturati che il Client passa al Model.
Esempio 1: Analyze Error Logs
{
name: "analyze_error_logs",
description: "Analisi strutturata di log di errore con identificazione pattern, root cause analysis e azioni raccomandate. Usa questo prompt quando l'utente fornisce log di errore e vuole un'analisi sistematica.",
arguments: [
{
name: "logs",
description: "I log di errore da analizzare (testo grezzo o path al file)",
required: true
},
{
name: "timeframe",
description: "Intervallo temporale di riferimento (es: 'ultime 24 ore', 'settimana scorsa')",
required: false
},
{
name: "severity_focus",
description: "Livello di severità su cui concentrarsi: 'all', 'critical_only', 'high_and_above'",
required: false
}
]
}Template del Prompt:
Analizza i seguenti log di errore in modo sistematico:
LOG DATA:
{logs}
TIMEFRAME: {timeframe}
FOCUS: {severity_focus}
Esegui la seguente analisi:
1. PATTERN IDENTIFICATION
- Identifica errori ricorrenti
- Raggruppa errori simili per tipo e origine
- Calcola frequenza per categoria
2. ROOT CAUSE ANALYSIS
- Per ogni pattern identificato, proponi cause probabili
- Ordina per impatto sul sistema
3. SEVERITY ASSESSMENT
- Critical: errori che bloccano il sistema o causano perdita dati
- High: errori che impattano utenti attivi
- Medium: errori che degradano performance
- Low: errori informativi o warning
4. RECOMMENDED ACTIONS
- Azioni immediate per errori critical (con stima del tempo)
- Fix suggeriti per errori high/medium
- Miglioramenti preventivi per ridurre errori futuri
Formato output: JSON strutturato con sezioni separate per ogni puntoPerché è utile: Senza questo template, se chiedete a un LLM "analizza questi log" ottenete risposte di qualità variabile. Con il template, ottenete sempre le quattro sezioni, sempre la classificazione per severity, sempre le azioni raccomandate. È il vostro standard di qualità, codificato.
Esempio 2: Code Review
{
name: "code_review",
description: "Code review strutturata con focus su bug, performance, sicurezza e manutenibilità. Usa questo prompt quando l'utente chiede una revisione del codice.",
arguments: [
{
name: "code",
description: "Il codice sorgente da revisionare",
required: true
},
{
name: "language",
description: "Linguaggio di programmazione (es: 'typescript', 'python', 'go')",
required: true
},
{
name: "context",
description: "Contesto del codice: cosa fa, dove viene usato, requisiti specifici",
required: false
}
]
}Template del Prompt:
Esegui una code review professionale del seguente codice:
LINGUAGGIO: {language}
CONTESTO: {context}
CODICE:
{code}
Analizza il codice secondo questi criteri:
1. CORRETTEZZA
- Bug potenziali o logica errata
- Edge case non gestiti
- Errori di tipo o null safety
2. SICUREZZA
- Input non validati
- Injection vulnerabilities (SQL, XSS, command injection)
- Gestione credenziali e dati sensibili
- Permessi e autorizzazioni
3. PERFORMANCE
- Operazioni O(n²) o peggio evitabili
- Memory leak potenziali
- Query N+1 o chiamate ridondanti
- Opportunità di caching
4. MANUTENIBILITÀ
- Naming e leggibilità
- Complessità ciclomatica elevata
- Duplicazione di codice
- Aderenza ai principi SOLID
5. TESTING
- Testabilità del codice
- Test case suggeriti
- Mock necessari
Per ogni issue trovata, specifica:
- Riga o sezione del codice
- Severità: 🔴 Critico | 🟡 Importante | 🔵 Suggerimento
- Codice corretto suggerito
Concludi con un punteggio complessivo da 1 a 10 e un summary delle priorità.Esempio 3: API Documentation Generator
{
name: "generate_api_docs",
description: "Genera documentazione API completa da codice sorgente. Produce output in formato Markdown con endpoint, parametri, esempi di richiesta/risposta e codici di errore.",
arguments: [
{
name: "code",
description: "Il codice sorgente contenente le definizioni degli endpoint",
required: true
},
{
name: "api_name",
description: "Nome dell'API per il titolo della documentazione",
required: true
},
{
name: "base_url",
description: "URL base dell'API (es: 'https://api.example.com/v1')",
required: false
}
]
}Template del Prompt:
Genera documentazione API completa dal seguente codice:
API: {api_name}
BASE URL: {base_url}
CODICE:
{code}
Per ogni endpoint trovato nel codice, genera:
1. ENDPOINT OVERVIEW
- Metodo HTTP e path
- Descrizione breve (una riga)
- Autenticazione richiesta (sì/no, tipo)
2. PARAMETRI
- Path parameters (con tipo e descrizione)
- Query parameters (con tipo, default, obbligatorio/opzionale)
- Request body (schema JSON con tipi e descrizioni)
3. RESPONSE
- Status code di successo con esempio di response body
- Status code di errore con significato
4. ESEMPIO COMPLETO
- Richiesta curl funzionante
- Response JSON di esempio
Formato output: Markdown con heading per ogni endpoint.
Usa tabelle per i parametri e code block per gli esempi.Esempio 4: Test Case Generation
{
name: "generate_test_cases",
description: "Genera test case completi da requisiti funzionali o codice. Produce test case strutturati con precondizioni, step e risultati attesi.",
arguments: [
{
name: "source",
description: "Requisiti funzionali o codice sorgente da cui generare i test",
required: true
},
{
name: "type",
description: "Tipo di test: 'unit', 'integration', 'e2e', 'all'",
required: false
},
{
name: "framework",
description: "Framework di test (es: 'vitest', 'jest', 'pytest')",
required: false
}
]
}Template del Prompt:
Genera test case dal seguente input:
TIPO: {type}
FRAMEWORK: {framework}
SOURCE:
{source}
Per ogni funzionalità identificata, genera:
1. HAPPY PATH
- Test con input validi e flusso normale
- Verifica di tutti gli output attesi
2. EDGE CASES
- Input ai limiti (stringhe vuote, numeri 0, array vuoti)
- Input al massimo consentito
- Valori null/undefined
3. ERROR CASES
- Input invalidi (tipo sbagliato, fuori range)
- Errori di rete/timeout (per integration test)
- Stato inconsistente
4. SECURITY CASES (se applicabile)
- Input malicious (injection attempts)
- Accesso non autorizzato
- Rate limiting
Per ogni test case specifica:
- Nome descrittivo (it('should...'))
- Arrange: setup e precondizioni
- Act: azione da eseguire
- Assert: risultato atteso
Se specificato un framework, genera codice eseguibile.
Altrimenti, genera test case in formato tabellare.Quando usare i Prompts e quando no
I Prompts sono potenti ma non sempre necessari. Ecco una guida pratica:
Usate i Prompts quando:
- Avete task ripetitivi che richiedono output strutturato e consistente
- Il vostro team ha standard di qualità specifici (format di code review, checklist di analisi)
- Volete codificare domain expertise che altrimenti andrebbe perso
- Più persone fanno lo stesso tipo di richiesta e volete risultati uniformi
Non servono Prompts quando:
- Le richieste sono sempre diverse e non standardizzabili
- Il Model produce già risposte di qualità sufficiente senza guida
- State costruendo un server semplice con uno o due tools (in quel caso, concentratevi su tools e resources)
Per la nostra knowledge base: Nella serie di video ci concentreremo su Tools e Resources, che sono i pilastri più immediati per un server di knowledge base. Ma se in futuro volete aggiungere, ad esempio, un prompt per "analizza tutti i documenti della KB e trova inconsistenze", sapete come farlo.
4. Transport
Definizione: Il Transport è il livello più basso. Definisce come i messaggi MCP vengono trasmessi tra Client e Server.
Punto chiave: Il protocollo MCP è indipendente dal Transport.
MCP definisce cosa comunicare (initialize, tools/list, tools/call), ma non come trasmetterlo. Questo permette flessibilità nell'implementazione.
Transport: stdio
Uso: Comunicazione locale tra processi sullo stesso sistema.
Funzionamento:
- Il Server legge da standard input
- Il Server scrive su standard output
- Il Client e Server comunicano via pipe
Vantaggi:
- Semplice da implementare
- Veloce (nessun overhead di rete)
- Perfetto per integrazioni locali
Casi d'uso:
- Claude Desktop
- Cursor
- IDE plugins
- Tool da command line
Esempio di setup:
// Server in Node.js
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const server = new Server({
name: "my-mcp-server",
version: "1.0.0"
});
const transport = new StdioServerTransport();
await server.connect(transport);Warning critico per stdio: stdout è il canale del protocollo. Mai usare console.log() in un server MCP su stdio. Usa console.error() per logging.
Transport: HTTP con Server-Sent Events
Uso: Comunicazione remota per deployment cloud.
Funzionamento:
- Server espone endpoint HTTP
- Client si connette via HTTP
- Server può scalare orizzontalmente
- Supporta migliaia di client concorrenti
Vantaggi:
- Deployment in cloud
- Scalabilità orizzontale
- Load balancing
- Monitoring e logging centralizzati
Casi d'uso:
- Server MCP in produzione
- Servizi multi-tenant
- Integrazioni enterprise
- API pubbliche
Esempio di setup:
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { SSEServerTransport } = require("@modelcontextprotocol/sdk/server/sse.js");
const express = require("express");
const app = express();
const server = new Server({
name: "my-mcp-server",
version: "1.0.0"
});
app.post("/mcp", async (req, res) => {
const transport = new SSEServerTransport("/mcp", res);
await server.connect(transport);
});
app.listen(3000);Punto fondamentale: Cambiare Transport non richiede modifiche al codice del Server. La logica dei Tools, Resources e Prompts resta identica.
JSON-RPC 2.0: Il Protocollo Sottostante
MCP non ha inventato un nuovo protocollo di comunicazione. Si basa su JSON-RPC 2.0, uno standard aperto definito nel 2010 e ampiamente utilizzato in sistemi distribuiti.
Perché JSON-RPC?
1. Standard collaudato: 15+ anni di uso in produzione
2. Semplice: Definizione chiara e concisa
3. Testato: Librerie disponibili in ogni linguaggio
4. Interoperabile: Supporto nativo in molti framework
Struttura di un Messaggio JSON-RPC
Ogni messaggio JSON-RPC ha quattro campi principali:
{
"jsonrpc": "2.0", // Versione del protocollo
"id": 1, // ID per associare richiesta/risposta
"method": "nome_metodo", // Metodo da invocare
"params": { } // Parametri del metodo
}Semplicità pura. Nessuna magia.
Nota: Le notifications usano lo stesso formato ma senza il campo id. L'assenza dell'id è ciò che le distingue da una richiesta: il mittente non si aspetta risposta.
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}I Tre Messaggi Fondamentali di MCP
MCP definisce tre messaggi che costituiscono l'intero lifecycle di una connessione. Vediamoli in dettaglio con i JSON reali.
1. Initialize: Handshake & Capability Negotiation
Scopo: Stabilire la connessione e negoziare le capabilities.
Richiesta del Client
Quando il Client si connette al Server, invia questo messaggio:
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "claude-desktop",
"version": "1.2.0"
}
}
}Analisi dei campi:
protocolVersion: La versione del protocollo MCP che il Client supporta. Questo permette evoluzione del protocollo mantenendo backward compatibility.
capabilities: Le capabilities che il Client supporta. In questo esempio:
roots: Il Client può gestire filesystem rootssampling: Il Client supporta richieste di sampling (completamento dall'LLM)
clientInfo: Informazioni sul Client per debugging e telemetry.
Risposta del Server
Il Server risponde con le sue capabilities:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {},
"resources": {
"subscribe": true,
"listChanged": true
},
"prompts": {
"listChanged": true
}
},
"serverInfo": {
"name": "my-knowledge-base-server",
"version": "1.0.0"
}
}
}Analisi dei campi:
protocolVersion: Deve corrispondere a quella del Client. Se non corrisponde, la connessione viene terminata immediatamente.
capabilities: Le capabilities che il Server supporta:
tools: Espone tools eseguibiliresources: Espone resources leggibili (con supporto per subscription e notifiche di cambio)prompts: Espone prompt template (con supporto per notifiche di cambio)
serverInfo: Informazioni sul Server per debugging.
Nota sui listChanged: Quando vedete "listChanged": true nelle capabilities, significa che quel componente supporta le notifications. Ad esempio, resources.listChanged: true indica che il Server invierà una notification notifications/resources/list_changed quando la lista delle resources cambia. Il Client potrà allora fare una nuova resources/list per aggiornarsi.
Capability Negotiation: Fail Fast
Se le capabilities non sono compatibili, la connessione fallisce immediatamente.
Esempio di failure:
Client richiede: { "mandatory_capability": "advanced_features" }
Server supporta: { "tools": {}, "resources": {} }
→ Connection failed: unsupported capabilityQuesto è fail fast: meglio un errore chiaro all'inizio che comportamenti indefiniti dopo.
2. tools/list: Discovery
Scopo: Il Client scopre quali tools il Server espone.
Richiesta del Client
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}Semplice richiesta senza parametri: "Quali tools hai?"
Risposta del Server
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "search_kb",
"description": "Cerca documenti nella knowledge base usando ricerca semantica. Supporta query in linguaggio naturale e ritorna i documenti più rilevanti con score di similarità. Usa questo tool quando l'utente chiede informazioni presenti nella documentazione aziendale.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Query di ricerca in linguaggio naturale"
},
"maxResults": {
"type": "number",
"description": "Numero massimo di risultati da ritornare",
"default": 5,
"minimum": 1,
"maximum": 20
},
"filters": {
"type": "object",
"properties": {
"category": {
"type": "string",
"enum": ["technical", "business", "process"]
},
"dateRange": {
"type": "object",
"properties": {
"start": { "type": "string", "format": "date" },
"end": { "type": "string", "format": "date" }
}
}
}
}
},
"required": ["query"]
}
},
{
"name": "get_document",
"description": "Recupera il contenuto completo di un documento specifico dato il suo ID. Usa questo tool quando hai già l'ID del documento da una ricerca precedente.",
"inputSchema": {
"type": "object",
"properties": {
"documentId": {
"type": "string",
"description": "ID univoco del documento"
}
},
"required": ["documentId"]
}
}
]
}
}Punti Critici della Discovery
1. Il Model vede SOLO questo
Il Model non ha accesso al vostro codice. Non vede l'implementazione. Prende decisioni basandosi esclusivamente su:
- Nome del tool
- Description
- Input schema
2. Quality della Description
Una description ben scritta include:
- Cosa fa il tool: Descrizione chiara della funzionalità
- Quando usarlo: Trigger condition per il Model
- Cosa ritorna: Tipo e struttura del risultato
- Limitazioni: Cosa NON può fare
3. Input Schema Dettagliato
Più dettagli fornite nello schema, meglio il Model può costruire richieste valide:
{
"maxResults": {
"type": "number",
"description": "Numero massimo di risultati",
"default": 5,
"minimum": 1,
"maximum": 20
}
}Con questi vincoli, il Model sa che:
- Il valore deve essere numerico
- Ha un default se non specificato
- Deve essere tra 1 e 20
3. tools/call: Execution
Scopo: Il Model ha deciso di usare un tool. Il Client lo invoca.
Richiesta del Client
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search_kb",
"arguments": {
"query": "come configurare il database di produzione",
"maxResults": 5,
"filters": {
"category": "technical"
}
}
}
}Flusso che ha portato a questo messaggio:
- Utente: "Come configuro il database di produzione?"
- Model analizza la richiesta
- Model decide: "Ho bisogno di cercare nella KB"
- Model guarda i tools disponibili
- Model identifica:
search_kbè appropriato - Model costruisce gli arguments
- Client traduce in JSON-RPC
- Client invia il messaggio al Server
Elaborazione sul Server
Quando il Server riceve questo messaggio:
async function handleToolCall(name, arguments) {
// 1. Validazione
if (name !== "search_kb") {
throw new Error("Unknown tool");
}
// 2. Validazione schema
validateSchema(arguments, searchKbSchema);
// 3. Esecuzione
const { query, maxResults, filters } = arguments;
// 3.1 Tokenizza query
const tokens = tokenize(query);
// 3.2 Genera embedding
const embedding = await generateEmbedding(tokens);
// 3.3 Applica filtri
let searchParams = { embedding, maxResults };
if (filters?.category) {
searchParams.category = filters.category;
}
// 3.4 Cerca nell'indice
const results = await vectorIndex.search(searchParams);
// 3.5 Formatta risultati
return formatResults(results);
}Risposta del Server
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Trovati 3 documenti rilevanti:\n\n1. **Configurazione Database Produzione** (score: 0.95)\n - PostgreSQL 14.5\n - Host: db.prod.internal\n - Port: 5432\n - Max connections: 100\n - Connection timeout: 30s\n - SSL: required\n \n2. **Best Practices Database** (score: 0.87)\n - Connection pooling: PgBouncer\n - Backup schedule: 3am daily\n - Replication: streaming async\n \n3. **Troubleshooting Database Issues** (score: 0.82)\n - Common errors and solutions\n - Performance tuning\n - Monitoring dashboards"
}
],
"isError": false
}
}Flusso Completo di Ritorno
- Server formatta e ritorna il result
- Client riceve la risposta
- Client passa il result al Model
- Model integra l'informazione nel suo contesto
- Model genera la risposta finale all'utente
"Ho trovato la configurazione del tuo database di produzione:
Il database è PostgreSQL 14.5 hostato su db.prod.internal
sulla porta 5432. La configurazione prevede:
- Massimo 100 connessioni contemporanee
- Timeout di 30 secondi per le connessioni
- SSL obbligatorio per tutte le connessioni
Per il connection pooling viene utilizzato PgBouncer,
e i backup sono schedulati alle 3am ogni giorno con
replicazione streaming asincrona.
Vuoi che approfondisca qualche aspetto specifico?"Gestione Errori
Se qualcosa va storto, il Server ritorna un errore JSON-RPC:
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32602,
"message": "Invalid params",
"data": {
"param": "maxResults",
"reason": "Must be between 1 and 20",
"received": 50
}
}
}Errori standard JSON-RPC:
-32700: Parse error-32600: Invalid request-32601: Method not found-32602: Invalid params-32603: Internal error
Esempio End-to-End Completo
Vediamo ora l'intero flusso dall'inizio alla fine, con tutti i messaggi e tutte le decisioni.
Scenario
Utente: "Cerca nella KB la configurazione del database di produzione"
Step 1: Il Model Analizza
Il Model riceve la richiesta e inizia l'analisi:
Input: "Cerca nella KB la configurazione del database di produzione"
Analisi:
- Intent: ricerca di informazioni
- Source: knowledge base (KB)
- Target: configurazione database
- Environment: produzione
Decisione: Usare il tool search_kbIl Model consulta i tools disponibili (dalla precedente tools/list):
{
"name": "search_kb",
"description": "Cerca documenti nella knowledge base...",
"inputSchema": {
"properties": {
"query": { "type": "string" },
"maxResults": { "type": "number" }
}
}
}Match perfetto! Il Model decide di usare questo tool.
Step 2: Il Client Costruisce la Richiesta
Il Model comunica la sua decisione al Client:
Tool: search_kb
Arguments:
- query: "database production configuration"
- maxResults: 5Il Client traduce in JSON-RPC:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "search_kb",
"arguments": {
"query": "database production configuration",
"maxResults": 5
}
}
}E invia il messaggio al Server via transport (stdio o HTTP).
Step 3: Il Server Riceve e Valida
Il Server riceve il messaggio:
// Server riceve via transport
const message = await transport.receive();
// Parse JSON-RPC
const { method, params } = message;
// Verifica metodo
if (method !== "tools/call") {
return errorResponse("Invalid method");
}
// Estrae parametri
const { name, arguments } = params;
// Verifica tool exists
if (name !== "search_kb") {
return errorResponse("Unknown tool");
}
// Valida schema
const validation = validateSchema(arguments, searchKbSchema);
if (!validation.valid) {
return errorResponse("Invalid params", validation.errors);
}Validazione passata ✅
Step 4: Il Server Esegue la Ricerca
async function executeSearch(query, maxResults) {
console.error(`[LOG] Starting search: query="${query}", max=${maxResults}`);
// 1. Tokenizzazione
const tokens = tokenize(query);
console.error(`[LOG] Tokenized into ${tokens.length} tokens`);
// 2. Generazione embedding
const embedding = await embeddingModel.encode(tokens);
console.error(`[LOG] Generated embedding vector of size ${embedding.length}`);
// 3. Ricerca nell'indice vettoriale
const vectorResults = await vectorIndex.search({
vector: embedding,
topK: maxResults * 2 // Recupera il doppio per il re-ranking
});
console.error(`[LOG] Vector search returned ${vectorResults.length} results`);
// 4. Re-ranking con modello cross-encoder
const reranked = await reranker.rank(query, vectorResults);
// 5. Prendi top maxResults
const topResults = reranked.slice(0, maxResults);
// 6. Recupera contenuti completi
const documents = await Promise.all(
topResults.map(async (result) => {
const doc = await database.getDocument(result.id);
return {
id: doc.id,
title: doc.title,
excerpt: doc.excerpt,
relevanceScore: result.score,
metadata: {
category: doc.category,
lastUpdated: doc.updatedAt,
author: doc.author
}
};
})
);
console.error(`[LOG] Search completed, returning ${documents.length} documents`);
return documents;
}Nota l'uso di console.error() per logging. Mai console.log() su stdio!
Step 5: Il Server Risponde
const results = await executeSearch(
arguments.query,
arguments.maxResults
);
// Formatta risposta
const response = {
jsonrpc: "2.0",
id: message.id,
result: {
content: [
{
type: "text",
text: formatSearchResults(results)
}
]
}
};
// Invia via transport
await transport.send(response);Response JSON inviata:
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "Trovati 3 documenti rilevanti:\n\n1. **Configurazione Database Produzione** (score: 0.95)\n PostgreSQL 14.5, host: db.prod.internal, port: 5432\n Max connections: 100, timeout: 30s, SSL required\n \n2. **Database Best Practices** (score: 0.87)\n Connection pooling con PgBouncer\n Backup giornalieri alle 3am\n Replicazione streaming asincrona\n \n3. **Troubleshooting Database** (score: 0.82)\n Errori comuni e soluzioni\n Performance tuning guide"
}
]
}
}Step 6: Il Client Passa il Result al Model
Il Client riceve la risposta e la passa al Model:
const response = await transport.receive();
// Verifica corrispondenza ID
if (response.id !== request.id) {
throw new Error("ID mismatch");
}
// Verifica presenza result
if (response.error) {
throw new MCPError(response.error);
}
// Estrae contenuto
const content = response.result.content;
// Passa al Model
model.addToolResult({
toolName: "search_kb",
result: content
});Step 7: Il Model Genera la Risposta Finale
Il Model ora ha:
- La richiesta originale dell'utente
- Il risultato del tool search_kb
Genera la risposta finale:
Input context:
- User query: "Cerca nella KB la configurazione del database di produzione"
- Tool result: [3 documents about database configuration]
Model output:
"Ho trovato la configurazione del database di produzione nella nostra
knowledge base. Il database è PostgreSQL 14.5 hostato su db.prod.internal
sulla porta 5432.
Configurazione principale:
- Connessioni massime: 100
- Timeout connessione: 30 secondi
- SSL: obbligatorio
Per le connessioni viene utilizzato PgBouncer come connection pooler,
i backup sono schedulati giornalmente alle 3am, e c'è una replica
configurata con streaming asincrono.
Ho anche trovato una guida per il troubleshooting di problemi comuni e
best practices per il tuning delle performance. Vuoi che approfondisca
qualcuno di questi aspetti?"Flusso Completo Riassunto
[User]
↓ "Cerca nella KB..."
[Model]
↓ Analizza → Decide: search_kb
[Client]
↓ Costruisce tools/call JSON-RPC
[Transport]
↓ Invia messaggio
[Server]
↓ Riceve, valida, esegue
[Server]
↓ Genera embedding, cerca, rankizza
[Server]
↓ Formatta e ritorna result
[Transport]
↓ Invia response
[Client]
↓ Riceve, passa al Model
[Model]
↓ Integra result, genera risposta
[User]
↓ Legge risposta completaTempo totale: ~2-3 secondi (dipende dalla complessità della ricerca)
Separazione dei ruoli mantenuta:
- Model: Ha deciso COSA fare
- Client: Ha gestito COME comunicare
- Server: Ha eseguito COSA richiesto
Nessun componente ha invaso le responsabilità degli altri.
Lifecycle del Server MCP
Un server MCP attraversa diversi stati durante il suo lifecycle. Comprendere questi stati è fondamentale per gestire correttamente le connessioni e debuggare problemi.
Stati del Lifecycle
1. Startup
Cosa succede:
- Caricamento configurazione
- Lettura variabili d'ambiente
- Inizializzazione connessioni (database, cache, ecc.)
- Caricamento risorse (indici, modelli, ecc.)
- Setup logging
Esempio:
async function startup() {
console.error("[STARTUP] Loading configuration...");
const config = loadConfig();
console.error("[STARTUP] Connecting to database...");
const db = await connectDatabase(config.db);
console.error("[STARTUP] Loading vector index...");
const vectorIndex = await loadVectorIndex(config.indexPath);
console.error("[STARTUP] Initializing embedding model...");
const embeddingModel = await loadEmbeddingModel();
console.error("[STARTUP] Server ready");
return {
db,
vectorIndex,
embeddingModel
};
}Failure handling: Se lo startup fallisce, il processo deve terminare con exit code non-zero. Non tentate di continuare con risorse parzialmente inizializzate.
2. Listening
Cosa succede:
- Il Server entra in ascolto sul transport
- Per stdio: legge da standard input
- Per HTTP: ascolta su porta configurata
3. Initialize (Handshake)
Cosa succede:
- Arriva il messaggio
initializedal Client - Server valida protocol version
- Server valida capabilities
- Se OK: connessione stabilita
- Se KO: connessione rifiutata
Fail fast: Se la validazione fallisce, terminare la connessione immediatamente. Non proseguire con una connessione in stato inconsistente.
4. Ready (Idle)
Cosa succede:
- Server ha completato handshake
- Attende richieste dal Client
- Può ricevere:
tools/list,tools/call,resources/list, ecc.
5. Processing
Cosa succede:
- Server riceve una richiesta (es:
tools/call) - Parse del JSON
- Validazione dello schema
- Esecuzione della funzione
- Generazione della risposta
- Invio della risposta
Dopo il processing, il Server torna in stato Ready.
6. Shutdown
Cosa succede:
- Client si disconnette, oppure
- Errore fatale, oppure
- Signal di terminazione (SIGTERM, SIGINT)
Cleanup necessario:
- Chiusura connessioni database
- Flush di log pendenti
- Chiusura di file aperti
- Rilascio di risorse
Senza shutdown corretto, potreste avere connessioni database lasciate aperte, log persi, file corrotti, o stato inconsistente. Vedremo l'implementazione concreta del graceful shutdown nei prossimi video della serie.
Critical Warnings
1. STDOUT è il Canale del Protocollo (stdio transport)
IL WARNING PIÙ IMPORTANTE: Su transport stdio, mai usare console.log().
Perché?
Su stdio:
- stdout = canale del protocollo JSON-RPC
- Il Client legge da stdout aspettandosi SOLO JSON valido
Se fate:
console.log("Server started!"); // ❌ MALE!Il Client riceve:
Server started!
{"jsonrpc":"2.0","id":1,"result":{...}}Il parser JSON riceve testo misto e crasha:
Error: Unexpected token 'S' at position 0Dal vostro punto di vista: "Il server è partito!"
Dal punto di vista del Client: "Protocollo corrotto, impossibile comunicare"
E il peggio: L'errore che vedete è sempre generico come:
JSON parse error
Unexpected token
Invalid protocol messageNiente che vi dica "hai usato console.log".
Dovete saperlo già.
Debugging Pratico
99% delle volte che vedete "JSON parse error":
- ✓ Primo posto dove guardare: console.log nel codice
- ✓ Secondo posto: console.log in dependencies
- ✓ Terzo posto: stdout da processi child
Come fare logging corretto:
// ❌ MALE - va su stdout
console.log("Processing request...");
// ✅ BENE - va su stderr
console.error("Processing request...");
// ✅ ANCORA MEGLIO - logger su file
logger.info("Processing request...");2. Never Trust Input (Even from AI)
Regola d'oro: Validare SEMPRE gli input, anche quelli provenienti dal Model.
Perché?
- Il Model può sbagliare i parametri
- Il Client può avere bug
- L'utente può manipolare i messaggi (in teoria)
Esempio di input invalido dal Model:
{
"name": "search_kb",
"arguments": {
"query": "test",
"maxResults": 1000 // Limite è 20!
}
}Senza validazione:
// ❌ Trust input
const results = await search(arguments.query, arguments.maxResults);
// → Cerca 1000 risultati, sovraccarica il sistemaCon validazione:
// ✅ Validate first
if (arguments.maxResults > 20) {
throw new InvalidParamsError(
`maxResults must be <= 20, got ${arguments.maxResults}`
);
}
const results = await search(arguments.query, arguments.maxResults);Nella serie vedremo come usare Zod per rendere la validazione dichiarativa e automatica. Per ora il concetto chiave è: mai fidarsi dell'input, nemmeno se viene dall'AI.
Conclusione
Model Context Protocol rappresenta un cambio di paradigma nel modo in cui integriamo le AI con i nostri sistemi. Non è solo un protocollo tecnico: è uno standard aperto che permette di costruire integrazioni robuste, portabili e manutenibili.
Cosa Abbiamo Visto
Il Problema: Integration hell causato dalla frammentazione delle integrazioni AI proprietarie.
La Soluzione: MCP come standard unificato per tutte le integrazioni.
L'Architettura: Separazione chiara tra Host, Client e Server con comunicazione basata su request-response e notifications.
I Quattro Pilastri: Resources (dati), Tools (azioni), Prompts (template), Transport (canale).
Il Protocollo: JSON-RPC 2.0 con tre messaggi fondamentali (initialize, tools/list, tools/call) più notifications per segnalazioni.
Il Lifecycle: Stati ben definiti da startup a shutdown.
Prossimi Passi
Questa guida vi ha fornito le fondamenta teoriche di MCP. I prossimi passi sono:
- Setup del Progetto: Inizializzare un progetto TypeScript con MCP SDK
- Primo Server: Implementare un server funzionante con un tool reale
- Testing: Testare l'integrazione con Claude Desktop
- Production: Deploy e monitoring in ambiente production
Questo articolo è parte di una serie completa su Model Context Protocol. Per video tutorial, esempi di codice e deployment guide, visita il canale.