Comment construire et déployer un serveur MCP personnalisé en 10 minutes

💡Vous souhaitez créer votre propre flux de travail d'IA agentive sans code ? Vous pouvez facilement créer des flux de travail d'IA avec Anakin AI sans aucune connaissance en programmation. Connectez-vous aux API LLM telles que : GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, Web Scraping.... en un seul

Build APIs Faster & Together in Apidog

Comment construire et déployer un serveur MCP personnalisé en 10 minutes

Start for free
Inhalte
💡
Vous souhaitez créer votre propre flux de travail d'IA agentive sans code ?

Vous pouvez facilement créer des flux de travail d'IA avec Anakin AI sans aucune connaissance en programmation. Connectez-vous aux API LLM telles que : GPT-4, Claude 3.5 Sonnet, Uncensored Dolphin-Mixtral, Stable Diffusion, DALLE, Web Scraping.... en un seul flux de travail !

Oubliez le codage compliqué, automatisez votre travail quotidien avec Anakin AI !

Pour une durée limitée, vous pouvez également utiliser Google Gemini 1.5 et Stable Diffusion gratuitement !
Construisez facilement des flux de travail d'IA agentive avec Anakin AI !
Construisez facilement des flux de travail d'IA agentive avec Anakin AI

Introduction

Le Protocole de Contexte de Modèle (MCP) représente une avancée significative dans l'écosystème de l'IA, offrant un moyen standardisé de communiquer avec de grands modèles de langage. Plutôt que chaque plateforme d'IA n'implémente son propre format unique pour les messages, le MCP vise à fournir une interface cohérente pour les invites, les réponses et les appels de fonction à travers divers modèles et plateformes.

Bien que le protocole lui-même soit en évolution, la création d'un serveur de base compatible avec MCP peut être simple. Dans ce guide, je vais vous guider pour créer un serveur simple, mais fonctionnel, qui respecte les principes fondamentaux de traitement des messages dans les systèmes d'IA modernes. Notre mise en œuvre se concentrera sur la création d'une base que vous pourrez éventuellement étendre avec des fonctionnalités plus avancées.

10 minutes sont-elles suffisantes ? Pour un système prêt pour la production, certainement pas. Mais pour un prototype fonctionnel qui démontre les concepts clés ? Absolument. Commençons !

Prérequis

Avant de commencer, vous aurez besoin de :

  • Node.js (v16+) installé sur votre système
  • Connaissances de base en JavaScript/TypeScript
  • Familiarité avec Express.js ou des frameworks web similaires
  • Un éditeur de code (VS Code recommandé)
  • Accès à un terminal/ligne de commande
  • Gestionnaire de paquets npm ou yarn

Étape 1 : Configuration de votre projet (2 minutes)

Tout d'abord, créons un nouveau répertoire et initialisons notre projet :

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

Maintenant, installez les dépendances nécessaires :

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

Créez un fichier de configuration TypeScript :

npx tsc --init

Modifiez le fichier généré tsconfig.json pour inclure ces paramètres essentiels :

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

Mettez à jour la section des scripts de votre package.json :

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

Étape 2 : Création du serveur de base (3 minutes)

Créez votre répertoire source et le fichier principal du serveur :

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

Définissons d'abord nos types dans src/types.ts :

// Structure de message de base
export interface Content {
  type: string;
  text?: string;
}

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

// Structures de demande et de réponse
export interface ModelRequest {
  messages: Message[];
  max_tokens?: number;
  temperature?: number;
  stream?: boolean;
}

export interface ModelResponse {
  message: Message;
}

// Interfaces d'appel d'outil
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;
}

Maintenant, implémentez le serveur de base dans 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' }));

// Point d'entrée principal pour le traitement des messages
app.post('/v1/messages', handleMessageRequest);

// Point de contrôle de santé
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Démarrer le serveur
app.listen(PORT, () => {
  console.log(`Serveur en cours d'exécution sur le port ${PORT}`);
  console.log(`Vérification de la santé : http://localhost:${PORT}/health`);
  console.log(`Point de terminaison des messages : http://localhost:${PORT}/v1/messages`);
});

