Construcción de un servidor MCP con el SDK de TypeScript
El SDK oficial de TypeScript de MCP refleja la estructura del SDK de Python: registra manejadores de herramientas y recursos en un objeto servidor, y luego lo ejecuta a través de un transporte.
Si tu servicio ya reside en una base de código Node, este SDK te permite exponerlo a Claude sin introducir un segundo idioma.
Resumen
Un servidor MCP de TypeScript se construye alrededor de McpServer, la clase de servidor de alto nivel del SDK.
Registras capacidades llamando directamente a server.tool(...) y server.resource(...), en lugar de usar decoradores.
Los esquemas Zod definen la forma de entrada de cada herramienta y sirven también como validación en tiempo de ejecución.
El servidor se conecta a un transporte, comúnmente StdioServerTransport para desarrollo local, de la misma manera que lo hace el SDK de Python.
Esta página refleja la profundidad del artículo del SDK de Python: argumentos tipados, manejo de errores y recursos parametrizados, expresados en TypeScript idiomático.
Receta
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "tickets-server", version: "1.0.0" });
server.tool(
"create_ticket",
{ title: z.string(), priority: z.enum(["low", "normal", "high"]).default("normal") },
async ({ title, priority }) => ({
content: [{ type: "text", text: `Creado ticket para "${title}" (${priority})` }],
})
);
await server.connect(new StdioServerTransport());Cuándo recurrir a esto:
- Tu servicio o equipo existente ya se basa en TypeScript/Node, y quieres evitar un segundo runtime.
- Quieres verificación de tipos en tiempo de compilación en los argumentos de las herramientas junto con validación en tiempo de ejecución desde el mismo esquema.
- Estás construyendo para desarrollo local primero,
StdioServerTransportes la opción correcta antes de añadir una capa de red. - Quieres compartir tipos entre tu servidor MCP y una superficie de API TypeScript existente.
Ejemplo de trabajo
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
interface Ticket {
id: string;
title: string;
priority: "low" | "normal" | "high";
status: "open" | "closed";
}
const tickets = new Map<string, Ticket>([
["T-1", { id: "T-1", title: "Printer offline", priority: "low", status: "open" }],
["T-2", { id: "T-2", title: "VPN outage", priority: "high", status: "closed" }],
]);
let nextId = 3;
const server = new McpServer({ name: "tickets-server", version: "1.0.0" });
server.tool(
"create_ticket",
{
title: z.string().min(1, "el título es requerido"),
priority: z.enum(["low", "normal", "high"]).default("normal"),
},
async ({ title, priority }) => {
const id = `T-${nextId++}`;
tickets.set(id, { id, title, priority, status: "open" });
return { content: [{ type: "text", text: id }] };
}
);
server.tool(
"close_ticket",
{ ticketId: z.string() },
async ({ ticketId }) => {
const ticket = tickets.get(ticketId);
if (!ticket) {
throw new Error(`no existe ticket con id '${ticketId}'`);
}
ticket.status = "closed";
return { content: [{ type: "text", text: `${ticketId} cerrado` }] };
}
);
server.resource(
"ticket",
"tickets://{ticketId}",
async (uri, { ticketId }) => {
const ticket = tickets.get(ticketId as string);
if (!ticket) {
throw new Error(`no existe ticket con id '${ticketId}'`);
}
return {
contents: [
{
uri: uri.href,
text: `${ticket.id} [${ticket.priority}] ${ticket.title} - ${ticket.status}`,
},
],
};
}
);
server.resource("open-tickets", "tickets://open", async (uri) => {
const openIds = [...tickets.values()].filter((t) => t.status === "open").map((t) => t.id);
return { contents: [{ uri: uri.href, text: openIds.join("\n") || "no hay tickets abiertos" }] };
});
await server.connect(new StdioServerTransport());Lo que esto demuestra:
- Dos herramientas,
create_ticketyclose_ticket, con esquemas Zod que imponen tanto el tipo como los valores permitidos depriority. - Un recurso parametrizado,
tickets://{ticketId}, junto a un recurso fijo,tickets://open, reflejando los dos estilos de recursos del ejemplo de Python. - Objetos
Errorlanzados usados para fallos de validación, convertidos por el SDK en respuestas de error estructuradas. - La forma de retorno de la herramienta,
{ content: [...] }, que es el formato de resultado estructurado del SDK de TypeScript.
Profundización
Cómo funciona
new McpServer({ name, version })crea una instancia del servidor y configura las etapas de conexión y negociación por ti, idénticamente aFastMCPen Python.server.tool(name, schema, handler)registra una herramienta bajoname, usando el objetoschemade Zod para construir el esquema JSON enviado a los clientes durante la negociación.server.resource(name, uriTemplate, handler)registra un recurso; un segmento{placeholder}en la plantilla URI se pasa al manejador como un parámetro.- El SDK valida los argumentos de entrada de la herramienta contra el esquema Zod antes de invocar tu manejador, por lo que la entrada malformada nunca llega a tu código.
server.connect(transport)inicia el servidor contra un transporte elegido,StdioServerTransportpara desarrollo local, un transporte basado en HTTP para despliegue remoto.
Manejo de errores
Lanzar un Error estándar dentro de un manejador es la forma correcta de señalar un fallo.
server.tool("divide", { a: z.number(), b: z.number() }, async ({ a, b }) => {
if (b === 0) {
throw new Error("no se puede dividir por cero");
}
return { content: [{ type: "text", text: String(a / b) }] };
});El SDK captura el error lanzado y devuelve un resultado de error estructurado al cliente en lugar de colgar el proceso.
Evita devolver un resultado con forma de éxito con un mensaje de error incrustado en el texto, el cliente no tiene forma fiable de distinguir eso de un resultado real.
Herramientas vs. Recursos de un vistazo
| Capacidad | Registrado con | Tiene efectos secundarios | Ejemplo |
|---|---|---|---|
| Herramienta | server.tool(name, schema, handler) | A menudo sí | create_ticket, close_ticket |
| Recurso | server.resource(name, uri, handler) | No | tickets://open, tickets://{ticketId} |
Notas de TypeScript
// Los esquemas Zod sirven también como tipos en tiempo de compilación a través de z.infer.
const CreateTicketInput = z.object({
title: z.string(),
priority: z.enum(["low", "normal", "high"]).default("normal"),
});
type CreateTicketInput = z.infer<typeof CreateTicketInput>;Definir el esquema una vez con Zod y derivar el tipo TypeScript de él mantiene la validación en tiempo de ejecución y el tipo en tiempo de compilación sincronizados, por lo que un cambio de esquema no puede desviarse silenciosamente del tipo que espera tu manejador.
Parámetros y valores de retorno
| Método | Registra | Forma de retorno del manejador |
|---|---|---|
server.tool(name, schema, handler) | Una acción invocable | { content: [{ type: "text", text: string }] } |
server.resource(name, uri, handler) | Contenido de solo lectura en una URI | { contents: [{ uri: string, text: string }] } |
Trampas comunes
- Usar un tipo Zod laxo como
z.string()para un valor similar a enum. Esto permite que pasen valores inválidos comopriority: "urgent"sin un rechazo a nivel de esquema. Solución: usaz.enum([...])para cualquier argumento con un conjunto fijo de valores válidos. - Devolver una cadena de texto simple desde un manejador de herramientas en lugar de la forma de contenido. El SDK espera
{ content: [...] }, no una cadena de texto desnuda. Solución: siempre envuelve el resultado en el array de contenido estructurado, incluso para una respuesta de texto simple. - Olvidar
awaitenserver.connect(...). El proceso puede salir antes de que el transporte termine de adjuntarse, haciendo que el servidor parezca que silenciosamente no hace nada. Solución: siempre usaawait server.connect(transport)en el nivel superior. - Mutar un
Mapa nivel de módulo sin proteger para sesiones concurrentes. Bajo HTTP/SSE con múltiples clientes, las llamadas superpuestas a herramientas pueden competir por el estado compartido en memoria. Solución: respalda el estado real con una base de datos una vez que pases de la prototipación local. - Nombrar una herramienta algo genérico como
runoexecute. Los nombres vagos dan poca señal a Claude sobre lo que la herramienta realmente hace, especialmente una vez que un servidor tiene varias herramientas. Solución: nombra las herramientas según la acción específica que realizan,create_ticketen lugar derun. - Omitir la validación
.min()o de formato en las entradas de cadena. Una cadenatitlevacía pasa una verificaciónz.string()desnuda y puede crear registros malformados silenciosamente. Solución: añade los refinamientos Zod específicos (.min(1),.email(), etc.) que el campo realmente necesita.
Alternativas
| Alternativa | Usar cuando | No usar cuando |
|---|---|---|
| SDK MCP de Python | Tu equipo o pila existente se basa en Python, o quieres reutilizar una base de código de datos/ML existente | Quieres compartir tipos directamente con un frontend o API TypeScript existente |
Clases de protocolo MCP de bajo nivel (sin McpServer) | Necesitas control granular que la API de alto nivel no expone | Las herramientas y recursos estándar cubren tus necesidades, lo cual es la mayoría de las veces |
| Una API REST simple (sin MCP) | El consumidor no es un cliente LLM y no necesita negociación de capacidades | Quieres que Claude descubra y llame a tu funcionalidad dinámicamente |
Preguntas frecuentes
¿En qué se diferencia McpServer de FastMCP en el SDK de Python?
- Ambos son la clase de alto nivel del servidor para su respectivo SDK.
FastMCPusa decoradores (@mcp.tool());McpServerusa llamadas directas a métodos (server.tool(...)).- Ambos manejan la conexión y la negociación de capacidades automáticamente y exponen los mismos conceptos de herramienta/recurso/prompt.
¿Por qué este SDK usa Zod en lugar de tipos TypeScript simples?
Los tipos TypeScript se borran en tiempo de ejecución, por lo que no pueden validar datos entrantes por sí solos. Los esquemas Zod proporcionan tanto un validador en tiempo de ejecución como, a través de z.infer, un tipo de tiempo de compilación coincidente a partir de la misma definición.
¿Qué sucede si un manejador de herramientas lanza un error?
server.tool("risky", { n: z.number() }, async ({ n }) => {
if (n < 0) throw new Error("n debe ser no negativo");
return { content: [{ type: "text", text: String(n) }] };
});El SDK captura el error lanzado y devuelve una respuesta de error estructurada al cliente en lugar de colgar el proceso del servidor.
¿Puede un manejador de recursos tomar múltiples parámetros de su URI?
Sí. Una plantilla de URI puede incluir más de un segmento {placeholder}, y cada uno se pasa al manejador, de la misma manera que los campos del esquema de una herramienta se pasan como argumentos.
¿Los argumentos de las herramientas siempre deben tener valores predeterminados?
No, solo cuando existe un valor predeterminado sensato, como priority que por defecto es "normal". Los campos requeridos, como title en el ejemplo del ticket, deben seguir siendo requeridos para que el esquema rechace llamadas incompletas.
¿Qué transporte debo usar durante el desarrollo local?
StdioServerTransport. Es el transporte local estándar, el cliente inicia tu servidor como un subproceso y se comunica con él a través de stdin/stdout, sin necesidad de configuración de red.
¿Cómo pruebo un servidor MCP de TypeScript sin un cliente real?
Escribe pruebas unitarias que llamen directamente a tus funciones manejadoras con argumentos de ejemplo, y luego ejércítalas por separado a través de un cliente simulado que hable el protocolo. Consulta la página dedicada a pruebas para ver el patrón completo.
¿Puedo registrar tanto una herramienta como un recurso con los mismos datos subyacentes?
Sí, y es un patrón común. Una herramienta que muta un registro (close_ticket) y un recurso que lo lee (tickets://{ticketId}) pueden compartir la misma capa de almacenamiento en memoria o base de datos.
¿El SDK soporta manejadores asíncronos?
Sí, por convención se espera que los manejadores sean funciones asíncronas, ya que la mayoría de las herramientas y recursos reales implican E/S como una llamada a base de datos o una solicitud a una API.
¿Cuál es la forma de retorno que debe producir un manejador de herramientas?
Un objeto con un array content, cada entrada típicamente { type: "text", text: string }. Devolver una cadena de texto simple o un objeto sin este envoltorio no coincidirá con lo que espera el cliente.
¿Cómo evito romper clientes existentes cuando cambio el esquema de una herramienta?
Añade nuevos campos como opcionales con .optional() o un .default(...) sensato en lugar de renombrar o eliminar campos existentes. Consulta la página dedicada a la versión de esquemas de herramientas para la estrategia más amplia.
¿Es McpServer específico de Node.js, o también se ejecuta en navegadores?
El SDK está dirigido a Node.js para uso en el lado del servidor, ya que los servidores MCP son típicamente procesos de larga duración con acceso a un transporte como stdio o un socket de red, ninguno de los cuales está disponible en un contexto de navegador.
Relacionado
- Conceptos básicos del servidor MCP - el esqueleto mínimo sobre el que se construye este artículo
- Construcción de un servidor MCP con el SDK de Python - los mismos patrones en Python
- Cómo los servidores MCP manejan las solicitudes - el ciclo de vida en el que se conectan estos manejadores
- Prueba de servidores MCP antes del despliegue - ejercitando estos manejadores con un cliente simulado
- Comparación de despliegue de MCP stdio vs HTTP/SSE - elección de un transporte más allá del desarrollo local
Versiones de la pila: Escrito contra la línea de modelos Claude actual a partir de ~junio de 2026 - Claude Fable 5, Claude Opus 4.8, Claude Sonnet 5 (el predeterminado), y Claude Haiku 4.5 - y los SDK actuales de Model Context Protocol para Python/TypeScript. Los nombres de los modelos, las versiones de los SDK y la especificación MCP cambian rápidamente; verifica los detalles actuales en platform.claude.com/docs y modelcontextprotocol.io antes de confiar en ellos.