In questa lezione iniziamo a costruire concretamente un MCP Server, cioè una componente fondamentale dell’ecosistema Model Context Protocol. Dopo aver compreso l’architettura, la comunicazione JSON-RPC e i principi di sicurezza e consenso, siamo finalmente in grado di vedere come un server MCP espone strumenti e dati a un modello linguistico in modo standardizzato, controllato e riutilizzabile. L’obiettivo è di comprendere come progettiamo un’interfaccia tra mondo dei modelli e mondo dei sistemi reali, come database, API e servizi locali.
Progettazione di un MCP Server
Quando implementiamo un MCP Server, partiamo sempre da un principio fondamentale, ovvero che tutto ciò che esponiamo deve essere descrivibile, invocabile e controllabile tramite il protocollo JSON-RPC. Questo significa che ogni "tool" non è altro che una funzione dichiarata, con input e output ben definiti, accessibile attraverso una chiamata standardizzata.
Per rendere il concetto concreto, immaginiamo di voler costruire un piccolo MCP Server che espone due strumenti: uno per ottenere la temperatura di una città e uno per sommare due numeri. Nonostante la semplicità, questo esempio ci permette di coprire l’intero ciclo di vita di un tool MCP: registrazione, discovery ed esecuzione.
Implementazione di un MCP Server con Node.js
Scegliamo Node.js per la sua semplicità e diffusione, ma lo stesso concetto sarebbe identico in Python, Go o altri linguaggi.
Iniziamo creando un server HTTP che riceve richieste JSON-RPC. Non stiamo ancora introducendo dipendenze MCP ufficiali, ma stiamo simulando il comportamento standard del protocollo.
import express from "express";
const app = express();
app.use(express.json());
// Database simulato per la temperatura
const weatherDB = {
rome: 22,
milan: 18,
naples: 26
};
// Dispatcher JSON-RPC
app.post("/mcp", (req, res) => {
const { id, method, params } = req.body;
if (method === "tools/list") {
return res.json({
jsonrpc: "2.0",
id,
result: {
tools: [
{
name: "get_temperature",
description: "Restituisce la temperatura di una città",
inputSchema: {
type: "object",
properties: {
city: { type: "string" }
},
required: ["city"]
}
},
{
name: "add_numbers",
description: "Somma due numeri interi",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}
]
}
});
}
if (method === "tools/call") {
const { name, arguments: args } = params;
if (name === "get_temperature") {
const city = args.city.toLowerCase();
const temp = weatherDB[city] ?? null;
return res.json({
jsonrpc: "2.0",
id,
result: {
content: temp !== null
? `La temperatura a ${city} è ${temp}°C`
: `Dati non disponibili per ${city}`
}
});
}
if (name === "add_numbers") {
const result = args.a + args.b;
return res.json({
jsonrpc: "2.0",
id,
result: {
content: `Risultato: ${result}`
}
});
}
}
return res.status(404).json({
jsonrpc: "2.0",
id,
error: {
code: -32601,
message: "Metodo non supportato"
}
});
});
app.listen(3000, () => {
console.log("MCP Server in esecuzione su http://localhost:3000/mcp");
});
Chiamata ed esecuzione
La chiamata tools/list rappresenta il momento in cui il client MCP interroga il server per capire quali strumenti sono disponibili. Questo passaggio è cruciale perché consente ai modelli di adattarsi dinamicamente alle capacità del sistema, senza hardcoding.
La chiamata tools/call invece rappresenta l’esecuzione effettiva. Qui il modello (tramite il client MCP) invia il nome dello strumento e i parametri, e il server restituisce un risultato strutturato.
Questa separazione tra scoperta ed esecuzione è uno degli elementi che rende MCP un protocollo robusto e scalabile.
Simulazione di una richiesta MCP
Per capire meglio il flusso, immaginiamo una richiesta JSON-RPC inviata al nostro server per ottenere la temperatura di Roma.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_temperature",
"arguments": {
"city": "Rome"
}
}
}
La risposta del server sarà:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": "La temperatura a rome è 22°C"
}
}
Questo formato standardizzato consente a qualsiasi client MCP di interpretare la risposta senza ambiguità, indipendentemente dall’implementazione interna del server.
Integrazione con dati reali e servizi esterni
Un MCP Server reale raramente lavora su dati statici come nel nostro esempio. Più spesso, siamo chiamati a integrare API esterne, database o sistemi aziendali.
Immaginiamo di voler sostituire il nostro dizionario statico con una chiamata a un servizio meteo esterno. In questo caso, il tool get_temperature diventerebbe una funzione asincrona che interroga una API REST.
async function fetchTemperature(city) {
const response = await fetch(`https://api.weather.example.com/${city}`);
const data = await response.json();
return data.temperature;
}
All’interno del nostro handler MCP, possiamo quindi integrare questa funzione trasformando il tool in un vero punto di accesso a dati esterni in tempo reale. In questo passaggio, il server non si limita più a restituire valori predefiniti, ma diventa responsabile di orchestrare la chiamata verso l’API, gestire eventuali errori e normalizzare la risposta in un formato coerente con il protocollo.
if (name === "get_temperature") {
const city = args.city.toLowerCase();
const temp = await fetchTemperature(city);
return res.json({
jsonrpc: "2.0",
id,
result: {
content: 'La temperatura a ${city} è ${temp}°C'
}
});
}
Conclusioni
Nel momento in cui passiamo da un’implementazione dimostrativa a un utilizzo reale, emerge con chiarezza un aspetto fondamentale: un MCP Server non può essere considerato soltanto come un semplice esecutore di funzioni, ma deve essere progettato tenendo conto dello stato e della scalabilità del sistema nel suo complesso. Nel nostro esempio abbiamo lavorato in un contesto completamente stateless, dove ogni richiesta è indipendente e non esiste memoria tra una chiamata e l’altra.
Questo approccio è utile per comprendere i meccanismi di base del protocollo, ma nella pratica i tool MCP interagiscono molto spesso con sessioni utente, contesti applicativi o dati temporanei che devono essere preservati nel tempo. Pensiamo, ad esempio, a un sistema che gestisce conversazioni multi-turno, oppure a un flusso operativo che richiede più passaggi consecutivi: in questi casi diventa necessario introdurre un livello di gestione dello stato.
Per risolvere questa esigenza, possiamo integrare componenti come sistemi di caching o database persistenti. Tecnologie come Redis consentono di mantenere uno stato leggero e veloce tra richieste successive, mentre database relazionali o NoSQL permettono di gestire informazioni strutturate e durature. È importante sottolineare che questa scelta non è imposta dal protocollo MCP: al contrario, MCP rimane volutamente agnostico rispetto alla gestione dello stato, lasciando a noi la libertà di definire l’architettura più adatta al contesto applicativo.
Questa flessibilità è uno dei punti di forza del protocollo, perché consente di adattarlo tanto a microservizi leggeri quanto a sistemi enterprise complessi. Nella prossima lezione entreremo nel mondo delle applicazioni reali, esplorando integrazioni avanzate e best practice.
Se vuoi aggiornamenti su Implementare un MCP Server e integrare strumenti e dati inserisci la tua email nel box qui sotto: