Come costruire e distribuire un server MCP personalizzato in 10 minuti

💡Vuoi creare il tuo flusso di lavoro AI Agentic senza codice? Puoi facilmente creare flussi di lavoro AI con Anakin AI senza alcuna conoscenza di programmazione. Collegati alle API LLM come: GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, Web Scraping.... in un unico flusso di lavoro! Dimentica

Build APIs Faster & Together in Apidog

Come costruire e distribuire un server MCP personalizzato in 10 minuti

Start for free
Inhalte
💡
Vuoi creare il tuo flusso di lavoro AI Agentic senza codice?

Puoi facilmente creare flussi di lavoro AI con Anakin AI senza alcuna conoscenza di programmazione. Collegati alle API LLM come: GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, Web Scraping.... in un unico flusso di lavoro!

Dimentica la programmazione complicata, automatizza il tuo lavoro quotidiano con Anakin AI!

Per un tempo limitato, puoi anche utilizzare Google Gemini 1.5 e Stable Diffusion gratuitamente!
Costruisci facilmente flussi di lavoro AI Agentic con Anakin AI!
Costruisci facilmente flussi di lavoro AI Agentic con Anakin AI

Introduzione

Il Protocollo di Contesto del Modello (MCP) rappresenta un notevole progresso nell'ecosistema AI, offrendo un modo standardizzato per comunicare con grandi modelli di linguaggio. Invece che ogni piattaforma AI implementi la propria formattazione unica per i messaggi, MCP mira a fornire un'interfaccia coerente per richieste, risposte e chiamate a funzioni attraverso vari modelli e piattaforme.

Sebbene il protocollo stesso sia in evoluzione, costruire un server di base compatibile con MCP può essere semplice. In questa guida, ti guiderò nella creazione di un server semplice, ma funzionale, che aderisca ai principi fondamentali della gestione dei messaggi nei moderni sistemi AI. La nostra implementazione si concentrerà sulla creazione di una base che potrai successivamente ampliare con funzionalità più avanzate.

Dieci minuti sono sufficienti? Sicuramente non per un sistema pronto per la produzione. Ma per un prototipo funzionante che dimostri i concetti chiave? Assolutamente. Cominciamo!

Requisiti

Prima di cominciare, avrai bisogno di:

  • Node.js (v16+) installato sul tuo sistema
  • Conoscenze di base di JavaScript/TypeScript
  • Familiarità con Express.js o framework web simili
  • Un editor di codice (VS Code consigliato)
  • Accesso al terminale/riga di comando
  • npm o yarn come gestore di pacchetti

Passaggio 1: Configurare il tuo progetto (2 minuti)

Prima di tutto, creiamo una nuova directory e inizializziamo il nostro progetto:

mkdir mcp-server
cd mcp-server
npm init -y

Ora, installa le dipendenze necessarie:

npm install express cors typescript ts-node @types/node @types/express @types/cors
npm install --save-dev nodemon

Crea un file di configurazione TypeScript:

npx tsc --init

Modifica il file generato tsconfig.json per includere queste impostazioni essenziali:

