Guide · SaaS & dev

Créer un serveur MCP en TypeScript : méthode, tests et sécurité

Construire un serveur MCP minimal en TypeScript : périmètre, outil lecture seule, validation, tests et règles de sécurité avant toute intégration.

Créer un serveur MCP en TypeScript ne consiste pas à connecter une IA à toutes vos données. Commencez par un outil minuscule, en lecture seule, dont l’entrée et la sortie sont faciles à vérifier. Par exemple, un serveur de démonstration peut exposer une recherche dans trois fiches fictives. Il ne doit ni lire votre répertoire personnel, ni exécuter des commandes arbitraires, ni recevoir un token de production.

TypeScript est un choix pratique parce qu’il permet de décrire les données attendues et de repérer certaines incohérences avant l’exécution. Il ne remplace pas la validation au runtime : un client externe peut envoyer une valeur inattendue même si vos types sont corrects. Votre serveur doit donc contrôler ses entrées, limiter ses capacités et produire des erreurs compréhensibles sans révéler de secret.

Ce que vous allez apprendre

Vous allez définir un premier cas d’usage, préparer un projet TypeScript isolé, déclarer un outil conceptuel, tester son comportement et documenter les limites. L’objectif est d’apprendre le cycle serveur MCP sans installer un connecteur à haut privilège.

Prérequis

Vous avez besoin de Node.js 20 ou plus récent, de npm, et du guide qui correspond à la ligne de SDK installée. Les commandes, le résultat attendu et l’erreur fréquente sont dans la section du 2026-10-18. Les APIs évoluent : vérifiez les exemples officiels de la version réellement installée plutôt que de copier un import depuis un article ancien.

Travaillez dans un dossier de test et contrôlez son état Git :

git status --short
node --version

Vérification : le dépôt est identifié, les changements existants ont un propriétaire et vous connaissez la commande de test prévue. Ne créez pas le serveur dans le même dossier qu’une application de production pour un premier essai.

1. Définir un outil sans effet de bord

Écrivez un contrat avant le code. Exemple : « search_notes reçoit un mot-clé court et retourne les titres de notes fictives qui le contiennent ». Définissez les limites : longueur maximale, résultat vide, caractères inattendus et absence d’écriture. Cette description permet de choisir un schéma d’entrée et un test utile.

Évitez d’abord les outils qui créent une issue, modifient une table ou lancent une commande. Un outil d’écriture demande des permissions, une confirmation adaptée, une cible de test et un retour arrière. Le fait qu’un SDK rende une action possible ne signifie pas qu’elle est nécessaire à votre pilote.

Vérification : vous pouvez expliquer en une phrase quelles données sont lues et ce que l’outil ne peut pas faire. Si le contrat est flou, ne passez pas à l’implémentation.

2. Préparer le projet TypeScript

Créez le projet avec les outils approuvés par votre équipe. Consultez d’abord les scripts existants : gestionnaire de paquets, formatage, tests et règles de version. Installez le SDK officiel ou une dépendance explicitement choisie, puis verrouillez sa version selon la politique du dépôt. N’ajoutez pas de paquet parce qu’un exemple de blog en utilise un sans vérifier sa provenance.

Organisez le code pour séparer la logique métier de l’adaptateur MCP. Par exemple, une fonction searchNotes(query) peut retourner des données de démonstration sans connaître le protocole. Le serveur MCP traduit ensuite l’appel entrant vers cette fonction. Cette séparation rend la logique testable sans processus réseau ni client IA.

Vérification : la logique de recherche peut être appelée avec une entrée fictive depuis un test local. Les imports MCP restent concentrés dans une couche dédiée.

3. Décrire les entrées et les sorties

Un outil MCP doit déclarer clairement les arguments qu’il attend. Utilisez le mécanisme de schéma recommandé par le SDK pour refuser les entrées absentes, trop longues ou mal formées. Les types TypeScript vous aident pendant le développement, mais une validation runtime protège le serveur contre les valeurs réellement reçues.

