Déployer un serveur MCP sur Vercel : le guide complet
Comment j'ai résolu les défis du déploiement d'un serveur MCP (Model Context Protocol) sur Vercel serverless, en contournant les limitations de mcp-handler avec le SDK MCP natif.
Par Alane Jaunet • 18 mars 2026
Déployer un serveur MCP sur Vercel : le guide complet
Le Model Context Protocol (MCP) permet à Claude et d'autres LLMs d'interagir avec des outils externes via une API standardisée. Mais déployer un serveur MCP sur Vercel serverless n'est pas aussi simple qu'il y paraît.
Voici les problèmes que j'ai rencontrés et comment je les ai résolus.
Le contexte
Mon portfolio expose un serveur MCP avec 38 outils (CRUD projets, articles, compétences, certifications...). L'objectif : permettre à Claude de gérer mon portfolio directement via le MCP connector.
Stack : Next.js App Router + Prisma + Vercel.
Problème 1 : La route dynamique
La bibliothèque mcp-handler (de Vercel) attend une route dynamique app/api/[transport]/route.ts avec basePath: "/api". Cela génère automatiquement :
/api/mcp→ Streamable HTTP/api/sse→ Server-Sent Events/api/message→ Messages SSE
Une route statique app/api/mcp/route.ts ne fonctionne pas correctement.
Problème 2 : Le streaming sur Vercel
Sans export const dynamic = "force-dynamic", Vercel bufferise la réponse SSE (ReadableStream) et retourne une erreur 500 avec un body vide. Il faut aussi export const maxDuration = 60 pour éviter les timeouts.
Problème 3 : Le vrai bug — mcp-handler vs serverless
C'est là que ça se complique. mcp-handler crée un seul transport réutilisé entre les requêtes. Or, le SDK MCP v1.27+ a deux modes :
Mode stateless (sans sessionIdGenerator)
Le transport refuse d'être réutilisé :
Stateless transport cannot be reused across requests.
Résultat : seule la première requête fonctionne (cold start), les suivantes crashent.
Mode stateful (avec sessionIdGenerator)
Le transport crée des sessions... en mémoire. Sur Vercel serverless, chaque cold start perd l'état. Claude envoie initialize, obtient un session ID, puis envoie tools/list — mais la session a disparu.
La solution : SDK MCP natif, transport frais par requête
J'ai contourné mcp-handler en utilisant directement le SDK MCP :
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; async function handleMcpRequest(request: Request) { // Transport frais par requête = zéro état partagé const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); const server = createServer(); await server.connect(transport); return transport.handleRequest(request); } export async function POST(request: Request) { return handleMcpRequest(request); }
Chaque invocation serverless crée son propre serveur et transport. Pas de session, pas d'état partagé, pas de cold start qui casse tout.
Bonus : OAuth2 pour sécuriser le tout
Pour protéger le endpoint MCP, j'ai implémenté un flow OAuth2 complet compatible avec le MCP connector de Claude.ai :
- Discovery :
/.well-known/oauth-protected-resourceet/.well-known/oauth-authorization-server - Dynamic client registration : Claude s'enregistre automatiquement
- Authorization code + PKCE S256 : l'utilisateur se connecte via GitHub/email, consent, et Claude reçoit un token
- Bearer auth sur le endpoint MCP avec fallback CLI token
Le tout stocké en PostgreSQL via Prisma — zéro état en mémoire, 100% compatible serverless.
Résultat
- 5/5 requêtes : HTTP 200 (cold start et warm)
- 38 outils détectés par le MCP connector Claude
- OAuth2 sécurisé avec login GitHub
- Compatible avec
https://domaine.com/api/mcp(Streamable HTTP)
Points clés à retenir
- Utilisez
app/api/[transport]/route.tsavecbasePath: "/api" - Ajoutez
export const dynamic = "force-dynamic"etmaxDuration - Sur serverless, créez un transport frais par requête au lieu de réutiliser un singleton
- Le SDK MCP natif (
@modelcontextprotocol/sdk) est plus fiable quemcp-handlerpour le serverless - Implémentez OAuth2 si vous voulez sécuriser l'accès depuis Claude.ai