# ¿Por qué los fallos de inicio de los servidores MCP parecen iguales?

Un cliente MCP puede indicar que un servidor «no pudo iniciarse» incluso después de que el sistema operativo haya iniciado el proceso, el proceso haya leído la entrada y el servidor ya haya contactado con algo fuera de la máquina. Ese mensaje no es un diagnóstico. Es un contenedor donde se mezclan fallos de inicio, del protocolo, de descubrimiento y, a veces, de ejecución de herramientas.

Trata el inicio como una secuencia de límites que dejan pruebas. Si no puedes decir qué límite cruzó el servidor, no puedes indicar al operador si es seguro reintentar, si se pudo usar una credencial o si el cliente simplemente no mostró un servidor saludable. La solución no es aumentar el timeout. Hay que separar los estados y hacer observable cada uno.

## Un único estado rojo oculta cuatro fallos distintos

El operador necesita cuatro respuestas, en este orden: ¿el cliente inició el comando configurado?, ¿ambos lados completaron la inicialización de MCP?, ¿el cliente recibió una lista de herramientas utilizable?, ¿algún código alcanzó un canal externo? Cada respuesta demuestra algo diferente.

Un proceso puede fallar antes de existir en el sentido habitual. Puede faltar el ejecutable, no existir el directorio de trabajo, fallar un ejecutor de paquetes antes de invocar tu código o terminar el proceso hijo de inmediato porque falta una variable de entorno necesaria. Llama a esto **fallo de inicio**. No hay una sesión MCP y es posible que el código de tu aplicación ni siquiera se haya ejecutado.

El proceso también puede existir y aun así fallar en el intercambio del protocolo. Con stdio, el proceso hijo tiene stdin y stdout conectados al cliente. El servidor debe leer JSON-RPC desde stdin y escribir únicamente mensajes JSON-RPC en stdout. Después debe responder a la solicitud `initialize` del cliente con una versión de protocolo compatible y las capacidades declaradas. El cliente continúa con `notifications/initialized`. Si la secuencia no termina, es un **fallo de handshake**.

Un handshake correcto no demuestra que el cliente haya conocido las herramientas. El servidor puede declarar que admite herramientas pero fallar al registrarlas, generar un esquema de entrada no válido, devolver un resultado incorrecto de `tools/list` o devolver una lista vacía porque su propia configuración desactivó todas las herramientas. Es un **fallo de descubrimiento de herramientas**. Cliente y servidor pueden estar suficientemente sanos para intercambiar mensajes, pero no habrá nada que el agente pueda llamar.

Por último, un servidor puede completar el descubrimiento y fallar solo cuando se ejecuta una herramienta. Es un **fallo de ejecución de herramientas** y debe constar en otro incidente. Si lo mezclas con el inicio, tarde o temprano alguien reintentará un servidor que ya envió una solicitud HTTP o abrió una conexión SSH.

La documentación de Model Context Protocol deja clara esta separación, aunque muchas interfaces de cliente no lo hagan. Sus indicaciones de depuración distinguen los problemas de proceso y configuración de los registros del protocolo, y advierten que los servidores stdio locales deben mantener stdout libre de registros normales. El ciclo de inicialización del protocolo y la solicitud `tools/list` son intercambios distintos. Conserva esa diferencia en tu propia telemetría en lugar de aceptar la etiqueta genérica del cliente.

## El fallo de inicio termina antes de que exista MCP

Un fallo de inicio significa que el cliente no obtuvo un proceso hijo utilizable con un flujo de protocolo legible. No significa que el comando «pareciera correcto» en un terminal.

Las shells interactivas ocultan muchas cosas. Tu shell tiene un `PATH`, un directorio actual, gestores de versiones de lenguajes, credenciales y archivos de configuración que una aplicación de escritorio o un subproceso de agente quizá no herede. Un cliente puede iniciarse con `/` como directorio de trabajo en macOS. Puede usar un entorno restringido. También puede pasar el comando como un ejecutable con un array de argumentos en lugar de hacerlo mediante una shell, por lo que los alias y las redirecciones no tendrán efecto.

Captura el registro exacto del inicio antes de intentar razonar sobre MCP:

```text
run_id=run_01JX...
phase=launch
command=/usr/local/bin/node
argv=["/Users/dev/work/acme-mcp/dist/index.js"]
cwd=/
pid=84217
started_at=2026-07-22T14:03:12.417Z
```

Después captura un evento terminal cuando el proceso termine o expire el plazo del handshake:

```text
run_id=run_01JX...
phase=launch
exit_code=1
signal=null
stderr=Error: ENOENT: no such file or directory, open './config.json'
```

Ese registro resuelve rápidamente una discusión habitual. El servidor no «tenía un problema de MCP». Suponía que una ruta relativa se resolvería desde el directorio del proyecto, pero el cliente lo inició desde `/`.

Usa rutas absolutas para el ejecutable, el punto de entrada, los archivos de configuración y cualquier archivo que se lea durante el inicio. La guía de depuración de MCP señala expresamente los directorios de trabajo indefinidos de los servidores iniciados por el cliente y recomienda rutas absolutas. No es una precaución de estilo. Elimina una fuente de fallos que solo aparece cuando alguien instala la misma configuración en otra máquina.

No declares que el inicio tuvo éxito solo porque recibiste un PID. Un PID indica que el kernel creó un proceso. No dice si el programa se cargó, si la tubería stdout está intacta o si el proceso ya se convirtió en zombie a la espera de que lo recojan.

Una máquina de estados de inicio útil es pequeña:

```text
not_requested
  -> spawn_requested
  -> spawned
  -> executable_ready
  -> handshake_pending
```

`spawned` significa que el proceso padre recibió el PID del hijo. `executable_ready` significa que el hijo escribió en stderr un evento deliberado de disponibilidad, no perteneciente al protocolo, después de cargar la configuración e instalar su controlador de errores fatales. No envíes ese evento a stdout. En un servidor stdio, stdout no es un canal de registro cercano al protocolo. Es el cable.

El evento de disponibilidad no debe afirmar que el servidor está conectado a una API, una base de datos o un host remoto. Solo debe indicar lo que demuestra: que el proceso alcanzó la configuración de su transporte MCP. Una línea como `ready=true` se vuelve peligrosa cuando los equipos la interpretan en silencio como «es seguro llamar a las herramientas». Nombra la fase.

## El handshake tiene una definición precisa

Un fallo de handshake de MCP comienza después de que exista un proceso utilizable y termina antes de completar el ciclo de inicialización. No lo llames fallo de conexión sin comprobar los mensajes.

En un transporte stdio importan los primeros bytes de stdout. Un banner de inicio puede corromper el flujo antes de que tu servidor vea la solicitud. Lo mismo puede hacer una dependencia que imprime un aviso de actualización, un `console.log`, un `print` de Python, un formateador de excepciones del framework o un script envoltorio que escribe texto de estado en stdout. La guía oficial de compilación y depuración de MCP lo dice claramente: en servidores stdio, escribe los registros en stderr porque stdout transporta los mensajes del protocolo.

Esta es la traza mínima de una inicialización correcta:

```json
{"direction":"in","id":1,"method":"initialize"}
{"direction":"out","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"acme","version":"1.4.0"}}}
{"direction":"in","method":"notifications/initialized"}
```

La versión exacta del protocolo depende de las versiones compatibles con el cliente y el servidor. Lo importante es que el servidor seleccionó una versión que el cliente acepta, devolvió un resultado válido y recibió la notificación de finalización. Guarda un evento interpretado para cada mensaje, no cargas completas que puedan contener credenciales.

Si la traza empieza así, el diagnóstico cambia:

```text
stdout: Starting Acme MCP server
{"jsonrpc":"2.0","id":1,"method":"initialize",...}
```

El servidor puede ser perfectamente capaz de responder, pero el analizador JSON del cliente ya encontró una entrada no válida. Un timeout después de ese punto no indica que el servidor fuera lento. Indica que el transporte se corrompió.

Otro fallo habitual parece más saludable:

```text
phase=handshake
initialize_received=true
initialize_response_sent=false
fatal_error=Cannot read properties of undefined (reading 'tools')
```