Votre réponse doit être structurée et limitée. Si une recherche n’a aucun résultat, retournez une réponse explicite. Si une entrée est invalide, indiquez ce qui est attendu sans afficher de stack trace ni de configuration interne. N’incluez jamais une variable d’environnement, une URL avec identifiants ou un objet complet de requête dans un message d’erreur.

Voici une logique volontairement indépendante du SDK :

type Note = { title: string; body: string };

export function searchNotes(notes: Note[], query: string) {
  const normalized = query.trim().toLowerCase();
  if (normalized.length < 2) throw new Error("La recherche doit contenir au moins deux caractères.");
  return notes.filter((note) => note.title.toLowerCase().includes(normalized));
}

Vérification : une recherche valide renvoie uniquement les données de démonstration prévues ; une recherche trop courte produit une erreur contrôlée.

4. Brancher l’outil au serveur MCP

Suivez l’exemple officiel du SDK pour créer le serveur, déclarer le nom de l’outil et connecter le transport prévu par votre client. Les noms de classes et fonctions peuvent varier selon la version : ne les inventez pas pour faire correspondre un extrait. Gardez votre configuration locale et évitez tout démarrage automatique à l’ouverture d’un projet.

Lors de l’enregistrement, décrivez l’outil avec un nom précis, une courte explication et le schéma de ses arguments. Le client doit pouvoir présenter cette capacité de manière compréhensible. Un nom vague comme run cache le risque ; search_notes indique déjà que l’action est conçue pour la lecture.

Vérification : listez les capacités du serveur dans un environnement de test et confirmez qu’une seule action de lecture est exposée. Si une action inattendue apparaît, arrêtez le pilote.

5. Écrire des tests avant d’ajouter des données réelles

Testez la logique indépendante avec des notes fictives. Couvrez un résultat normal, une absence de résultat et une entrée invalide. Vérifiez que l’outil ne modifie pas la collection passée en entrée. Cette dernière assertion est importante : une fonction de recherche ne doit pas avoir d’effet de bord.

Un pseudo-test adapté à votre framework pourrait vérifier :

expect(searchNotes(notes, "déploiement")).toHaveLength(1);
expect(searchNotes(notes, "inconnu")).toEqual([]);

Exécutez réellement la commande de test du projet. Un test proposé par un assistant ou une suite qui compile ne constitue pas une validation tant que son résultat n’a pas été observé. Notez aussi les contrôles que vous n’avez pas lancés, plutôt que de les supposer verts.

Vérification : changez temporairement le résultat attendu. Le test doit échouer. Cette étape démontre que l’assertion protège bien le comportement annoncé.

6. Sécuriser la configuration et la maintenance

Avant de connecter une source réelle, rédigez une fiche : serveur, version, mainteneur, données accessibles, compte utilisé, outils exposés, logs et procédure de retrait. Si un identifiant est requis, utilisez une variable protégée et un compte de service avec le scope minimal. Un .env.example peut documenter le nom d’une variable, jamais sa valeur active.

Traitez les contenus retournés par un serveur comme non fiables. Un document pourrait contenir des instructions demandant de révéler des secrets ou de contourner les règles. Ce texte est une donnée à analyser, pas une permission. Les confirmations de votre client et les politiques du projet priment toujours.

Mise à jour du 2026-10-18 : commandes reproductibles

Cette section rend le pilote local exécutable. Les commandes d’installation et de lancement viennent du guide officiel « Build your first server » de la ligne v2 du SDK TypeScript, relu le 2026-10-18. Seuls le nom du dossier et l’outil changent : trois fiches fictives, en lecture seule, sans appel réseau. Aucun numéro de paquet n’est figé. Ces commandes n’ont pas été réexécutées dans l’environnement de publication : le résultat attendu est celui décrit par ce guide, ou le contrat de l’outil de cette page. S’il diffère, arrêtez et relisez le guide de la version installée.