Ensuite, implémentez le gestionnaire de messages dans 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;
    
    // Validation de base
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: 'Format de demande invalide. Un tableau de messages est requis.' });
    }
    
    // Journaliser la demande entrante (pour déboguer)
    console.log('Demande reçue avec', request.messages.length, 'messages');
    
    // Traiter les messages
    const response = processMessages(request.messages);
    
    // Retourner la réponse
    return res.status(200).json(response);
  } catch (error) {
    console.error('Erreur lors du traitement de la demande:', error);
    return res.status(500).json({ error: 'Erreur interne du serveur' });
  }
}

function processMessages(messages: Message[]): ModelResponse {
  // Extraire le dernier message de l'utilisateur
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("Aucun message de l'utilisateur trouvé dans la conversation");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // Logique simple de génération de réponse
  let responseText = "";
  
  if (userQuery.toLowerCase().includes('bonjour') || userQuery.toLowerCase().includes('salut')) {
    responseText = "Bonjour ! Comment puis-je vous aider aujourd'hui ?";
  } else if (userQuery.toLowerCase().includes('météo')) {
    responseText = "Je n'ai pas accès aux données météorologiques en temps réel, mais je peux vous aider à comprendre les tendances météorologiques en général.";
  } else if (userQuery.toLowerCase().includes('heure')) {
    responseText = `L'heure actuelle du serveur est ${new Date().toLocaleTimeString()}.`;
  } else {
    responseText = "J'ai reçu votre message. Ceci est une réponse simple du serveur modèle.";
  }
  
  // Construire et retourner la réponse
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: responseText 
      }]
    }
  };
}

function findLastUserMessage(messages: Message[]): Message | undefined {
  // Trouver le dernier message avec le rôle '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: `Erreur : ${errorMessage}` 
      }]
    }
  };
}

Étape 3 : Ajout de la capacité d'appel d'outil (3 minutes)

Créez un nouveau fichier pour les définitions et implémentations d'outils :

touch src/tools.ts

Implémentez quelques outils de base dans src/tools.ts :

import { Tool } from './types';

// Définitions des outils
export const availableTools: Tool[] = [
  {
    name: "get_current_time",
    description: "Obtenir l'heure actuelle du serveur",
    input_schema: {
      type: "object",
      properties: {
        timezone: {
          type: "string",
          description: "Fuseau horaire optionnel (par défaut au fuseau horaire du serveur)"
        }
      }
    }
  },
  {
    name: "calculate",
    description: "Effectuer un calcul mathématique",
    input_schema: {
      type: "object",
      properties: {
        expression: {
          type: "string",
          description: "Expression mathématique à évaluer"
        }
      },
      required: ["expression"]
    }
  }
];