El hijo se inició, recibió la solicitud y falló mientras preparaba la respuesta. Es un error del servidor o una suposición de configuración no controlada, no una mala configuración del cliente.

Haz explícitos los límites en los registros:

```text
phase=handshake event=initialize_received run_id=run_01JX request_id=1
phase=handshake event=initialize_responded run_id=run_01JX request_id=1 protocol_version=2025-06-18
phase=handshake event=initialized_received run_id=run_01JX
```

Si solo escribes `connected=true`, borras el dato que separa una respuesta enviada de un ciclo de inicialización completado. Los clientes pueden cerrar o reiniciar después de recibir la respuesta, pero antes de enviar la notificación. Operativamente, eso no es lo mismo que un error del analizador de inicialización.

Asigna al handshake su propio plazo. Inícialo cuando se cree el proceso, o cuando el transporte esté listo si puedes observar ese momento. Deténlo cuando llegue `notifications/initialized`. Al expirar, informa del último evento confirmado, como `spawned_no_initialize`, `initialize_received_no_response` o `response_sent_no_initialized`. Esos nombres indican al operador dónde debe mirar primero.

## El descubrimiento de herramientas falla después de que el servidor ya sea accesible

Un fallo de descubrimiento de herramientas significa que cliente y servidor pueden hablar MCP, pero el cliente no recibió una respuesta utilizable a `tools/list`. A menudo se informa como un fallo de inicio porque muchos clientes descubren las herramientas justo después de la inicialización.

No supongas que un panel de herramientas vacío demuestra que el resultado de `tools/list` estaba vacío. Algunos clientes ocultan las herramientas cuando falla la validación del esquema. Otros guardan en caché los resultados del descubrimiento. Algunos solicitan las herramientas de forma diferida, solo cuando el agente empieza una tarea. Otros se conectan a un servidor MCP para obtener recursos o prompts y nunca solicitan herramientas. Tus pruebas deben capturar tanto la solicitud como la respuesta.

Una traza de descubrimiento correcta tiene esta forma:

```json
{"direction":"in","id":2,"method":"tools/list"}
{"direction":"out","id":2,"result":{"tools":[{"name":"issue_lookup","description":"Fetch one issue by identifier","inputSchema":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}}]}}
```

El registro de descubrimiento debe incluir el número de herramientas y un resumen criptográfico de los esquemas normalizados. Ese resumen permite saber si dos ejecuciones anunciaron la misma interfaz sin guardar descripciones o configuraciones sensibles. También detecta cambios accidentales en los que una herramienta sigue existiendo, pero desaparecen sus parámetros obligatorios.

No construyas las definiciones de herramientas contactando con un servicio externo durante `tools/list`. Ese diseño convierte el descubrimiento en un efecto secundario, hace que una actualización del cliente parezca una ejecución y crea la peor pregunta posible en un incidente: «¿Enumerar las herramientas cambió algo?». El registro de herramientas debe ser local y determinista siempre que sea posible.

Un servidor puede necesitar configuración para decidir si anuncia una herramienta. Lee esa configuración al inicio y registra el resultado, pero no hagas que el descubrimiento espere a una renovación de token o a una comprobación SSH. Si una herramienta necesita una credencial, valida que exista su referencia local sin usarla. Deja la acción remota real para cuando el cliente llame a la herramienta.

Esta diferencia importa para el control de los agentes. Si un agente llega a Sallyport mediante `sp mcp`, un registro de descubrimiento MCP correcto solo demuestra que el puente expuso operaciones invocables, no que Sallyport realizara una acción HTTP o SSH.

Hay una razón legítima para devolver una lista vacía: la configuración actual no tiene herramientas activadas. Indícalo en una respuesta estructurada o en un registro visible para el cliente. No falles durante el registro y dejes que el cliente adivine si el conjunto de herramientas está vacío a propósito.

```text
phase=discovery event=tools_list_responded run_id=run_01JX tool_count=0 reason=no_enabled_tools
```

Ese registro da al operador un problema de configuración que resolver. Un error genérico de inicio solo le da una superstición que repetir.

## La accesibilidad externa necesita una prueba propia