Le tutoriel TypeScript du site Model Context Protocol, relu le même jour, exige aussi Node.js 20 ou plus récent, mais il importe StdioServerTransport et zod autrement. Ne mélangez pas les deux extraits. Les adresses déjà citées en fin de page restent les seules : retrouvez le dépôt du SDK, puis la révision courante, avant d’installer.

Prérequis du pilote

Node.js 20 ou plus récent, npm, un dossier vide hors de toute application de production, et aucun secret. Le guide « Build your first server » indique qu’aucune autre installation n’est requise pour ce parcours.

Commande :

node --version
npm --version

Résultat attendu : deux numéros de version s’affichent. Le premier nombre de Node.js est 20 ou plus.

Erreur fréquente : garder un Node.js plus ancien parce qu’il est déjà installé. Les pages relues ne donnent pas le texte exact de cet échec : ne continuez pas si le numéro est inférieur à 20.

Créer le projet

Prérequis : le contrôle de version ci-dessus est passé. Le répertoire parent n’est pas l’application de production.

Commande : le guide officiel crée un dossier weather. Ici il s’appelle notes-mcp, pour rester sur le contrat de cette page. Le reste est inchangé, sans suffixe de version.

mkdir notes-mcp && cd notes-mcp
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server zod tsx
mkdir src
npm pkg get type

Résultat attendu : npm pkg get type affiche "module". Un dossier node_modules et un package-lock.json existent. Les versions réellement résolues sont dans ce lockfile : notez-les. Ne les remplacez pas par un numéro lu dans un article, y compris celui-ci.

Erreur fréquente : installer @modelcontextprotocol/sdk parce qu’un exemple de la ligne v1 l’utilise encore. La fiche du paquet serveur, relue le 2026-10-18, présente @modelcontextprotocol/server comme la ligne stable v2 et @modelcontextprotocol/sdk comme la ligne de maintenance. Si un import ne se résout pas, comparez-le au guide de la ligne installée au lieu de forcer les types.

Écrire le serveur de démonstration

Prérequis : le projet ci-dessus existe et type vaut module. Créez src/index.ts. Les imports et serveStdio viennent du guide v2 relu ; search_notes reste l’outil local de cette page. Le champ version passé à McpServer est l’identifiant annoncé au client, comme dans l’exemple officiel. Ce n’est pas une version npm.

import { McpServer } from "@modelcontextprotocol/server";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import * as z from "zod/v4";

type Note = { title: string; body: string };

const notes: Note[] = [
  { title: "Déploiement local", body: "Fiche fictive." },
  { title: "Sauvegarde", body: "Fiche fictive." },
  { title: "Journaux", body: "Fiche fictive." },
];

export function searchNotes(items: Note[], query: string) {
  const normalized = query.trim().toLowerCase();
  if (normalized.length < 2) {
    throw new Error("La recherche doit contenir au moins deux caractères.");
  }
  return items.filter((note) => note.title.toLowerCase().includes(normalized));
}

function createServer(): McpServer {
  const server = new McpServer({ name: "notes-demo", version: "1.0.0" });
  server.registerTool(
    "search_notes",
    {
      description: "Recherche en lecture seule dans trois fiches fictives.",
      inputSchema: z.object({
        query: z.string().min(2).max(40),
      }),
    },
    async ({ query }) => {
      const found = searchNotes(notes, query);
      const text = found.length === 0 ? "Aucun résultat." : found.map((note) => note.title).join("\n");
      return { content: [{ type: "text", text }] };
    },
  );
  return server;
}

void serveStdio(createServer);
console.error("notes-demo MCP server running on stdio");

Commande :

npx tsx src/index.ts

Résultat attendu : la ligne notes-demo MCP server running on stdio apparaît sur la sortie d’erreur, pas sur la sortie standard. Le processus attend ensuite un client et n’affiche rien d’autre. Arrêtez-le avec Ctrl+C. Le guide officiel décrit le même silence pour son exemple, avec le message weather MCP server running on stdio.

Erreur fréquente : un console.log écrit sur la sortie standard et corrompt le flux JSON-RPC. Les deux guides officiels relus demandent console.error pour un serveur stdio.

