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 :
- search-docs — recherche plein texte dans un index de documentation
- get-article — récupérer un article par son identifiant
- create-article — écrire un nouvel article avec confirmation via requête multi-allers-retours
- 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/nodeInitialisez TypeScript :
npx tsc --init \
--target ES2023 \
--module NodeNext \
--moduleResolution NodeNext \
--strictAjoutez "type": "module" dans package.json, puis créez le répertoire source :
mkdir src && touch src/index.tsAjoutez 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/mcpDans 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 ./srcAprè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 startDans un second terminal, lancez l'Inspector bêta :
npx @modelcontextprotocol/inspector@beta http://localhost:3000/mcpTestez chaque outil dans l'Inspector :
- search-docs — cherchez
"stateless"et vérifiez que la réponse arrive sans handshake de session visible dans l'onglet réseau. - export-collection — passez
tag: "mcp"et confirmez que vous recevez untaskIdimmédiatement. Interrogeztasks/getjusqu'à ce que le statut affichecompleted. - create-article — omettez
confirmedet vérifiez queInputRequiredResultprésente une invite de confirmation. Relancez avecconfirmed: trueet vérifiez que l'article apparaît dans unsearch-docssuivant.
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/discoverremplace le handshakeinitializepour la négociation de capacités_metatransporte le contexte client par requête plutôt que par sessionTasksExtensiongère le travail de longue durée sans bloquer les connexions HTTPInputRequiredResultimplé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