Salta al contenuto principale

04 - Creare il Primo Server MCP in TypeScript

Guida passo-passo per creare un server MCP: setup progetto, definizione tools con Zod, handler e connessione con Claude Desktop.

Cosa ne pensi di questo articolo?

04 - Creare il Primo Server MCP in TypeScript
MCPFoto:SiddePexels

In Breve

Guida passo-passo per creare un server MCP: setup progetto, definizione tools con Zod, handler e connessione con Claude Desktop.

Introduzione: Dal Protocollo al Codice

Negli articoli precedenti abbiamo esplorato i fondamenti del Model Context Protocol, le tre primitive (Tools, Resources, Prompts) e l'architettura monorepo. Ora e il momento di scrivere codice: in questo tutorial costruiremo un server MCP completo in TypeScript, partendo da un progetto vuoto fino ad arrivare a un server funzionante testabile con Claude Desktop.

Il server che creeremo gestira un sistema di note in memoria, esponendo tool per aggiungere, leggere, cercare e cancellare note. Implementeremo validazione avanzata con Zod, gestione errori strutturata e una risorsa per accedere alle note via URI. Tutto il codice e disponibile nel repository Tech-MCP su GitHub.

Cosa Imparerai in Questo Articolo

  • Come configurare un progetto TypeScript per un server MCP (package.json, tsconfig.json)
  • Come creare un server con la classe McpServer dell'SDK ufficiale
  • Come registrare tool con server.tool() e validare input con schemi Zod
  • Come esporre risorse con server.resource() e URI template
  • Come gestire errori con il pattern isError: true
  • Come configurare il transport StdioServerTransport
  • Come testare il server con MCP Inspector e Claude Desktop
  • Regole di debugging: perchè non usare mai console.log()

Prerequisiti

Prima di iniziare, assicurati di avere installato i seguenti strumenti sul tuo sistema:

  • Node.js 18+: runtime JavaScript/TypeScript (node --version per verificare)
  • npm o pnpm: package manager per gestire le dipendenze
  • TypeScript 5.x: il linguaggio tipizzato che useremo per tutto il server
  • Claude Desktop (opzionale): per testare il server con un client MCP reale

Step 1: Inizializzazione del Progetto

Creiamo la struttura del progetto da zero. Il nostro server si chiamara mcp-notes-server e sarà un progetto Node.js con TypeScript.

Creazione della Directory e Inizializzazione


# Crea la directory del progetto
mkdir mcp-notes-server && cd mcp-notes-server

# Inizializza il progetto Node.js
npm init -y

# Installa le dipendenze di produzione
npm install @modelcontextprotocol/sdk zod

# Installa le dipendenze di sviluppo
npm install -D typescript @types/node

# Crea la directory per il codice sorgente
mkdir -p src
    

Le dipendenze installate hanno ruoli specifici:

  • @modelcontextprotocol/sdk: l'SDK ufficiale MCP che fornisce McpServer, i transport e le utility del protocollo
  • zod: libreria di validazione schema che MCP usa per definire e validare i parametri dei tool
  • typescript: compilatore TypeScript per la tipizzazione statica
  • @types/node: definizioni di tipo per le API di Node.js

Configurazione package.json

Aggiorna il file package.json generato con la configurazione necessaria per un server MCP. Il campo "type": "module" e fondamentale perchè l'SDK MCP utilizza ESM (ECMAScript Modules):


{
  "name": "mcp-notes-server",
  "version": "1.0.0",
  "description": "Server MCP per la gestione di note",
  "type": "module",
  "main": "dist/index.js",
  "bin": {
    "mcp-notes": "dist/index.js"
  },
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "start": "node dist/index.js",
    "inspect": "npx @modelcontextprotocol/inspector dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "zod": "^3.22.0"
  },
  "devDependencies": {
    "typescript": "^5.5.0",
    "@types/node": "^20.0.0"
  }
}
    

Nota sul Campo "type": "module"

