Servidor MCP en Next.js en 2026: mcp-handler 2 y OAuth 2.1

Esta guía te lleva desde cero a un servidor Model Context Protocol (MCP) listo para producción, corriendo dentro de un SaaS Next.js 16 existente. Al final tendrás una ruta que habla el protocolo stateless 2026-07-28, y el Streamable HTTP de los clientes de 2025, con Claude, ChatGPT, Cursor y VS Code, una herramienta tipada que lee de tu base de datos, OAuth 2.1 en la ruta caliente, un despliegue que aguanta en serverless sin store de sesión y un registro funcional que puedes probar hoy mismo.
Asumimos que el producto ya está en producción. Sin reescrituras. El servidor MCP se monta junto a tus rutas API existentes y reutiliza la auth, la base de datos y el pipeline de despliegue que ya tienes.
Lo que necesitas antes de empezar
- Next.js 16.0 o posterior, App Router.
- Node.js 22 o posterior. Node.js 20 llegó al final de su vida el 30 de abril de 2026 y Vercel lo deprecó el 1 de octubre de 2026.
- Un proyecto en Vercel con Fluid Compute activado, o un servidor Node capaz de mantener abiertas peticiones de larga duración.
- Redis solo si te quedas en mcp-handler 1.x. La versión 2 no guarda estado de sesión, así que no necesita store.
- Un proveedor OAuth 2.1. Clerk, Better Auth, Auth.js y WorkOS entregan flujos compatibles con MCP; hacerlo en casa vale si ya tienes uno corriendo.
Paso 1. Instala los paquetes
El paquete mcp-handler, mantenido por Vercel, envuelve el SDK TypeScript oficial del Model Context Protocol para route handlers de Next.js. La versión 2.0, publicada el 30 de julio de 2026, se apoya en el SDK v2 y sirve la especificación 2026-07-28. Instálala junto al paquete server del SDK y a Zod 4 para validar los inputs de las herramientas.
npm install mcp-handler@^2 @modelcontextprotocol/server@^2 zod@^4El SDK es la implementación canónica del protocolo; en la v2 @modelcontextprotocol/server sustituye al antiguo @modelcontextprotocol/sdk. mcp-handler es el adaptador fino que convierte la definición del servidor en un route handler Next.js, en un handler Nuxt o en un endpoint SvelteKit.
Paso 2. Crea la ruta del transporte
La especificación 2026-07-28 vuelve stateless el protocolo: desaparecen el handshake initialize y la cabecera Mcp-Session-Id, y cada petición lleva su versión del protocolo y las capacidades del cliente. mcp-handler 2.x la sirve de forma nativa y responde desde el mismo handler a los clientes Streamable HTTP de 2025. El antiguo transporte HTTP+SSE se ha eliminado: /sse y /message devuelven 410 Gone, así que un cliente que todavía necesite SSE te deja en la 1.x hasta que migre.
Crea src/app/api/mcp/route.ts:
import { createMcpHandler } from "mcp-handler"
import { z } from "zod"
const handler = createMcpHandler((server) => {
server.registerTool(
"ping",
{
description: "Health check, devuelve pong",
inputSchema: z.object({})
},
async () => ({ content: [{ type: "text", text: "pong" }] })
)
})
export { handler as GET, handler as POST }Exporta GET y POST. Las llamadas a herramientas van por POST; las operaciones de sesión GET y DELETE de los clientes de 2025 responden 405, y es lo esperado ahora que nadie guarda una sesión. La ruta la eliges tú: el handler nunca la lee, así que a los clientes les das la URL completa de la ruta.
Paso 3. Expón una herramienta real con un esquema tipado
Las herramientas son los verbos que tu SaaS entrega al agente. Cada una declara un nombre, una descripción que el modelo leerá, un esquema Zod de objeto para los argumentos y una función async que corre en el servidor en cada llamada. En la v2 la llamada es registerTool; el antiguo server.tool(...) variádico ya no existe.
server.registerTool(
"list-invoices",
{
description: "Devuelve las facturas más recientes del workspace actual. Úsala cuando el usuario pida ver, listar o revisar facturas.",
inputSchema: z.object({
limit: z.number().int().min(1).max(100).default(25)
})
},
async ({ limit }, ctx) => {
const workspaceId = ctx.http?.authInfo?.extra?.workspaceId
const rows = await db
.from("invoices")
.select("id, number, total, issued_at")
.eq("workspace_id", workspaceId)
.order("issued_at", { ascending: false })
.limit(limit)
return { content: [{ type: "text", text: JSON.stringify(rows.data) }] }
}
)Escribe la descripción como la escribiría el usuario en un prompt. El modelo elige herramientas leyendo esas cadenas, no leyendo tu implementación. Cortas, específicas, guiadas por el verbo. Los esquemas declarativos dejan al cliente validar los argumentos antes de que toquen tu código, lo que reduce los failure modes a la mitad.
Paso 4. Conecta OAuth 2.1
La especificación MCP 2026-07-28 exige que los servidores remotos implementen OAuth 2.1 con PKCE (SHA-256) y publiquen un documento de Protected Resource Metadata (RFC 9728) en /.well-known/oauth-protected-resource. No lances un servidor MCP remoto sin auth; las auditorías de 2025 encontraron más de mil endpoints MCP públicos expuestos con auth débil o ausente.
Envuelve el handler en withMcpAuth y un verificador de tokens:
import type { AuthInfo } from "@modelcontextprotocol/server"
import { withMcpAuth } from "mcp-handler"
import { verifyToken } from "@/lib/auth"
const auth = async (_req: Request, token?: string): Promise<AuthInfo | undefined> => {
if (!token) return undefined
const session = await verifyToken(token)
if (!session) return undefined
return {
token,
clientId: session.clientId,
scopes: session.scopes,
extra: { workspaceId: session.workspaceId, userId: session.userId }
}
}
const authHandler = withMcpAuth(handler, auth, {
required: true,
resourceMetadataPath: "/.well-known/oauth-protected-resource"
})
export { authHandler as GET, authHandler as POST }Clerk entrega un adaptador withMcpAuth drop-in; Better Auth, Auth.js y WorkOS documentan cada uno un flujo MCP. Si lo haces en casa, las partes críticas son el documento de metadata well-known y la verificación PKCE SHA-256; la especificación prohíbe explícitamente PKCE plano. El protectedResourceHandler de mcp-handler sirve ese documento por ti. La especificación 2026-07-28 también depreca el Dynamic Client Registration en favor de los Client ID Metadata Documents, que debe soportar tu authorization server, no la ruta MCP.
Paso 5. Sobrevivir al serverless: Fluid Compute, y Redis solo en la 1.x
Una llamada a herramienta puede durar segundos o minutos mientras la respuesta sigue abierta. Una función con un límite corto la corta. En la v2 es el único problema de serverless que queda.
Activa Fluid Compute en el proyecto. Deja que una sola instancia sirva peticiones concurrentes, algo que MCP aprovecha mucho porque la mayoría de las llamadas son I/O-bound. Según los límites de funciones de Vercel, en Pro y Enterprise el valor por defecto es 300 segundos y se puede subir a 800 (1.800 en beta). Fija el límite en el archivo de la ruta:
Sin Redis en la v2. Sin sesión en el protocolo, cualquier petición puede llegar a cualquier instancia, y las opciones 1.x redisUrl, maxDuration y basePath se han eliminado. Si una herramienta necesita recordar algo entre llamadas, devuelve un identificador explícito (un id de tarea, un id de borrador) y deja que el modelo lo pase de vuelta como argumento.
// app/api/mcp/route.ts
export const maxDuration = 300 // seconds; Pro allows up to 800Si te quedas en la 1.x por un cliente SSE, Redis sigue siendo necesario: pasa redisUrl a createMcpHandler y coloca la instancia Redis en la misma región que el despliegue, porque la 1.x reparte el tráfico SSE entre instancias con el pub/sub de Redis.
Paso 6. Registra el servidor con un cliente
Para Claude Code, añade el servidor al archivo .mcp.json del proyecto:
{
"mcpServers": {
"your-saas": {
"type": "http",
"url": "https://your-saas.com/api/mcp"
}
}
}Claude hace el handshake OAuth en el primer uso y cachea el token. Cursor, los Custom Connectors de Claude.ai y los de ChatGPT aceptan el mismo formato de URL desde su UI de ajustes.
Verificar que funciona
- Envía
POST /api/mcpcon un Bearer token válido y una petición JSON-RPCtools/list. Deberías recibir la lista de tus herramientas. - Ejecuta el MCP Inspector con
npx @modelcontextprotocol/inspector. Conéctate a tu URL, autentícate, invoca cada herramienta. - Desde Claude Code, escribe
/mcpy confirma que tu servidor aparece en verde. Dispara una herramienta e inspecciona los argumentos que envió el modelo. - Ponlo bajo carga con 20 sesiones concurrentes del Inspector. La latencia debe mantenerse plana; si sube, Fluid Compute está apagado o la base de datos está en otra región.
Fallos comunes y cómo arreglarlos
- Stream ended unexpectedly. La herramienta dura más que el límite de la función, o Fluid Compute está desactivado. Sube
maxDurationen el archivo de la ruta (hasta 800 segundos en Pro) y vuelve a desplegar. - Loops de auth en cada llamada.
withMcpAuthdevuelveundefinedpara un token válido. Suele ser que el audience o el issuer del token no coinciden con los Protected Resource Metadata que publicas. Loggéalos ambos y alinéalos. - Descripciones de herramientas ignoradas por el modelo. Están escritas como comentarios de código en vez de como prompts de usuario. Reescribe cada una como una frase corta en la forma "haz X cuando el usuario pida Y".
- Sesiones que se reinician a mitad del flujo. En la 1.x falta
REDIS_URLen alguno de tus entornos de despliegue; preview y production deben llevar la misma variable. En la 2.x no hay sesión que reiniciar: lleva el estado en un identificador que devuelva la herramienta. - 410 Gone en
/sse. El cliente todavía usa el transporte HTTP+SSE que mcp-handler 2.x eliminó. Apúntalo a la URL Streamable HTTP, o deja ese cliente en un despliegue 1.x mientras migra. - Errores de validación Zod en el cliente. El esquema de la herramienta exige un campo que el modelo no sabe enviar. Añade un
.default()o reestructura la descripción para que el campo requerido se deduzca del lenguaje natural.
Para ir más allá
Una vez el servidor esté en pie, el trabajo pasa del plumbing del protocolo al diseño de herramientas. El conjunto de herramientas que expones se vuelve parte de la superficie de tu producto, por eso la pregunta sobre la curación pesa más que la del transporte. Lee nuestro artículo sobre qué es MCP y cuándo un SaaS lo necesita para el argumento de producto, y nuestro árbol de decisión server versus client components para ver dónde encaja la ruta MCP en un codebase Next.js 16 que ya apoya sus datos en server components.
Actualizado 2026-08-09: la especificación MCP actual es ahora la 2026-07-28, que vuelve stateless el núcleo del protocolo (se eliminan el handshake initialize y la cabecera Mcp-Session-Id). Siguen vigentes OAuth 2.1 con PKCE SHA-256 y los Protected Resource Metadata.
Actualizado 2026-11-08: el código ahora usa mcp-handler 2.0 (publicado el 30 de julio de 2026), que sirve el protocolo stateless 2026-07-28, abandona el transporte SSE y Redis y usa registerTool; Node.js 22 sustituye a Node.js 20, ya fuera de soporte, y los límites de las funciones siguen los actuales de Vercel, de 300 a 800 segundos.
Preguntas frecuentes
No. El servidor MCP se monta como una route dinámica junto a tus routes API existentes y reusa el auth, el database y el pipeline de deploy que ya ejecutas. La route nueva vive en /api/mcp/route.ts y, con mcp-handler 2.x, sirve desde el mismo handler el protocolo stateless 2026-07-28 y el Streamable HTTP de los clientes de 2025. Sin reescrituras, sin infraestructura paralela, sin deploy separado. Si tu SaaS está en Next.js 16 App Router con Node 22+, el primer tool sale en una tarde.
Sí para servidores remotos. La especificación MCP 2026-07-28 requiere OAuth 2.1 con PKCE (SHA-256) y un Protected Resource Metadata document publicado en /.well-known/oauth-protected-resource. El PKCE plain está explícitamente prohibido. Las auditorías de 2025 encontraron más de mil endpoints MCP públicos con auth débil o ausente. Clerk, Better Auth, Auth.js y WorkOS ofrecen flujos MCP-aware integrados; hacer el tuyo está bien si ya tienes un provider OAuth, pero el documento well-known es obligatorio igualmente.
Desde mcp-handler 2.0 (30 de julio de 2026) ya no lo necesita. La especificación 2026-07-28 quitó las sesiones del protocolo, así que cualquier petición puede llegar a cualquier instancia serverless y el handler no guarda nada entre llamadas. Redis sigue siendo necesario con mcp-handler 1.x, que reparte el antiguo transporte SSE entre instancias con el pub/sub de Redis: pasa el URL a createMcpHandler({redisUrl}) y mantén Redis en la región del deploy. Con cualquiera de las dos versiones, activa Fluid Compute y fija maxDuration (300 segundos por defecto en Pro, hasta 800).
Sí. MCP es un protocolo a nivel de transporte, no un protocolo propietario. El mismo URL funciona para el .mcp.json de Claude Code, para los Custom Connectors de Claude.ai y ChatGPT, para Cursor, para VS Code. Cada cliente hace el handshake OAuth en el primer uso y cachea el token. El servidor no sabe qué cliente lo está llamando, y no necesita saberlo. Construye la superficie de tools una sola vez, exponla a cada agente MCP-aware que adopten tus usuarios.
SSE (HTTP+SSE, de la especificación 2024-11-05) es el transporte antiguo que mantenía una conexión abierta; Streamable HTTP lo sustituyó en marzo de 2025 y la especificación 2026-07-28 lo volvió stateless. mcp-handler 2.x solo sirve Streamable HTTP: las peticiones a /sse y /message devuelven 410 Gone. Exporta GET y POST desde una sola route en /api/mcp. Si un cliente todavía depende de SSE, déjalo en un despliegue mcp-handler 1.x, con Redis, mientras migra.
Artículos relacionados
19 de abril de 2026 · 7 min de lectura
7 de agosto de 2026 · 8 min de lectura
2 de noviembre de 2026 · 9 min de lectura