{
  "compilerOptions": {
    "target": "es2018",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

Aggiorna la sezione scripts del tuo package.json:

"scripts": {
  "start": "node dist/index.js",
  "dev": "nodemon --exec ts-node src/index.ts",
  "build": "tsc"
}

Passaggio 2: Creare il server core (3 minuti)

Crea la tua directory sorgente e il file principale del server:

mkdir -p src/handlers
touch src/index.ts
touch src/handlers/messageHandler.ts
touch src/types.ts

Definiamo prima i nostri tipi in src/types.ts:

// Struttura di messaggio di base
export interface Content {
  type: string;
  text?: string;
}

export interface Message {
  role: "user" | "assistant" | "system";
  content: Content[] | string;
}

// Strutture di richiesta e risposta
export interface ModelRequest {
  messages: Message[];
  max_tokens?: number;
  temperature?: number;
  stream?: boolean;
}

export interface ModelResponse {
  message: Message;
}

// Interfacce di chiamata agli strumenti
export interface Tool {
  name: string;
  description: string;
  input_schema: {
    type: string;
    properties: Record;
    required?: string[];
  };
}

export interface ToolCall {
  type: "tool_call";
  id: string;
  name: string;
  input: Record;
}

export interface ToolResult {
  type: "tool_result";
  tool_call_id: string;
  content: string;
}

Ora implementa il server di base in src/index.ts:

import express from 'express';
import cors from 'cors';
import { handleMessageRequest } from './handlers/messageHandler';

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json({ limit: '10mb' }));

// Endpoint principale per l'elaborazione dei messaggi
app.post('/v1/messages', handleMessageRequest);

// Endpoint di controllo dello stato
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Avvia il server
app.listen(PORT, () => {
  console.log(`Server in esecuzione sulla porta ${PORT}`);
  console.log(`Controllo stato: http://localhost:${PORT}/health`);
  console.log(`Endpoint messaggi: http://localhost:${PORT}/v1/messages`);
});

Successivamente, implementa il gestore dei messaggi in src/handlers/messageHandler.ts:

import { Request, Response } from 'express';
import { ModelRequest, ModelResponse, Message, Content } from '../types';

export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // Validazione di base
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: 'Formato di richiesta non valido. L\'array di messaggi è obbligatorio.' });
    }
    
    // Registra la richiesta in arrivo (per il debug)
    console.log('Richiesta ricevuta con', request.messages.length, 'messaggi');
    
    // Elabora i messaggi
    const response = processMessages(request.messages);
    
    // Restituisci la risposta
    return res.status(200).json(response);
  } catch (error) {
    console.error('Errore durante l\'elaborazione della richiesta:', error);
    return res.status(500).json({ error: 'Errore interno del server' });
  }
}

function processMessages(messages: Message[]): ModelResponse {
  // Estrai l'ultimo messaggio dell'utente
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("Nessun messaggio dell'utente trovato nella conversazione");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // Logica semplice di generazione della risposta
  let responseText = "";
  
  if (userQuery.toLowerCase().includes('ciao') || userQuery.toLowerCase().includes('salve')) {
    responseText = "Ciao! Come posso aiutarti oggi?";
  } else if (userQuery.toLowerCase().includes('meteo')) {
    responseText = "Non ho accesso ai dati meteorologici in tempo reale, ma posso aiutarti a capire i modelli meteorologici in generale.";
  } else if (userQuery.toLowerCase().includes('ora')) {
    responseText = `L'ora attuale del server è ${new Date().toLocaleTimeString()}.`;
  } else {
    responseText = "Ho ricevuto il tuo messaggio. Questa è una risposta del server modello semplice.";
  }
  
  // Costruisci e restituisci la risposta
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: responseText 
      }]
    }
  };
}

function findLastUserMessage(messages: Message[]): Message | undefined {
  // Trova l'ultimo messaggio con ruolo 'user'
  for (let i = messages.length - 1; i >= 0; i--) {
    if (messages[i].role === 'user') {
      return messages[i];
    }
  }
  return undefined;
}

function extractTextContent(message: Message): string {
  if (typeof message.content === 'string') {
    return message.content;
  } else if (Array.isArray(message.content)) {
    return message.content
      .filter(item => item.type === 'text' && item.text)
      .map(item => item.text)
      .join(' ');
  }
  return '';
}

function createErrorResponse(errorMessage: string): ModelResponse {
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `Errore: ${errorMessage}` 
      }]
    }
  };
}

Passaggio 3: Aggiungere la capacità di chiamata agli strumenti (3 minuti)

Crea un nuovo file per le definizioni e implementazioni degli strumenti:

touch src/tools.ts

Implementa alcuni strumenti di base in src/tools.ts:

import { Tool } from './types';