Il campo "type": "module" e obbligatorio. Senza di esso, Node.js trattera i file .js come CommonJS e l'import dell'SDK MCP fallirà con un errore di sintassi. Se dimentichi questo campo, vedrai un errore come SyntaxError: Cannot use import statement in a module.

Configurazione TypeScript (tsconfig.json)

Crea il file tsconfig.json nella root del progetto. La configurazione deve essere compatibile con ESM e Node.js 16+:


{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": true
  },
  "include": ["src"],
  "exclude": ["node_modules"]
}
    

I campi più importanti della configurazione sono:

  • target: "ES2022": compila verso una versione moderna di JavaScript con supporto per top-level await
  • module: "Node16": genera moduli compatibili con il sistema ESM di Node.js 16+
  • moduleResolution: "Node16": risolve i moduli secondo le regole di Node.js 16 (richiede estensioni .js negli import)
  • strict: true: abilita tutti i controlli di tipo rigorosi per maggiore sicurezza
  • declaration: true: genera file .d.ts per il type checking esterno

Struttura Finale del Progetto

Alla fine del setup, la struttura del progetto sarà la seguente:


mcp-notes-server/
  src/
    index.ts          # Entry point e logica del server
  package.json        # Configurazione npm con type: module
  tsconfig.json       # Configurazione TypeScript
  node_modules/       # Dipendenze installate
    

Step 2: Creare il Server MCP Minimo

Iniziamo con un server minimo che registra un singolo tool. Crea il file src/index.ts:


#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// ============================================
// Stato in memoria
// ============================================
const notes: Map<string, string> = new Map();

// ============================================
// Creazione del server MCP
// ============================================
const server = new McpServer({
  name: "notes-server",
  version: "1.0.0",
});

// ============================================
// Tool: aggiungere una nota
// ============================================
server.tool(
  "add-note",
  "Aggiunge una nuova nota con titolo e contenuto",
  {
    title: z.string().describe("Titolo della nota"),
    content: z.string().describe("Contenuto della nota"),
  },
  async ({ title, content }) => {
    notes.set(title, content);
    return {
      content: [
        {
          type: "text",
          text: `Nota "${title}" salvata con successo.`,
        },
      ],
    };
  },
);

// ============================================
// Avvio del server con transport STDIO
// ============================================
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Notes MCP Server avviato su stdio");
}

main().catch((error) => {
  console.error("Errore fatale:", error);
  process.exit(1);
});
    

Anatomia del Codice

Analizziamo ogni sezione del codice per comprendere il ruolo di ciascun componente.

1. Lo Shebang e gli Import


#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
    

