Módulo 4: MCP Server en TypeScript
Transports: stdio, HTTP/SSE y Streamable HTTP
Transports: stdio, HTTP/SSE y Streamable HTTP
Descripción de la cápsula
Has implementado tools y resources. Tu MCP server tiene capabilities reales. Pero falta una pieza fundamental: ¿cómo se comunica tu server con el host? Eso es exactamente lo que define un transport.
Un transport es el canal de comunicación entre el MCP client (dentro del host) y tu MCP server. Es la "tubería" por donde viajan los mensajes JSON-RPC. Y la elección del transport determina dónde y cómo puede ejecutarse tu server.
Hasta ahora has usado stdio — stdin/stdout como canal. Es el default para desarrollo local y para Claude Code. Pero stdio tiene limitaciones: requiere que el host lance el proceso del server directamente. ¿Qué pasa si quieres un server que corra en otro equipo? ¿O un server que varios hosts compartan? Ahí entran HTTP/SSE y Streamable HTTP.
Esta cápsula cubre los tres transports que el SDK de TypeScript soporta, cuándo usar cada uno, y cómo configurarlos.
Los tres transports de MCP
Visión general
Transport Comunicación Caso de uso
─────────────────────────────────────────────────────────────
stdio stdin/stdout Desarrollo local, CLI
HTTP/SSE HTTP POST + SSE Servidores remotos (legacy)
Streamable HTTP HTTP con streaming Servidores remotos (moderno)
stdio: el transport local
┌─────────────┐ stdin/stdout ┌──────────────┐
│ Host │ ◄──────────────► │ MCP Server │
│ (Claude │ │ (tu código) │
│ Code) │ │ │
└─────────────┘ └──────────────┘
El host lanza el proceso del server como un subprocess.
La comunicación es bidireccional via stdin/stdout.
Cómo funciona:
- El host (Claude Code) ejecuta tu server como un proceso hijo:
node build/index.js - El host escribe mensajes JSON-RPC a stdin del server
- El server escribe respuestas JSON-RPC a stdout
- Logs y errores van a stderr (nunca a stdout)
Ventajas:
- Setup más simple — no necesitas HTTP server
- Sin configuración de red — todo es local
- Sin autenticación necesaria — el proceso es hijo del host
- Aislamiento natural — un proceso por conexión
Limitaciones:
- Solo funciona si el host puede ejecutar el proceso directamente
- No se puede compartir entre múltiples hosts
- No funciona para servidores remotos
- Un proceso por conexión (no es eficiente para muchos clients)
HTTP/SSE: el transport remoto (legacy)
┌─────────────┐ HTTP POST ┌──────────────┐
│ Host │ ──────────────► │ MCP Server │
│ (Claude │ │ (HTTP server) │
│ Code) │ ◄────────────── │ │
└─────────────┘ SSE stream └──────────────┘
El client envía requests vía HTTP POST.
El server envía responses y notificaciones vía SSE (Server-Sent Events).
Cómo funciona:
- Tu server arranca como un HTTP server en un puerto
- El client se conecta al endpoint SSE para recibir mensajes del server
- El client envía mensajes al server vía HTTP POST
- El server puede enviar notificaciones en cualquier momento vía SSE
Ventajas:
- Funciona de forma remota — el server puede estar en otra máquina
- Múltiples clients pueden conectarse al mismo server
- Se puede poner detrás de un reverse proxy (nginx, CloudFlare)
- Compatible con firewalls y redes corporativas (usa HTTP estándar)
Limitaciones:
- Más complejo de configurar que stdio
- Necesita autenticación si está expuesto a internet
- SSE es unidireccional (server → client) — los requests van por POST separado
- Considerado "legacy" — Streamable HTTP es el reemplazo recomendado
Streamable HTTP: el transport moderno
┌─────────────┐ HTTP POST/GET ┌──────────────┐
│ Host │ ◄────────────────► │ MCP Server │
│ (Claude │ (streaming) │ (HTTP server) │
│ Code) │ │ │
└─────────────┘ └──────────────┘
Todo va por HTTP. Las responses pueden ser streaming.
Diseñado como el futuro estándar del transporte MCP.
Cómo funciona:
- Tu server arranca como un HTTP server
- El client envía todos los mensajes vía HTTP POST a un solo endpoint
- El server responde directamente en el mismo request (o vía streaming)
- Las notificaciones del server van como SSE en un GET endpoint
Ventajas:
- API más simple que HTTP/SSE separado
- Soporta streaming nativo
- Mejor soporte para stateless deployments (serverless, containers)
- Es el transport recomendado para nuevas implementaciones remotas
- Puede manejar sesiones vía headers
Limitaciones:
- Es el más nuevo — aún en maduración
- Requiere soporte del client (no todos los hosts lo implementan aún)
Implementación: stdio
Este es el transport que has usado hasta ahora. La implementación es mínima:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new McpServer({
name: "my-server",
version: "1.0.0",
});
// ... registrar tools, resources, prompts ...
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio");
}
main().catch(console.error);
Configuración en Claude Code
# Agregar server con transport stdio
claude mcp add my-server -s user -- node /ruta/absoluta/build/index.js
# Con variables de entorno
claude mcp add my-server -s user -e API_KEY=xxx -- node /ruta/absoluta/build/index.js
# Verificar
claude
/mcp
Cuándo usar stdio
- Desarrollo local — siempre
- Servers personales — que solo tú usas en tu máquina
- Integración con Claude Code — el caso más común
- Scripts y automaciones — servers que se ejecutan como parte de un pipeline
Implementación: HTTP/SSE
Para HTTP/SSE necesitas un HTTP server. El SDK provee SSEServerTransport:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
const app = express();
const server = new McpServer({
name: "my-server-remote",
version: "1.0.0",
});
// ... registrar tools, resources, prompts ...
const transports: Map<string, SSEServerTransport> = new Map();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
const sessionId = transport.sessionId;
transports.set(sessionId, transport);
res.on("close", () => {
transports.delete(sessionId);
});
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(400).json({ error: "Session not found" });
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`MCP Server running on http://localhost:${PORT}`);
console.log(`SSE endpoint: http://localhost:${PORT}/sse`);
console.log(`Messages endpoint: http://localhost:${PORT}/messages`);
});
Dependencia adicional
npm install express
npm install -D @types/express
Configuración en Claude Code
# Para un server HTTP/SSE local
claude mcp add my-server-remote -s user --transport sse http://localhost:3001/sse
Cuándo usar HTTP/SSE
- Servers que corren en otra máquina — otro equipo en tu red, un VPS
- Servers compartidos — múltiples desarrolladores usan el mismo server
- Servers existentes — muchos MCP servers legacy usan este transport
- Cuando Streamable HTTP no está soportado por el host
Implementación: Streamable HTTP
Streamable HTTP es el transport más moderno. El SDK provee StreamableHTTPServerTransport:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { randomUUID } from "crypto";
const app = express();
app.use(express.json());
const server = new McpServer({
name: "my-server-streamable",
version: "1.0.0",
});
// ... registrar tools, resources, prompts ...
const transports: Map<string, StreamableHTTPServerTransport> = new Map();
app.post("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports.has(sessionId)) {
transport = transports.get(sessionId)!;
} else {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: (newSessionId) => {
transports.set(newSessionId, transport);
},
});
transport.onclose = () => {
const id = [...transports.entries()]
.find(([, t]) => t === transport)?.[0];
if (id) transports.delete(id);
};
await server.connect(transport);
}
await transport.handleRequest(req, res);
});
app.get("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(400).json({ error: "Session not found" });
return;
}
await transport.handleRequest(req, res);
});
app.delete("/mcp", async (req, res) => {
const sessionId = req.headers["mcp-session-id"] as string;
const transport = transports.get(sessionId);
if (transport) {
await transport.close();
transports.delete(sessionId);
}
res.status(200).end();
});
const PORT = process.env.PORT || 3002;
app.listen(PORT, () => {
console.log(`MCP Streamable HTTP Server on http://localhost:${PORT}/mcp`);
});
Cuándo usar Streamable HTTP
- Nuevas implementaciones remotas — es el estándar recomendado
- Deploy en containers/serverless — mejor soporte para stateless
- Cuando necesitas streaming — responses que llegan incrementalmente
- Cuando el host lo soporta — verificar documentación del host
Comparación: cuándo usar cuál
| Criterio | stdio | HTTP/SSE | Streamable HTTP |
|---|---|---|---|
| Setup | Mínimo | Medio | Medio |
| Ubicación | Local | Local o remoto | Local o remoto |
| Clientes | 1 por proceso | Múltiples | Múltiples |
| Autenticación | No necesaria | Necesaria si expuesto | Necesaria si expuesto |
| Claude Code | ✅ Soporte nativo | ✅ Con --transport sse | ⚠️ Verificar soporte |
| Ideal para | Desarrollo, uso personal | Servers compartidos | Nuevas implementaciones |
| Complejidad | Baja | Media | Media |
Diagrama de decisión
¿Tu server corre en la misma máquina que el host?
├── Sí → ¿Solo tú lo usas?
│ ├── Sí → stdio (la opción más simple)
│ └── No → HTTP/SSE o Streamable HTTP
└── No → ¿El host soporta Streamable HTTP?
├── Sí → Streamable HTTP (recomendado)
└── No → HTTP/SSE (compatible con más hosts)
Recomendación práctica
Para el 90% de los casos en este curso y en uso real con Claude Code:
- Usa stdio para desarrollo y uso personal con Claude Code
- Usa HTTP/SSE si necesitas que el server sea accesible remotamente
- Usa Streamable HTTP para nuevos proyectos que necesiten acceso remoto
Server multi-transport
Puedes implementar un server que soporte múltiples transports según cómo se ejecute:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
const server = new McpServer({
name: "multi-transport-server",
version: "1.0.0",
});
// ... registrar tools, resources, prompts ...
async function main() {
const transportType = process.env.MCP_TRANSPORT || "stdio";
if (transportType === "stdio") {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Server running on stdio");
} else if (transportType === "sse") {
const express = (await import("express")).default;
const app = express();
const transports = new Map<string, SSEServerTransport>();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
res.on("close", () => transports.delete(transport.sessionId));
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) {
res.status(400).json({ error: "Session not found" });
return;
}
await transport.handlePostMessage(req, res);
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
}
}
main().catch(console.error);
Uso:
# Modo stdio (default)
node build/index.js
# Modo HTTP/SSE
MCP_TRANSPORT=sse node build/index.js
# Modo HTTP/SSE con puerto custom
MCP_TRANSPORT=sse PORT=8080 node build/index.js
Este patrón es útil cuando quieres un server que funcione tanto en desarrollo (stdio) como en un servidor compartido (HTTP/SSE).
Configuración avanzada de Claude Code
Agregar server stdio
# Forma básica
claude mcp add my-server -s user -- node /ruta/build/index.js
# Con variables de entorno
claude mcp add my-server -s user -e API_KEY=secret -e DEBUG=true -- node /ruta/build/index.js
# Con directorio de trabajo
claude mcp add my-server -s user -- sh -c "cd /my/project && node /ruta/build/index.js"
Agregar server HTTP/SSE
# Server SSE remoto
claude mcp add my-server-remote -s user --transport sse http://localhost:3001/sse
# Server SSE con headers de autenticación
claude mcp add my-server-remote -s user --transport sse \
-H "Authorization: Bearer my-token" \
http://my-server.example.com/sse
Gestión de servers
# Listar servers configurados
claude mcp list
# Ver detalles de un server
claude mcp get my-server
# Remover un server
claude mcp remove my-server
Scopes de configuración
# user — disponible en todas las sesiones de Claude Code del usuario
claude mcp add my-server -s user -- ...
# project — solo en el proyecto actual (guardado en .claude/settings.json del proyecto)
claude mcp add my-server -s project -- ...
user es lo más común para desarrollo. project es útil cuando el server es específico para un repo y quieres que otros contributors lo tengan configurado.
Seguridad en transports remotos
Cuando tu server es accesible via HTTP, necesitas considerar seguridad:
Autenticación básica
app.use((req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res.status(401).json({ error: "Authorization header required" });
return;
}
const token = authHeader.split(" ")[1];
if (token !== process.env.MCP_AUTH_TOKEN) {
res.status(403).json({ error: "Invalid token" });
return;
}
next();
});
CORS para web clients
import cors from "cors";
app.use(cors({
origin: ["http://localhost:3000", "https://my-app.com"],
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization", "mcp-session-id"],
}));
Rate limiting
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
message: { error: "Too many requests" },
});
app.use(limiter);
Nota: Para este módulo, stdio es suficiente. La seguridad de transports remotos se profundiza en el módulo 7.
El flujo completo: de código a Claude Code
Para cerrar esta cápsula, el flujo completo de un MCP server con transport stdio:
1. Escribes código TypeScript
└── src/index.ts con McpServer, tools, resources
2. Compilas
└── npm run build → build/index.js
3. Pruebas con Inspector
└── npm run inspect → verificas tools y resources
4. Agregas a Claude Code
└── claude mcp add my-server -s user -- node /ruta/build/index.js
5. Verificas conexión
└── claude → /mcp → "my-server: connected"
6. Usas en conversación
└── "Claude, busca archivos con extensión .ts en mi proyecto"
└── Claude usa tu tool search_files
7. Iteras
└── Editas código → recompilas → reinicias Claude Code
Troubleshooting
"Server arranca pero no recibe mensajes (stdio)"
Causa: Algo escribe a stdout antes del protocolo.
Solución:
// ❌ Esto rompe stdio
console.log("Server starting...");
// ✅ Siempre stderr para logs
console.error("Server starting...");
"Connection refused (HTTP/SSE)"
Causa: El server no está corriendo o el puerto está mal.
Solución:
# Verificar que el server está corriendo
curl http://localhost:3001/sse
# Debería recibir headers SSE, no un error
# Verificar el puerto
lsof -i :3001
"Claude Code muestra 'disconnected' para un server SSE"
Causa: El server SSE se cayó o la URL es incorrecta.
Solución:
# Verificar la configuración
claude mcp get my-server
# Re-agregar con la URL correcta
claude mcp remove my-server
claude mcp add my-server -s user --transport sse http://localhost:3001/sse
"CORS error al conectar desde navegador"
Causa: El server no tiene configuración CORS.
Solución:
npm install cors
npm install -D @types/cors
import cors from "cors";
app.use(cors());
"Session not found (Streamable HTTP)"
Causa: El client no envía el header mcp-session-id después de la inicialización.
Solución: Verificar que el client almacena y reenvía el session ID que el server retorna en la respuesta de inicialización.
Ejercicios
Ejercicio 1: Verificar transport stdio (Fácil)
Toma tu server de la cápsula anterior, compila, y verifica que:
- Arranca con
node build/index.jsy muestra log en stderr - Se conecta al MCP Inspector
- Se conecta a Claude Code
- Los tools y resources funcionan
Ver solución
npm run build
node build/index.js
# Debería imprimir a stderr y esperar
# Ctrl+C para salir
npm run inspect
# Verificar tools y resources en el Inspector
claude mcp add test-server -s user -- node $(pwd)/build/index.js
claude
/mcp
# Verificar: test-server: connected
Ejercicio 2: Implementar server HTTP/SSE (Medio)
Convierte tu server para que funcione con HTTP/SSE. Instala express, crea los endpoints /sse y /messages, y verifica que el MCP Inspector puede conectar por HTTP.
Ver solución
npm install express
npm install -D @types/express
Crea src/http-server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";
import { z } from "zod";
const app = express();
const server = new McpServer({ name: "sse-server", version: "1.0.0" });
server.tool(
"ping",
"Retorna pong con timestamp",
{},
async () => ({
content: [{ type: "text" as const, text: `pong - ${new Date().toISOString()}` }],
})
);
const transports = new Map<string, SSEServerTransport>();
app.get("/sse", async (req, res) => {
const transport = new SSEServerTransport("/messages", res);
transports.set(transport.sessionId, transport);
res.on("close", () => transports.delete(transport.sessionId));
await server.connect(transport);
});
app.post("/messages", async (req, res) => {
const sessionId = req.query.sessionId as string;
const transport = transports.get(sessionId);
if (!transport) { res.status(400).end(); return; }
await transport.handlePostMessage(req, res);
});
app.listen(3001, () => console.log("SSE server on http://localhost:3001"));
npm run build
node build/http-server.js
# En otra terminal:
npx @modelcontextprotocol/inspector --transport sse http://localhost:3001/sse
Ejercicio 3: Server multi-transport (Medio)
Implementa un server que seleccione el transport según la variable de entorno MCP_TRANSPORT (stdio o sse). Verifica que ambos modos funcionan.
Ver solución
Usa el ejemplo de la sección "Server multi-transport" de esta cápsula. Verifica con:
# Modo stdio
npm run build
node build/index.js
# Ctrl+C
# Modo SSE
MCP_TRANSPORT=sse node build/index.js
# En otra terminal, verificar con curl o Inspector
Ejercicio 4: Agregar autenticación a HTTP/SSE (Difícil)
Agrega autenticación por Bearer token a tu server HTTP/SSE. El token se lee de la variable de entorno MCP_AUTH_TOKEN. Verifica que requests sin token reciben 401.
Ver solución
const AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
app.use((req, res, next) => {
if (!AUTH_TOKEN) {
next();
return;
}
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
res.status(401).json({ error: "Authorization required" });
return;
}
if (authHeader.split(" ")[1] !== AUTH_TOKEN) {
res.status(403).json({ error: "Invalid token" });
return;
}
next();
});
# Sin token (debe funcionar)
node build/http-server.js
# Con token
MCP_AUTH_TOKEN=secret123 node build/http-server.js
# En otra terminal:
curl http://localhost:3001/sse
# → 401 Unauthorized
curl -H "Authorization: Bearer secret123" http://localhost:3001/sse
# → Conexión SSE establecida
Resumen
En esta cápsula aprendiste:
- stdio es el transport local — stdin/stdout, el más simple, ideal para Claude Code
- HTTP/SSE es el transport remoto legacy — HTTP POST para requests, SSE para responses
- Streamable HTTP es el transport moderno — todo por HTTP con streaming nativo
- stdio para desarrollo y uso personal; HTTP/SSE o Streamable HTTP para servidores remotos
- Un server puede soportar múltiples transports seleccionables por variable de entorno
- La seguridad es necesaria para transports HTTP expuestos — autenticación, CORS, rate limiting
console.error(nuncaconsole.log) para logs en servers stdio- Claude Code soporta tanto stdio como SSE para configurar MCP servers
Recursos adicionales
- MCP Specification — Transports - Especificación oficial de transports
- MCP TypeScript SDK — Transports - Implementación en el SDK
- Server-Sent Events (SSE) - Referencia de SSE
- Express.js - Framework HTTP usado en los ejemplos
- Claude Code MCP Configuration - Configuración oficial de MCP en Claude Code
- JSON-RPC 2.0 Specification - Protocolo subyacente de MCP
Siguiente cápsula: Proyecto — MCP Server TypeScript completo. Todo lo que aprendiste en las cápsulas 02-05 se integra en un server funcional con múltiples tools para un caso de uso real.