La pregunta «¿el proceso alcanzó un canal externo?» no puede responderse con un PID, un handshake correcto ni una lista de herramientas completa. Necesitas un evento en el límite donde tu código intenta realizar la acción externa.

Define «canal externo» de forma estricta. Aquí incluye una solicitud HTTP saliente, la invocación de un asistente SSH, una conexión a una base de datos fuera del proceso local, una renovación de credenciales en la nube, la publicación en una cola de mensajes o cualquier llamada que pueda crear un efecto o revelar información fuera de la sesión MCP. Leer un archivo de configuración local no cuenta. Cargar una credencial desde un almacén local protegido tampoco cuenta por sí solo. Enviarla en una solicitud sí.

Registra el intento antes de iniciar la llamada y después registra su resultado. Usa un ID de acción opaco que pueda relacionarse con el ID de solicitud MCP y el ID de ejecución del servidor.

```text
run_id=run_01JX phase=execution event=external_attempt action_id=act_8Qf tool=issue_lookup channel=https host=api.example.test
run_id=run_01JX phase=execution event=external_result action_id=act_8Qf status=200 duration_ms=184
```

No incluyas en esos registros encabezados de autorización, tokens bearer, URL firmadas, argumentos de comandos que contengan secretos ni cuerpos completos de respuestas. Un registro de incidente que filtre la credencial que debía investigar empeora el incidente.

La ubicación de `external_attempt` no es un detalle académico. Si lo colocas demasiado pronto, afirmarás que hubo una llamada externa cuando el código solo creó un objeto de solicitud. Si lo colocas demasiado tarde, un timeout o un fallo del proceso puede dejar un hueco después de que los bytes ya hayan salido de la máquina. Emítelo justo antes de la llamada de biblioteca que puede iniciar actividad de red o SSH. Si la biblioteca ofrece un hook de conexión o solicitud de bajo nivel, registra allí un segundo evento solo si puedes hacerlo sin confundir el significado de «intento».

Un caso práctico muestra por qué importa. Un operador añade un servidor MCP que lee un token del sistema de incidencias durante la inicialización del módulo y llama a un endpoint «who am I» para validarlo. El proceso hijo se inicia, escribe una línea de depuración en stdout y corrompe el primer mensaje MCP. El cliente muestra «el servidor no pudo iniciarse». El equipo reinicia el cliente dos veces.

Sin registros por fase, concluyen que ninguna solicitud salió de la máquina porque el servidor nunca apareció en la interfaz del cliente. La conclusión es falsa. La llamada de inicialización del módulo se ejecutó antes de que el cliente enviara `initialize` y contactó tres veces con el sistema de incidencias. El estado de la interfaz no decía nada sobre la accesibilidad externa.

Mueve la comprobación de identidad a una herramienta deliberadamente de solo lectura o conviértela en parte de la primera acción que realmente necesite el servicio remoto. Después regístrala como ejecución de herramienta. El servidor puede iniciarse, inicializarse y listar herramientas sin tocar la red. El operador puede distinguir ahora entre «el servidor está disponible» y «la credencial y el servicio remoto funcionan». Son hechos distintos y deben mantenerse separados.

## Un registro de fases convierte incidentes vagos en afirmaciones comprobables

Crea una entrada por ejecución y añade eventos de fase inmutables. No necesitas un motor de reglas complejo. Necesitas nombres estables, marcas de tiempo y suficientes campos de correlación para reconstruir lo ocurrido.

Usa esta estructura:

```json
{
  "run_id": "run_01JX",
  "server_name": "acme",
  "pid": 84217,
  "phase": "discovery",
  "event": "tools_list_responded",
  "request_id": 2,
  "tool_count": 4,
  "at": "2026-07-22T14:03:13.083Z"
}
```

El registro debe guardar eventos, no conclusiones pegadas en una cadena. `phase=handshake` y `event=initialize_received` se pueden contar, consultar y probar. `message="MCP parece atascado"` no.

Mantén el modelo de estados deliberadamente sencillo:

