Creare un MCP Server in Node.js: Tutorial Pratico

Il Model Context Protocol (MCP) ha cambiato le regole del gioco per chi vuole integrare strumenti e dati con i modelli AI come Claude. Se hai già letto la nostra MCP intro e vuoi andare oltre, questo tutorial hands-on ti guida nella costruzione di un MCP server completo in Node.js — da zero, con codice funzionante. Scoprirai anche come sfruttare il MCP Connector per connessioni remote.

L’SDK ufficiale @modelcontextprotocol/sdk (disponibile su npm) e la MCP spec rendono tutto questo sorprendentemente accessibile. In questo articolo costruiamo un server che espone tools, resources e prompts — e lo testiamo direttamente con Claude Desktop.

Setup del progetto MCP

Prima di tutto, inizializza il progetto Node.js e installa le dipendenze necessarie:

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

La struttura del progetto sarà semplice ma efficace:

my-mcp-server/
├── src/
│   ├── index.ts          # Entry point
│   ├── server.ts         # MCP server setup
│   ├── tools/
│   │   └── fetch-api.ts  # Tool personalizzato
│   └── resources/
│       └── filesystem.ts # Resource accesso file
├── package.json
└── tsconfig.json

Il cuore del server MCP è l’istanziazione del Server con il suo manifest. Ogni server dichiara nome, versione e le capacità che intende esporre:

// src/server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

export const server = new Server(
  {
    name: 'my-mcp-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
      resources: {},
      prompts: {},
    },
  }
);

💡 Tip: Usa StdioServerTransport per Claude Desktop (comunicazione via stdin/stdout). Per un server remoto, usa invece SSEServerTransport con Express.

Implementare un Tool personalizzato

I tools sono la funzionalità più potente di MCP: permettono a Claude di eseguire azioni nel mondo reale. Ogni tool ha un nome, una descrizione, uno schema di input (in JSON Schema / Zod) e un handler.

Costruiamo un tool che interroga un’API esterna — in questo caso, recupera i dati di un repository GitHub:

// src/tools/fetch-api.ts
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';

export const fetchGithubRepoSchema = z.object({
  owner: z.string().describe('Owner del repository GitHub'),
  repo:  z.string().describe('Nome del repository'),
});

export async function fetchGithubRepo(
  args: z.infer<typeof fetchGithubRepoSchema>
) {
  const url = `https://api.github.com/repos/${args.owner}/${args.repo}`;
  const res = await fetch(url, {
    headers: { 'User-Agent': 'MCP-Server/1.0' },
  });

  if (!res.ok) {
    throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
  }

  const data = await res.json() as Record<string, unknown>;
  return {
    name:        data.full_name,
    description: data.description,
    stars:       data.stargazers_count,
    language:    data.language,
    url:         data.html_url,
  };
}

Ora registra il tool nel server con il handler e il relativo schema:

// Registra i tools disponibili
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name:        'fetch_github_repo',
      description: 'Recupera informazioni su un repository GitHub',
      inputSchema: zodToJsonSchema(fetchGithubRepoSchema),
    },
  ],
}));

// Handler per la chiamata al tool
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'fetch_github_repo') {
    const parsed = fetchGithubRepoSchema.parse(args);
    const result = await fetchGithubRepo(parsed);
    return {
      content: [
        {
          type: 'text',
          text: JSON.stringify(result, null, 2),
        },
      ],
    };
  }

  throw new Error(`Tool non trovato: ${name}`);
});

🔧 Pattern importante: Valida sempre gli argomenti con Zod prima di passarli all’handler. Se la validazione fallisce, MCP restituisce automaticamente un errore strutturato a Claude.

Implementare una Resource (file system)

Le resources espongono dati che Claude può leggere — file, database, output di API. A differenza dei tools, le resources sono read-only e Claude le richiede esplicitamente quando ne ha bisogno nel contesto.

// src/resources/filesystem.ts
import * as fs from 'fs/promises';
import * as path from 'path';

const ALLOWED_DIR = process.env.MCP_DOCS_DIR ?? './docs';

