Transport Layers en MCP: stdio, SSE y WebSocket — El Framework de Selección que el 80% Ignora
Guía completa sobre transport layers en MCP: stdio vs SSE vs WebSocket. Aprende a elegir el transporte correcto para cada tipo de herramienta en tus agentes IA.
El 80% de los Agentes MCP Usan el Transporte Equivocado
El 80% de los agentes MCP que he visto en producción usan stdio. No porque sea la mejor opción para su caso de uso. Sino porque viene por defecto.
Esto no es un problema técnico. Es un problema de diseño arquitectónico.
Elegir stdio sin auditar qué tipo de interacciones necesita tu agente significa renunciar a tres capacidades que diferencian un demo de un sistema productivo: streaming de resultados parciales, notificaciones del servidor, y estado compartido entre invocaciones.
*El transporte que eliges no determina la simplicidad de tu implementación. Determina qué tipo de agente puedes construir.*
En esta guía voy a mostraros cómo auditar vuestras herramientas MCP por patrón de interacción, qué transporte corresponde a cada patrón, y cómo migrar entre transportes sin reescribir todo el sistema.
El Fallo Oculto: Transport y Tool Schema No Son Independientes
La documentación actual de MCP trata el transporte como una decisión ortogonal al diseño de herramientas. Elige stdio, SSE, o WebSocket, y luego diseña tus schemas independientemente.
Esto es un error.
Un schema diseñado para stdio asume llamadas síncronas. El modelo invoca la herramienta, espera la respuesta, continúa. No necesita modelar estado entre invocaciones porque no hay estado que modelar.
Cuando migras ese mismo schema a SSE, el servidor puede empezar a devolver resultados parciales. La herramienta que antes era "buscar_usuario" y devolvía un objeto completo ahora puede devolver streaming de logs, actualizaciones progresivas, o notificaciones de progreso. Pero el schema no tiene dónde expresar eso.
Y cuando migras a WebSocket, la bidireccionalidad introduce conceptos que stdio no necesita: sessions, subscriptions, cancelaciones, notificaciones push. Tu schema carece de parámetros para modelarlos.
❌ Diseño incorrecto: Elegir transporte → luego diseñar schemas
✅ Diseño correcto: Auditar interacciones → elegir transporte → diseñar schemas para ese transporte desde el inicio
La decisión de transporte condiciona el diseño del schema desde el día uno. Postergar esta decisión no la hace más fácil. La hace más costosa.
La Auditoría de Herramientas: Tu Primer Paso Obligatorio
Antes de elegir transporte, necesitas saber qué patrones de interacción tienen tus herramientas. Esta auditoría tiene tres categorías.
Herramientas de Consulta (GET-like)
Devuelven datos. No modifican estado. Respuesta completa en una llamada. Ejemplos: buscar usuario, consultar precio, obtener configuración.
Estas herramientas no necesitan estado entre invocaciones. El modelo pregunta, la herramienta responde, fin.
Herramientas de Efecto (POST-like)
Modifican estado en el servidor. Una llamada, un efecto, una respuesta. Ejemplos: crear usuario, actualizar precio, enviar notificación.
Estas herramientas pueden funcionar en un modelo request-response simple, pero el servidor podría querer notificar cambios downstream que el agente necesita conocer.
Herramientas de Streaming (Observables)
Devuelven datos que llegan progresivamente. Logs, análisis, procesos largos. Ejemplos: streaming de logs, procesamiento de dataset, búsqueda con resultados parciales.
Estas herramientas rompen el modelo síncrono. Si el agente espera la respuesta completa, se bloquea. Necesitan un transporte que soporte streaming.
El Framework de Selección de Transporte
Después de auditar vuestras herramientas, aplicad este framework de tres capas para elegir el transporte correcto.
Capa 1: stdio para Consultas Síncronas Locales
Si todas vuestras herramientas son de consulta pura sin estado compartido, stdio es suficiente. No añadáis complejidad de red cuando no la necesitáis.
```javascript
// Cliente MCP con transporte stdio
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
command: 'node',
args: ['./mi-servidor-mcp.js']
});
const client = new Client(
{ name: 'mi-cliente', version: '1.0.0' },
{ capabilities: { tools: true } }
);
await client.connect(transport);
// Llamada síncrona simple
const result = await client.callTool({
name: 'buscar_usuario',
arguments: { id: 'usuario-123' }
});
```
Este patrón funciona cuando servidor y cliente están en la misma máquina o contenedor. Sin overhead de red, sin autenticación de red, sin puertos abiertos.
Capa 2: SSE para Streaming y Resultados Parciales
Cuando tengáis herramientas que devuelven streams largos,迁移 a SSE. El modelo puede procesar resultados parciales sin bloquearse.
```javascript
// Cliente MCP con transporte SSE
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
const transport = new SSEClientTransport(
new URL('http://localhost:3000/mcp/sse')
);
const client = new Client(
{ name: 'mi-cliente-sse', version: '1.0.0' },
{ capabilities: { tools: true, streaming: true } }
);
await client.connect(transport);
// El servidor puede enviar eventos parciales
client.on('progress', (notification) => {
console.log('Progreso:', notification.params.progress);
// El modelo puede procesar esto mientras continúan llegando datos
});
const result = await client.callTool({
name: 'procesar_dataset',
arguments: { dataset_id: 'dataset-456' }
});
```
El servidor envía eventos Server-Sent Events que el cliente recibe progresivamente. El modelo no espera a que termine el procesamiento completo para empezar a trabajar con los resultados parciales.
Capa 3: WebSocket para Estado Compartido y Bidireccionalidad
Cuando necesitéis que el servidor notifique al agente sin que el agente lo haya solicitado, WebSocket es la opción. Sesiones de usuario, carritos de compra, workflows donde múltiples herramientas comparten estado.
```javascript
// Cliente MCP con transporte WebSocket
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { WebSocketClientTransport } from '@modelcontextprotocol/sdk/client/websocket.js';
const transport = new WebSocketClientTransport(
new URL('ws://localhost:3000/mcp/ws')
);
const client = new Client(
{ name: 'mi-cliente-ws', version: '1.0.0' },
{ capabilities: { tools: true, sessions: true, subscriptions: true } }
);
await client.connect(transport);
// Suscribirse a cambios en una sesión
client.subscribe({
method: 'sessions/updates',
params: { session_id: 'sesion-789' }
});
client.on('notification', (notification) => {
// Recibir notificaciones del servidor sin polling
console.log('Notificación del servidor:', notification);
});
// El servidor puede enviar notificaciones cuando el estado cambia
// sin que el agente lo haya solicitado
```
El Selector Automático de Transporte
Para sistemas con herramientas de múltiples tipos, este wrapper selecciona el transporte según el patrón de interacción:
```javascript
class MCPTransportSelector {
constructor(config) {
this.transports = {
stdio: this.createStdioTransport(config.stdio),
sse: this.createSSETransport(config.sse),
ws: this.createWebSocketTransport(config.ws)
};
}
getTransport(toolPattern) {
if (toolPattern === 'query') return this.transports.stdio;
if (toolPattern === 'streaming') return this.transports.sse;
if (toolPattern === 'shared-state') return this.transports.ws;
// Default: stdio para backwards compatibility
return this.transports.stdio;
}
async executeTool(toolName, args, toolPattern) {
const transport = this.getTransport(toolPattern);
const client = this.getOrCreateClient(toolPattern, transport);
return client.callTool({ name: toolName, arguments: args });
}
}
// Uso
const selector = new MCPTransportSelector({
stdio: { command: 'node', args: ['./local-tools.js'] },
sse: { url: 'http://api.internal/mcp/sse' },
ws: { url: 'ws://api.internal/mcp/ws' }
});
// Consulta local: stdio
const usuario = await selector.executeTool('buscar_usuario', { id: '123' }, 'query');
// Análisis con streaming: SSE
const resultados = await selector.executeTool('analizar_logs', { rango: '7d' }, 'streaming');
// Sesión compartida: WebSocket
const sesion = await selector.executeTool('unir_sesion', { session_id: 'abc' }, 'shared-state');
```
El Punto Ciego: Negociación de Capacidades
Hay un problema en la especificación actual de MCP que la mayoría ignora: no existe un estándar para que cliente y servidor negocien qué transportes soporta cada uno.
Implementar un cliente que soporte múltiples transportes requiere lógica ad-hoc:
```javascript
async function negotiateTransport(serverUrl) {
// Obtener capacidades del servidor
const response = await fetch(`${serverUrl}/.well-known/mcp-capabilities`);
const capabilities = await response.json();
// Detectar qué transportes están disponibles
const availableTransports = capabilities.transports || ['stdio'];
// Preferencias ordenadas
const preferredOrder = ['ws', 'sse', 'stdio'];
for (const transport of preferredOrder) {
if (availableTransports.includes(transport)) {
return transport;
}
}
throw new Error('No hay transporte compatible disponible');
}
```
Esto no está estandarizado. No hay header `X-MCP-Transport-Capabilities` en el handshake inicial. No hay negociación formal de capacidades.
*Los primeros en proponer convenciones para esta negociación ganarán ventaja. Es un gap real en el ecosistema MCP que necesita una solución comunitaria.*
Migración Gradual: No Todo a la Vez
La迁移 a nuevos transportes no tiene que ser un big bang. Seguid esta progresión:
Paso 1: Auditoría completa
Clasificad cada herramienta existente en query, streaming, o shared-state.
Paso 2: Grupo A — stdio para todo
Confirmad que las herramientas de consulta funcionan correctamente en stdio. Este es vuestro baseline.
Paso 3: Grupo B — Migrad streaming a SSE
Identificad las herramientas que más se benefician del streaming. Procesamiento de datos largos, logs, análisis progresivos. Migrad una a SSE, medid latencia percibida, comparar con stdio.
Paso 4: Grupo C — WebSocket solo cuando sea necesario
Solo cuando necesitéis notificaciones push del servidor o sesiones compartidas entre herramientas. No antes.
No migréis todo a WebSocket porque "es más moderno" o "escala mejor". Usadlo cuando el caso de uso lo requiera naturalmente.
Comparativa: Cuándo Usar Cada Transporte
| Aspecto | stdio | SSE | WebSocket |
|---------|-------|-----|-----------|
| Latencia | Mínima (local) | Media (red) | Baja (persistente) |
| Streaming | ❌ No | ✅ Sí | ✅ Sí |
| Bidireccionalidad | ❌ No | ❌ No | ✅ Sí |
| Complejidad | Baja | Media | Alta |
| Estado entre llamadas | ❌ No | ⚠️ Limitado | ✅ Sí |
| Notificaciones push | ❌ No | ❌ No | ✅ Sí |
Conclusión: Decisión Explícita, No Por Defecto
La próxima vez que configuréis un cliente MCP, no uséis stdio porque viene por defecto. Haced la auditoría. Clasificad vuestras herramientas. Elegid el transporte que corresponde a cada patrón.
stdio no es "más simple y por tanto mejor". Es suficiente para consultas síncronas locales. Cuando necesitéis streaming, elegid SSE. Cuando necesitéis bidireccionalidad y estado compartido, elegid WebSocket.
El transporte no es una decisión técnica neutral. Determina qué tipo de agente puedes construir.
Haced la auditoría. Tomad la decisión conscientemente.
Lee el artículo completo en brianmenagomez.com
Más sobre mis servicios en brianmenagomez.com
Herramientas: Conversor IAE CNAE · Gestorias cerca de ti · Calculadora IRPF