1. `spawn_requested`, `spawned`, `executable_ready` y `exited` pertenecen al inicio.
2. `initialize_received`, `initialize_responded` e `initialized_received` pertenecen al handshake.
3. `tools_list_received` y `tools_list_responded` pertenecen al descubrimiento.
4. `tool_call_received`, `external_attempt` y `external_result` pertenecen a la ejecución.
5. `revoked`, `terminated` y `client_disconnected` describen una interrupción, no un éxito.

La distinción que los equipos suelen confundir es **establecimiento de sesión frente a autorización para actuar**. Un servidor puede establecer una sesión MCP sin tener permiso para usar una credencial o conectarse a un sistema remoto. Si tratas ambos estados como uno solo, un evento de aprobación puede parecer un evento de conectividad y una acción denegada puede parecer un inicio fallido.

Mantén los eventos de autorización junto a la llamada que controlan. Por ejemplo, registra `authorization_requested` y `authorization_granted` después de `tool_call_received`, pero antes de `external_attempt`. Así, el operador puede afirmar con pruebas que llegó la solicitud de herramienta, que una persona la denegó y que no hubo ningún intento externo. Es mucho más preciso que decir que la solicitud «no se completó».

Usa un ID de ejecución que solo dure lo que dura un proceso hijo. No reutilices el nombre del servidor como identificador de correlación. Un cliente puede iniciar dos copias del mismo servidor, reiniciar una después de un timeout y conservar metadatos antiguos de herramientas. Los identificadores reutilizados convierten esos intentos separados en una historia inventada.

Aplica hashes o redacta los valores que revelen datos de usuario. Por lo general necesitas el nombre de la herramienta, el host del endpoint, la clase de estado, la clase de error y la duración. Rara vez necesitas una cadena de consulta, un cuerpo de solicitud o una respuesta. El operador debe demostrar que se cruzó el límite, no reproducir datos del usuario a partir de los registros.

## Prueba los límites sin confiar en el cliente completo

Un cliente completo sirve para las pruebas de integración, pero es un primer testigo poco fiable. Su interfaz puede comprimir errores, guardar capacidades en caché, reiniciar procesos hijos y aplicar su propio timeout. Prueba cada límite por una ruta más estrecha antes de culpar al servidor o al cliente.

Empieza con el comando exacto, el entorno y el directorio de trabajo que usa el cliente. No sustituyas el comando configurado por `npm run dev`. No lo ejecutes desde la carpeta del proyecto si el cliente lo inicia desde otro lugar. Redirige stderr a un archivo para inspeccionarlo, pero deja stdout intacto si otro proceso hablará MCP a través de él.

Para un servidor stdio, usa MCP Inspector como primera prueba del protocolo. La documentación de MCP recomienda Inspector para probar servidores en distintos transportes, y el proyecto Inspector puede iniciar directamente un comando stdio. Permite observar el intercambio de inicialización e invocar `tools/list` sin adivinar qué hizo un cliente de escritorio con el resultado.

Después reduce la prueba a tres comprobaciones:

```text
1. ¿El comando configurado permanece activo el tiempo suficiente para recibir initialize?
2. ¿Devuelve una respuesta initialize válida y recibe initialized?
3. ¿tools/list devuelve los nombres y esquemas de herramientas esperados?
```

Solo después de superar esas comprobaciones debes llamar a una herramienta que alcance un sistema externo. Elige una acción de solo lectura con un objetivo inofensivo. Confirma que el registro de ejecución contiene un `external_attempt` y un resultado final. Si una llamada puede modificar datos, pruébala en un entorno desechable o proporciona una operación de simulación específica que no contacte con el endpoint de producción.

El repositorio oficial de MCP Inspector resulta útil porque hace visible el límite del transporte. No es un proxy de interceptación de red para el tráfico de tu servidor. Actúa como cliente MCP del servidor seleccionado y ofrece una interfaz de navegador para la prueba. La diferencia importa al investigar una corrupción del transporte: Inspector puede reproducir el lado cliente del protocolo, pero no puede demostrar qué escribió en la tubería otro cliente de producción.