Appeler l’outil sans hôte

Prérequis : le fichier démarre comme ci-dessus, puis a été arrêté. Aucun secret n’est présent dans l’environnement.

Commande : celle du guide officiel, appliquée à ce fichier. N’ajoutez pas de numéro de version.

npx @modelcontextprotocol/inspector npx tsx src/index.ts

Résultat attendu : un onglet de navigateur s’ouvre. Le guide officiel demande de cliquer sur Connect, d’ouvrir l’onglet Tools, de choisir l’outil et de l’exécuter. Ici, le seul outil listé doit être search_notes. Avec la requête déploiement, le bloc texte contient Déploiement local et pas les deux autres titres. Avec inconnu, le bloc texte est Aucun résultat.

Erreur fréquente : le flux de l’inspecteur est illisible parce que le serveur a écrit sur la sortie standard. Autre écart fréquent : recopier le message de validation du tutoriel météo. Ce message concerne un autre schéma. Pour une requête trop courte, le SDK doit refuser l’appel avant le handler ; recopiez le texte affiché par l’inspecteur, ne l’inventez pas.

Limite de cette revue

Le guide « Build your first server » ne fournit pas de commande de test unitaire. N’ajoutez pas un script test seulement pour faire passer une commande absente. La vérification reproductible de ce pilote est le démarrage stdio et l’appel dans l’inspecteur. Les versions de @modelcontextprotocol/server, zod, tsx et de l’inspecteur doivent être lues dans le lockfile, ou sur le registre au moment de l’installation.

Problèmes fréquents et solutions

Le SDK ne correspond pas à l’exemple trouvé

Consultez la documentation de la version installée et les exemples officiels associés. Ne forcez pas un import ou ne baissez pas les types pour faire compiler du code conçu pour une autre version.

Le serveur nécessite un token large

Revenez à une source de démonstration, recherchez un scope en lecture seule ou créez une API intermédiaire limitée. Un token administrateur n’est pas une solution de développement.

Une erreur révèle trop d’informations

Retournez un message utilisateur contrôlé et conservez les détails techniques dans un journal d’opérateur minimalisé. Revoyez les logs pour vérifier qu’ils ne contiennent pas de secret.

FAQ

Faut-il connaître TypeScript pour créer un serveur MCP ?

Il faut comprendre les bases du runtime Node.js, des modules et de la validation d’entrée. TypeScript aide, mais la sécurité dépend aussi de la conception et des permissions.

Puis-je exposer une base PostgreSQL directement ?

Évitez-le pour un premier serveur. Préférez une base locale, un compte lecture seule ou un outil limité à une requête précise après revue de sécurité.

Un serveur MCP doit-il être public ?

Non. Un serveur peut être local ou interne. Son exposition réseau doit être décidée séparément selon le besoin et les contrôles disponibles.

Comment publier un serveur ?

La publication, l’hébergement et les secrets sont des tâches distinctes qui exigent une revue, une cible et une procédure de retour arrière. Ce tutoriel s’arrête au pilote local.

Les types suffisent-ils à valider les entrées ?

Non. Les types TypeScript disparaissent à l’exécution ; utilisez une validation runtime compatible avec le SDK et testez les entrées invalides.

Conclusion

Créer un serveur MCP en TypeScript devient plus sûr quand le premier outil est petit, en lecture seule et couvert par des tests. Séparez la logique de l’adaptateur, validez les entrées au runtime, gardez les secrets hors du projet et ne passez à une donnée réelle qu’après revue du périmètre.

Sources et limites

Les APIs du SDK changent selon les versions. La section du 2026-10-18 suit les imports du guide v2 relu ce jour-là ; elle reste un pilote local, pas une configuration de production. Aucune version de paquet n’y est figée : vérifiez le lockfile à l’installation. La spécification déjà citée porte la révision 2025-06-18 ; une révision plus récente était référencée par les pages du SDK relues le 2026-10-18, donc ne vous appuyez pas sur l’ancienne date sans la revérifier.