Tutoriel : migrer votre serveur MCP vers la spec stateless 2026-07-28
Guide pratique pour migrer un serveur MCP en production vers la spec 2026-07-28 : supprimer Mcp-Session-Id, Streamable HTTP stateless, load balancer, checklist breaking changes et procédures de test.
Tutoriel : migrer votre serveur MCP vers la spec stateless 2026-07-28
La spec MCP 2026-07-28 ne demande pas de réécrire vos outils — elle demande de réécrire la couche transport. Ce tutoriel vous guide pas à pas, avec du code TypeScript et Python, de l'audit à la production.
Le 28 juillet 2026, le Model Context Protocol publie sa révision stateless : fin du handshake initialize, suppression de l'en-tête Mcp-Session-Id, headers Mcp-Method et Mcp-Name obligatoires sur Streamable HTTP. Si vous avez suivi notre analyse de la spec MCP 2026 stateless pour les entreprises, vous connaissez le pourquoi. Cet article couvre le comment — avec des extraits de code testables, une configuration load balancer, une checklist de breaking changes et des procédures de test reproductibles.
Ce tutoriel s'adresse aux équipes qui ont déjà un serveur MCP en production — souvent créé via notre guide créer un serveur MCP en TypeScript en 30 minutes ou déployer un agent IA MCP en 20 minutes. Vous n'avez pas besoin de repartir de zéro : vous migrez la couche transport et les conventions, pas votre logique métier.
Ce qui change concrètement pour votre serveur
Avant d'ouvrir l'éditeur, alignons le vocabulaire. Sous 2025-11-25, un client MCP distant établissait une session, recevait un Mcp-Session-Id, et chaque requête suivante devait frapper la même instance serveur. Sous 2026-07-28, chaque requête est autonome : version de protocole, identité client et capacités voyagent dans _meta ; le load balancer peut router en round-robin.
| Élément | Avant (2025-11-25) | Après (2026-07-28) |
|---|---|---|
| Handshake | initialize + notifications/initialized | Supprimé (SEP-2575) |
| Session | En-tête Mcp-Session-Id | Supprimé (SEP-2567) |
| Routing LB | Sticky sessions obligatoires | Round-robin possible |
| Headers HTTP | Optionnels | Mcp-Method, Mcp-Name requis (SEP-2243) |
| Découverte | Via initialize | RPC server/discover |
| Tasks | Core expérimental | Extension avec nouveau cycle de vie |
| JSON Schema | Draft limité | JSON Schema 2020-12 complet (SEP-2106) |
De l'audit initial au déploiement production : audit, breaking changes, SDK, transport, load balancer, tests, canary, bascule
Phase 1 — Audit de votre stack MCP existante
Commencez par un inventaire factuel. Sans lui, vous risquez de découvrir un breaking change en production un vendredi soir.
Checklist d'audit (30 minutes)
Exécutez ces commandes sur votre dépôt et votre infrastructure :
# Rechercher les dépendances à session MCP
rg -n "Mcp-Session-Id|sessionId|StreamableHTTPServerTransport" --type ts --type py
# Handshake initialize legacy
rg -n "initialize|notifications/initialized" src/
# Transport HTTP+SSE déprécié
rg -n "SSEServerTransport|sse" src/
# API Tasks expérimentale 2025-11-25
rg -n "tasks/list|experimental.*tasks" src/
# Code d'erreur legacy -32002
rg -n "\-32002" src/
Notez pour chaque serveur MCP :
- Transport : stdio, Streamable HTTP, ou HTTP+SSE legacy (à migrer en priorité).
- Dépendances session : Redis, sticky cookie, affinité Kubernetes.
- Clients connectés : Cursor, Claude Desktop, n8n, SDK custom.
- Version SDK :
@modelcontextprotocol/sdk≥ compatible RC 2026-07-28.
Quatre axes : breaking changes, transport stateless, infrastructure, validation
Matrice de décision rapide
Serveur HTTP distant, session MCP, Tasks expérimental : trois questions pour prioriser les actions
- Serveur stdio local uniquement → mise à jour SDK + validation schémas ; pas de load balancer.
- Streamable HTTP avec sessions → refactor transport complet + infra.
- HTTP+SSE legacy → migration Streamable HTTP avant ou pendant le passage stateless.
Phase 2 — Migration TypeScript : du code 2025-11-25 au stateless
Nous partons d'un serveur type celui décrit dans notre tutoriel TypeScript, adapté pour HTTP avec sessions. Voici la version legacy simplifiée :
// ❌ AVANT — pattern 2025-11-25 avec session (extrait illustratif)
import express from 'express';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
const app = express();
app.use(express.json());
const sessions = new Map<string, StreamableHTTPServerTransport>();
app.post('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
if (!sessionId) {
// Première requête : initialize
const transport = new StreamableHTTPServerTransport('/mcp', res);
const server = createMcpServer();
await server.connect(transport);
sessions.set(transport.sessionId!, transport);
return;
}
const transport = sessions.get(sessionId);
if (!transport) {
res.status(404).json({ error: 'Session inconnue' });
return;
}
await transport.handleRequest(req, res);
});
Version 2026-07-28 stateless
// ✅ APRÈS — Streamable HTTP stateless (2026-07-28)
import express, { Request, Response } from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
const PROTOCOL_VERSION = '2026-07-28';
function createMcpServer(): McpServer {
const server = new McpServer(
{ name: 'crm-connector', version: '2.0.0' },
{ capabilities: { tools: {} } }
);
server.registerTool(
'search_contacts',
{
title: 'Rechercher des contacts CRM',
description: 'Recherche full-text dans le CRM entreprise',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', minLength: 1 },
limit: { type: 'integer', minimum: 1, maximum: 50, default: 10 },
},
required: ['query'],
},
},
async ({ query, limit = 10 }) => {
const results = await crmSearch(query, limit);
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
structuredContent: results,
};
}
);
// Pattern handle explicite pour état métier
server.registerTool(
'init_export_session',
{
title: "Initialiser une session d'export",
description: 'Crée un export_id pour les appels export_chunk suivants',
inputSchema: { type: 'object', properties: {} },
},
async () => {
const exportId = await createExportSession();
return {
content: [{ type: 'text', text: `export_id: ${exportId}` }],
structuredContent: { export_id: exportId },
};
}
);
return server;
}
const app = express();
app.use(express.json());
// Endpoint de découverte obligatoire (SEP-2575)
app.get('/mcp/discover', (_req: Request, res: Response) => {
res.json({
protocolVersions: [PROTOCOL_VERSION, '2025-11-25'],
serverInfo: { name: 'crm-connector', version: '2.0.0' },
capabilities: { tools: {} },
});
});
app.post('/mcp', async (req: Request, res: Response) => {
const protocolVersion = req.headers['mcp-protocol-version'] as string;
const mcpMethod = req.headers['mcp-method'] as string;
const mcpName = req.headers['mcp-name'] as string | undefined;
if (protocolVersion !== PROTOCOL_VERSION) {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32602, message: 'UnsupportedProtocolVersionError' },
id: null,
});
return;
}
// SEP-2243 : cohérence en-têtes / corps
const bodyMethod = req.body?.method as string | undefined;
if (mcpMethod && bodyMethod && mcpMethod !== bodyMethod) {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32602, message: 'Header/body method mismatch' },
id: req.body?.id ?? null,
});
return;
}
if (bodyMethod === 'tools/call') {
const toolName = req.body?.params?.name as string;
if (mcpName && toolName && mcpName !== toolName) {
res.status(400).json({
jsonrpc: '2.0',
error: { code: -32602, message: 'Header/body tool name mismatch' },
id: req.body?.id ?? null,
});
return;
}
}
// Injection _meta client si absent (compatibilité clients legacy)
if (!req.body?.params?._meta) {
req.body.params = {
...req.body.params,
_meta: {
'io.modelcontextprotocol/protocolVersion': PROTOCOL_VERSION,
'io.modelcontextprotocol/clientInfo': {
name: req.headers['user-agent'] ?? 'unknown-client',
version: '1.0.0',
},
},
};
}
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // stateless : pas de session
});
const server = createMcpServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(3000, () => console.log('MCP stateless sur :3000/mcp'));
Points clés du diff :
- Suppression du
Mapde sessions et de toute logiqueMcp-Session-Id. - Validation des en-têtes
Mcp-Method/Mcp-Namevs corps JSON-RPC. - Endpoint
GET /mcp/discoverpour la sonde de compatibilité. - État métier via
export_idexplicite, pas via session transport.
Mise à jour des schémas JSON Schema 2020-12
SEP-2106 autorise oneOf, anyOf et $ref/$defs. Exemple pour un outil polymorphe :
server.registerTool(
'update_entity',
{
inputSchema: {
type: 'object',
oneOf: [
{
type: 'object',
properties: {
entity_type: { const: 'contact' },
contact_id: { type: 'string' },
email: { type: 'string', format: 'email' },
},
required: ['entity_type', 'contact_id', 'email'],
},
{
type: 'object',
properties: {
entity_type: { const: 'deal' },
deal_id: { type: 'string' },
stage: { type: 'string', enum: ['lead', 'qualified', 'won'] },
},
required: ['entity_type', 'deal_id', 'stage'],
},
],
},
},
async (args) => {
// handler unifié
return { content: [{ type: 'text', text: 'OK' }] };
}
);
Ajoutez une étape CI qui valide vos schémas contre JSON Schema 2020-12 avec ajv v8+.
Phase 3 — Migration Python : FastMCP vers stateless
Si votre stack est Python (souvent avec FastMCP ou le SDK officiel), la logique est identique. Voici un serveur FastAPI stateless :
# server_stateless.py — MCP 2026-07-28 (extrait illustratif)
from fastapi import FastAPI, Request, Response, HTTPException
from mcp.server.fastmcp import FastMCP
PROTOCOL_VERSION = "2026-07-28"
mcp = FastMCP("inventory-connector", json_response=True)
@mcp.tool()
async def check_stock(sku: str, warehouse: str = "main") -> dict:
"""Vérifie le stock d'un SKU dans un entrepôt."""
qty = await db_get_stock(sku, warehouse)
return {"sku": sku, "warehouse": warehouse, "quantity": qty}
@mcp.tool()
async def reserve_stock(sku: str, quantity: int, reservation_id: str | None = None) -> dict:
"""Réserve du stock. Passe reservation_id pour les appels suivants."""
if reservation_id is None:
reservation_id = await create_reservation(sku, quantity)
else:
await confirm_reservation(reservation_id)
return {"reservation_id": reservation_id, "status": "held"}
app = FastAPI()
@app.get("/mcp/discover")
async def discover():
return {
"protocolVersions": [PROTOCOL_VERSION],
"serverInfo": {"name": "inventory-connector", "version": "2.0.0"},
"capabilities": {"tools": {}},
}
@app.post("/mcp")
async def handle_mcp(request: Request):
protocol_version = request.headers.get("mcp-protocol-version")
mcp_method = request.headers.get("mcp-method")
mcp_name = request.headers.get("mcp-name")
if protocol_version != PROTOCOL_VERSION:
raise HTTPException(status_code=400, detail="UnsupportedProtocolVersionError")
body = await request.json()
if mcp_method and body.get("method") and mcp_method != body["method"]:
raise HTTPException(status_code=400, detail="Header/body method mismatch")
if body.get("method") == "tools/call":
tool_name = body.get("params", {}).get("name")
if mcp_name and tool_name and mcp_name != tool_name:
raise HTTPException(status_code=400, detail="Header/body tool name mismatch")
# Pas de session store — chaque requête est indépendante
return await mcp.handle_stateless_request(request, body)
Installez la version SDK compatible :
pip install "mcp>=1.10.0" # vérifier la version exacte dans la doc officielle
Phase 4 — Configuration load balancer (nginx)
L'un des gains majeurs du stateless : plus de sticky sessions imposées par le protocole. Voici une configuration nginx production-ready :
# /etc/nginx/conf.d/mcp-upstream.conf
upstream mcp_backend {
least_conn; # ou round_robin par défaut
server mcp-1.internal:3000 max_fails=3 fail_timeout=30s;
server mcp-2.internal:3000 max_fails=3 fail_timeout=30s;
server mcp-3.internal:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# Rate limiting par méthode MCP (SEP-2243 rend le routing trivial)
map $http_mcp_method $mcp_rate_zone {
default "mcp_default";
"tools/call" "mcp_tools";
"tools/list" "mcp_list";
}
limit_req_zone $binary_remote_addr zone=mcp_default:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=mcp_tools:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=mcp_list:10m rate=60r/s;
server {
listen 443 ssl http2;
server_name mcp.example.com;
ssl_certificate /etc/ssl/mcp/fullchain.pem;
ssl_certificate_key /etc/ssl/mcp/privkey.pem;
location /mcp/discover {
proxy_pass http://mcp_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# discover est cacheable
proxy_cache mcp_discover_cache;
proxy_cache_valid 200 60s;
}
location /mcp {
limit_req zone=$mcp_rate_zone burst=20 nodelay;
proxy_pass http://mcp_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# PAS de sticky session — c'est le point
# proxy_cookie_path off; # ne pas réintroduire d'affinité
proxy_read_timeout 120s;
proxy_send_timeout 120s;
client_max_body_size 4m;
}
}
Client MCP → load balancer round-robin → N instances stateless → APIs métier, avec OAuth/OIDC en amont
Cache CDN pour tools/list (SEP-2549)
Les réponses tools/list peuvent porter ttlMs et cacheScope. Côté serveur TypeScript :
// Dans le handler tools/list — métadonnées de cache
return {
tools: toolDefinitions,
_meta: {
'io.modelcontextprotocol/cache': {
ttlMs: 300_000, // 5 minutes
cacheScope: 'shared', // cacheable entre utilisateurs (listes publiques)
},
},
};
Configurez votre CDN ou nginx proxy_cache pour respecter ces TTL — moins de charge sur vos instances, latence réduite pour les clients.
Phase 5 — Breaking changes : checklist complète
Cochez chaque item avant la bascule production.
Transport et sessions
- Supprimer toute logique
initialize/notifications/initialized - Supprimer l'en-tête
Mcp-Session-Idcôté client et serveur - Supprimer Redis / Map de sessions MCP au niveau protocole
- Désactiver sticky sessions sur le load balancer
- Migrer HTTP+SSE legacy vers Streamable HTTP
- Ajouter
GET /mcp/discover(ou équivalent RPCserver/discover)
Headers et routing
- Exiger
MCP-Protocol-Version: 2026-07-28 - Envoyer
Mcp-MethodetMcp-Namesur chaque POST Streamable HTTP - Rejeter les requêtes header/corps incohérents (400, pas 500)
- Configurer rate limiting par
Mcp-Methodà la gateway
État applicatif
- Remplacer l'état session par des handles explicites (
workflow_id, etc.) - Documenter les handles dans les descriptions d'outils (le modèle doit les voir)
- Migrer l'elicitation vers
InputRequiredResult+requestState(SEP-2322)
API et schémas
- Migrer
inputSchema/outputSchemavers JSON Schema 2020-12 - Remplacer le match sur l'erreur
-32002par-32602(SEP-2164) - Migrer Tasks expérimental vers l'extension Tasks (suppression de
tasks/list) - Planifier le remplacement Roots / Sampling / Logging (dépréciés, 12 mois minimum)
OAuth / sécurité
- Implémenter validation
iss(SEP-2468) — priorité haute - Vérifier Dynamic Client Registration avec
application_typeOIDC (SEP-837) - Tester refresh tokens et step-up auth (SEPs 2207, 2350)
Observabilité
- Propager
traceparent/tracestatedans_meta(SEP-414) - Remplacer
logging/setLevelpar OpenTelemetry ou stderr structuré - Dashboards : latence p95 par
Mcp-Method, taux 4xx vs 5xx
Audit ~2j, SDK+transport ~5j, load balancer ~3j, tests ~4j, OAuth ~3j, déploiement ~2j — ordre de grandeur pour un serveur moyen
Phase 6 — Procédures de test
Test manuel avec MCP Inspector
# Lancer l'Inspector contre votre instance canary
npx @modelcontextprotocol/inspector \
--url https://mcp-v2.example.com/mcp \
--header "MCP-Protocol-Version: 2026-07-28"
Vérifiez dans l'Inspector :
server/discoverretourne2026-07-28.tools/listfonctionne sans session préalable.tools/callsur chaque outil critique retourne un résultat valide.- Deux appels consécutifs peuvent toucher des instances différentes (testez via header
X-Debug-Instanceen dev).
Suite de tests CI (Vitest + fetch)
// tests/mcp-stateless.integration.test.ts
import { describe, it, expect } from 'vitest';
const MCP_URL = process.env.MCP_TEST_URL ?? 'http://localhost:3000/mcp';
const HEADERS = {
'Content-Type': 'application/json',
'MCP-Protocol-Version': '2026-07-28',
};
describe('MCP 2026-07-28 stateless', () => {
it('discover annonce la bonne version', async () => {
const res = await fetch(`${MCP_URL.replace('/mcp', '')}/mcp/discover`);
const data = await res.json();
expect(data.protocolVersions).toContain('2026-07-28');
});
it('tools/list sans session', async () => {
const res = await fetch(MCP_URL, {
method: 'POST',
headers: { ...HEADERS, 'Mcp-Method': 'tools/list' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/list',
params: { _meta: { 'io.modelcontextprotocol/protocolVersion': '2026-07-28' } },
}),
});
expect(res.status).toBe(200);
const data = await res.json();
expect(data.result.tools.length).toBeGreaterThan(0);
});
it('rejette mismatch header/body', async () => {
const res = await fetch(MCP_URL, {
method: 'POST',
headers: { ...HEADERS, 'Mcp-Method': 'tools/call', 'Mcp-Name': 'wrong_tool' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 2,
method: 'tools/call',
params: { name: 'search_contacts', arguments: { query: 'test' } },
}),
});
expect(res.status).toBe(400);
});
it('tools/call stateless sur deux requêtes indépendantes', async () => {
for (let i = 0; i < 5; i++) {
const res = await fetch(MCP_URL, {
method: 'POST',
headers: {
...HEADERS,
'Mcp-Method': 'tools/call',
'Mcp-Name': 'search_contacts',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: i,
method: 'tools/call',
params: {
name: 'search_contacts',
arguments: { query: 'acme' },
_meta: { 'io.modelcontextprotocol/protocolVersion': '2026-07-28' },
},
}),
});
expect(res.status).toBe(200);
}
});
});
Intégrez cette suite dans GitHub Actions :
# .github/workflows/mcp-migration-test.yml
name: MCP Stateless Migration Tests
on:
pull_request:
paths: ['src/mcp/**', 'tests/mcp/**']
jobs:
integration:
runs-on: ubuntu-latest
services:
mcp:
image: ghcr.io/your-org/crm-mcp:${{ github.sha }}
ports: ['3000:3000']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '22' }
- run: npm ci
- run: npm run test:mcp
env:
MCP_TEST_URL: http://localhost:3000/mcp
Test de charge post-migration
Validez que le round-robin tient la charge sans sticky sessions :
# 1000 requêtes tools/list — aucune ne doit échouer avec "session unknown"
hey -n 1000 -c 50 -m POST \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/list" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' \
https://mcp-v2.example.com/mcp
Critères de succès : taux d'erreur < 0,1 %, p95 latence ≤ baseline + 15 %.
Phase 7 — Déploiement canary et rollback
Stratégie de bascule en 4 étapes
- Semaine 1 : déployer
mcp-v2en parallèle demcp-v1(legacy 2025-11-25). - Semaine 2 : router les clients internes (CI, staging) vers v2 ; monitorer 72 h.
- Semaine 3 : canary 10 % trafic production via header ou sous-domaine.
- Semaine 4 : bascule 100 % ; conserver v1 en read-only 2 semaines pour rollback.
Plan de rollback
Gardez l'ancienne image Docker taguée mcp:2025-11-25-final. Si le taux d'erreur dépasse 1 % post-bascule :
kubectl rollout undo deployment/mcp-server
# ou bascule DNS weighted routing vers pool v1
Documentez la fenêtre de rollback (max 30 minutes) dans votre runbook incident.
Phase 8 — Migration côté client MCP
La migration serveur ne suffit pas : vos clients (Cursor, n8n, agents custom) doivent cesser d'envoyer Mcp-Session-Id et adopter les nouveaux en-têtes. Voici un wrapper fetch TypeScript pour vos agents internes :
// lib/mcpClient2026.ts — client stateless minimal
const PROTOCOL = '2026-07-28';
export async function mcpCall<T>(
baseUrl: string,
method: string,
params: Record<string, unknown>,
name?: string
): Promise<T> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'MCP-Protocol-Version': PROTOCOL,
'Mcp-Method': method,
};
if (name) headers['Mcp-Name'] = name;
const body = {
jsonrpc: '2.0',
id: crypto.randomUUID(),
method,
params: {
...params,
_meta: {
'io.modelcontextprotocol/protocolVersion': PROTOCOL,
'io.modelcontextprotocol/clientInfo': { name: 'bovo-agent', version: '2.0.0' },
},
},
};
const res = await fetch(`${baseUrl}/mcp`, { method: 'POST', headers, body: JSON.stringify(body) });
if (!res.ok) throw new Error(`MCP HTTP ${res.status}: ${await res.text()}`);
const data = await res.json();
if (data.error) throw new Error(data.error.message);
return data.result as T;
}
// Usage
const tools = await mcpCall<{ tools: unknown[] }>(MCP_URL, 'tools/list', {});
const result = await mcpCall(MCP_URL, 'tools/call', {
name: 'search_contacts',
arguments: { query: 'dupont' },
}, 'search_contacts');
Pour n8n, mettez à jour le nœud MCP Client vers une version compatible 2026-07-28 et vérifiez que l'URL de credential n'inclut plus de paramètre de session. Testez un workflow simple tools/list → tools/call avant de réactiver les workflows production.
Pour Cursor et Claude Desktop, la mise à jour de l'application suffit généralement — ces clients Tier 1 adoptent la spec dans la fenêtre de dix semaines post-RC. Vos serveurs stdio locaux bénéficient de la mise à jour automatique du client sans changement d'infrastructure.
Phase 9 — Déploiement Kubernetes sans sticky sessions
En production containerisée, le stateless MCP simplifie le scaling horizontal. Exemple de Deployment + HPA :
# k8s/mcp-server.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
replicas: 3
selector:
matchLabels: { app: mcp-server }
template:
metadata:
labels: { app: mcp-server }
spec:
containers:
- name: mcp
image: ghcr.io/your-org/crm-mcp:2026-07-28
ports: [{ containerPort: 3000 }]
env:
- name: MCP_PROTOCOL_VERSION
value: "2026-07-28"
readinessProbe:
httpGet: { path: /mcp/discover, port: 3000 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /mcp/discover, port: 3000 }
periodSeconds: 30
resources:
requests: { cpu: "250m", memory: "512Mi" }
limits: { cpu: "1", memory: "1Gi" }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: mcp-server-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: mcp-server
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
Important : n'ajoutez pas sessionAffinity: ClientIP sur le Service — cela réintroduirait le sticky routing que la spec 2026-07-28 rend obsolète. Si vous utilisiez auparavant un Ingress avec cookie d'affinité, supprimez-le lors de la migration.
Le readiness probe sur /mcp/discover garantit qu'aucune instance n'entre dans le pool tant qu'elle n'annonce pas la version 2026-07-28. Couplez-le à un PodDisruptionBudget (minAvailable: 2) pour des déploiements rolling sans interruption.
Phase 10 — Gérer l'elicitation multi-tour (SEP-2322)
Les serveurs qui demandaient une confirmation utilisateur via une connexion SSE persistante doivent migrer vers Multi Round-Trip Requests. Exemple de handler côté serveur :
// Gestion InputRequiredResult — pattern 2026-07-28
server.registerTool('delete_files', { /* schema */ }, async (args, extra) => {
if (!args.confirmed) {
return {
resultType: 'inputRequired',
inputRequests: {
confirm: {
type: 'elicitation',
message: `Supprimer ${args.file_ids.length} fichiers ?`,
schema: { type: 'boolean' },
},
},
requestState: Buffer.from(JSON.stringify({ file_ids: args.file_ids })).toString('base64url'),
};
}
await bulkDelete(args.file_ids);
return { content: [{ type: 'text', text: 'Suppression effectuée' }] };
});
Le client reçoit inputRequired, collecte la réponse utilisateur, puis réémet l'appel original avec inputResponses et le requestState echo. N'importe quelle instance du pool peut traiter le retry — le contexte est entièrement dans le payload, pas dans une session transport.
Testez ce flux dans MCP Inspector en simulant deux requêtes consécutives avec le même requestState. C'est le scénario le plus oublié lors des migrations, et le plus visible pour les utilisateurs finaux.
Phase 11 — Docker Compose pour environnement de test local
Avant de toucher la production, reproduisez un cluster minimal local :
# docker-compose.mcp-migration.yml
services:
mcp-1:
build: .
environment: { MCP_PROTOCOL_VERSION: "2026-07-28", INSTANCE_ID: "1" }
mcp-2:
build: .
environment: { MCP_PROTOCOL_VERSION: "2026-07-28", INSTANCE_ID: "2" }
nginx:
image: nginx:1.27-alpine
ports: ["8443:443"]
volumes:
- ./nginx-mcp.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: [mcp-1, mcp-2]
Lancez la suite de tests CI contre https://localhost:8443/mcp et vérifiez via les logs INSTANCE_ID que les requêtes alternent bien entre les conteneurs — preuve que le stateless fonctionne avant le déploiement cloud.
Erreurs fréquentes et corrections
| Symptôme | Cause probable | Correction |
|---|---|---|
404 Session inconnue après LB | Sticky supprimé mais code serveur garde sessions | Retirer le session store |
400 Header/body mismatch | Client n'envoie pas Mcp-Name | Mettre à jour SDK client |
| Outils OK en dev, KO en prod | Rate limit gateway sans Mcp-Method | Configurer map nginx (cf. §4) |
| Tasks bloquées mid-flight | API Tasks 2025-11-25 supprimée | Migrer extension Tasks |
| Cache stale tools/list | ttlMs ignoré côté CDN | Respecter _meta.cache |
Lien avec vos agents IA existants
Cette migration transport ne remplace pas la conception de vos agents. Si vous construisez un agent complet — orchestration, HITL, front-end — notre offre création d'agents IA intègre désormais l'audit de compatibilité MCP 2026-07-28 dès le cadrage. Pour un agent opérationnel connecté à n8n et vos APIs internes, le parcours déployer un agent IA MCP reste le point de départ ; ce tutoriel en est la suite naturelle dès que vous passez en HTTP multi-instances.
Conclusion — ce qu'il faut retenir
Migrer vers MCP 2026-07-28 stateless, ce n'est pas réécrire vos outils CRM ou inventory : c'est retirer la session transport, ajouter trois en-têtes HTTP, configurer un load balancer round-robin, et externaliser l'état via des handles explicites. Le gain opérationnel est immédiat : scaling horizontal sans sticky sessions, cache CDN sur les listes, routing gateway sans parser JSON-RPC.
Checklist finale avant le go-live :
- Audit terminé, breaking changes cochés.
- SDK client et serveur ≥ version compatible RC.
- Tests Inspector + CI verts sur canary.
- Load test 1000 req sans erreur session.
- Runbook rollback testé.
Vous migrez plusieurs serveurs MCP en parallèle ? Notre équipe accompagne les migrations transport avec revue OAuth, configuration load balancer et suites de tests CI. Contactez-nous pour un audit de votre stack MCP — nous commençons par inventorier vos breaking changes, pas par vendre une réécriture inutile.
Besoin d'un agent IA compatible 2026-07-28 dès le POC ? Consultez notre offre création d'agents IA : audit protocole, pattern handles, observabilité OpenTelemetry inclus.
Serveur MCP pas encore créé ? Repartez de notre tutoriel TypeScript 30 minutes en ciblant directement la spec 2026-07-28 — vous évitez une double migration.
Étiquettes
FAQ
Dois-je migrer mon serveur MCP stdio local vers la spec 2026-07-28 ?
Pour un serveur stdio utilisé uniquement en local (Cursor, Claude Desktop), la migration est surtout une mise à jour du SDK @modelcontextprotocol/sdk vers une version compatible 2026-07-28. Les changements transport (Mcp-Session-Id, headers Mcp-Method) concernent principalement le Streamable HTTP distant. En stdio, concentrez-vous sur la suppression du handshake initialize, le passage des métadonnées dans _meta et la validation de vos schémas JSON Schema 2020-12.
Comment tester la migration sans couper la production ?
Déployez une instance canary sur un sous-domaine (mcp-v2.example.com) derrière le même load balancer avec un pool séparé. Exécutez la suite de tests MCP Inspector en CI, puis routez 5 à 10 % du trafic via un header X-MCP-Version: 2026-07-28. Comparez latence, taux d''erreur et logs OpenTelemetry pendant 48 heures avant bascule complète.
Que faire si mon serveur dépend d'un Redis de sessions MCP ?
Supprimez le store de sessions au niveau protocole. Si votre logique métier nécessite de la continuité (panier, navigateur headless, workflow multi-étapes), exposez un handle explicite — workflow_id, browser_id — retourné par un outil et repassé en argument par le modèle. Le protocole devient stateless ; votre application peut rester stateful via des IDs opaques stockés en base ou Redis applicatif, pas en session MCP.
Quels headers HTTP sont obligatoires après migration ?
Chaque requête Streamable HTTP POST doit porter MCP-Protocol-Version: 2026-07-28, Mcp-Method (ex. tools/call) et Mcp-Name (nom de l''outil ou de la ressource). Le serveur rejette les requêtes où en-têtes et corps JSON-RPC divergent (SEP-2243). Propagation W3C traceparent dans _meta recommandée pour l''observabilité (SEP-414).
Combien de temps prend une migration MCP stateless typique ?
Pour un serveur HTTP déjà en Streamable HTTP avec 5 à 15 outils : comptez 2 jours d''audit, 3 à 5 jours de refactor transport et headers, 2 jours de configuration load balancer, 3 à 4 jours de tests CI et canary. Total : 10 à 15 jours-homme selon la dette (Tasks expérimental, HTTP+SSE legacy, OAuth custom). Un serveur stdio local se migre souvent en une demi-journée via mise à jour SDK.
Prêt à l'implémenter ?
Réservez un appel stratégique gratuit de 30 min avec nos experts
Nous analyserons votre situation et proposerons un plan d'action concret.

William Aklamavo
Expert en développement web et automatisation, passionné par l'innovation technologique et l'entrepreneuriat digital.