Para transportes HTTP, añade pruebas HTTP sin confundirlas con el estado de MCP. Registra el método de la solicitud, la ruta del endpoint, el estado, el identificador de sesión cuando exista y si la respuesta contenía JSON o comenzaba un flujo de eventos. Una conexión TCP o un HTTP 200 no significan automáticamente que se completara la inicialización de MCP. Aplica los mismos registros del ciclo de vida después de que la solicitud HTTP llegue a tu servidor.

Mantén en tu conjunto de pruebas un servidor de prueba que falle deliberadamente en cada límite. Un dispositivo termina antes de leer la entrada. Otro escribe `hello` en stdout antes de responder. Un tercero responde a `initialize` y después devuelve un esquema de herramienta no válido. Un cuarto enumera una herramienta cuyo controlador registra un intento externo y devuelve un error controlado. Si tu integración de cliente convierte los cuatro casos en la misma alerta, corrige la integración antes de que un servidor real te obligue a depurar a ciegas.

## Los timeouts y los reintentos necesitan una fase responsable

Un único timeout de inicio fomenta la reparación equivocada. Hace que una descarga lenta de paquetes, un error del analizador de inicialización, una excepción del esquema y una espera de la API remota parezcan lo mismo. Usa plazos separados porque cada uno pertenece a un responsable diferente.

El iniciador es responsable del periodo entre `spawn_requested` y `spawned`. El servidor y el transporte lo son del periodo entre el inicio y `initialized_received`. La ruta de registro del servidor es responsable del descubrimiento. El controlador de la herramienta y su dependencia remota lo son de la ejecución. Nombra el timeout según su responsable y emite el último evento de fase confirmado.

```text
error=handshake_timeout last_event=initialize_received run_id=run_01JX
```

Eso permite actuar. Indica al responsable del servidor que revise la creación de la respuesta y stderr, no la API remota.

El reintento automático solo es seguro cuando la fase fallida no tiene efectos externos. Puede ser aceptable reintentar un proceso que no se creó porque el ejecutable no estaba disponible temporalmente. Reintentar una solicitud de descubrimiento suele ser aceptable si el descubrimiento es local y puro. Reintentar una llamada de herramienta después de `external_attempt` es peligroso, salvo que la operación remota tenga un mecanismo de idempotencia documentado y se le asigne un valor de idempotencia.

No ocultes el reintento detrás de un reinicio del servidor. Si el código de inicio renueva un token, crea un túnel, envía telemetría o valida una identidad remota, el reinicio ya es una acción externa. Es otra razón para mantener el inicio local y trasladar el trabajo remoto a herramientas explícitas.

Cuando un cliente termina un proceso hijo al vencer un plazo, emite un evento de interrupción antes de terminarlo si puedes. Es posible que el servidor no llegue a vaciarlo. El proceso padre debe registrar la solicitud de terminación como su propio evento, incluido el último evento del hijo que observó. Así queda un registro honesto: el proceso quizá estaba a punto de responder, pero no afirmas que lo hiciera.

## Haz que el inicio sea aburrido antes de hacerlo rápido

Un buen servidor MCP puede iniciarse sin red, sin usar credenciales, sin efectos secundarios mutables y sin ambigüedad sobre el estado de su protocolo. Carga la configuración local, instala su transporte, responde a la inicialización y anuncia una interfaz determinista. Ese comportamiento es más fácil de operar y más seguro de reintentar.

La recomendación errónea más popular es «verificarlo todo al iniciar». Parece responsable porque los errores aparecen pronto. En la práctica mezcla configuración local, identidad, disponibilidad remota y autorización en un único ritual opaco. También hace que los clientes reintenten acciones externas bajo la etiqueta de error de inicio.

Valida localmente todo lo que puedas. Informa de la accesibilidad remota mediante una herramienta explícita o mediante la primera operación que la necesite. Mantén estables los nombres de las fases. En stdio, reserva stdout exclusivamente para el protocolo. Prueba un servidor roto deliberadamente para cada límite que afirmes observar.

Cuando el próximo cliente diga que un servidor MCP no pudo iniciarse, debes poder responder cuatro preguntas a partir de un único registro de ejecución: si el proceso se inició, si terminó la inicialización, si las herramientas eran descubribles y si algo llegó al mundo exterior. Si no puedes responder las cuatro, el estado sigue siendo ambiguo.