// Implémentations d'outils
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(`Outil inconnu : ${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('fr-FR', options);
  } catch (error) {
    return `${new Date().toLocaleTimeString()} (Heure du serveur)`;
  }
}

function calculate(expression: string): string {
  try {
    // ATTENTION : Dans une véritable application, vous devriez utiliser une méthode d'évaluation plus sûre
    // Ceci est un exemple simplifié pour des fins de démonstration uniquement
    const sanitizedExpression = expression.replace(/[^0-9+\-*/().\s]/g, '');
    const result = eval(sanitizedExpression);
    return `${expression} = ${result}`;
  } catch (error) {
    return `Erreur lors du calcul de ${expression} : ${error instanceof Error ? error.message : String(error)}`;
  }
}

Maintenant, mettez à jour le gestionnaire de messages pour prendre en charge les appels d'outils en modifiant src/handlers/messageHandler.ts :

// Ajoutez ces imports en haut
import { availableTools, executeToolCall } from '../tools';
import { ToolCall, ToolResult } from '../types';

// Mettez à jour la fonction handleMessageRequest
export async function handleMessageRequest(req: Request, res: Response) {
  try {
    const request = req.body as ModelRequest;
    
    // Validation de base
    if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
      return res.status(400).json({ error: 'Format de demande invalide. Un tableau de messages est requis.' });
    }
    
    // Vérifiez si c'est une demande avec des résultats d'outil
    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) {
        // C'est un suivi avec des résultats d'outil
        return handleToolResultsResponse(request, res);
      }
    }
    
    // Traitez comme un message normal
    const response = processMessages(request.messages);
    
    // Retourner la réponse
    return res.status(200).json(response);
  } catch (error) {
    console.error('Erreur lors du traitement de la demande :', error);
    return res.status(500).json({ error: 'Erreur interne du serveur' });
  }
}

// Ajoutez cette fonction pour gérer les appels d'outils
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: 'Format de résultats d\'outils invalide' });
  }
  
  // Trouver les appels et résultats d'outils
  const toolCalls = lastAssistantMessage.content.filter(
    item => item.type === 'tool_call'
  ) as ToolCall[];
  
  const toolResults = lastAssistantMessage.content.filter(
    item => item.type === 'tool_result'
  ) as ToolResult[];
  
  // Traiter les résultats
  let finalResponse = "J'ai traité les informations suivantes :\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 }]
    }
  });
}

// Modifiez la fonction processMessages pour gérer les appels d'outils potentiels
function processMessages(messages: Message[]): ModelResponse {
  const lastUserMessage = findLastUserMessage(messages);
  
  if (!lastUserMessage) {
    return createErrorResponse("Aucun message de l'utilisateur trouvé dans la conversation");
  }
  
  const userQuery = extractTextContent(lastUserMessage);
  
  // Rechercher des mots clés pouvant déclencher l'utilisation d'outils
  if (userQuery.toLowerCase().includes('heure')) {
    return {
      message: {
        role: "assistant",
        content: [
          { type: "tool_call", id: "call_001", name: "get_current_time", input: {} },
          { 
            type: "text", 
            text: "Je vais vérifier l'heure actuelle pour vous." 
          }
        ]
      }
    };
  } else if (userQuery.toLowerCase().match(/calculer|faire|quel est \d+[\+\-\*\/]/)) {
    // Extraire un calcul potentiel
    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: `Je vais calculer ${expression} pour vous.` 
          }
        ]
      }
    };
  }
  
  // Réponse par défaut pour d'autres requêtes
  return {
    message: {
      role: "assistant",
      content: [{ 
        type: "text", 
        text: `J'ai reçu votre message : "${userQuery}". Comment puis-je vous aider davantage ?` 
      }]
    }
  };
}

Étape 4 : Test et déploiement (2 minutes)

Créons un simple script de test pour vérifier notre serveur. Créez un fichier test.js dans le répertoire racine :

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: "Quelle heure est-il en ce moment ?"
            }
          ]
        }
      ]
    })
  });
  
  const data = await response.json();
  console.log(JSON.stringify(data, null, 2));
}

testServer().catch(console.error);

Pour tester votre serveur :

# Démarrez le serveur
npm run dev

# Dans un autre terminal, exécutez le test
node test.js

Pour un déploiement rapide, créons un simple Dockerfile :

FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "start"]

Construisez et exécutez le conteneur :

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

Conclusion

En seulement 10 minutes, nous avons construit un serveur de base qui met en œuvre les concepts clés des protocoles de message d'IA modernes. Notre serveur peut :

  1. Traiter des demandes de message structurées
  2. Répondre dans un format standard
  3. Gérer des fonctionnalités de base d'appel d'outils
  4. Traiter et répondre aux résultats d'outils

Bien que cette mise en œuvre soit simplifiée, elle fournit une base solide pour un développement ultérieur. Pour étendre ce serveur pour une utilisation en production, envisagez :

  • Ajouter une authentification et des limites de débit
  • Mettre en œuvre une gestion des erreurs et une validation appropriées
  • Se connecter à de véritables modèles d'IA pour le traitement
  • Ajouter des outils plus sophistiqués
  • Mettre en œuvre des réponses en streaming
  • Ajouter une journalisation et un suivi complets

Rappelez-vous que bien que notre mise en œuvre suive les principes généraux des protocoles de messagerie d'IA modernes, des mises en œuvre spécifiques comme l'API d'OpenAI ou l'API Claude d'Anthropic peuvent avoir des exigences supplémentaires ou des variations mineures dans leurs formats attendus. Consultez toujours la documentation officielle pour le service spécifique avec lequel vous vous intégrez.

Le domaine des protocoles de messagerie d'IA évolue rapidement, alors restez informé des derniers développements et soyez prêt à adapter votre mise en œuvre alors que les normes évoluent. En comprenant les concepts de base démontrés dans ce guide, vous serez bien équipé pour travailler avec toutes nouvelles avancées qui émergent dans ce domaine passionnant.