Lo shebang (#!/usr/bin/env node) permette di eseguire il file direttamente come script dalla riga di comando. Gli import caricano tre componenti essenziali:

  • McpServer: la classe principale che gestisce il lifecycle del protocollo, la registrazione dei tool e la negoziazione delle capability
  • StdioServerTransport: il transport che comunica tramite stdin/stdout del processo
  • z (Zod): la libreria di validazione per definire gli schemi dei parametri

2. Creazione dell'Istanza Server


const server = new McpServer({
  name: "notes-server",    // Identificativo univoco del server
  version: "1.0.0",        // Versione seguendo il formato semver
});
    

L'istanza McpServer accetta un oggetto di configurazione con name (identificativo univoco che il client usa per riconoscere il server) e version (la versione in formato semver). Questi dati vengono inviati al client durante la fase di inizializzazione del protocollo.

3. La Firma di server.tool()

Il metodo server.tool() e il cuore della registrazione dei tool. Accetta quattro argomenti:

Argomenti di server.tool()

Argomento Tipo Descrizione
name string Identificativo univoco del tool (es. "add-note")
description string Descrizione letta dal modello AI per decidere quando invocare il tool
inputSchema object Oggetto con chiavi Zod che definisce i parametri accettati
handler async function Funzione asincrona che riceve i parametri tipizzati e ritorna il risultato

4. Il Formato del Risultato

Ogni tool handler deve ritornare un oggetto con un campo content, che e un array di elementi tipizzati. MCP supporta tre tipi di contenuto nel risultato:


// Tipo 1: Testo semplice
return {
  content: [
    { type: "text", text: "Il risultato dell'operazione" }
  ],
};

// Tipo 2: Immagine (base64)
return {
  content: [
    { type: "image", data: "iVBORw0KGgo...", mimeType: "image/png" }
  ],
};

// Tipo 3: Risorsa embedded
return {
  content: [
    { type: "resource", resource: { uri: "note://lista", text: "..." } }
  ],
};
    

5. Il Transport STDIO


const transport = new StdioServerTransport();
await server.connect(transport);
    

StdioServerTransport configura il server per comunicare tramite stdin (ricezione messaggi dal client) e stdout (invio risposte al client). Il metodo connect() avvia il loop di lettura/scrittura e da quel momento il server e in ascolto di messaggi JSON-RPC.

Regola Fondamentale: Mai Scrivere su stdout

Con il transport STDIO, il canale stdout e riservato esclusivamente ai messaggi JSON-RPC del protocollo. Qualsiasi altro output su stdout (come un console.log()) corromperebbe il protocollo e causerebbe errori di parsing nel client. Per il logging e il debugging, usa sempre console.error(), che scrive su stderr.

Step 3: Aggiungere Tool Completi (CRUD)

Espandiamo il server con un set completo di operazioni CRUD sulle note. Aggiungi i seguenti tool dopo il tool add-note nel file src/index.ts:

Tool: Leggere una Nota


server.tool(
  "get-note",
  "Recupera il contenuto di una nota dal titolo",
  {
    title: z.string().describe("Titolo della nota da leggere"),
  },
  async ({ title }) => {
    const content = notes.get(title);
    if (!content) {
      return {
        content: [{ type: "text", text: `Nota "${title}" non trovata.` }],
        isError: true,
      };
    }
    return {
      content: [{ type: "text", text: content }],
    };
  },
);
    

Tool: Elencare Tutte le Note


server.tool(
  "list-notes",
  "Elenca tutte le note salvate con i rispettivi titoli",
  {},  // Nessun parametro richiesto: schema vuoto
  async () => {
    if (notes.size === 0) {
      return {
        content: [{ type: "text", text: "Nessuna nota salvata." }],
      };
    }
    const list = Array.from(notes.entries())
      .map(([title, content], i) =>
        `${i + 1}. ${title} (${content.length} caratteri)`
      )
      .join("\n");
    return {
      content: [{ type: "text", text: `Note salvate:\n${list}` }],
    };
  },
);
    

Tool: Cancellare una Nota


server.tool(
  "delete-note",
  "Cancella una nota esistente dal titolo",
  {
    title: z.string().describe("Titolo della nota da cancellare"),
  },
  async ({ title }) => {
    const deleted = notes.delete(title);
    if (!deleted) {
      return {
        content: [{ type: "text", text: `Nota "${title}" non trovata.` }],
        isError: true,
      };
    }
    return {
      content: [{ type: "text", text: `Nota "${title}" cancellata con successo.` }],
    };
  },
);
    

Step 4: Validazione Avanzata con Zod

Zod non e solo una libreria di validazione: e il linguaggio con cui descrivi i parametri dei tuoi tool al modello AI. Ogni schema Zod viene convertito in JSON Schema e inviato al client durante la fase di discovery (tools/list). Il metodo .describe() e particolarmente importante perchè il testo viene incluso nello schema e aiuta l'AI a capire cosa fornire come argomento.

Aggiungiamo un tool di ricerca con validazione avanzata per dimostrare le capacità di Zod:


server.tool(
  "search-notes",
  "Cerca note per parola chiave con opzioni di filtro avanzate",
  {
    query: z.string()
      .min(2)
      .describe("Testo da cercare (minimo 2 caratteri)"),
    caseSensitive: z.boolean()
      .optional()
      .default(false)
      .describe("Se true, la ricerca e case-sensitive"),
    limit: z.number()
      .int()
      .min(1)
      .max(100)
      .optional()
      .default(10)
      .describe("Numero massimo di risultati (1-100)"),
  },
  async ({ query, caseSensitive, limit }) => {
    const results: string[] = [];
    for (const [title, content] of notes) {
      const haystack = caseSensitive ? content : content.toLowerCase();
      const needle = caseSensitive ? query : query.toLowerCase();
      if (haystack.includes(needle)) {
        results.push(title);
      }
      if (results.length >= limit) break;
    }

    return {
      content: [{
        type: "text",
        text: results.length > 0
          ? `Trovate ${results.length} note:\n${results.join("\n")}`
          : `Nessun risultato per "${query}".`,
      }],
    };
  },
);
    

Pattern Zod Comuni per MCP

Ecco un riferimento rapido dei pattern Zod più utilizzati nella definizione degli schemi dei tool MCP:

Riferimento Rapido Zod per MCP

Pattern Codice Uso
Stringa obbligatoria z.string() Parametro testuale richiesto
Stringa non vuota z.string().min(1) Parametro che non può essere vuoto
Email valida z.string().email() Validazione formato email
URL valido z.string().url() Validazione formato URL
Intero positivo z.number().int().positive() ID, contatori
Range numerico z.number().min(0).max(100) Percentuali, punteggi
Booleano z.boolean() Flag on/off
Enum di stringhe z.enum(["low", "medium", "high"]) Valori predefiniti
Opzionale con default z.string().optional().default("valore") Parametri con valore predefinito
Array di stringhe z.array(z.string()) Liste, tag
Descrizione per l'AI z.string().describe("spiegazione") Guida il modello AI nella compilazione

Il metodo .describe() e cruciale per l'esperienza con l'AI: il testo viene incluso nello schema JSON inviato al modello durante il discovery, permettendogli di capire esattamente cosa fornire come valore per ogni parametro. Scrivi descrizioni chiare e concise.

Step 5: Gestione Errori con isError

MCP definisce un pattern chiaro per la gestione degli errori a livello di tool. Esistono due categorie di errori da gestire:

Errori di Esecuzione (Gestiti dal Tool)

Quando un tool incontra un errore prevedibile (risorsa non trovata, validazione fallita, servizio non disponibile), deve ritornare un risultato con isError: true. Questo segnala al modello AI che l'operazione e fallita, permettendogli di reagire in modo appropriato:


server.tool(
  "get-note",
  "Recupera una nota dal titolo",
  {
    title: z.string().describe("Titolo della nota"),
  },
  async ({ title }) => {
    try {
      const content = notes.get(title);
      if (!content) {
        // Errore prevedibile: nota non trovata
        return {
          content: [{
            type: "text",
            text: `Errore: la nota "${title}" non esiste.`,
          }],
          isError: true,
        };
      }
      return {
        content: [{ type: "text", text: content }],
      };
    } catch (error) {
      // Errore imprevisto: catturato e gestito
      return {
        content: [{
          type: "text",
          text: `Errore: ${error instanceof Error ? error.message : String(error)}`,
        }],
        isError: true,
      };
    }
  },
);
    

Errori di Protocollo (Eccezioni Non Gestite)

Se un tool handler lancia un'eccezione non gestita, l'SDK la trasforma automaticamente in un errore JSON-RPC con codice -32603 (Internal Error). E buona pratica gestire sempre gli errori esplicitamente con try/catch per fornire messaggi d'errore più informativi al modello.

Best Practice: Gestisci Sempre gli Errori

Il pattern consigliato e avvolgere tutta la logica del tool in un blocco try/catch, ritornando isError: true con un messaggio descrittivo in caso di errore. Questo permette al modello AI di ricevere l'errore come contesto e decidere come procedere (riprovare, chiedere all'utente, o provare un approccio diverso).

