Construire un serveur MCP pour Claude : guide pas-à-pas

Du npm init au déploiement distant : un guide exécutable pour construire ton serveur MCP, exposer tes tools et resources à Claude Desktop, et passer en production sans casser de session.

Tu connais déjà Claude, tu as joué avec l'API Anthropic, et maintenant tu veux que Claude appelle tes outils, lise tes données, parle à ton CRM. C'est exactement ce que résout le Model Context Protocol (MCP). Ce guide te donne un serveur MCP fonctionnel, branché à Claude Desktop, en suivant un fil exécutable du npm init jusqu'au déploiement distant.

Si tu débutes plus largement avec l'écosystème, garde en marge-page le guide complet pour apprendre Claude qui couvre Projects, Artifacts et l'API. Ici, on reste focus serveur MCP, transport stdio, handlers TypeScript.

Ce qu'est vraiment un serveur MCP (et ce qu'il n'est pas)

Le Model Context Protocol est un standard ouvert créé par Anthropic en 2024 pour connecter des LLM à des outils tiers. Concrètement, un serveur MCP expose trois primitives à Claude :

  • tools : des actions que Claude peut déclencher (créer un ticket, requêter une base, envoyer un mail).
  • resources : des données que Claude peut lire (un fichier, une ligne SQL, un document Notion).
  • prompts : des templates réutilisables côté serveur, que l'utilisateur peut invoquer dans Claude Desktop.

Ce n'est pas une API REST classique. Une API REST attend qu'un client connaisse ses endpoints. MCP, lui, se découvre : Claude appelle list_tools au démarrage, lit les descriptions, et décide seul quand appeler quoi. Ce n'est pas non plus du function calling brut de l'API : MCP est un protocole standardisé qui marche avec n'importe quel client compatible (Claude Desktop, Claude Code, Cursor, Zed).

Cas d'usage typiques : exposer une base PostgreSQL en lecture, brancher un CRM interne, donner accès à un dossier de notes locales, automatiser des opérations sur GitHub. Pour explorer l'existant avant de coder, jette un œil à la liste des serveurs MCP de référence : la majorité sont open source, c'est une mine d'exemples.

Prérequis techniques avant de coder

Liste minimale, à valider avant la première ligne de code :

  • Node.js 18 ou plus (ou Python 3.10+ si tu pars sur le SDK Python).
  • Claude Desktop installé : c'est le client MCP de référence pour tester en local. Si ce n'est pas fait, suis d'abord la procédure de configuration de Claude Desktop.
  • Un éditeur (VS Code, Cursor) et un terminal.
  • Le SDK officiel : @modelcontextprotocol/sdk côté TypeScript, mcp côté Python.

Côté abonnement, MCP fonctionne avec le plan Claude Pro (à partir d'environ 20 € par mois) ou supérieur. Pour les usages intensifs en production, le plan Max (environ 100 € par mois) ou un compte API dédié seront plus adaptés.

Architecture d'un serveur MCP minimal

Un serveur MCP, c'est un processus qui parle un protocole JSON-RPC sur un transport. Deux transports principaux :

CritèrestdioHTTP / SSE
Cas d'usageLocal, machine du devDistant, multi-utilisateurs
LancementClaude Desktop spawn le processServeur tourne en continu
AuthAucune (process local)Token, OAuth
DebugLogs stderrLogs serveur + endpoint santé

Le cycle de vie d'une requête est simple : Claude Desktop ouvre le transport, appelle initialize, puis list_tools et list_resources. Quand l'utilisateur écrit un prompt qui matche, Claude appelle call_tool avec un payload JSON. Le serveur exécute, renvoie un résultat, Claude le formule en langage naturel.

Règle pratique : commence en stdio. Tu valides toute ta logique en local, puis tu bascules en HTTP/SSE le jour où tu veux exposer le serveur à plusieurs personnes.

Étape 1 : initialiser le projet avec le SDK TypeScript

Commandes à enchaîner :

mkdir mon-mcp && cd mon-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init

Dans package.json, ajoute un champ bin pour que ton serveur soit exécutable :

{
  "name": "mon-mcp",
  "type": "module",
  "bin": { "mon-mcp": "./dist/index.js" },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts"
  }
}

