Vai al contenuto principale

MCP: Deep Dive dell'architettura e del protocollo

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 riscrivere

Aggiornamento API del provider:

- Schema cambiato
- Nuovi campi obbligatori
- Deprecazione di endpoint
→ Metà della logica di integrazione obsoleta

Nuova fonte dati da integrare:

- Formato diverso
- Autenticazione diversa
- Semantica diversa
→ Nuovo adattatore custom da zero

3. 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 → Server

Mai:

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 risultati

Questa 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 → Client

Questo è 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.md

Un 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/production

Record di configurazione del database di produzione esposti come Resource.

logs://system/errors/2024-02-15

Log 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 architettura

Quando un utente chiede "Come è strutturata la nostra architettura a microservizi?", il Model:

  1. Identifica che serve il documento
  2. Il Client richiede la Resource
  3. Il Server legge il file dal filesystem
  4. Ritorna il contenuto
  5. 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 operazioni

Esempi 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 punto

Perché è 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 roots
  • sampling: 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 eseguibili
  • resources: 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 capability

Questo è 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:

  1. Utente: "Come configuro il database di produzione?"
  2. Model analizza la richiesta
  3. Model decide: "Ho bisogno di cercare nella KB"
  4. Model guarda i tools disponibili
  5. Model identifica: search_kb è appropriato
  6. Model costruisce gli arguments
  7. Client traduce in JSON-RPC
  8. 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

  1. Server formatta e ritorna il result
  2. Client riceve la risposta
  3. Client passa il result al Model
  4. Model integra l'informazione nel suo contesto
  5. 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_kb

Il 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: 5

Il 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:

  1. La richiesta originale dell'utente
  2. 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 completa

Tempo 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 initialize dal 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 0

Dal 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 message

Niente che vi dica "hai usato console.log".

Dovete saperlo già.

Debugging Pratico

99% delle volte che vedete "JSON parse error":

  1. ✓ Primo posto dove guardare: console.log nel codice
  2. ✓ Secondo posto: console.log in dependencies
  3. ✓ 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 sistema

Con 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:

  1. Setup del Progetto: Inizializzare un progetto TypeScript con MCP SDK
  2. Primo Server: Implementare un server funzionante con un tool reale
  3. Testing: Testare l'integrazione con Claude Desktop
  4. 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.

Unisciti a WebTea

Niente spam. Solo contenuti formativi su Software Engineering.

Ci sono due cose che non ci piacciono: lo spam e il mancato rispetto della privacy. Seleziona come vuoi restare in contatto:

Preferenze di contatto

Usiamo Mailchimp come piattaforma di marketing. Cliccando su iscriviti, accetti la nostra privacy policy e che le tue informazioni vengano trasferite a Mailchimp per l'elaborazione. Termini e Privacy. Puoi disiscriverti in qualsiasi momento.