Step 6: Esporre Risorse con server.resource()

Oltre ai tool, un server MCP può esporre risorse: dati contestuali accessibili dal client tramite URI. Le risorse sono utili per fornire informazioni di sola lettura senza richiedere un'azione esplicita del modello.

Aggiungiamo una risorsa che espone l'elenco completo delle note:


import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";

// Risorsa: elenco di tutte le note
server.resource(
  "notes-list",
  "note://list",
  "Elenco completo di tutte le note salvate",
  async (uri) => {
    const allNotes = Array.from(notes.entries())
      .map(([title, content]) => `## ${title}\n${content}`)
      .join("\n\n---\n\n");

    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "text/markdown",
          text: allNotes || "Nessuna nota disponibile.",
        },
      ],
    };
  },
);

// Risorsa con template URI: singola nota
server.resource(
  "note-by-title",
  new ResourceTemplate("note://{title}", { list: undefined }),
  "Accede a una singola nota tramite il titolo",
  async (uri, { title }) => {
    const content = notes.get(title as string);
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "text/plain",
          text: content ?? `Nota "${title}" non trovata.`,
        },
      ],
    };
  },
);
    

La differenza tra tool e risorse e fondamentale nel protocollo MCP:

  • Tools: azioni invocate dal modello AI, possono avere effetti collaterali (creare, modificare, cancellare dati)
  • Resources: dati di sola lettura accessibili dall'applicazione client, non hanno effetti collaterali