// Lista delle risorse disponibili
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri:      'file://docs/readme',
      name:     'README del progetto',
      mimeType: 'text/markdown',
    },
    {
      uri:      'file://docs/config',
      name:     'Configurazione corrente',
      mimeType: 'application/json',
    },
  ],
}));

// Lettura di una resource specifica
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === 'file://docs/readme') {
    const content = await fs.readFile(
      path.join(ALLOWED_DIR, 'README.md'),
      'utf-8'
    );
    return {
      contents: [{ uri, mimeType: 'text/markdown', text: content }],
    };
  }

  throw new Error(`Resource non trovata: ${uri}`);
});

Nota l’uso di ALLOWED_DIR: non esporre mai path arbitrari. Definisci una whitelist delle directory accessibili tramite variabile d’ambiente.

Testing con Claude Desktop e Debugging

Per testare il server MCP con Claude Desktop, devi configurare il file claude_desktop_config.json. Su macOS si trova in ~/Library/Application Support/Claude/:

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

Per il debugging, MCP scrive i log sullo stderr — Claude Desktop li cattura e li mostra nella sezione “Logs”. Per debuggare in modo più interattivo, usa il MCP Inspector:

npx @modelcontextprotocol/inspector node dist/index.js

MCP Inspector apre una UI browser dove puoi chiamare tools, leggere resources e simulare le interazioni con il server senza aprire Claude Desktop.

🎯 Pattern di deployment: Per un server locale, usa stdio transport e avvialo con Claude Desktop. Per un server cloud (accessibile da più client), usa SSE transport con autenticazione Bearer token e fai il deploy su un server HTTPS. Puoi pubblicare il tuo server sul registro ufficiale MCP per renderlo disponibile alla community.

Autenticazione e Deployment

Per un MCP server esposto via HTTP/SSE, l’autenticazione è fondamentale. Il pattern più comune è il Bearer token:

// src/index.ts — Server HTTP con SSE transport
import express from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';

const app = express();

// Middleware autenticazione
app.use((req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  if (token !== process.env.MCP_SECRET_TOKEN) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

// Endpoint SSE per MCP
app.get('/sse', async (req, res) => {
  const transport = new SSEServerTransport('/messages', res);
  await server.connect(transport);
});

app.post('/messages', async (req, res) => {
  // Gestione dei messaggi MCP
});

app.listen(3000, () => {
  console.log('MCP Server in ascolto su porta 3000');
});

Per il deployment su cloud (Railway, Render, Fly.io) basta un semplice Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist ./dist
ENV NODE_ENV=production
CMD ["node", "dist/index.js"]

FAQ: MCP Server in Node.js

Qual è la differenza tra Tool e Resource in MCP?

I Tools eseguono azioni (scrivono, modificano, chiamano API) e il loro output viene incluso nella conversazione. Le Resources sono dati read-only che Claude carica nel contesto quando necessario, simili a documenti allegati.

Posso usare JavaScript puro invece di TypeScript?

Sì, ma TypeScript è fortemente consigliato: l’SDK MCP usa tipi complessi e avere il type checking ti evita errori difficili da debuggare. Puoi usare tsx per eseguire TypeScript direttamente senza compilazione in sviluppo.

Come gestisco gli errori nei tool handlers?

Lancia un’eccezione normale — l’SDK MCP la cattura e la restituisce come errore strutturato a Claude. Per errori attesi (input non valido, risorsa non trovata), usa messaggi descrittivi. Per errori inaspettati, logga su stderr prima di rilanciarli.

Quanto è sicuro esporre un MCP server su internet?

Dipende da cosa esponi. Valida sempre gli input con Zod, usa variabili d’ambiente per segreti, implementa autenticazione Bearer token, non esporre mai path o comandi arbitrari. Per usi aziendali, considera di limitare l’accesso a IP specifici o usare una VPN.

Posso pubblicare il mio MCP server perché lo usino altri?

Assolutamente sì. Il registro community MCP su GitHub raccoglie server pubblici. Pubblica il codice su npm come pacchetto (@tuo-nome/mcp-server-nome), documenta la configurazione per Claude Desktop, e apri una PR al registry. Molti developer cercano attivamente nuovi server MCP da integrare.

Condividi

Articoli Recenti

Categorie popolari