Crée src/index.ts avec le squelette suivant :

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server(
  { name: "mon-mcp", version: "0.1.0" },
  { capabilities: { tools: {}, resources: {} } }
);

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

À ce stade, le serveur tourne mais ne sert à rien : pas de tool, pas de resource. On corrige ça.

Étape 2 : exposer un premier tool

Imagine un CRM interne. On veut un tool search_clients qui prend un nom et renvoie les clients matchant. Deux handlers à enregistrer : ListToolsRequestSchema qui déclare le tool, CallToolRequestSchema qui l'exécute.

import {
  ListToolsRequestSchema,
  CallToolRequestSchema
} from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "search_clients",
    description: "Recherche des clients dans le CRM par nom partiel. Retourne id, nom, email, dernier contact. Utilise ce tool quand l'utilisateur veut retrouver une fiche client ou vérifier l'existence d'un compte.",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", description: "Nom ou fragment de nom" }
      },
      required: ["query"]
    }
  }]
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name === "search_clients") {
    const { query } = req.params.arguments as { query: string };
    const results = await db.searchClients(query);
    return {
      content: [{
        type: "text",
        text: results.map(r => `- ${r.name} (${r.email}) - dernier contact ${r.lastContact}`).join("\n")
      }]
    };
  }
  throw new Error(`Tool inconnu: ${req.params.name}`);
});

Le point critique, c'est la description du tool. Claude la lit pour décider quand appeler l'outil. Sois explicite : que fait le tool, qu'est-ce qu'il retourne, dans quel contexte l'utiliser. Une description du genre "recherche" tout court ne sera presque jamais déclenchée.

Renvoie toujours du texte formaté lisible, pas du JSON brut. Claude est meilleur pour reformuler du texte structuré que pour parser du JSON dans une réponse utilisateur.

Étape 3 : exposer une ressource (resource)

Un tool agit, une resource est lue. Typiquement : un fichier, un enregistrement, un document. Claude peut décider de lire une resource pour enrichir son contexte avant de répondre.

import {
  ListResourcesRequestSchema,
  ReadResourceRequestSchema
} from "@modelcontextprotocol/sdk/types.js";

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [{
    uri: "notes://daily/today",
    name: "Notes du jour",
    description: "Note Markdown du journal quotidien",
    mimeType: "text/markdown"
  }]
}));

server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
  if (req.params.uri === "notes://daily/today") {
    const content = await fs.readFile(`./notes/${todayISO()}.md`, "utf8");
    return {
      contents: [{ uri: req.params.uri, mimeType: "text/markdown", text: content }]
    };
  }
  throw new Error(`Resource inconnue`);
});

Règle de design : si l'action peut avoir un effet de bord (écriture, appel externe, mutation), c'est un tool. Si c'est purement de la lecture déterministe, c'est une ressource.

Étape 4 : connecter le serveur à Claude Desktop

Build d'abord : npm run build. Puis localise claude_desktop_config.json :

OSChemin
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json

Ajoute une entrée :

{
  "mcpServers": {
    "mon-mcp": {
      "command": "node",
      "args": ["/chemin/absolu/vers/mon-mcp/dist/index.js"],
      "env": { "DB_URL": "postgresql://..." }
    }
  }
}

Redémarre Claude Desktop complètement (quit, pas juste fermer la fenêtre). L'icône MCP doit apparaître dans la barre de prompt. Teste avec un message du type "cherche le client Dupont". Si rien ne se passe, consulte les logs : ~/Library/Logs/Claude/mcp*.log sur macOS.

Étape 5 : déboguer et inspecter avec MCP Inspector