Step 7: Il Server Completo

Ecco il codice completo del server con tutti i tool, le risorse e la gestione degli errori. Questo e il file src/index.ts finale:


#!/usr/bin/env node

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// ============================================
// Stato in memoria
// ============================================
const notes: Map<string, string> = new Map();

// ============================================
// Creazione del server MCP
// ============================================
const server = new McpServer({
  name: "notes-server",
  version: "1.0.0",
});

// ============================================
// TOOL: Aggiungere una nota
// ============================================
server.tool(
  "add-note",
  "Aggiunge una nuova nota con titolo e contenuto",
  {
    title: z.string().min(1).describe("Titolo della nota"),
    content: z.string().min(1).describe("Contenuto della nota"),
  },
  async ({ title, content }) => {
    if (notes.has(title)) {
      return {
        content: [{
          type: "text",
          text: `Nota "${title}" già esistente. Usa un titolo diverso.`,
        }],
        isError: true,
      };
    }
    notes.set(title, content);
    return {
      content: [{
        type: "text",
        text: `Nota "${title}" salvata con successo (${content.length} caratteri).`,
      }],
    };
  },
);

// ============================================
// TOOL: Leggere una nota
// ============================================
server.tool(
  "get-note",
  "Recupera il contenuto di una nota dal titolo",
  {
    title: z.string().describe("Titolo della nota da leggere"),
  },
  async ({ title }) => {
    const content = notes.get(title);
    if (!content) {
      return {
        content: [{ type: "text", text: `Nota "${title}" non trovata.` }],
        isError: true,
      };
    }
    return {
      content: [{ type: "text", text: content }],
    };
  },
);

// ============================================
// TOOL: Elencare tutte le note
// ============================================
server.tool(
  "list-notes",
  "Elenca tutte le note salvate con i rispettivi titoli",
  {},
  async () => {
    if (notes.size === 0) {
      return {
        content: [{ type: "text", text: "Nessuna nota salvata." }],
      };
    }
    const list = Array.from(notes.entries())
      .map(([title, content], i) =>
        `${i + 1}. ${title} (${content.length} caratteri)`
      )
      .join("\n");
    return {
      content: [{ type: "text", text: `Note salvate:\n${list}` }],
    };
  },
);