// Definizioni degli strumenti
export const availableTools: Tool[] = [
  {
    name: "get_current_time",
    description: "Ottieni l'ora attuale del server",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "Fuso orario opzionale (predefinito al fuso orario del server)"
        }
      }
    }
  },
  {
    name: "calculate",
    description: "Esegui un calcolo matematico",
    input_schema: {
      type: "object",
      properties: {
        expression: {
          type: "string",
          description: "Espressione matematica da valutare"
        }
      },
      required: ["expression"]
    }
  }
];

// Implementazioni degli strumenti
export function executeToolCall(name: string, params: Record): string {
  switch (name) {
    case "get_current_time":
      return getTime(params.timezone);
    case "calculate":
      return calculate(params.expression);
    default:
      throw new Error(`Strumento sconosciuto: ${name}`);
  }
}

function getTime(timezone?: string): string {
  const options: Intl.DateTimeFormatOptions = { 
    hour: '2-digit', 
    minute: '2-digit', 
    second: '2-digit',
    timeZoneName: 'short' 
  };
  
  try {
    if (timezone) {
      options.timeZone = timezone;
    }
    return new Date().toLocaleTimeString('it-IT', options);
  } catch (error) {
    return `${new Date().toLocaleTimeString()} (Ora del server)`;
  }
}

function calculate(expression: string): string {
  try {
    // ATTENZIONE: In un'applicazione reale, dovresti utilizzare un metodo di valutazione più sicuro
    // Questo è un esempio semplificato solo per dimostrazione
    const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, '');
    const result = eval(sanitizedExpression);
    return `${expression} = ${result}`;
  } catch (error) {
    return `Errore nel calcolo di ${expression}: ${error instanceof Error ? error.message : String(error)}`;
  }
}

Ora, aggiorna il gestore dei messaggi per supportare la chiamata agli strumenti modificando src/handlers/messageHandler.ts:

// Aggiungi queste importazioni in cima
import { availableTools, executeToolCall } from '../tools';
import { ToolCall, ToolResult } from '../types';

// Aggiorna la funzione handleMessageRequest
export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // Validazione di base
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: 'Formato di richiesta non valido. L\'array di messaggi è obbligatorio.' });
    }
    
    // Controlla se si tratta di una richiesta con risultati degli strumenti
    const lastMessage = request.messages[request.messages.length - 1];
    if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
      const toolCalls = lastMessage.content.filter(item => 
        item.type === 'tool_call') as ToolCall[];
      
      if (toolCalls.length > 0) {
        // Questa è una continuazione con risultati degli strumenti
        return handleToolResultsResponse(request, res);
      }
    }
    
    // Elabora come un messaggio normale
    const response = processMessages(request.messages);
    
    // Restituisci la risposta
    return res.status(200).json(response);
  } catch (error) {
    console.error('Errore durante l\'elaborazione della richiesta:', error);
    return res.status(500).json({ error: 'Errore interno del server' });
  }
}

// Aggiungi questa funzione per gestire le chiamate agli strumenti
function handleToolResultsResponse(request: ModelRequest, res: Response) {
  const messages = request.messages;
  const lastAssistantMessage = messages[messages.length - 1];
  
  if (lastAssistantMessage.role !== 'assistant' || !Array.isArray(lastAssistantMessage.content)) {
    return res.status(400).json({ error: 'Formato dei risultati dello strumento non valido' });
  }
  
  // Trova le chiamate e i risultati degli strumenti
  const toolCalls = lastAssistantMessage.content.filter(
    item => item.type === 'tool_call'
  ) as ToolCall[];
  
  const toolResults = lastAssistantMessage.content.filter(
    item => item.type === 'tool_result'
  ) as ToolResult[];
  
  // Elabora i risultati
  let finalResponse = "Ho elaborato le seguenti informazioni:\n\n";
  
  toolResults.forEach(result => {
    const relatedCall = toolCalls.find(call => call.id === result.tool_call_id);
    if (relatedCall) {
      finalResponse += `- ${relatedCall.name}: ${result.content}\n`;
    }
  });
  
  return res.status(200).json({
    message: {
      role: "assistant",
      content: [{ type: "text", text: finalResponse }]
    }
  });
}

