AI and Automation

Cómo construir un servidor MCP para un SaaS Next.js existente

Una guía pragmática para lanzar un servidor MCP sobre un SaaS Next.js 16 en producción: paquetes, rutas, auth, estado de sesión y los fallos típicos.

22 de abril de 20267 min de lectura
Cómo construir un servidor MCP para un SaaS Next.js existente

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 dinámica que habla Streamable HTTP con Claude, ChatGPT, Cursor y VS Code, una herramienta tipada que lee de tu base de datos, OAuth 2.1 en la ruta caliente, estado de sesión respaldado por Redis que sobrevive al serverless 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 20 o posterior.
  • Un proyecto en Vercel con Fluid Compute activado, o un servidor Node capaz de mantener abiertas peticiones de larga duración.
  • Upstash Redis (o cualquier store compatible con Redis) para coordinar sesiones entre instancias serverless.
  • 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. Instálalo junto al SDK y a Zod para validar los inputs de las herramientas.

npm install mcp-handler @modelcontextprotocol/sdk zod

El SDK es la implementación canónica del protocolo. 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 dinámica del transporte

Los servidores MCP hablan dos transportes. Streamable HTTP es el predeterminado desde la revisión de marzo 2025 de la especificación; SSE sigue soportado para clientes antiguos. Un único segmento dinámico maneja ambos.

Crea src/app/api/mcp/[transport]/route.ts:

import { createMcpHandler } from "mcp-handler"
import { z } from "zod"

const handler = createMcpHandler(
  (server) => {
    server.tool(
      "ping",
      "Health check, devuelve pong",
      {},
      async () => ({ content: [{ type: "text", text: "pong" }] })
    )
  },
  {},
  {
    basePath: "/api/mcp",
    streamableHttpEndpoint: "/mcp",
    sseEndpoint: "/sse"
  }
)

export { handler as GET, handler as POST, handler as DELETE }

Exporta GET, POST y DELETE. Streamable HTTP usa POST para las llamadas a herramientas y GET para reanudar el stream; DELETE cierra la sesión limpiamente. Si falta alguno de los tres, algunos clientes fallan en silencio.

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 para los argumentos y una función async que corre en el servidor en cada llamada.

server.tool(
  "list-invoices",
  "Devuelve las facturas más recientes del workspace actual. Úsala cuando el usuario pida ver, listar o revisar facturas.",
  { limit: z.number().int().min(1).max(100).default(25) },
  async ({ limit }, { authInfo }) => {
    const rows = await db
      .from("invoices")
      .select("id, number, total, issued_at")
      .eq("workspace_id", authInfo.extra.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 2025-11-25 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 { withMcpAuth } from "mcp-handler"
import { verifyToken } from "@/lib/auth"

const auth = async (_req: Request, token: string | 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 }
  }
}

export const POST = withMcpAuth(handler, auth, { required: true })

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.

Paso 5. Sobrevivir al serverless: Redis y Fluid Compute

Streamable HTTP mantiene abierta una respuesta mientras corre una llamada a herramienta. Una función serverless Vercel por defecto la corta. Dos cambios eliminan el problema.

Activa Fluid Compute en el proyecto. Extiende la vida de la invocación a 15 minutos y deja que una sola instancia sirva peticiones concurrentes, algo que MCP aprovecha mucho porque la mayoría de las llamadas son I/O-bound.

Añade una Redis URL para que el handler pueda coordinar entre instancias. Sin ella, una sesión iniciada en una función no puede reanudarse en otra y los clientes se caen a medio camino.

const handler = createMcpHandler(
  register,
  {},
  {
    basePath: "/api/mcp",
    redisUrl: process.env.REDIS_URL,
    maxDuration: 900
  }
)

En producción, coloca Upstash Redis en la misma región que tu despliegue. El handler usa Redis pub/sub entre las llamadas GET y POST, así que la latencia aquí se nota en cada invocación de herramienta.

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/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

  1. Envía POST /api/mcp/mcp con un Bearer token válido y el body JSON-RPC de initialize. Deberías recibir un objeto capabilities que lista tus herramientas.
  2. Ejecuta el MCP Inspector con npx @modelcontextprotocol/inspector. Conéctate a tu URL, autentícate, invoca cada herramienta.
  3. Desde Claude Code, escribe /mcp y confirma que tu servidor aparece en verde. Dispara una herramienta e inspecciona los argumentos que envió el modelo.
  4. Ponlo bajo carga con 20 sesiones concurrentes del Inspector. La latencia debe mantenerse plana; si sube, la región de Redis está mal o Fluid Compute está apagado.

Fallos comunes y cómo arreglarlos

  • Stream ended unexpectedly. Fluid Compute está desactivado o maxDuration está por debajo de 300 segundos. Súbelo a 900 y vuelve a desplegar.
  • Loops de auth en cada llamada. withMcpAuth devuelve undefined para 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. Falta REDIS_URL en alguno de tus entornos de despliegue. Revisa el dashboard de Vercel; preview y production deben llevar la misma variable.
  • Capabilities vacías al conectar. Solo exportaste POST. Exporta también GET y DELETE; initialize completa por POST pero el cliente hace GET para leer el stream.
  • 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.

Foto de Shubham Dhage en Unsplash

Preguntas frecuentes

¿Tengo que reescribir mi SaaS Next.js para añadir un servidor MCP?
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/[transport]/route.ts y gestiona tanto Streamable HTTP como SSE a través de un solo segmento dinámico. Sin reescrituras, sin infraestructura paralela, sin deploy separado. Si tu SaaS está en Next.js 16 App Router con Node 20+, el primer tool sale en una tarde.
¿Un servidor MCP necesita OAuth 2.1?
Sí para servidores remotos. La especificación MCP 2025-11-25 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.
¿Por qué un servidor MCP en Vercel necesita Redis?
El Streamable HTTP mantiene una respuesta abierta mientras una tool call se ejecuta. Sin Redis, una sesión que empieza en una instancia serverless no puede reanudarse en otra, y los clientes caen a mitad de la llamada. Añade Upstash Redis en la misma región del deploy y pasa el URL a createMcpHandler({redisUrl}). El handler usa Redis pub/sub entre GET y POST, así que la latencia entre regiones se nota en cada invocación de tool. Combínalo con Fluid Compute activo (15 minutos de vida de invocación) para un runtime de producción estable.
¿Un solo servidor MCP puede servir a Claude Code, ChatGPT y Cursor a la vez?
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.
¿Cuál es la diferencia entre Streamable HTTP y SSE como transportes MCP?
Streamable HTTP es el default desde la revisión de marzo 2025 de la especificación. Usa POST para tool calls y GET para reanudar el stream, con DELETE para cerrar limpiamente una sesión. SSE (Server-Sent Events) es el transporte antiguo, todavía soportado para clientes legacy. Un solo segmento dinámico Next.js [transport] gestiona ambos, así que no tienes que elegir uno. Exporta GET, POST y DELETE del handler. Si falta uno, algunos clientes fallan en silencio.

Studio

Empieza un proyecto.

Un partner único para empresas, sector público, startups y SaaS. Producción más rápida, tecnología moderna, costes reducidos. Un equipo, una factura.