Cómo construir un agente de IA con Next.js y MCP
Una aplicación Next.js completa que habla con un servidor MCP propio: arquitectura, código real y las decisiones que cambian entre desarrollo local y producción.
Hay una distancia grande entre “he probado un servidor MCP desde Claude Code” y “tengo una aplicación Next.js en producción que usa MCP para darle herramientas a un agente”. La primera es cuestión de minutos. La segunda obliga a tomar decisiones concretas: qué transporte usar, dónde vive el servidor, cómo se transmite la respuesta al navegador y qué pasa cuando una tool tarda más de lo que tu función serverless está dispuesta a esperar.
Este artículo construye esa segunda cosa. Si no sabes qué es MCP o quieres entender el protocolo antes de tocar código, empieza por MCP explicado a fondo; aquí se da por hecho ese contexto y se va directo a la implementación.
Lo que vamos a construir
Una aplicación Next.js (App Router) con un chat que responde preguntas sobre el estado de unos pedidos ficticios. El modelo no conoce esos datos: los obtiene invocando herramientas expuestas por un servidor MCP propio. La arquitectura final tiene tres piezas:
graph LR U[Usuario en el navegador] -->|useChat| R[Route Handler /api/chat] R -->|streamText + tools| M[Modelo: Claude] R -->|MCP Client| S[Servidor MCP: pedidos] S --> DB[(Base de datos de pedidos)]
El Route Handler hace de host: mantiene la conexión con el servidor MCP, le pasa las herramientas descubiertas al modelo y transmite la respuesta al cliente en streaming. Ni el navegador ni el modelo hablan directamente con el servidor MCP; solo lo hace el backend de Next.js.
Paso 1: el servidor MCP
Empezamos por el servidor porque es la pieza independiente de todo lo demás: no sabe que existe Next.js, ni React, ni un navegador. Solo expone una herramienta.
mkdir mcp-pedidos-server && cd mcp-pedidos-server
npm init -y
npm install @modelcontextprotocol/sdk zod
// mcp-pedidos-server/src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const PEDIDOS = [
{ referencia: 'PED-2026-101', estado: 'enviado', transportista: 'GLS' },
{ referencia: 'PED-2026-102', estado: 'preparando', transportista: null },
{ referencia: 'PED-2026-103', estado: 'entregado', transportista: 'Correos Express' },
];
const server = new McpServer({ name: 'pedidos-server', version: '1.0.0' });
server.tool(
'buscar_pedido',
'Busca el estado de un pedido por su número de referencia',
{ referencia: z.string().describe('Referencia del pedido, formato PED-AAAA-NNN') },
async ({ referencia }) => {
const pedido = PEDIDOS.find((p) => p.referencia === referencia);
if (!pedido) {
return { content: [{ type: 'text', text: `No existe ningún pedido con la referencia ${referencia}.` }] };
}
return { content: [{ type: 'text', text: JSON.stringify(pedido) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Nada de esto es específico de Next.js: es exactamente el mismo servidor que podrías registrar en Claude Code o en Cursor. Esa es la gracia del protocolo. El SDK de TypeScript ha evolucionado hacia una segunda versión con paquetes modulares (@modelcontextprotocol/server, @modelcontextprotocol/client) para proyectos que arrancan de cero, pero la línea @modelcontextprotocol/sdk que usamos aquí sigue activa y es la que documenta la mayoría de tutoriales y servidores ya publicados a día de hoy.
Paso 2: conectar el servidor desde Next.js
Aquí es donde entra la primera decisión de arquitectura importante. El transporte stdio (el que usa el servidor de arriba) arranca el servidor como subproceso del propio host. Funciona perfectamente en un servidor Node.js que corre de forma persistente, pero es un mal encaje para una función serverless: cada invocación puede ejecutarse en una instancia nueva, y levantar un subproceso en cada petición añade latencia de arranque y complica el ciclo de vida de la conexión.
Instalamos el paquete que integra MCP con el SDK de IA de Vercel:
npm install ai @ai-sdk/anthropic @ai-sdk/react @ai-sdk/mcp
// src/lib/mcp-client.ts
import { createMCPClient } from '@ai-sdk/mcp';
import { StreamableHTTPClientTransport } from '@ai-sdk/mcp/streamable-http';
let clientPromise: ReturnType<typeof createMCPClient> | null = null;
export function getMcpClient() {
if (!clientPromise) {
clientPromise = createMCPClient({
transport: new StreamableHTTPClientTransport(
new URL(process.env.MCP_PEDIDOS_URL ?? 'http://localhost:8787/mcp')
),
});
}
return clientPromise;
}
Reutilizar la promesa del cliente entre invocaciones (cuando el runtime lo permite) evita renegociar la conexión en cada petición. En local, mientras desarrollas el servidor con stdio, puedes sustituir el transporte por Experimental_StdioMCPTransport sin tocar el resto del código; el contrato de createMCPClient es el mismo.
Paso 3: el Route Handler con tool calling
// src/app/api/chat/route.ts
import { anthropic } from '@ai-sdk/anthropic';
import { streamText, convertToModelMessages, type UIMessage } from 'ai';
import { getMcpClient } from '@/lib/mcp-client';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const mcpClient = await getMcpClient();
const tools = await mcpClient.tools();
const result = streamText({
model: anthropic('claude-opus-4.8'),
system:
'Eres un asistente de atención al cliente. Usa la herramienta buscar_pedido ' +
'cuando el usuario pregunte por el estado de un pedido. No inventes referencias.',
messages: convertToModelMessages(messages),
tools,
stopWhen: ({ steps }) => steps.length >= 5,
});
return result.toUIMessageStreamResponse();
}
mcpClient.tools() hace el trabajo que en el artículo sobre MCP describíamos como tools/list: pregunta al servidor qué herramientas ofrece y las traduce automáticamente al formato de tool que espera streamText. El modelo decide cuándo llamarlas; el SDK se encarga de ejecutar la llamada contra el servidor MCP, devolver el resultado al modelo y continuar la conversación hasta que hay una respuesta final o se alcanza el límite de pasos (stopWhen), que conviene fijar siempre para no dejar un bucle de tool calls sin freno.
Paso 4: la interfaz
// src/app/chat/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';
export default function ChatPage() {
const { messages, sendMessage, status } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role === 'user' ? 'Tú' : 'Asistente'}:</strong>{' '}
{m.parts.map((part, i) =>
part.type === 'text' ? <span key={i}>{part.text}</span> : null
)}
</div>
))}
<form
onSubmit={(e) => {
e.preventDefault();
const input = new FormData(e.currentTarget).get('mensaje') as string;
if (input.trim()) sendMessage({ text: input });
e.currentTarget.reset();
}}
>
<input name="mensaje" disabled={status !== 'ready'} placeholder="Pregunta por un pedido..." />
</form>
</div>
);
}
useChat gestiona el estado de la conversación y el streaming; no necesita saber que detrás hay un servidor MCP. Esa es la separación de responsabilidades que hace que este patrón escale: el frontend habla con tu API, tu API habla con el modelo y con MCP, y puedes cambiar de servidor MCP, añadir uno segundo o quitar herramientas sin tocar una línea de la interfaz.
Añadir un segundo servidor MCP
El patrón se repite igual para cada servidor adicional: creas un cliente, obtienes sus tools y las fusionas.
const [pedidosClient, githubClient] = await Promise.all([
getMcpClient(),
getGithubMcpClient(),
]);
const tools = {
...(await pedidosClient.tools()),
...(await githubClient.tools()),
};
Si dos servidores exponen una tool con el mismo nombre, la segunda pisa a la primera en ese spread; en un agente con varios servidores de terceros conviene prefijar o filtrar nombres antes de fusionar, precisamente para evitar colisiones silenciosas.
Seguridad: no es solo “añadir una tool más”
En cuanto el resultado de una tool MCP entra en el contexto del modelo, ese contenido puede intentar manipular al agente para que haga algo que no le pediste —cambiar de tarea, revelar el system prompt, invocar otra tool con parámetros distintos—. Esto es especialmente relevante si alguna de tus tools expone contenido que no controlas del todo (una página web, un documento subido por un usuario, un ticket de soporte). Antes de dar por cerrado un agente con tools reales, conviene revisar cómo proteger un agente frente a prompt injection: la mayoría de mitigaciones (permisos mínimos por tool, no ejecutar acciones irreversibles sin confirmación humana, tratar los resultados de las tools como datos y no como instrucciones) se aplican directamente a esta misma arquitectura.
Errores frecuentes al montar esto
- No fijar un límite de pasos. Sin
stopWhen, un modelo que entra en un patrón de “llamar tool → no está satisfecho → volver a llamar” puede generar decenas de llamadas y disparar la factura y la latencia. - Reconectar el cliente MCP en cada petición en un servidor Node persistente, en vez de reutilizarlo. Es coste de conexión innecesario que se nota en la latencia percibida.
- Devolver JSON crudo y enorme desde una tool. El resultado de la tool entra en el contexto del modelo tal cual; una tool que devuelve un objeto de 10.000 líneas satura el contexto y encarece cada turno. Filtra y resume en el propio servidor MCP antes de devolver.
- No validar el input del usuario antes de pasarlo a una tool con efectos secundarios. El esquema Zod de la tool valida tipos, pero no valida intención: una tool que borra un pedido necesita una capa de confirmación explícita, no solo un parámetro obligatorio.
- Ignorar los timeouts de la función serverless. Si una tool tarda 25 segundos y tu plataforma corta a los 15, el usuario ve un error genérico sin pista de la causa real. Instrumenta cada llamada a tool con su propio tiempo de ejecución.
Cuándo esta arquitectura es excesiva
Si solo necesitas una función que tu propia aplicación va a llamar y ningún otro cliente de IA va a reutilizar ese conector, monta function calling directo contra la API del modelo: un objeto tools local, sin servidor MCP de por medio. MCP gana claramente cuando ese servidor de pedidos también lo va a usar el equipo de soporte desde Claude Code, o cuando lo vas a publicar para que terceros lo conecten desde su propio host. Levantar un proceso servidor, un transporte y un ciclo de conexión para una tool que solo va a llamar tu propio backend es infraestructura que no se está aprovechando.
Artículos relacionados
MCP explicado a fondo: qué es, cómo funciona y cómo construir tu propio servidor
El protocolo que estandariza cómo los agentes de IA acceden a herramientas y datos externos. Arquitectura, transportes, seguridad y un servidor MCP construido paso a paso.
Cómo proteger un agente frente a prompt injection
El riesgo número uno para agentes con acceso a herramientas no es que el modelo se equivoque: es que algo que procesa le diga qué hacer sin que tú lo autorices.
Testing con IA: generación de tests y self-healing locators, hasta dónde confiar
Los agentes de IA ya generan y reparan tests de Playwright solos. Analizamos qué hacen de verdad, qué benchmarks dicen sobre su fiabilidad y cómo evitar que una suite verde deje de significar algo.