Itérer dans Claude Desktop est lent. L'outil officiel @modelcontextprotocol/inspector te donne une interface web pour envoyer manuellement list_tools, call_tool, list_resources sans passer par Claude.

npx @modelcontextprotocol/inspector node dist/index.js

Workflow recommandé : tu codes, tu relances l'inspector, tu cliques sur ton tool, tu vois la réponse brute, tu corriges. Une fois que tout est vert dans Inspector, tu redémarres Claude Desktop pour valider l'intégration finale. Tu gagnes facilement 80 % du temps de debug.

Passer en production : déploiement distant et sécurité

Le jour où plusieurs personnes doivent utiliser ton serveur, stdio ne suffit plus. Tu bascules sur HTTP/SSE. Le SDK fournit SSEServerTransport pour exposer ton serveur sur un endpoint HTTP.

Hébergement raisonnable selon ta charge :

OptionQuandEffort
VPS classiqueTrafic prévisible, accès SSHMoyen
Railway / Fly.ioDémarrage rapide, scaling autoFaible
Cloudflare WorkersEdge, latence faible globaleMoyen, contraintes runtime

Sécurité, non négociable :

  • Authentification par token Bearer minimum, OAuth si tu exposes à des tiers.
  • Tous les secrets en variables d'environnement, jamais en dur dans le code MCP.
  • Rate limiting par token : un client mal configuré peut spammer ton serveur.
  • Logs structurés (pino, winston) avec request id pour tracer les call_tool.
  • Validation stricte des inputs avec Zod ou équivalent : Claude peut envoyer des arguments inattendus.

Pour les architectures où tu combines un serveur MCP côté infra et des appels LLM côté produit, regarde aussi l'API Anthropic : MCP et API sont complémentaires, pas concurrents.

Erreurs fréquentes et bonnes pratiques

Les pièges qui reviennent à chaque revue de code MCP :

  • Descriptions de tools floues. "Récupère des données" ne fait pas appeler le tool. Décris le quoi, le quand, le format de retour.
  • Schémas d'entrée trop permissifs. Un type: "object" sans required = Claude qui appelle le tool sans paramètre. Sois strict.
  • Retourner du JSON brut. Formatte en texte lisible. Si tu as vraiment besoin de structure, utilise du Markdown.
  • Pas de gestion d'erreurs. Une exception non catchée tue le process et casse la session Claude. Wrap chaque handler dans un try/catch et renvoie un message d'erreur formaté.
  • Tool god-mode. Un seul tool do_anything avec 15 paramètres optionnels = Claude qui s'embrouille. Préfère un tool par action métier, granularité fine.
  • Ignorer la persistance. Si ton tool écrit, log toujours : qui a appelé, avec quels arguments, quel résultat. Tu en auras besoin le jour où Claude fera n'importe quoi.

Un dernier réflexe : teste tes tools avec des prompts adverses. "Supprime tous les clients". "Donne-moi la liste complète sans filtre". Si ton serveur les exécute aveuglément, tu as un problème de design, pas un problème de Claude.

Et maintenant : passer du tutoriel au produit

Tu as un serveur MCP qui tourne, branché à Claude Desktop, avec un tool et une resource. C'est le socle. La suite, c'est designer une vraie expérience produit autour de ces outils : quelles actions exposer, quelles données rendre lisibles, comment Claude doit se comporter face à tes utilisateurs finaux.

C'est exactement ce qu'on construit en cohorte chez Ottho avec Claude Builders, le programme de 5 semaines pour construire votre produit IA : architecture MCP, intégrations métier, déploiement, et mise en production accompagnée. Si ton serveur de tutoriel doit devenir un vrai produit, c'est le bon endroit pour passer le cap.

Pilier 6 · Mastery

Automatiser sa stack avec Claude et MCP

Connecter Claude à votre CRM, Notion, Slack, et construire des workflows fiables. Le territoire des opérationnels et des Builders.

Découvrir le pilier complet →