Cos’è il Model Context Protocol e perché interessa ai developer Node.js
I modelli di linguaggio sono bravi a ragionare e conversare, ma da soli non possono eseguire operazioni reali sui tuoi sistemi. Possono suggerire query SQL o chiamate API, ma non possono farle girare concretamente. Il Model Context Protocol (MCP) risolve questo limite: fornisce ai modelli AI un modo strutturato per interagire con i tuoi strumenti, dai database ai file, fino alle API esterne. Invece di generare testo su ciò che dovrebbe accadere, il modello può invocare funzioni che lo fanno davvero accadere.
In pratica, questo apre la strada a strumenti come chatbot che creano e cercano voci nel database, assistenti AI che interrogano tool interni o attivano workflow, e agenti che leggono file, eseguono comandi e restituiscono risultati reali.
In questo tutorial imparerai a costruire il tuo primo MCP server da zero con Node.js e TypeScript: partiremo da un sistema di note basato su file per capire i concetti fondamentali, poi passeremo a un backend MySQL per mostrare come un LLM possa guidare operazioni deterministiche. Entro la fine avrai un server MCP funzionante pronto per essere collegato al client AI che preferisci.
Come funziona MCP
MCP segue un modello client-server: l’applicazione AI fa da client, il tuo codice gira come server. In una configurazione tipica, il client (Claude Desktop, Claude Code, Cursor, ecc.) si interpone tra l’utente e il tuo server, inoltrandogli le richieste e restituendogli i risultati. Il modello stesso non chiama mai direttamente il tuo server: quando l’utente manda un messaggio, il client condivide col modello la lista dei tool esposti dal tuo server. Il modello decide quale tool chiamare (e con quali argomenti), il client esegue la chiamata e rimanda il risultato al modello.
Utente → Client MCP → Modello AI → Tool selezionato → Server MCP → Risposta
Prerequisiti
Per seguire questo tutorial ti serviranno:
- Node.js 18+
- Familiarità di base con TypeScript
- Un client compatibile con MCP per il test (Claude Desktop, Claude Code, Cursor, ecc.)
- MySQL installato localmente (solo per la sezione avanzata con database)
Costruire il server MCP: sistema di note su file
Iniziamo creando un nuovo progetto Node.js. Questo sarà un sistema di note basato su file, utile per comprendere i concetti prima di introdurre un database.
mkdir mcp-notes && cd mcp-notes
npm init -y
Installa le dipendenze necessarie: l’SDK MCP per costruire il server, Zod per la validazione degli input e TypeScript per la type safety:
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
Apri il package.json e aggiungi "type": "module" (l’SDK MCP usa i moduli ES) e gli script di build e start:
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Crea un file tsconfig.json nella root del progetto:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*"]
}
Scrivere il server
Crea il file src/index.ts con il codice base del server:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import fs from "fs/promises";
import path from "path";
import { z } from "zod";
const NOTES_DIR = path.join(process.cwd(), "notes");
await fs.mkdir(NOTES_DIR, { recursive: true });
const server = new McpServer({
name: "mcp-notes",
version: "1.0.0",
});
// I tool verranno aggiunti qui
const transport = new StdioServerTransport();
await server.connect(transport);
Questo è un MCP server completo e funzionante: crea la directory delle note, inizializza il server e lo connette tramite stdio per comunicare con un client MCP.
Aggiungere i tool
Ogni tool definisce una singola azione che il modello può compiere. Include nome, descrizione, schema di input e una funzione handler. Aggiungiamo i tre tool fondamentali: creazione, lettura e lista delle note.
server.tool(
"create_note",
"Create a new note with a given title and content",
{
title: z.string().min(1).describe("The note title"),
content: z.string().min(1).describe("The body of the note"),
},
async ({ title, content }) => {
const filename = `${title.replace(/[^a-z0-9_-]/gi, "_")}.txt`;
const filepath = path.join(NOTES_DIR, filename);
try {
await fs.access(filepath);
return {
content: [{ type: "text", text: `Error: note "${title}" already exists.` }],
isError: true,
};
} catch {}
await fs.writeFile(filepath, content, "utf-8");
return { content: [{ type: "text", text: `Note "${title}" created.` }] };
}
);
server.tool(
"read_note",
"Read the content of a note by its title",
{ title: z.string().min(1).describe("The title of the note to read") },
async ({ title }) => {
const filename = `${title.replace(/[^a-z0-9_-]/gi, "_")}.txt`;
try {
const content = await fs.readFile(path.join(NOTES_DIR, filename), "utf-8");
return { content: [{ type: "text", text: content }] };
} catch {
return {
content: [{ type: "text", text: `Error: note "${title}" not found.` }],
isError: true,
};
}
}
);
server.tool(
"list_notes",
"List all available notes",
{},
async () => {
const files = await fs.readdir(NOTES_DIR);
const notes = files.filter(f => f.endsWith(".txt")).map(f => f.replace(".txt", ""));
if (notes.length === 0) return { content: [{ type: "text", text: "No notes found." }] };
return { content: [{ type: "text", text: notes.join("\n") }] };
}
);
Testare il server con Claude Desktop
Compila il progetto con npm run build. Poi apri il file di configurazione di Claude Desktop:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Aggiungi il server sotto la chiave mcpServers:
{
"mcpServers": {
"mcp-notes": {
"command": "node",
"args": ["/percorso/del/progetto/mcp-notes/dist/index.js"],
"cwd": "/percorso/del/progetto/mcp-notes"
}
}
}
Riavvia Claude Desktop e prova con prompt come: “Crea una nota chiamata standup con gli aggiornamenti di oggi” oppure “Elenca tutte le mie note”.
Passare a MySQL: dati strutturati per uso reale
Il sistema basato su file funziona bene per comprendere i fondamentali, ma ha limiti evidenti. Passare a MySQL mette in luce un pattern importante nel design MCP: il modello decide quale azione intraprendere, ma il tuo codice rimane responsabile di come quella azione viene eseguita. Quando il modello chiama search_notes, non genera né esegue SQL da solo: il tuo handler gestisce l’operazione in modo controllato, con query parametrizzate.
Installa il driver MySQL:
npm install mysql2
Crea il database e la tabella:
CREATE DATABASE mcp_notes;
USE mcp_notes;
CREATE TABLE notes (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) UNIQUE NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
La versione MySQL del tool create_note inserisce una riga invece di scrivere un file e gestisce i duplicati intercettando l’errore ER_DUP_ENTRY. La versione di search_notes permette ricerche full-text su titoli e contenuti — una funzionalità che con i file richiederebbe un codice molto più complesso.
server.tool(
"search_notes",
"Search notes by keyword across titles and content",
{ query: z.string().min(1).describe("Keyword or phrase to search for") },
async ({ query }) => {
const like = `%${query}%`;
const [rows] = await pool.execute<mysql.RowDataPacket[]>(
"SELECT title, created_at FROM notes WHERE title LIKE ? OR content LIKE ? ORDER BY created_at DESC",
[like, like]
);
if (rows.length === 0) {
return { content: [{ type: "text", text: `No notes found matching "${query}".` }] };
}
return { content: [{ type: "text", text: rows.map(r => `- ${r.title} (${r.created_at})`).join("\n") }] };
}
);
Principi per progettare buoni tool MCP
Un tool MCP che funziona in fase di test può fallire in produzione se il modello fraintende quando o come usarlo. Alcune regole pratiche:
- Descrizioni esplicite: frasi come “Gestisce le note” sono troppo vaghe. Usa descrizioni che spiegano chiaramente cosa fa il tool e quando va usato.
- Singola responsabilità: ogni tool deve fare una sola cosa. Strumenti troppo “jolly” costringono il modello a indovinare l’intento.
- Errori azionabili: usa
isError: truecon messaggi che guidano il modello su come riprovare:"Note non trovata. Usa list_notes per vedere quelle disponibili." - Boundary sicuri: mai interpolazione diretta dell’input utente in SQL o comandi shell. Usa sempre query parametrizzate.
Conclusione
Costruire un MCP server con Node.js e TypeScript è sorprendentemente accessibile grazie all’SDK ufficiale. Il pattern che hai imparato in questo tutorial — definire tool con schema Zod, gestire gli errori con isError e connettere il server via stdio — si scala facilmente a scenari più complessi: integrazione di API REST, automazione di workflow, connessione di agenti AI a sistemi legacy.
Il codice completo del tutorial è disponibile su GitHub.
Fonte: How to build your first MCP server with Node.js — LogRocket Blog (Elijah Asaolu, 5 maggio 2026)