1. Qué construiremos hoy y cómo encaja en la aplicación
Recordemos nuestra aplicación didáctica: estamos construyendo un asistente para elegir regalos. En módulos anteriores ya teníamos:
- un widget en ChatGPT (Next.js 16 + Apps SDK), que muestra el UI, el estado y sabe invocar callTool;
- un backend sencillo (a través de Apps SDK / rutas de Next.js) que devolvía stubs de regalos.
Ahora queremos «externalizar el cerebro» de nuestro asistente en un MCP‑servidor independiente. En consecuencia, el diagrama quedará así:
flowchart TD
subgraph ChatGPT
U[Usuario
en el chat]
W["Widget de la App
(Apps SDK)"]
end
subgraph Cliente MCP
C[ChatGPT MCP client]
end
subgraph OurServer[Nuestro servidor MCP]
T1[Tool: suggest_gifts]
R1[Resource: gift_catalog]
P1[Prompt: birthday_template]
end
U --> W
W -- callTool --> C
C <-- JSON-RPC / HTTP --> OurServer
OurServer --> C
C --> W
Es decir, ahora:
- el modelo dentro de ChatGPT ve nuestro MCP‑servidor como un conjunto estándar de tools/resources/prompts;
- callTool desde el widget lógicamente se convierte en una invocación interna de MCP;
- nuestro servidor describe contratos (esquemas, descripciones) y implementa la lógica de negocio.
Al final de esta lección deberías tener un proyecto separado en Node/TypeScript con un MCP‑servidor que:
- se levanta localmente con un solo comando;
- registra al menos una herramienta y un recurso;
- devuelve datos con sentido (aunque con mocks sencillos);
- está estructurado para poder seguir evolucionándolo.
Al mismo tiempo, el backend existente a través de Apps SDK/Next.js no lo reescribimos ahora: se queda como está, y levantamos el MCP‑servidor como un servicio aparte al lado. Más adelante podrás «conectarlo» a la ChatGPT App y trasladar poco a poco allí la lógica de regalos en lugar de los stubs antiguos.
2. Stack: TypeScript + MCP SDK + transporte HTTP
Escribiremos el MCP‑servidor en TypeScript sobre Node.js. El SDK oficial de JS/TS para MCP vive en el paquete @modelcontextprotocol/sdk. Se encarga de la rutina de JSON‑RPC, validación y conversión de esquemas: describes los argumentos con esquemas de Zod, y el SDK los traduce a JSON Schema, que el modelo entiende.
Para el transporte necesitamos una variante HTTP: ChatGPT se comunica con MCP‑servidores remotos por red, no por stdio/local. La especificación de MCP describe un formato estándar de «HTTP en streaming», en esencia una evolución del esquema clásico HTTP+SSE. En la práctica es un único endpoint HTTP que procesa la petición (POST/GET) y, si hace falta, transmite la respuesta en streaming. En el SDK de TypeScript para MCP suele existir ya un transporte listo para este formato, que puedes acoplar a Express o Hono.
Para no dispersarnos, asumiremos que tenemos:
- un objeto servidor McpServer de @modelcontextprotocol/sdk;
- transporte HTTP (por ejemplo, StreamableHttpServerTransport o similar), al que podemos conectar con Express.
Los nombres exactos de clases pueden variar ligeramente entre versiones del SDK, pero arquitectónicamente siempre es:
- creas el objeto del MCP‑servidor;
- registras en él tools/resources/prompts;
- conectas el transporte a la aplicación HTTP.
3. Estructura del proyecto y preparación
Crearemos una carpeta aparte para el MCP‑servidor. Es cómodo mantenerla junto a la aplicación frontend, pero como proyecto Node independiente:
chatgpt-gift-app/
app/ ← Next.js + Apps SDK (widget)
mcp-server/ ← nuestro MCP-servidor
Dentro de mcp-server:
mcp-server/
src/
server.ts ← punto de entrada del MCP-servidor
gifts.ts ← lógica de negocio para elegir regalos
package.json
tsconfig.json
Haremos un ejemplo sencillo de gifts.ts un poco más adelante; ahora nos centramos en server.ts.
Supongamos que ya has inicializado el proyecto:
mkdir mcp-server
cd mcp-server
npm init -y
npm install typescript ts-node-dev zod express @modelcontextprotocol/sdk
tsconfig.json — el típico (módulos esnext, target node, strict). Puedes tomarlo de cualquier proyecto TS tuyo.
4. Extraemos la lógica de negocio a un módulo separado
Apetece escribir de inmediato server.registerTool(..., async () => {...}) y meter ahí toda la lógica. Pero es mejor desde el principio separar:
- un módulo que no sabe nada sobre MCP, JSON‑RPC ni otras complejidades;
- un módulo que solo sabe de MCP, pero sabe poco de la lógica de negocio.
En src/gifts.ts describimos una función simple para sugerir regalos:
// src/gifts.ts
export type GiftIdea = {
id: string;
title: string;
price: number;
occasion: string;
};
export type SuggestGiftsInput = {
age: number;
relationship: "friend" | "partner" | "child" | "coworker";
budget: number;
};
export function suggestGifts(input: SuggestGiftsInput): GiftIdea[] {
// por ahora, solo mocks
return [
{
id: "book-1",
title: "Libro sobre su afición favorita",
price: Math.min(input.budget, 30),
occasion: "generic",
},
{
id: "game-1",
title: "Juego de mesa para grupos",
price: Math.min(input.budget, 50),
occasion: "party",
},
];
}
Esta función es pura: recibe parámetros y devuelve un array de ideas. Se puede testear con unit tests, reutilizar en otro sitio y no depende de MCP. Es exactamente lo recomendable: la envoltura de servidor por un lado, las funciones de negocio por otro.
5. Creamos el MCP‑servidor y conectamos el transporte HTTP
Ahora el punto de entrada src/server.ts. A grandes rasgos necesitamos:
- crear una instancia del MCP‑servidor;
- registrar en él herramientas, recursos y prompts;
- levantar un servidor HTTP (por ejemplo, Express) y acoplarle el transporte de MCP.
Empezamos con un esqueleto:
// src/server.ts
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server";
import { StreamableHttpServerTransport } from "@modelcontextprotocol/sdk/transport/streamable-http";
const app = express();
// 1. Creamos el servidor MCP
const mcpServer = new McpServer({
name: "gift-assistant-mcp",
version: "0.1.0",
});
// 2. Aquí registraremos más tarde tools/resources/prompts
// 3. Configuramos el transporte sobre HTTP
const transport = new StreamableHttpServerTransport({
path: "/mcp", // endpoint MCP único
app, // nos integramos en la app de Express
});
transport.attach(mcpServer);
const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
});
Los nombres concretos de la clase de transporte pueden diferir, pero el patrón es el mismo: creas un endpoint HTTP y conectas a él el MCP‑servidor como manejador de JSON‑RPC sobre HTTP/stream.
En este punto el servidor aún no hace nada útil, pero ya sabe:
- pasar el MCP‑handshake;
- responder a peticiones básicas de descubrimiento (lista de tools/resources/prompts — por ahora vacía).
El siguiente paso es registrar la primera herramienta.
6. Registramos la tool suggest_gifts mediante el SDK de MCP
El Apps SDK oficial y la documentación de MCP muestran el mismo patrón de registro de una herramienta: el método registerTool, al que pasas el nombre, el descriptor (título, descripción, esquema de argumentos) y el manejador.
Ya describimos el tipo SuggestGiftsInput en gifts.ts. Ahora añadimos un esquema de Zod para que el servidor pueda validar los argumentos de entrada y entregar automáticamente a la LLM un JSON Schema correcto.
// src/server.ts (fragmento)
import { z } from "zod";
import { suggestGifts } from "./gifts";
const suggestGiftsInputSchema = z.object({
age: z.number().int().min(0).max(120),
relationship: z.enum(["friend", "partner", "child", "coworker"]),
budget: z.number().min(0),
});
Ahora registramos la herramienta:
// seguimos en server.ts
mcpServer.registerTool(
"suggest_gifts",
{
title: "Suggest gift ideas",
description:
"Sugiere ideas de regalo según la edad, el tipo de relación y el presupuesto.",
// El SDK convertirá el esquema de Zod en JSON Schema para el modelo
inputSchema: suggestGiftsInputSchema,
},
async ({ input }) => {
const ideas = suggestGifts(input);
const text = ideas
.map(
(g) =>
`• ${g.title} — ~${g.price} USD (occasion: ${g.occasion}, id: ${g.id})`
)
.join("\n");
return {
content: [
{
type: "text",
text,
},
],
// structuredContent se puede usar en el widget
structuredContent: {
ideas,
},
};
}
);
Puntos clave:
- inputSchema — esquema de Zod. El SDK para TS sabe convertirlo a JSON Schema y así describe automáticamente la herramienta al modelo.
- El manejador recibe un objeto con input (cuyo tipo obtienes del esquema). Dentro puedes llamar a tu función de negocio.
- En el result devuelves content — es el texto que el modelo verá como resultado y, si quieres, structuredContent con una estructura JSON que después puede consumir tu widget.
Si en módulos anteriores ya hiciste una herramienta con Apps SDK, este código debería resultarte muy familiar: el patrón es exactamente el mismo, solo que ahora vive en un MCP‑servidor separado.
7. Añadimos el recurso gift_catalog para datos
Las herramientas son acciones. A veces queremos también proporcionar datos como recurso, para que el modelo pueda leerlos, buscarlos o para que tu widget pueda cargar plantillas, componentes, etc. MCP describe por separado el concepto de recursos con URI, tipos MIME y contenido.
Hagamos un recurso sencillo gift_catalog, que devuelve la lista de regalos disponibles. Por ahora serán los mismos mocks, pero en la realidad podría ser una exportación de la base de datos o un product feed.
Primero, el propio catálogo:
// src/gifts.ts (ampliación)
export const giftCatalog: GiftIdea[] = [
{
id: "book-1",
title: "Libro de programación",
price: 25,
occasion: "learning",
},
{
id: "lego-1",
title: "Set de LEGO",
price: 60,
occasion: "fun",
},
];
Ahora registramos el recurso en el servidor:
// src/server.ts (fragmento)
import { giftCatalog } from "./gifts";
mcpServer.registerResource(
"gift_catalog",
{
title: "Gift catalog",
description: "Catálogo sencillo de regalos para demo y depuración.",
mimeType: "application/json",
},
async () => {
return {
contents: [
{
uri: "mcp://gift-catalog",
mimeType: "application/json",
text: JSON.stringify(giftCatalog, null, 2),
},
],
};
}
);
Qué ocurre aquí lógicamente:
- el nombre del recurso gift_catalog será visible para el cliente en el discovery (en el inspector de MCP lo verás después en la lista de recursos);
- el descriptor contiene una descripción legible y el tipo MIME;
- el manejador devuelve un array contents con URI y texto — es el formato estándar de un recurso en MCP.
Más adelante podrás:
- leer este recurso desde el cliente (por ejemplo, un agente o inspector);
- usarlo como plantillas/datos para el UI;
- hacer experimentos: cómo usa el modelo el catálogo para explicar opciones al usuario.
8. Registramos un prompt sencillo
La tercera entidad de MCP son los prompts, indicaciones preparadas de antemano. Permiten no repetir prompts de sistema o de usuario largos, sino almacenarlos en el servidor con nombres.
Hagamos un mini‑ejemplo: el prompt birthday_gift, que se podrá invocar como «plantilla pre‑rellenada de conversación sobre un regalo de cumpleaños».
// src/server.ts (fragmento)
mcpServer.registerPrompt("birthday_gift", {
title: "Birthday gift helper",
description: "Plantilla de solicitud para elegir un regalo de cumpleaños.",
messages: [
{
role: "system",
content:
"Eres un asistente para encontrar regalos. Haz preguntas de aclaración y ofrece varias opciones.",
},
{
role: "user",
content:
"Necesito un regalo de cumpleaños. Haz las preguntas necesarias y ayúdame a elegir.",
},
],
});
Bajo el capó, MCP permitirá a los clientes:
- obtener la lista de prompts (en el inspector verás birthday_gift);
- solicitar su contenido y usarlo como indicación base para el modelo.
Aparte, en el módulo sobre system‑prompt e instrucciones, analizamos en detalle cómo se combinan estos prompts con las instrucciones globales de la aplicación. Aquí nos interesa simplemente «verlos» como parte del MCP‑servidor.
9. Cómo funciona todo esto en tiempo de ejecución
Compongamos el panorama completo.
Cuando un cliente (por ejemplo, MCP Inspector o ChatGPT) se conecta a nuestro endpoint HTTP /mcp:
- se produce el handshake: cliente y servidor intercambian información sobre capacidades soportadas (tools/resources/prompts, etc.);
- el cliente invoca métodos de descubrimiento: obtiene la lista de herramientas, recursos y prompts junto con sus descripciones y esquemas;
- cuando el modelo decide llamar a una herramienta, forma una petición JSON‑RPC con un método como tools/call o similar — el SDK en el servidor lo transforma en una invocación interna del manejador registrado con registerTool;
- el manejador ejecuta la lógica de negocio (en nuestro caso suggestGifts o la entrega de giftCatalog) y devuelve el resultado en un formato estandarizado;
- el SDK serializa la respuesta de vuelta a JSON‑RPC y la envía al cliente a través del mismo transporte HTTP/stream.
Todos los detalles de JSON‑RPC, formación del id, enrutado de métodos, etc., quedan dentro de @modelcontextprotocol/sdk. Para ti, la interfaz se parece mucho a Apps SDK: trabajas con registerTool/registerResource/registerPrompt y sus manejadores, sin preocuparte por el protocolo.
10. Ejecución local y primera prueba sencilla
Supongamos que añadiste todo lo anterior. Solo queda ejecutar.
En package.json puedes añadir un script:
{
"scripts": {
"dev": "ts-node-dev src/server.ts"
}
}
Ejecutamos:
npm run dev
En la consola debería aparecer algo como:
MCP server listening on http://localhost:4000/mcp
La inspección completa y las invocaciones manuales de herramientas las haremos en la siguiente lección con MCP Inspector / MCP Jam. Pero incluso ahora puedes hacer un smoke test super sencillo con curl:
curl -X POST http://localhost:4000/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Este curl es un smoke test opcional para quien le gusta ver respuestas JSON «crudas». En desarrollo real casi siempre te comunicas con el MCP‑servidor a través de un SDK, no montas a mano peticiones JSON‑RPC.
El nombre exacto del método depende de la versión del protocolo y del SDK, pero la idea es que recibirás una lista JSON donde, entre las tools, se verá suggest_gifts. Si el método no coincide, no pasa nada: el objetivo de la lección no es memorizar todos los nombres, sino que no te dé miedo mirar las respuestas JSON y entender su estructura, gracias a las lecciones previas.
11. Conexión con nuestra ChatGPT App y evolución futura
Por ahora el MCP‑servidor vive por su cuenta. En los siguientes módulos tú:
- lo conectarás al MCP Inspector y aprenderás a depurar tools/resources/prompts por separado, sin tocar ChatGPT;
- configurarás la ChatGPT App para que vea este MCP‑servidor como fuente de herramientas;
- trasladarás parte de la lógica que antes estaba dentro de Apps SDK (por ejemplo, mediante tools integradas) a la capa MCP;
- añadirás autorización, logging y escenarios en streaming — ya sobre el esqueleto listo.
Ahora es importante que:
- tienes un servicio separado responsable de las «habilidades» y los «datos» de la aplicación;
- este servicio habla con los clientes a través del estándar MCP, y no mediante un REST a medida;
- ya sabes registrar a mano herramientas, recursos y prompts, sin temer al protocolo.
12. Un poco sobre estructura del código y buenas prácticas
Incluso en un ejemplo tan pequeño se pueden sentar buenas costumbres.
En primer lugar, mantén la configuración del servidor separada. Todo lo relativo al nombre, versión, logging, ajustes del transporte (puerto, ruta /mcp) se puede extraer fácilmente a un pequeño módulo config.ts. Luego, cuando despliegues en Vercel o detrás de un MCP‑gateway, tendrás que añadir variables de entorno y lo agradecerás.
En segundo lugar, intenta que los métodos registerTool/registerResource/registerPrompt sean lo más «finos» posible. La descripción de esquemas, textos y la lógica de negocio son piezas que lucen bien en archivos aparte:
- gifts.ts — funciones para elegir regalos;
- catalog.ts — trabajo con el catálogo de productos;
- prompts.ts — conjunto de prompts.
Así, server.ts se convierte en algo parecido a un «proveedor de MCP», que simplemente lo ensambla todo.
En tercer lugar, recuerda que un MCP‑servidor por naturaleza es reactivo: espera conexiones de clientes y sus peticiones. Esto significa que cualquier operación bloqueante o excesivamente larga dentro de las herramientas impactará directamente en la experiencia de ChatGPT. En los siguientes módulos hablaremos de timeouts, operaciones asíncronas y respuestas en streaming, pero ya ahora conviene pensar qué operaciones se pueden derivar a background y cuáles deben responder rápido.
Insight: ChatGPT solo admite una parte de MCP
Es importante entender: las ChatGPT Apps usan MCP como transporte y formato, pero no son un MCP‑cliente completo. Si lees solo el protocolo, es fácil hacerse falsas expectativas sobre cómo funcionará todo en tiempo de ejecución.
Lo que promete el MCP «puro»:
- los recursos (resources) pueden leerse dinámicamente bajo demanda del cliente, y no una sola vez para siempre;
- el servidor puede enviar notificaciones resourceChanged/toolChanged y así «empujar» actualizaciones sin reiniciar el cliente;
- se puede construir un sistema bastante flexible, donde el conjunto de tools/resources/prompts esté gobernado por configs o estado externo.
En el contexto de las ChatGPT Apps no es así. Para la aplicación el panorama es mucho más estático:
- al registrar la App, ChatGPT lee una vez la descripción de todas las tools y resources;
- después esta configuración se cachea de facto como parte de la versión de la aplicación;
- las actualizaciones dinámicas mediante notificaciones de MCP no se admiten — la plataforma simplemente las ignora.
13. Errores típicos al escribir tu primer MCP‑servidor
Error n.º 1: Volcar toda la lógica de negocio directamente en registerTool.
La tentación de «escribir rápidamente todo en el manejador de la herramienta» es enorme, especialmente en un ejemplo didáctico. Pero luego se convierte en una máquina ilegible donde se mezclan validación, trabajo con BD y formateo de la respuesta. Mejor extraer desde el principio las funciones de negocio (suggestGifts, trabajo con el catálogo) a módulos aparte, y en el manejador hacer solo el «pegado».
Error n.º 2: Atarse rígidamente a nombres concretos de métodos JSON de MCP.
A veces los estudiantes empiezan a escribir if (method === "tools/list") y a parsear JSON a mano. No hace falta: esa es tarea del SDK. La especificación de MCP y los nombres de métodos pueden evolucionar, y el SDK se ocupa de ello. Usa registerTool, registerResource, registerPrompt y deja que la biblioteca decida cómo se ve en JSON‑RPC.
Error n.º 3: No pensar en el transporte e intentar alimentar a ChatGPT con un servidor por stdio.
El transporte por stdio es ideal para clientes locales como entornos de escritorio, donde el cliente puede lanzar el servidor como subproceso. Pero ChatGPT se comunica por HTTPS y necesita un endpoint HTTP/stream. Intentar «hacer llegar stdio» a través de un túnel termina en dolor. Para una ChatGPT App crea directamente transporte HTTP (Streamable HTTP).
Error n.º 4: Ignorar los tipos MIME y la estructura de los recursos.
En los recursos importa no solo el contenido, sino también el tipo (mimeType) y el URI. Si en todas partes pones text/plain y lanzas cadenas JSON sin pensar, a los clientes (e inspectores) les costará más entender qué datos son. Procura indicar tipos MIME correctos (application/json, text/html para plantillas de UI, etc.) y URI estables.
Error n.º 5: Usar el MCP‑servidor como un «API HTTP arbitrario».
A veces surge la tentación: «Como ya tengo Express, colgaré también /api/whatever y le daré golpes directamente». Mezclar el endpoint de MCP con un REST arbitrario no es buena idea: complica la configuración, el enrutado y la seguridad. Es mejor tener un contrato claro: /mcp para MCP, rutas separadas para otras necesidades, o incluso otro servicio. En producción esto es especialmente importante para configurar gateways y autorización. Es decir, no conviertas el MCP‑servidor en un «API HTTP arbitrario» — un conjunto de endpoints aleatorios sin relación con el contrato de MCP.
Error n.º 6: No registrar logs de los mensajes de MCP entrantes y salientes.
Sin logs, un MCP‑servidor se convierte en una caja negra: «algo no funciona, pero no sé qué». Ya en el primer servidor tiene sentido escribir al menos en stderr logs estructurados y compactos: método de la herramienta, estado, tiempo de ejecución. Lo importante es no registrar datos sensibles ni tokens; esto lo trataremos aparte cuando lleguemos a seguridad.
Error n.º 7: Intentar depurarlo todo a la vez a través de ChatGPT, sin tener un inspector.
Escenario frecuente: el alumno escribe un MCP‑servidor, lo conecta inmediatamente a la ChatGPT App y «todo falla de forma incomprensible». El inspector ni siquiera se ha iniciado una vez. Resultado: es difícil entender si el problema está en el protocolo, en el servidor, en Apps SDK o en el comportamiento del modelo. El camino correcto es primero asegurarte de que tu MCP‑servidor funciona correctamente en aislamiento (con MCP Jam / Inspector), y solo después conectarlo a la aplicación.
GO TO FULL VERSION