Costruire il Tuo Primo Server MCP
Il Model Context Protocol (MCP) è la cosa più vicina a un adattatore universale nell’ecosistema AI. Definisce come i modelli AI scoprono e chiamano gli strumenti, e come questi strumenti restituiscono risultati strutturati. Una volta che un server MCP è in esecuzione, qualsiasi client compatibile — Claude, Claude Code, o qualsiasi framework agenti che parla MCP — può usarlo senza codice collante aggiuntivo.
Questa guida illustra la costruzione di un vero server MCP da zero. Non un esempio giocattolo, ma un pattern che puoi estendere per l’uso in produzione.
Cos’è Davvero MCP
MCP è un protocollo JSON-RPC 2.0 su stdio (o HTTP+SSE per server remoti). Il client e il server si scambiano un piccolo insieme di tipi di messaggi:
tools/list— il client chiede quali strumenti sono disponibilitools/call— il client invoca uno strumento con argomentiresources/list/resources/read— il client legge file, database, o qualsiasi contesto esposto dal server
Il server dichiara i suoi strumenti in anticipo con un JSON Schema per l’input di ogni strumento. Il client usa questo schema per costruire chiamate valide. L’intera interazione è stateless dalla prospettiva del client — il server gestisce internamente qualsiasi stato necessario.
Questa semplicità è il punto. MCP non è un framework. È un contratto che ti permette di avvolgere qualsiasi cosa — un database Postgres, un cluster Kubernetes, un repository GitHub, un’API interna proprietaria — ed esporla come un insieme di funzioni tipizzate e chiamabili che qualsiasi modello AI può usare.
Configurazione
Avrai bisogno di Node.js 18+ e un progetto TypeScript. L’SDK MCP ufficiale rende il boilerplate minimale:
mkdir mcp-server && cd mcp-servernpm init -ynpm install @modelcontextprotocol/sdk zodnpm install -D typescript @types/node tsxAggiungi un tsconfig.json:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "outDir": "./dist" }, "include": ["src"]}E uno script di avvio in package.json:
{ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "start": "node dist/index.js" }}Costruire il Server
Crea src/index.ts. La struttura è sempre la stessa: inizializza il server, registra gli strumenti, connetti al trasporto stdio.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";
const server = new McpServer({ name: "my-mcp-server", version: "1.0.0",});Ora registra gli strumenti. Ogni strumento ha un nome, una descrizione che il modello usa per decidere quando chiamarlo, e uno schema di input:
server.tool( "get_weather", "Get current weather conditions for a city", { city: z.string().describe("The city name to get weather for"), units: z.enum(["celsius", "fahrenheit"]).default("celsius"), }, async ({ city, units }) => { // Your actual implementation here const data = await fetchWeather(city, units); return { content: [ { type: "text", text: `Weather in ${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`, }, ], }; });Connetti il server al trasporto stdio:
const transport = new StdioServerTransport();await server.connect(transport);Questo è lo scheletro completo del server. Tutto il resto è aggiungere strumenti.
Pattern di Progettazione degli Strumenti
La qualità del tuo server MCP dipende da quanto bene progetti gli strumenti. Alcuni pattern che contano:
Strumenti stretti e componibili rispetto a monoliti ampi
Uno strumento che fa una cosa è più utile di uno che ne fa molte. Il modello può combinare strumenti stretti in modi inaspettati; uno strumento ampio lo limita.
Evita questo:
server.tool("manage_database", "Do anything with the database", { operation: z.enum(["read", "write", "delete", "schema"]), query: z.string(), // ... many optional params})Preferisci questo:
server.tool("query_records", "Run a SELECT query and return rows as JSON", { table: z.string(), where: z.string().optional().describe("SQL WHERE clause, e.g. 'status = active'"), limit: z.number().default(50),})
server.tool("get_schema", "Return the column definitions for a table", { table: z.string(),})Descrivi gli input con precisione
Il modello legge le tue stringhe describe() per sapere cosa passare. Trattale come documentazione per uno sviluppatore che non ha mai visto il tuo codebase:
{ date_range: z.string().describe( "ISO 8601 date range in the format YYYY-MM-DD/YYYY-MM-DD. " + "Example: 2025-01-01/2025-03-31" ),}Descrizioni vaghe producono input errati. Descrizioni precise producono quelli corretti.
Restituisci testo strutturato, non JSON grezzo
I modelli analizzano il testo più affidabilmente dei blob JSON grezzi nei risultati degli strumenti. Formatta il tuo output:
return { content: [ { type: "text", text: [ `Found ${rows.length} records:`, ...rows.map(r => `- ${r.id}: ${r.name} (${r.status})`), ].join("\n"), }, ],};Se il chiamante ha genuinamente bisogno di dati strutturati per l’elaborazione downstream, incorpora JSON all’interno di un blocco di testo chiaramente etichettato.
Gestione degli errori
Restituisci gli errori come risultati degli strumenti, non eccezioni lanciate. L’SDK MCP traduce le eccezioni lanciate in errori a livello di protocollo, che i client gestiscono diversamente dagli errori a livello di strumento:
async ({ table, where }) => { try { const rows = await db.query(table, where); return { content: [{ type: "text", text: formatRows(rows) }] }; } catch (err) { return { content: [ { type: "text", text: `Query failed: ${err instanceof Error ? err.message : "unknown error"}. ` + `Check that the table name is correct and the WHERE clause uses valid column names.`, }, ], isError: true, }; }}Il flag isError: true segnala al client che il risultato rappresenta un fallimento, permettendo all’agente di decidere come recuperare.
Un Esempio Reale: Server di Strumenti GitHub
Ecco uno strumento concreto che avvolge le GitHub API per permettere a un agente di leggere i contenuti del repository:
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
server.tool( "get_file_contents", "Read the contents of a file from a GitHub repository", { owner: z.string().describe("Repository owner (username or org)"), repo: z.string().describe("Repository name"), path: z.string().describe("File path within the repository"), ref: z.string().optional().describe("Branch, tag, or commit SHA. Defaults to the default branch."), }, async ({ owner, repo, path, ref }) => { try { const { data } = await octokit.rest.repos.getContent({ owner, repo, path, ref, });
if (Array.isArray(data)) { // It's a directory — return listing const listing = data.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.name}`).join("\n"); return { content: [{ type: "text", text: `Directory listing for ${path}:\n${listing}` }], }; }
if (data.type !== "file" || !data.content) { return { content: [{ type: "text", text: `${path} is not a readable file` }], isError: true, }; }
const content = Buffer.from(data.content, "base64").toString("utf-8"); return { content: [ { type: "text", text: `Contents of ${owner}/${repo}/${path}:\n\`\`\`\n${content}\n\`\`\``, }, ], }; } catch (err: unknown) { const message = err instanceof Error ? err.message : "Unknown error"; return { content: [{ type: "text", text: `Failed to read ${path}: ${message}` }], isError: true, }; } });Questo è uno strumento completo e pronto per la produzione. Gestisce directory, errori e codifica — e avvolge il risultato in testo che un modello può facilmente leggere e citare.
Connessione a Claude Desktop
Una volta che il tuo server è in esecuzione, connettilo a Claude Desktop modificando la sua configurazione in ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) o %APPDATA%\Claude\claude_desktop_config.json (Windows):
{ "mcpServers": { "my-server": { "command": "node", "args": ["/absolute/path/to/dist/index.js"], "env": { "GITHUB_TOKEN": "your_token_here" } } }}Riavvia Claude Desktop. I tuoi strumenti appaiono nel selettore di strumenti automaticamente.
Per lo sviluppo, usa tsx direttamente:
{ "mcpServers": { "my-server-dev": { "command": "npx", "args": ["tsx", "/absolute/path/to/src/index.ts"] } }}Connessione a Claude Code
I server MCP si connettono a Claude Code tramite CLI:
claude mcp add my-server node /absolute/path/to/dist/index.jsO con variabili d’ambiente:
claude mcp add my-server --env GITHUB_TOKEN=your_token node /path/to/dist/index.jsClaude Code userà automaticamente i tuoi strumenti quando lavora su compiti che ne traggono beneficio. Non è necessario alcun prompt aggiuntivo.
Cosa Costruire Dopo
Un server MCP è essenzialmente un confine di capacità: cosa può vedere e fare l’agente? La domanda di progettazione è sempre quali capacità esporre e come limitarle in modo sicuro.
Alcune direzioni che vale la pena esplorare:
- Server di database — Esponi accesso a query di sola lettura a Postgres, SQLite o DynamoDB con strumenti di introspezione dello schema in modo che l’agente possa scoprire la struttura prima di eseguire query
- Server di analisi del codice — Avvolgi tree-sitter o un protocollo di language server per dare agli agenti una comprensione semantica dei codebase oltre alla semplice lettura di file
- Server di monitoring — Collega sistemi di metriche (Prometheus, Datadog) in modo che gli agenti possano investigare incidenti con dati di osservabilità reali
- Server di documentazione — Indicizza e servi documenti interni o runbook, permettendo agli agenti di rispondere a domande operative senza dover sapere dove si trovano le cose
L’ecosistema MCP è ancora agli inizi. La maggior parte dei server utili non è ancora stata costruita. Il pattern è abbastanza semplice che qualsiasi cosa a cui accedi attualmente tramite una dashboard o un’API è un candidato ragionevole per un server MCP.
Costruisci quello che risolve un problema reale nel tuo ambiente, e il protocollo si occuperà del resto.
Articoli Correlati
- Introduzione allo Sviluppo Agentico
- Pattern di Utilizzo degli Strumenti: Interfacce Agente-Strumento Affidabili
- Pattern Multi-Agente: Orchestratori, Worker e Pipeline