// ============================================
// TOOL: Cancellare una nota
// ============================================
server.tool(
  "delete-note",
  "Cancella una nota esistente dal titolo",
  {
    title: z.string().describe("Titolo della nota da cancellare"),
  },
  async ({ title }) => {
    const deleted = notes.delete(title);
    if (!deleted) {
      return {
        content: [{ type: "text", text: `Nota "${title}" non trovata.` }],
        isError: true,
      };
    }
    return {
      content: [{ type: "text", text: `Nota "${title}" cancellata.` }],
    };
  },
);

// ============================================
// TOOL: Cercare note con filtri avanzati
// ============================================
server.tool(
  "search-notes",
  "Cerca note per parola chiave con opzioni di filtro",
  {
    query: z.string().min(2).describe("Testo da cercare (minimo 2 caratteri)"),
    caseSensitive: z.boolean().optional().default(false)
      .describe("Se true, la ricerca e case-sensitive"),
    limit: z.number().int().min(1).max(100).optional().default(10)
      .describe("Numero massimo di risultati (1-100)"),
  },
  async ({ query, caseSensitive, limit }) => {
    const results: string[] = [];
    for (const [title, content] of notes) {
      const haystack = caseSensitive ? content : content.toLowerCase();
      const needle = caseSensitive ? query : query.toLowerCase();
      if (haystack.includes(needle)) {
        results.push(title);
      }
      if (results.length >= limit) break;
    }

    return {
      content: [{
        type: "text",
        text: results.length > 0
          ? `Trovate ${results.length} note:\n${results.join("\n")}`
          : `Nessun risultato per "${query}".`,
      }],
    };
  },
);

// ============================================
// RISORSA: Elenco completo note
// ============================================
server.resource(
  "notes-list",
  "note://list",
  "Elenco completo di tutte le note salvate",
  async (uri) => {
    const allNotes = Array.from(notes.entries())
      .map(([title, content]) => `## ${title}\n${content}`)
      .join("\n\n---\n\n");
    return {
      contents: [{
        uri: uri.href,
        mimeType: "text/markdown",
        text: allNotes || "Nessuna nota disponibile.",
      }],
    };
  },
);

// ============================================
// Avvio del server con transport STDIO
// ============================================
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Notes MCP Server avviato su stdio");
}

main().catch((error) => {
  console.error("Errore fatale:", error);
  process.exit(1);
});
    

Step 8: Build e Test con MCP Inspector

Compila il progetto e testa il server con l'MCP Inspector, lo strumento ufficiale di debug per server MCP:


# Compila il progetto TypeScript
npm run build

# Testa con MCP Inspector (apre un'interfaccia web)
npx @modelcontextprotocol/inspector dist/index.js
    

L'MCP Inspector apre un'interfaccia web nel browser dove puoi:

  • Visualizzare tutti i tool registrati con i rispettivi schemi
  • Invocare i tool manualmente fornendo parametri di test
  • Verificare il formato delle risposte
  • Testare le risorse tramite URI
  • Monitorare i messaggi JSON-RPC scambiati

Step 9: Configurazione Claude Desktop

Per utilizzare il server con Claude Desktop (il client MCP più diffuso), aggiungi la configurazione nel file appropriato per il tuo sistema operativo:

Percorsi del File di Configurazione

Sistema Operativo Percorso
macOS ~/Library/Application Support/Claude/claude_desktop_config.json
Windows %APPDATA%\Claude\claude_desktop_config.json
Linux ~/.config/claude/claude_desktop_config.json

Aggiungi la configurazione del server al file JSON:


{
  "mcpServers": {
    "notes": {
      "command": "node",
      "args": ["/percorso/assoluto/mcp-notes-server/dist/index.js"]
    }
  }
}
    

Dopo aver salvato il file e riavviato Claude Desktop, il modello vedra i tool del server notes e potra invocarli automaticamente durante le conversazioni. Puoi verificare che il server sia connesso correttamente cercando l'icona dei tool nella barra inferiore di Claude Desktop.

Aggiungere Variabili d'Ambiente

Se il tuo server ha bisogno di variabili d'ambiente (ad esempio per API key o configurazione), puoi aggiungerle nella configurazione con il campo env:


