Módulo 4: MCP Server en TypeScript
Setup del Proyecto y SDK de TypeScript
Setup del Proyecto y SDK de TypeScript
Descripción de la cápsula
Antes de escribir una sola línea de lógica MCP, necesitas un proyecto que compile y se ejecute. Esto no es un detalle menor — un setup incorrecto es la causa número uno de frustración al construir MCP servers. Un import mal configurado, un tsconfig.json incompleto, o una dependencia faltante pueden costarte horas de debugging.
En esta cápsula vas a crear un proyecto MCP en TypeScript desde cero. No copias un template — entiendes cada archivo, cada configuración, cada dependencia. Cuando termines, tendrás un proyecto que compila limpiamente, se ejecuta, y está listo para recibir tools y resources en las cápsulas siguientes.
El objetivo es que este setup se convierta en tu template personal. Cada vez que necesites un nuevo MCP server, empiezas desde aquí.
Paso 1: Inicializar el proyecto
Crear el directorio
mkdir mcp-server-ts
cd mcp-server-ts
Inicializar npm
npm init -y
Esto genera un package.json básico. Ahora lo vamos a configurar correctamente.
Configurar package.json
Reemplaza el contenido del package.json generado con esto:
{
"name": "mcp-server-ts",
"version": "1.0.0",
"description": "MCP Server en TypeScript con tools y resources",
"type": "module",
"main": "build/index.js",
"bin": {
"mcp-server-ts": "build/index.js"
},
"scripts": {
"build": "tsc",
"start": "node build/index.js",
"dev": "tsc --watch",
"inspect": "npm run build && npx @modelcontextprotocol/inspector node build/index.js"
},
"keywords": ["mcp", "model-context-protocol", "claude-code"],
"license": "MIT"
}
Cada campo importa:
| Campo | Por qué |
|---|---|
"type": "module" | Habilita ES modules (import/export en vez de require) — el SDK de MCP lo requiere |
"main" | Punto de entrada cuando alguien importa tu paquete |
"bin" | Permite ejecutar tu server como comando CLI |
"scripts.build" | Compila TypeScript a JavaScript |
"scripts.dev" | Recompila automáticamente cuando cambias código |
"scripts.inspect" | Compila y abre MCP Inspector en un solo comando |
Por qué "type": "module" es obligatorio
El SDK de MCP usa ES modules internamente. Sin "type": "module", Node.js trata los archivos como CommonJS y los imports del SDK fallan:
# ❌ Sin "type": "module"
Error [ERR_REQUIRE_ESM]: require() of ES Module
.../node_modules/@modelcontextprotocol/sdk/dist/esm/server/mcp.js
# ✅ Con "type": "module"
# Todo funciona correctamente
Paso 2: Instalar dependencias
Dependencias de producción
npm install @modelcontextprotocol/sdk zod
| Paquete | Versión | Propósito |
|---|---|---|
@modelcontextprotocol/sdk | latest | SDK oficial de MCP para TypeScript |
zod | latest | Validación de schemas para tools |
Dependencias de desarrollo
npm install -D typescript @types/node
| Paquete | Versión | Propósito |
|---|---|---|
typescript | latest | Compilador de TypeScript |
@types/node | latest | Tipos de Node.js (fs, path, process, etc.) |
Verificar instalación
npx tsc --version
# Debería mostrar algo como: Version 5.x.x
Si ves la versión, las dependencias están correctas.
Paso 3: Configurar TypeScript
Crea el archivo tsconfig.json en la raíz del proyecto:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "build"]
}
Cada opción explicada
| Opción | Valor | Por qué |
|---|---|---|
target | ES2022 | Genera JavaScript moderno — soporta top-level await, structuredClone |
module | Node16 | Sistema de módulos correcto para Node.js con ES modules |
moduleResolution | Node16 | Resolución de imports compatible con "type": "module" |
outDir | ./build | Directorio de salida para el JavaScript compilado |
rootDir | ./src | Directorio raíz del código fuente TypeScript |
strict | true | Habilita todas las verificaciones estrictas de tipos |
esModuleInterop | true | Permite importar módulos CommonJS con syntax de ES modules |
skipLibCheck | true | No verifica tipos en node_modules — acelera la compilación |
declaration | true | Genera archivos .d.ts — útil si publicas tu server como paquete |
sourceMap | true | Genera source maps para debugging |
Por qué strict: true no es opcional
El modo estricto de TypeScript activa verificaciones que previenen bugs reales en MCP servers:
// strict: true atrapa esto:
// 1. Parámetros posiblemente undefined
function handleTool(args: { name?: string }) {
console.log(args.name.toUpperCase());
// ^^^^ Error: 'name' is possibly undefined
}
// 2. Returns implícitos
async function getResource(): Promise<string> {
const data = await fetchData();
// Error: Not all code paths return a value
// (te obliga a manejar todos los casos)
}
// 3. Variables sin tipo
const result = JSON.parse(data);
// ^^^^^^ Tipo: any — strict te obliga a tipar
En un MCP server donde los inputs vienen de un modelo de lenguaje y los outputs van a un protocolo estricto, cada uno de estos checks previene un bug potencial en producción.
El error más común con imports
Con module: "Node16" y "type": "module", los imports de archivos locales necesitan la extensión .js (sí, .js, no .ts):
// ❌ Esto NO funciona
import { helper } from "./utils";
import { helper } from "./utils.ts";
// ✅ Esto SÍ funciona
import { helper } from "./utils.js";
Parece contraintuitivo — estás escribiendo TypeScript pero importas con .js. La razón es que TypeScript compila .ts a .js, y Node.js necesita la extensión del archivo compilado para resolver el import. Es una quirk del ecosistema, no un bug.
Paso 4: Crear la estructura de carpetas
mkdir -p src
Estructura mínima
mcp-server-ts/
├── package.json
├── tsconfig.json
├── node_modules/
└── src/
└── index.ts ← Punto de entrada del server
Estructura recomendada para servers con múltiples tools
Cuando tu server crece, organiza el código así:
mcp-server-ts/
├── package.json
├── tsconfig.json
├── node_modules/
└── src/
├── index.ts ← Entry point: crea server, registra, conecta
├── tools/
│ ├── create-file.ts
│ ├── search-files.ts
│ └── index.ts ← Re-exporta todos los tools
├── resources/
│ ├── project-structure.ts
│ ├── config.ts
│ └── index.ts ← Re-exporta todos los resources
└── utils/
├── validation.ts
└── filesystem.ts
Para este módulo, empezamos con la estructura mínima (todo en index.ts) y la refactorizamos en el proyecto de la cápsula 06.
Paso 5: Crear el punto de entrada
Crea el archivo src/index.ts:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "mcp-server-ts",
version: "1.0.0",
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server TypeScript running on stdio");
}
main().catch((error) => {
console.error("Fatal error starting server:", error);
process.exit(1);
});
Desglose del código
#!/usr/bin/env node — El shebang. Permite ejecutar el archivo directamente como ./build/index.js si lo marcas como ejecutable. Necesario si defines el campo bin en package.json.
McpServer — La clase principal del SDK. Recibe un nombre (identificador para el host) y una versión (útil para debugging y compatibilidad).
StdioServerTransport — El transport que usa stdin/stdout para comunicarse. Es el transport por defecto para desarrollo local y para Claude Code.
console.error — Nota que usamos console.error, no console.log. En un server stdio, stdout está reservado para la comunicación del protocolo MCP. Cualquier output que escribas a stdout rompe el protocolo. Los logs van siempre a stderr.
process.exit(1) — Si el server no puede arrancar, sale con código 1 para que el host sepa que algo falló.
Paso 6: Compilar y verificar
Compilar
npm run build
Resultado esperado:
(sin output = éxito)
TypeScript solo imprime mensajes cuando hay errores. Sin output significa que compiló correctamente.
Verificar el build
ls build/
# index.js index.js.map index.d.ts index.d.ts.map
Deberías ver 4 archivos:
index.js— El JavaScript compilado (lo que ejecuta Node.js)index.js.map— Source map para debuggingindex.d.ts— Declaraciones de tiposindex.d.ts.map— Source map de declaraciones
Ejecutar
node build/index.js
Resultado esperado:
MCP Server TypeScript running on stdio
El server arranca, imprime a stderr, y espera input en stdin. Presiona Ctrl+C para salir.
Probar con MCP Inspector
npm run inspect
Esto compila y abre MCP Inspector. Deberías ver tu server conectado pero sin tools ni resources (los agregaremos en las cápsulas siguientes).
Anatomía del SDK: las piezas que usarás
Antes de pasar a implementar tools y resources, necesitas un mapa mental del SDK:
Imports principales
// El server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
// Transports
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
// Resource templates
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
// Validación
import { z } from "zod";
La clase McpServer
McpServer es tu punto de entrada. Tiene 3 métodos principales para registrar primitivas:
const server = new McpServer({ name: "...", version: "..." });
// Registrar un tool
server.tool(name, description, schema, handler);
// Registrar un resource
server.resource(name, uri_or_template, metadata, handler);
// Registrar un prompt
server.prompt(name, description, schema, handler);
// Conectar un transport
await server.connect(transport);
El rol de Zod
Zod no es un detalle de implementación — es el contrato entre tu server y el modelo. Cuando defines un schema Zod, estás diciendo:
- Al modelo: "Estos son los parámetros que acepto, con estos tipos y restricciones"
- Al runtime: "Valida los inputs antes de que lleguen a mi handler"
- Al developer: "Este es el tipo TypeScript de los argumentos del handler"
// Este schema Zod...
{
filePath: z.string().min(1).describe("Ruta del archivo"),
content: z.string().describe("Contenido"),
overwrite: z.boolean().default(false).describe("Sobreescribir si existe"),
}
// ...genera automáticamente:
// 1. JSON Schema para el protocolo MCP
// 2. Validación runtime de los inputs
// 3. Tipos TypeScript inferidos para el handler
Verás Zod en profundidad en la cápsula 03. Por ahora, entiende que es el pegamento entre tus tools y el protocolo MCP.
Workflow de desarrollo
El ciclo: editar → compilar → probar
1. Edita src/index.ts (o archivos en src/)
2. Compila: npm run build
3. Prueba: npm run inspect (o conecta a Claude Code)
4. Repite
Compilación automática
Para evitar ejecutar npm run build manualmente cada vez:
npm run dev
# Equivale a: tsc --watch
Esto recompila automáticamente cada vez que guardas un archivo .ts. Deja este terminal abierto mientras trabajas.
Conectar a Claude Code durante desarrollo
# Agrega tu server a Claude Code
claude mcp add my-server -s user -- node /ruta/absoluta/a/mcp-server-ts/build/index.js
# Verifica la conexión
claude
/mcp
# Deberías ver: my-server: connected
Cada vez que recompilas, necesitas reiniciar Claude Code para que detecte los cambios. El transport stdio crea una nueva instancia del server por sesión.
Anatomía de un tool (preview)
Antes de la cápsula 03, un preview de cómo se ve un tool completo para que entiendas hacia dónde vamos:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
server.tool(
"greet_user", // nombre del tool
"Genera un saludo personalizado para un usuario", // descripción
{ // schema Zod
name: z.string().describe("Nombre del usuario"),
language: z.enum(["es", "en", "fr"]).default("es").describe("Idioma del saludo"),
},
async ({ name, language }) => { // handler
const greetings = {
es: `¡Hola, ${name}! Bienvenido.`,
en: `Hello, ${name}! Welcome.`,
fr: `Bonjour, ${name}! Bienvenue.`,
};
return {
content: [{
type: "text" as const,
text: greetings[language],
}],
};
}
);
Cuatro argumentos: nombre, descripción, schema, handler. Eso es todo. La cápsula 03 explora esto en profundidad con ejemplos progresivamente más complejos.
Comparación: Setup manual vs Starter templates
¿Por qué no usar un template pre-hecho?
Existen starter templates como create-mcp-server y repos de ejemplo. No los usamos aquí porque:
- Entender > copiar. Si no entiendes cada archivo de tu proyecto, no puedes debuggearlo cuando falla.
- Los templates se desactualizan. El SDK evoluciona rápido. Un template de hace 3 meses puede tener dependencias obsoletas.
- Tu template personal es mejor. Después de este módulo, tendrás tu propio setup que conoces al 100%.
Cuándo SÍ usar un template
Cuando ya dominas el setup manual y quieres velocidad:
# Para crear rápidamente un nuevo server después de este módulo
npx @modelcontextprotocol/create-server my-server
Pero primero, entiende lo que el template genera. Eso es lo que estás haciendo ahora.
Troubleshooting
"Error: Cannot find module '@modelcontextprotocol/sdk/server/mcp.js'"
Causa: Las dependencias no se instalaron o el path del import es incorrecto.
Solución:
rm -rf node_modules package-lock.json
npm install
Si persiste, verifica que @modelcontextprotocol/sdk está en dependencies de tu package.json.
"SyntaxError: Cannot use import statement outside a module"
Causa: Falta "type": "module" en package.json.
Solución:
{
"type": "module"
}
"Error: Unknown file extension '.ts'"
Causa: Estás ejecutando el archivo .ts directamente en vez del .js compilado.
Solución:
# ❌ No ejecutes el .ts
node src/index.ts
# ✅ Compila primero, ejecuta el .js
npm run build
node build/index.js
"TSError: ⨯ Unable to compile TypeScript"
Causa: Errores de tipo en tu código.
Solución: Lee el mensaje de error. Los más comunes:
# Error: Argument of type 'string' is not assignable to parameter of type 'number'
# → Verifica los tipos de tus variables
# Error: Property 'x' does not exist on type 'Y'
# → Verifica la interfaz/tipo que estás usando
# Error: Cannot find module './utils.js'
# → Crea el archivo o verifica la ruta (recuerda usar .js en imports)
"El server arranca pero Claude Code no lo detecta"
Causa: Error en la configuración de claude mcp add.
Solución:
# Verifica la configuración actual
claude mcp list
# Remueve y re-agrega con ruta absoluta
claude mcp remove my-server
claude mcp add my-server -s user -- node $(pwd)/build/index.js
# Reinicia Claude Code
claude
/mcp
Ejercicios
Ejercicio 1: Setup desde cero (Fácil)
Crea un proyecto MCP llamado hello-mcp siguiendo todos los pasos de esta cápsula. Verifica que:
- Compila sin errores
- Ejecuta y muestra el mensaje en stderr
- Se conecta al MCP Inspector
Ver solución
mkdir hello-mcp && cd hello-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node
package.json — agrega "type": "module" y los scripts.
tsconfig.json — copia la configuración de esta cápsula.
mkdir src
src/index.ts:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "hello-mcp",
version: "1.0.0",
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Hello MCP Server running on stdio");
}
main().catch(console.error);
npm run build # debe compilar sin errores
node build/index.js # debe imprimir "Hello MCP Server running on stdio"
npm run inspect # debe abrir MCP Inspector con el server conectado
Ejercicio 2: Agregar un tool trivial (Fácil)
Agrega un tool echo a tu server que reciba un message (string) y lo retorne exactamente como lo recibió. Verifica con MCP Inspector.
Ver solución
import { z } from "zod";
server.tool(
"echo",
"Retorna el mensaje exactamente como lo recibió",
{
message: z.string().describe("Mensaje a repetir"),
},
async ({ message }) => {
return {
content: [{
type: "text" as const,
text: message,
}],
};
}
);
Compila con npm run build, abre el Inspector con npm run inspect, ve al tab Tools, y prueba con { "message": "Hola MCP!" }.
Ejercicio 3: Experimentar con errores de compilación (Medio)
Introduce estos errores intencionalmente en tu src/index.ts y observa qué dice el compilador. Luego corrígelos:
- Cambia el import de
.jsa.ts - Quita
"type": "module"delpackage.json - Usa
console.logen vez deconsole.errory observa qué pasa al conectar con Inspector
Ver solución
- Import con
.ts:
# Error: An import path can only end with a '.ts' extension when 'allowImportingTsExtensions' is enabled.
# Solución: usar .js en los imports de archivos locales
- Sin
"type": "module":
# Error al ejecutar: SyntaxError: Cannot use import statement outside a module
# Solución: agregar "type": "module" en package.json
console.logen stdio:
# El Inspector no puede conectar o muestra errores de parsing
# Porque console.log escribe a stdout, que es el canal del protocolo MCP
# Cualquier texto que no sea JSON-RPC válido rompe la comunicación
# Solución: siempre usar console.error para logs
Ejercicio 4: Estructura de proyecto modular (Medio)
Refactoriza tu server para que el tool echo viva en su propio archivo src/tools/echo.ts. Exporta una función registerEchoTool(server: McpServer) que registre el tool.
Ver solución
src/tools/echo.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export function registerEchoTool(server: McpServer) {
server.tool(
"echo",
"Retorna el mensaje exactamente como lo recibió",
{
message: z.string().describe("Mensaje a repetir"),
},
async ({ message }) => {
return {
content: [{
type: "text" as const,
text: message,
}],
};
}
);
}
src/index.ts:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerEchoTool } from "./tools/echo.js";
const server = new McpServer({
name: "mcp-server-ts",
version: "1.0.0",
});
registerEchoTool(server);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server running on stdio");
}
main().catch(console.error);
Nota el import con .js extension: "./tools/echo.js".
mkdir -p src/tools
# Crea los archivos, luego:
npm run build
npm run inspect
Ejercicio 5: Shebang y ejecución directa (Medio)
Configura tu server para que pueda ejecutarse directamente como un script:
- Verifica que el shebang
#!/usr/bin/env nodeestá en la primera línea - Haz el archivo ejecutable
- Ejecútalo sin
nodeexplícito
Ver solución
npm run build
chmod +x build/index.js
./build/index.js
# Debería imprimir: MCP Server running on stdio
# Esto es lo que Claude Code ejecuta internamente cuando configuras el server
El shebang #!/usr/bin/env node le dice al sistema operativo que use Node.js para ejecutar el archivo. Sin el shebang, el sistema intenta ejecutar el JavaScript como bash y falla.
Checklist de setup completo
Antes de pasar a la siguiente cápsula, verifica que tienes todo:
-
package.jsoncon"type": "module"y scripts (build,start,dev,inspect) -
tsconfig.jsonconstrict: true,module: "Node16",outDir: "./build" -
@modelcontextprotocol/sdkyzodinstalados -
typescripty@types/nodecomo devDependencies -
src/index.tscon McpServer y StdioServerTransport -
npm run buildcompila sin errores -
node build/index.jsarranca y muestra mensaje en stderr -
npm run inspectabre MCP Inspector con el server conectado -
.gitignoreincluyenode_modules/ybuild/(si usas git)
Si todo está en verde, tu proyecto está listo para recibir tools y resources.
Resumen
En esta cápsula:
- Creaste un proyecto MCP en TypeScript desde cero — directorio, npm init, dependencias
- Configuraste
package.jsoncon"type": "module"y scripts de desarrollo - Configuraste
tsconfig.jsonconstrict: trueymodule: "Node16" - Instalaste el SDK (
@modelcontextprotocol/sdk) y Zod para validación - Creaste el punto de entrada (
src/index.ts) con McpServer y StdioServerTransport - Compilaste y verificaste que el server arranca correctamente
- Entendiste por qué cada configuración existe —
"type": "module",strict: true, extensiones.jsen imports - Preparaste el workflow de desarrollo: editar → compilar → probar con Inspector
Este setup es tu template. Cada MCP server que crees en el futuro empieza desde aquí.
Recursos adicionales
- MCP TypeScript SDK — README - Instrucciones oficiales del SDK
- Zod Documentation - Referencia completa de Zod
- TypeScript tsconfig Reference - Documentación de cada opción de tsconfig
- Node.js ES Modules - Cómo funcionan los ES modules en Node.js
- MCP Inspector - Herramienta de testing visual
- npm package.json Reference - Campos de package.json explicados
Siguiente cápsula: Implementar Tools — la primitiva más importante. Zod schemas progresivamente complejos, error handling, y patrones de diseño para tools reales.