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:

  1. El host (Claude Code) ejecuta tu server como un proceso hijo: node build/index.js
  2. El host escribe mensajes JSON-RPC a stdin del server
  3. El server escribe respuestas JSON-RPC a stdout
  4. 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:

  1. Tu server arranca como un HTTP server en un puerto
  2. El client se conecta al endpoint SSE para recibir mensajes del server
  3. El client envía mensajes al server vía HTTP POST
  4. 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:

  1. Tu server arranca como un HTTP server
  2. El client envía todos los mensajes vía HTTP POST a un solo endpoint
  3. El server responde directamente en el mismo request (o vía streaming)
  4. 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

CriteriostdioHTTP/SSEStreamable HTTP
SetupMínimoMedioMedio
UbicaciónLocalLocal o remotoLocal o remoto
Clientes1 por procesoMúltiplesMúltiples
AutenticaciónNo necesariaNecesaria si expuestoNecesaria si expuesto
Claude Code✅ Soporte nativo✅ Con --transport sse⚠️ Verificar soporte
Ideal paraDesarrollo, uso personalServers compartidosNuevas implementaciones
ComplejidadBajaMediaMedia

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:

  1. Usa stdio para desarrollo y uso personal con Claude Code
  2. Usa HTTP/SSE si necesitas que el server sea accesible remotamente
  3. 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:

  1. Arranca con node build/index.js y muestra log en stderr
  2. Se conecta al MCP Inspector
  3. Se conecta a Claude Code
  4. 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 (nunca console.log) para logs en servers stdio
  • Claude Code soporta tanto stdio como SSE para configurar MCP servers

Recursos adicionales

  1. MCP Specification — Transports - Especificación oficial de transports
  2. MCP TypeScript SDK — Transports - Implementación en el SDK
  3. Server-Sent Events (SSE) - Referencia de SSE
  4. Express.js - Framework HTTP usado en los ejemplos
  5. Claude Code MCP Configuration - Configuración oficial de MCP en Claude Code
  6. 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.