{
  "mcpServers": {
    "notes": {
      "command": "node",
      "args": ["/percorso/assoluto/mcp-notes-server/dist/index.js"],
      "env": {
        "STORAGE_PATH": "/home/utente/notes",
        "MAX_NOTES": "1000"
      }
    }
  }
}
    

Debugging: Regole e Suggerimenti

Il debugging di un server MCP presenta sfide uniche rispetto alle applicazioni tradizionali. Ecco le regole fondamentali e i suggerimenti per risolvere i problemi più comuni:

Regola 1: Mai Usare console.log()

Con il transport STDIO, console.log() scrive su stdout, che e il canale riservato ai messaggi JSON-RPC. Un singolo console.log() può corrompere l'intero protocollo. Usa sempre console.error() per il debugging:


// SBAGLIATO - corrompe il protocollo STDIO
console.log("Debug: nota aggiunta");

// CORRETTO - scrive su stderr, non interferisce
console.error("Debug: nota aggiunta");
console.error("[DEBUG]", JSON.stringify({ title, content }));
    

Regola 2: Verifica il Build

Un errore comune e dimenticare di ricompilare dopo le modifiche. Usa npm run dev per la compilazione automatica in modalità watch:


# Compilazione automatica ad ogni modifica
npm run dev

# In un altro terminale, testa con l'inspector
npx @modelcontextprotocol/inspector dist/index.js
    

Regola 3: Controlla i Log del Server

I messaggi scritti su stderr sono visibili nel terminale di Claude Desktop (Sviluppatore > Apri Console) e nell'MCP Inspector. Usa log strutturati per facilitare il debug:


async ({ title, content }) => {
  console.error(`[add-note] Ricevuta richiesta: title=${title}`);
  notes.set(title, content);
  console.error(`[add-note] Nota salvata. Totale note: ${notes.size}`);
  return {
    content: [{ type: "text", text: `Nota "${title}" salvata.` }],
  };
}
    

Checklist di Debugging

Problema Causa Probabile Soluzione
Server non si avvia Manca "type": "module" in package.json Aggiungi il campo al package.json
Import non trovato Manca .js nell'import path Aggiungi l'estensione: "...mcp.js"
Claude non vede i tool Percorso errato nel config JSON Usa un percorso assoluto nel campo args
Errore di protocollo console.log() nel codice Sostituisci con console.error()
Parametri non validati Schema Zod incompleto Aggiungi .describe() a ogni campo

Riepilogo

In questo articolo abbiamo costruito un server MCP completo partendo da zero. Ecco cosa abbiamo coperto:

  1. Setup del progetto: inizializzazione npm, configurazione TypeScript con ESM, struttura delle cartelle
  2. McpServer: creazione dell'istanza server con nome e versione
  3. server.tool(): registrazione di tool con nome, descrizione, schema Zod e handler asincrono
  4. Validazione Zod: pattern avanzati (min, max, enum, optional, default, describe) per definire schemi espressivi
  5. Error handling: pattern isError: true per errori gestiti e try/catch per eccezioni impreviste
  6. server.resource(): esposizione di dati contestuali tramite URI e template URI
  7. StdioServerTransport: configurazione del transport per comunicazione via stdin/stdout
  8. Testing: verifica con MCP Inspector e configurazione di Claude Desktop
  9. Debugging: regola fondamentale di non scrivere mai su stdout, uso di console.error() per i log

Prossimo Articolo

Nel prossimo articolo della serie affronteremo l'altro lato del protocollo: creare un Client MCP in TypeScript. Vedremo come connettersi a un server, negoziare le capability, elencare e invocare tool programmaticamente e gestire il transport HTTP per connessioni remote. Impareremo a costruire un client che può comunicare con qualsiasi server MCP compatibile con il protocollo standard.

Il codice completo di tutti gli esempi e disponibile nel repository Tech-MCP su GitHub.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti