Noqta
  • Accueil
  • Services
  • À propos
  • Écrits
  • Se connecter
écrits/tutorial/2026/07
● Tutorial20 juil. 2026·25 min

Créer des serveurs MCP sans état avec TypeScript : le guide de la spec 2026-07-28

Construisez un serveur MCP sans état prêt pour la production avec le SDK bêta 2026-07-28. Couvre la suppression du handshake de session, l'implémentation de server/discover, la gestion des Tasks asynchrones et les requêtes multi-allers-retours pour les confirmations — le tout évolutif horizontalement.

AI Bot
AI Bot
Author
·EN · FR · AR

Le release candidate MCP 2026-07-28 marque le plus grand changement architectural de l'histoire du protocole : les sessions disparaissent. Fini le handshake initialize, fini l'en-tête Mcp-Session-Id, fini le sticky routing sur votre load balancer.

Ce tutoriel construit un serveur MCP de base de connaissances de zéro avec le SDK TypeScript bêta. Vous implémenterez le transport sans état, la découverte de capacités, les Tasks asynchrones et les requêtes multi-allers-retours pour les confirmations utilisateur — les quatre patterns qui définissent le nouveau protocole.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ installé (node --version)
  • Une familiarité avec TypeScript — async/await, génériques
  • Une compréhension de base des concepts MCP — si vous débutez, commencez par notre guide du serveur MCP en TypeScript
  • Un terminal et un éditeur de code

Ce que vous allez construire

Un serveur MCP KnowledgeBase que les agents IA peuvent utiliser pour :

  1. search-docs — recherche plein texte dans un index de documentation
  2. get-article — récupérer un article par son identifiant
  3. create-article — écrire un nouvel article avec confirmation via requête multi-allers-retours
  4. export-collection — exporter une collection filtrée par tag sous forme de Task asynchrone

À la fin, vous aurez un serveur qui s'exécute de façon identique sur n'importe quelle instance, passe à l'échelle horizontalement derrière un simple load balancer round-robin, et gère les exports longs sans bloquer la connexion HTTP.

Étape 1 : Configuration du projet

Créez le projet et installez le SDK bêta :

mkdir kb-mcp-server && cd kb-mcp-server
pnpm init
pnpm add @modelcontextprotocol/server@beta zod
pnpm add -D typescript tsx @types/node

Initialisez TypeScript :

npx tsc --init \
  --target ES2023 \
  --module NodeNext \
  --moduleResolution NodeNext \
  --strict

Ajoutez "type": "module" dans package.json, puis créez le répertoire source :

mkdir src && touch src/index.ts

Ajoutez un script de démarrage dans package.json :

{
  "scripts": {
    "start": "tsx src/index.ts",
    "build": "tsc"
  }
}

Version SDK : Ces exemples ciblent @modelcontextprotocol/server@2.0.0-beta.x. Vérifiez avec pnpm list @modelcontextprotocol/server. Le SDK v2 bêta est rétrocompatible — les serveurs v2 répondent correctement aux clients v1, vous pouvez donc migrer à votre rythme.

Étape 2 : Un serveur sans état minimal

Le changement le plus visible en v2 est ce qui est absent : pas de handler initialize, pas de création de session, pas de Mcp-Session-Id à suivre. Chaque appel d'outil est autonome — les identifiants d'articles arrivent dans le corps de la requête, la réponse repart sur la même connexion HTTP.

Créez src/index.ts :

import { McpServer } from "@modelcontextprotocol/server/mcp.js";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/server/streamable-http.js";
import { z } from "zod";
import http from "node:http";
 
// Store en mémoire pour le tutoriel. Remplacez par une vraie base de données en production.
const articles = new Map([
  ["001", { title: "Débuter avec MCP", content: "MCP est un protocole...", tags: ["mcp", "intro"] }],
  ["002", { title: "Patterns de conception sans état", content: "Les services sans état éliminent...", tags: ["architecture"] }],
]);
 
const server = new McpServer({
  name: "kb-server",
  version: "2.0.0",
});
 
server.registerTool(
  "search-docs",
  {
    description: "Recherche plein texte dans la base de connaissances.",
    inputSchema: z.object({
      query: z.string().describe("Termes de recherche"),
      limit: z.number().int().min(1).max(20).default(5),
    }),
  },
  async ({ query, limit }) => {
    const q = query.toLowerCase();
    const results = [...articles.entries()]
      .filter(([, a]) => a.title.toLowerCase().includes(q) || a.content.toLowerCase().includes(q))
      .slice(0, limit)
      .map(([id, a]) => ({ id, title: a.title, tags: a.tags }));
 
    return {
      content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
    };
  }
);
 
server.registerTool(
  "get-article",
  {
    description: "Récupérer un article par son identifiant.",
    inputSchema: z.object({
      id: z.string().describe("Identifiant de l'article"),
    }),
  },
  async ({ id }) => {
    const article = articles.get(id);
    if (!article) {
      return {
        isError: true,
        content: [{ type: "text", text: `Article ${id} introuvable` }],
      };
    }
    return {
      content: [{ type: "text", text: JSON.stringify({ id, ...article }, null, 2) }],
    };
  }
);

Étape 3 : Transport sans état et server/discover

En v1, le handshake initialize informait le client des capacités du serveur. En v2, cette négociation se fait à la demande via la nouvelle méthode server/discover. Le SDK bêta gère le routage automatiquement — il suffit d'activer stateless: true sur le transport.

Ajoutez l'écouteur HTTP dans src/index.ts :

const transport = new StreamableHttpServerTransport({ stateless: true });
await server.connect(transport);
 
const httpServer = http.createServer(async (req, res) => {
  // Le transport route server/discover, tools/call, tasks/get, etc.
  await transport.handleRequest(req, res);
});
 
httpServer.listen(3000, () => {
  console.log("Serveur KB MCP → http://localhost:3000/mcp");
});

stateless: true indique au transport de ne plus faire de suivi de session. Les requêtes sans Mcp-Session-Id sont désormais le chemin nominal, pas un fallback.

Testez avec l'Inspector bêta :

npx @modelcontextprotocol/inspector@beta http://localhost:3000/mcp

Dans l'onglet réseau, vous verrez les outils listés après un seul appel server/discover, sans aucun échange initialize préalable.

Étape 4 : Métadonnées client via _meta

En v1, les informations client (nom, version, capacités) arrivaient une fois lors de initialize et étaient stockées en mémoire de session. En v2 elles transitent dans le champ _meta de chaque requête.

Accédez-y dans n'importe quel handler via le second argument :

server.registerTool(
  "get-article",
  {
    description: "Récupérer un article par son identifiant.",
    inputSchema: z.object({ id: z.string() }),
  },
  async ({ id }, context) => {
    const clientName = context.meta?.clientInfo?.name ?? "inconnu";
    const clientVersion = context.meta?.clientInfo?.version ?? "?";
    console.log(`[${clientName}@${clientVersion}] get-article id=${id}`);
 
    const article = articles.get(id);
    if (!article) {
      return { isError: true, content: [{ type: "text", text: `Introuvable : ${id}` }] };
    }
    return { content: [{ type: "text", text: JSON.stringify({ id, ...article }) }] };
  }
);

Chaque requête est auditée indépendamment. Vous pouvez limiter le débit par client, journaliser par client et appliquer des quotas par client — sans aucun store de session partagé.

Étape 5 : Extension Tasks — exports asynchrones

L'outil export-collection peut prendre plusieurs dizaines de secondes pour de grandes collections. Bloquer la connexion HTTP aussi longtemps n'est pas acceptable. L'extension Tasks vous permet de retourner un identifiant de tâche immédiatement et de laisser le client interroger la progression.

Le SDK v2 fournit TasksExtension pour gérer ce cycle de vie :

import { TasksExtension } from "@modelcontextprotocol/server/extensions/tasks.js";
 
const tasks = new TasksExtension(server);
 
server.registerTool(
  "export-collection",
  {
    description: "Exporter tous les articles correspondant à un tag sous forme de fichier JSON.",
    inputSchema: z.object({
      tag: z.string().describe("Tag pour filtrer les articles"),
    }),
  },
  async ({ tag }) => {
    const task = tasks.create(async (taskCtx) => {
      await taskCtx.updateStatus("running", "Collecte des articles...");
 
      const matching = [...articles.entries()].filter(([, a]) => a.tags.includes(tag));
      await taskCtx.updateStatus("running", `${matching.length} articles trouvés. Empaquetage...`);
 
      // Simuler un travail asynchrone (requête BDD, compression, upload, etc.)
      await new Promise((r) => setTimeout(r, 2000));
 
      const payload = JSON.stringify(Object.fromEntries(matching), null, 2);
      await taskCtx.complete({ data: payload, filename: `${tag}-export.json` });
    });
 
    // Retourner l'identifiant de tâche immédiatement — le client interroge tasks/get
    return { type: "task", taskId: task.id };
  }
);

Le client reçoit taskId en moins d'une milliseconde, puis appelle tasks/get à son rythme jusqu'à ce que le statut soit completed.

tasks/list est intentionnellement absent de v2. Sans sessions, il n'existe pas de définition sûre de « toutes les tâches en cours pour ce client ». Transmettez le taskId via les variables de contexte de votre agent plutôt que de compter sur une énumération côté serveur.

Étape 6 : Requêtes multi-allers-retours (MRTR)

Créer un nouvel article est une action significative — elle modifie l'état partagé et mérite une confirmation. MRTR permet à un outil de suspendre son exécution, de présenter une invite à l'utilisateur, et de reprendre avec sa réponse.

Retournez InputRequiredResult pour suspendre l'outil :

import { InputRequiredResult } from "@modelcontextprotocol/server/mcp.js";
 
server.registerTool(
  "create-article",
  {
    description: "Créer un nouvel article dans la base de connaissances.",
    inputSchema: z.object({
      title: z.string(),
      content: z.string(),
      tags: z.array(z.string()),
      confirmed: z.boolean().optional().describe("Passer true après confirmation utilisateur"),
    }),
  },
  async ({ title, content, tags, confirmed }) => {
    if (!confirmed) {
      return new InputRequiredResult({
        message: `Créer l'article "${title}" avec les tags [${tags.join(", ")}] ? Répondez oui ou non.`,
        schema: z.object({ confirmed: z.boolean() }),
      });
    }
 
    const id = String(articles.size + 1).padStart(3, "0");
    articles.set(id, { title, content, tags });
 
    return {
      content: [{ type: "text", text: `Article ${id} créé : "${title}"` }],
    };
  }
);

Quand l'outil retourne InputRequiredResult, le client MCP présente le message à l'utilisateur. L'utilisateur répond ; le client relance l'appel avec confirmed: true. Aucune machine d'état personnalisée de votre côté.

Étape 7 : En-têtes de passerelle pour le routage intelligent

Les deux nouveaux en-têtes de transport — Mcp-Method et Mcp-Name — permettent aux passerelles API de router les requêtes par méthode et nom d'outil sans analyser le corps JSON-RPC. Exemple de règle nginx :

upstream standard { server kb-mcp-1:3000; server kb-mcp-2:3000; server kb-mcp-3:3000; }
upstream heavy    { server kb-mcp-heavy:3000; }
 
server {
    listen 80;
    location /mcp {
        set $pool standard;
        if ($http_mcp_name = "export-collection") {
            set $pool heavy;
        }
        proxy_pass http://$pool;
    }
}

Le code des outils ne change pas. Les en-têtes sont émis automatiquement par les clients v2 lorsqu'ils communiquent avec des serveurs v2.

Étape 8 : Migrer un serveur v1 existant

Si vous avez déjà un serveur MCP v1, le codemod officiel gère la majeure partie de la migration :

npx @modelcontextprotocol/codemod@beta v1-to-v2 ./src

Après le codemod, vérifiez manuellement deux points :

Codes d'erreur — Si vous interceptez le code -32002 pour les ressources manquantes, changez-le en -32602 (paramètres JSON-RPC invalides).

Handlers initialize — Tous les callbacks onInitialize doivent être déplacés. Les capacités que vous poussiez lors de initialize doivent maintenant être retournées en réponse à server/discover. Si vous ne poussiez que la liste standard, vous pouvez simplement supprimer le handler — le SDK la remplit automatiquement.

Lancez votre suite de tests après le codemod. La plupart des projets nécessitent moins de dix minutes de nettoyage manuel.

Étape 9 : Déploiement en production

La conception sans état rend la mise à l'échelle horizontale triviale. Un stack Docker Compose à trois instances avec round-robin simple :

version: "3.9"
 
services:
  kb-mcp-1:
    build: .
    environment:
      NODE_ENV: production
 
  kb-mcp-2:
    build: .
    environment:
      NODE_ENV: production
 
  kb-mcp-3:
    build: .
    environment:
      NODE_ENV: production
 
  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
upstream mcp_pool {
    server kb-mcp-1:3000;
    server kb-mcp-2:3000;
    server kb-mcp-3:3000;
}
 
server {
    listen 80;
    location /mcp {
        proxy_pass http://mcp_pool;
    }
}

Pas de store de session. Pas de sticky routing. Pas de couche de coordination. Vous pouvez arrêter n'importe quelle instance pour un déploiement progressif et les requêtes en cours se terminent sur les instances restantes ou échouent rapidement et sont retentées — selon la configuration de votre client.

Tester votre implémentation

Démarrez le serveur :

pnpm start

Dans un second terminal, lancez l'Inspector bêta :

npx @modelcontextprotocol/inspector@beta http://localhost:3000/mcp

Testez chaque outil dans l'Inspector :

  1. search-docs — cherchez "stateless" et vérifiez que la réponse arrive sans handshake de session visible dans l'onglet réseau.
  2. export-collection — passez tag: "mcp" et confirmez que vous recevez un taskId immédiatement. Interrogez tasks/get jusqu'à ce que le statut affiche completed.
  3. create-article — omettez confirmed et vérifiez que InputRequiredResult présente une invite de confirmation. Relancez avec confirmed: true et vérifiez que l'article apparaît dans un search-docs suivant.

Résolution de problèmes

Rejet de Mcp-Session-Id — un client v1 envoyant des en-têtes de session continuera à fonctionner ; le serveur v2 ignore les en-têtes non reconnus. Si vous voyez des erreurs de rejet, vérifiez votre passerelle plutôt que le SDK.

Tasks introuvables — assurez-vous que TasksExtension est instancié avant tout appel registerTool qui le référence.

Boucle MRTR — si le client relance sans passer la réponse utilisateur, l'outil retourne InputRequiredResult indéfiniment. Vérifiez que confirmed est présent dans le schéma et que votre client le transmet au prochain appel.

Liste vide dans server/discover — appelez server.connect(transport) avant d'enregistrer vos outils, ou déplacez les enregistrements avant l'appel connect.

Conclusion

La spec MCP 2026-07-28 transforme la gestion de sessions de votre problème en non-problème. Supprimer le handshake a éliminé toute une catégorie de complexité des systèmes distribués — sticky routing, stores de session partagés, arrêts progressifs complexes — et les a remplacés par la sémantique HTTP standard.

Vos points clés à retenir :

  • Le transport sans état signifie que n'importe quelle instance gère n'importe quelle requête — le round-robin standard suffit
  • server/discover remplace le handshake initialize pour la négociation de capacités
  • _meta transporte le contexte client par requête plutôt que par session
  • TasksExtension gère le travail de longue durée sans bloquer les connexions HTTP
  • InputRequiredResult implémente les flux de confirmation sans machine d'état personnalisée
  • Le codemod officiel couvre la majeure partie de la migration v1 vers v2 en quelques minutes

La spec finale sort le 28 juillet. Le SDK bêta est prêt pour la production pour les nouveaux serveurs dès aujourd'hui.

Prochaines étapes

  • Lisez le guide du protocole MCP sans état 2026-07-28 pour une vue complète de la spécification
  • Explorez construire un client MCP en TypeScript pour comprendre le protocole côté client
  • Étudiez les patterns MCP enterprise pour les déploiements multi-tenant
  • Remplacez MCP Logging (déprécié en v2) par OpenTelemetry — consultez le guide de migration officiel pour la configuration recommandée
● Tags
#mcp#typescript#ai-agents#protocol#api#intermediate#25 min de lecture
● Partage
● Une question ?

Discutez de cet article avec un agent Noqta.

AI Bot
AI Bot
Author · noqta
Suivre ↗

● À lire ensuite

Automatisation Agentique Régie par MCP : Comment Déployer des Agents IA en Toute Sécurité en 2026
● Tutorial

Automatisation Agentique Régie par MCP : Comment Déployer des Agents IA en Toute Sécurité en 2026

3 févr. 2026
Protocole A2A en TypeScript : construire des agents IA interopérables avec le standard Agent2Agent (2026)
● Tutorial

Protocole A2A en TypeScript : construire des agents IA interopérables avec le standard Agent2Agent (2026)

6 juil. 2026
Créer un agent ACP en TypeScript : connecter n'importe quel éditeur à votre agent IA (2026)
● Tutorial

Créer un agent ACP en TypeScript : connecter n'importe quel éditeur à votre agent IA (2026)

30 juin 2026
Noqta
Conditions générales · Politique de Confidentialité
Services
  • Automatisation IA
  • Agents IA
  • Automatisation CX
  • Vibe Coding
  • Gestion de Projet
  • Assurance Qualité
  • Développement Web
  • Intégration API
  • Applications Métier
  • Maintenance
  • Low-Code/No-Code
Liens
  • À propos de nous
  • Comment ça marche?
  • Actualités
  • Tutoriels
  • Blog
  • Contact
  • FAQ
  • Ressources
Régions
  • Arabie Saoudite
  • Émirats Arabes Unis
  • Qatar
  • Bahreïn
  • Oman
  • Libye
  • Tunisie
  • Algérie
  • Maroc
Entreprise
  • Noqta, Tunisie, Tunis, téléphone +216 40 385 594
© Noqta. Tous droits réservés.