// Modifica la funzione processMessages per gestire potenziali chiamate agli strumenti
function processMessages(messages: Message[]): ModelResponse {
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("Nessun messaggio dell'utente trovato nella conversazione");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // Cerca parole chiave che potrebbero attivare l'uso degli strumenti
  if (userQuery.toLowerCase().includes('ora')) {
    return {
      message: {
        role: "assistant",
        content: [
          { type: "tool_call", id: "call_001", name: "get_current_time", input: {} },
          { 
            type: "text", 
            text: "Controllo l'ora attuale per te." 
          }
        ]
      }
    };
  } else if (userQuery.toLowerCase().match(/calcola|calcolare|che cos'è \d+[\+\-\*\/]/)) {
    // Estrai un potenziale calcolo
    const expression = userQuery.match(/(\d+[\+\-\*\/\(\)\.]*\d+)/)?.[0] || "1+1";
    
    return {
      message: {
        role: "assistant",
        content: [
          { 
            type: "tool_call", 
            id: "call_002", 
            name: "calculate", 
            input: { expression } 
          },
          { 
            type: "text", 
            text: `Calcolerò ${expression} per te.` 
          }
        ]
      }
    };
  }
  
  // Risposta predefinita per altre richieste
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `Ho ricevuto il tuo messaggio: "${userQuery}". Come posso aiutarti ulteriormente?` 
      }]
    }
  };
}

Passaggio 4: Test e distribuzione (2 minuti)

Creiamo un semplice script di test per controllare il nostro server. Crea un file test.js nella directory principale:

const fetch = require('node-fetch');

async function testServer() {
  const url = 'http://localhost:3000/v1/messages';
  
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      messages: [
        {
          role: "user",
          content: [
            {
              type: "text",
              text: "Che ora è adesso?"
            }
          ]
        }
      ]
    })
  });
  
  const data = await response.json();
  console.log(JSON.stringify(data, null, 2));
}

testServer().catch(console.error);

Per testare il tuo server:

# Avvia il server
npm run dev

# In un altro terminale, esegui il test
node test.js

Per una distribuzione rapida, vediamo di creare un semplice Dockerfile:

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

Costruisci e avvia il contenitore:

docker build -t mcp-server .
docker run -p 3000:3000 mcp-server

Conclusione

In soli 10 minuti, abbiamo costruito un server di base che implementa i concetti chiave dei moderni protocolli di messaggistica AI. Il nostro server può:

  1. Elaborare richieste di messaggi strutturati
  2. Rispondere in un formato standardizzato
  3. Gestire funzionalità di chiamata agli strumenti di base
  4. Elaborare e rispondere ai risultati degli strumenti

Sebbene questa implementazione sia semplificata, fornisce una solida base per ulteriori sviluppi. Per estendere questo server per l'uso in produzione, considera:

  • Aggiungere autenticazione e limitazione della larghezza di banda
  • Implementare una corretta gestione degli errori e convalida
  • Collegarsi a modelli AI reali per l'elaborazione
  • Aggiungere strumenti più sofisticati
  • Implementare risposte in streaming
  • Aggiungere registrazione e monitoraggio completo

Ricorda che, mentre la nostra implementazione segue i principi generali dei moderni protocolli di messaggistica AI, implementazioni specifiche come l'API di OpenAI o l'API Claude di Anthropic potrebbero avere requisiti aggiuntivi o lievi variazioni nei loro formati attesi. Consulta sempre la documentazione ufficiale per il servizio specifico con cui stai integrando.

Il campo dei protocolli di messaggistica AI è in rapida evoluzione, quindi rimani aggiornato con gli sviluppi più recenti e preparati ad adattare la tua implementazione man mano che gli standard evolvono. Comprendendo i concetti fondamentali dimostrati in questa guida, sarai ben equipaggiato per lavorare con qualsiasi nuovo sviluppo emerga in questo entusiasmante campo.