Cómo depurar un servidor MCP: errores comunes y cómo encontrarlos

Depurar un servidor MCP por síntomas: tools que no aparecen, stdio roto por un print, error JSON-RPC o isError, códigos, versión del protocolo e Inspector.

Por

Datos a 7 de octubre de 2026 · especificación MCP 2026-07-28

Depurar un servidor MCP es más sencillo cuando sabes en qué capa mirar: la configuración del cliente, el transporte, el protocolo o el texto que acaba leyendo el modelo. Esta guía va por los síntomas habituales con la especificación 2026-07-28: que el cliente no vea tus tools, que un print rompa el servidor por stdio, que no sepas si un fallo es de protocolo o de ejecución, o que el agente no acierte a usar una tool que funciona. Para cada uno verás la causa probable y cómo confirmarla con el MCP Inspector, los logs a stderr o un test.

Los ejemplos usan nortia-pedidos (TypeScript) y nortia-almacen (Python), los servidores de Nortia Logística, la empresa ficticia del curso de DesarrolloMCP. Los tutoriales de TypeScript y de Python los montan paso a paso, y el glosario de MCP define cada término en una frase.

¿Por dónde empiezo cuando algo no funciona?

Separa primero el servidor del cliente: lanza el servidor con el Inspector por línea de comandos y pide tools/list. Si aparecen tus tools, el fallo está en cómo lo configura o lo lanza el cliente. Si ni el Inspector consigue la lista, el problema es del servidor o de cómo lo lanzas.

El curso ejecutó esta configuración y este comando contra nortia-pedidos:

{ "mcpServers": {
  "nortia-pedidos": {
    "command": "npx", "args": ["tsx", "src/stdio.ts"], "env": { "NORTIA_INQUILINO": "es" }
  }
} }
export MCP_CLIENT_CONFIG_PATH="$PWD/.inspector/mcp.json"

npx -y @modelcontextprotocol/inspector@2.9.0 --cli --config .inspector/config.json \
  --server nortia-pedidos --protocol-era modern \
  --method tools/call --tool-name buscar_pedido --tool-args-json '{"pedido_id":"PED-2026-999999"}'
echo "exit=$?"

Fuente: lección 09 del curso, .inspector/config.json y bloque «Terminal».

El Inspector (@modelcontextprotocol/inspector, versión 2.9.0 del 30 de septiembre de 2026) necesita Node 22.19.0 o posterior. Trae tres interfaces: web, de terminal (--tui) y de línea de comandos (--cli), esta última pensada para scripts y CI. En el curso solo se ha ejecutado la CLI sobre stdio. Estos tres detalles te ahorran tiempo:

  • El código de salida sigue a isError. En la lección, un resultado con isError: true salió con código 5, y uno correcto, con 0.
  • --tool-arg clave=valor adivina tipos. Un identificador que parece un número llega al servidor como número y la validación lo rechaza. Con --tool-args-json mandas exactamente el JSON que quieres.
  • Sin la era modern estás probando la versión antigua. Lo verás más abajo. La forma documentada es el campo "protocolEra": "modern" en la entrada del servidor de config.json. Con la 2.9.0, la opción --protocol-era junto a --config funcionó.

¿Por qué un print o un console.log rompe mi servidor por stdio?

Porque en stdio la salida estándar es el cable del protocolo. La especificación lo dice sin matices: el servidor no puede escribir en stdout nada que no sea un mensaje MCP válido. Cada mensaje ocupa una línea sin saltos internos, y los logs pueden ir a stderr. Un print("depurando") en Python o un console.log en Node meten en ese cable una línea que no es JSON-RPC, y el cliente deja de entender al servidor.

Lo que despista es que el mismo código sigue funcionando por Streamable HTTP, porque allí ese texto acaba en la consola del proceso. Si el fallo solo aparece por stdio, busca escrituras a la salida estándar.

La solución es mandar todo diagnóstico a stderr. En Python, logging escribe ahí por defecto:

# logging escribe en stderr; en stdio, stdout queda reservado para los mensajes MCP
logging.basicConfig(level=logging.INFO, format="%(name)s %(message)s")
auditoria = logging.getLogger("nortia.auditoria")

Fuente: lección 10, fragmento de servidor.py (tres líneas consecutivas).

En TypeScript, usa console.error en lugar de console.log, también para el mensaje de arranque:

import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { dependencias } from './entorno.js';
import { crearServidor } from './servidor.js';

const deps = dependencias('stdio'); // falla al arrancar si la configuración es inválida
const handle = serveStdio(() => crearServidor(deps));
console.error(`nortia-pedidos (${deps.inquilino}) escuchando por stdio`);

process.on('SIGINT', () => void handle.close());

Fuente: lección 09, src/stdio.ts.

La utilidad de logging del protocolo está deprecada en 2026-07-28. La especificación propone justo esto: logs a stderr en stdio y OpenTelemetry para la observabilidad.

¿Es un error JSON-RPC o un resultado con isError?

MCP tiene dos canales de error, y conviene no confundirlos.

Un error de protocolo sustituye al result: la respuesta lleva un objeto error con code, message y, a veces, data. Significa que la petición no se pudo procesar: método desconocido, parámetros inválidos, versión no soportada, tool que no existe. La especificación trae este ejemplo:

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32602,
    "message": "Unknown tool: invalid_tool_name"
  }
}

Fuente: especificación 2026-07-28, página de tools (ejemplo literal recogido en la referencia del curso).

Un error de ejecución es un resultado normal con isError: true. Es lo que toca cuando una API interna está caída, cuando unos argumentos no pasan la validación o cuando una regla de negocio impide la operación:

{
  "jsonrpc": "2.0",
  "id": 60,
  "result": {
    "resultType": "complete",
    "content": [
      {
        "type": "text",
        "text": "No se puede emitir el abono: la factura F-2026-0142 ya tiene un abono por el importe total. Consulta la factura con buscar_factura antes de reintentar."
      }
    ],
    "isError": true
  }
}

Fuente: lección 03, error de ejecución de emitir_abono en nortia-facturacion.

La diferencia está en quién lo lee. Según la especificación, los clientes deberían pasar los errores de ejecución al modelo para que se corrija. Los de protocolo pueden pasarlos o no. Un fallo de negocio enviado como error JSON-RPC puede dejar al modelo sin explicación.

Así lo reflejan los SDK:

  • TypeScript (SDK v2): si los argumentos no cumplen el inputSchema, el SDK devuelve isError: true con un texto que empieza por Input validation error, y el handler ni se ejecuta. Si el structuredContent no cumple el outputSchema, el texto empieza por Output validation error. Una tool que no existe da -32602 y callTool rechaza con ProtocolError.
  • Python (mcp 2.x): un ToolError llega al modelo como Error executing tool <nombre>: <mensaje>. Una excepción no prevista llega solo como Error executing tool <nombre>, sin detalle: la traza queda en el log del servidor. Si lanzas MCPError dentro de una tool, sale un error JSON-RPC que el modelo no ve.

Resources y prompts no tienen isError: ahí cualquier fallo es de protocolo. Un resource que no existe devuelve -32602 con la URI en data.

¿Qué significa cada código de error?

Los que verás con más frecuencia:

Código Qué suele indicar
-32700 / -32600 JSON que no se puede leer / petición JSON-RPC inválida
-32601 Método desconocido o protegido por una capacidad que el servidor no anuncia: por ejemplo, prompts/list contra un servidor que solo declara tools. Por HTTP, un método no implementado llega con 404
-32602 Parámetros inválidos. También tool desconocida, resource inexistente y petición a la que le falta una clave obligatoria de _meta
-32603 Error interno del servidor
-32020 HeaderMismatch: por Streamable HTTP, una cabecera (MCP-Protocol-Version, Mcp-Method, Mcp-Name) no coincide con el cuerpo. Status 400
-32021 MissingRequiredClientCapability: la petición necesita una capacidad que el cliente no ha declarado. La que falta viene en data.requiredCapabilities. Status 400
-32022 UnsupportedProtocolVersion: el servidor no implementa la versión pedida. data.supported y data.requested dicen cuáles hay. Status 400

Dos pistas para interpretarlos:

  • Si pruebas a mano con curl y ves un -32602, revisa primero el _meta. En 2026-07-28 cada petición lleva su versión y las capacidades del cliente. El SDK de TypeScript, ante un tools/list sin clientCapabilities, responde -32602 con un message que nombra la clave que falta.
  • El material de la release candidate puede traer códigos viejos. Los tres códigos propios de MCP se renumeraron respecto al borrador de esta misma versión (-32001, -32003 y -32004 pasaron a -32020, -32021 y -32022). Si un ejemplo usa los primeros, está desactualizado.

Si defines códigos propios, ten en cuenta dos rangos. De -32020 a -32099 está reservado a la especificación. Los códigos de tu aplicación van fuera del rango reservado de JSON-RPC (-32768 a -32000). La lección 3 del curso, Cómo viaja una petición, lo explica con más contexto.

¿Qué versión del protocolo estoy hablando?

Es el error más silencioso: todo funciona, pero no con la versión que crees. La 2026-07-28 no tiene initialize ni sesiones, y un servidor puede atender también a clientes de versiones con handshake. Así que, si el cliente o el servidor cae a la versión anterior, las llamadas siguen saliendo bien.

En el Inspector, la opción protocolEra acepta legacy, auto o modern, y legacy es el valor por defecto. Su documentación lo justifica así: «A debugging tool must not auto-probe». auto pregunta primero con server/discover y, si falla, recurre a initialize. modern exige exactamente 2026-07-28, «so a non-modern server fails loudly».

La señal de que se negoció la versión actual es que el resultado trae io.modelcontextprotocol/serverInfo dentro de _meta. En la lección 09, al repetir la llamada sin --protocol-era, el resultado llegó sin _meta. Un CI que llama al Inspector sin esa opción pasa en verde aunque el servidor no hable 2026-07-28.

Contra un servidor que solo habla la versión anterior, modern falla así (código de salida 1):

{"error":{"code":"error","message":"Version negotiation failed: the server did not offer pinned protocol version 2026-07-28 via server/discover (no fallback in pin mode)"}}

Fuente: referencia del SDK de TypeScript del curso, ejemplo 8 (Inspector 2.9.0 contra un servidor servido con StdioServerTransport).

Las causas habituales, según el lado:

  • Servidor TypeScript: instalar el SDK v2 no basta. Con server.connect(new StdioServerTransport()), que es el ejemplo del README del SDK, el servidor solo habla 2025-11-25. Para hablar 2026-07-28, sírvelo con serveStdio(fábrica) por stdio o con createMcpHandler(fábrica) por HTTP.
  • Cliente TypeScript: sin versionNegotiation: { mode: 'auto' } negocia 2025-11-25 aunque el servidor ofrezca la actual.
  • Cliente Python: Client(destino) ya sondea con server/discover por defecto y solo recurre al handshake si el servidor no lo entiende.

Si el cliente es de una versión anterior y el servidor solo atiende 2026-07-28, la especificación prevé que falle: por stdio responde con un error JSON-RPC de código no definido, y por HTTP, con 400 Bad Request, porque faltan cabeceras.

¿Por qué Claude Code no muestra mi servidor?

Antes de tocar el servidor, mira qué dice Claude Code de él:

claude mcp list          # List all configured servers
claude mcp get notion    # Get details for a specific server
claude mcp remove notion # Remove a server
/mcp                     # (within Claude Code) Check server status

Fuente: documentación de Claude Code, sección «Managing your servers» (literal en la investigación del curso).

claude mcp list marca cada servidor como ✔ Connected, ! Needs authentication o ✘ Failed to connect. Desde la versión 2.1.219, este último estado incluye el detalle del error. Dentro de una sesión, /mcp enseña el estado y el número de tools de cada servidor. También permite reconectarlo o desactivarlo sin borrar su configuración.

Las causas que más se repiten:

  • Falta type en .mcp.json. Claude Code interpreta como stdio una entrada con url y sin type, y la omite. Cursor, en cambio, acepta solo la url, así que la misma entrada funciona en uno y no en otro, sin que llegue a salir ninguna petición.
  • El servidor del proyecto está pendiente de aprobación. En sesiones interactivas, Claude Code pide aprobación antes de usar los servidores de un .mcp.json; mientras tanto aparecen como ⏸ Pending approval. claude mcp reset-project-choices olvida lo aprobado.
  • Hay otra entrada con el mismo nombre. Si un nombre está en varios ámbitos, gana local, luego project, luego user, y los campos no se mezclan. Editar .mcp.json no cambia nada si tienes una entrada local con ese nombre.
  • El comando stdio está mal partido. En claude mcp add, el -- separa las opciones de Claude Code del comando del servidor. Y si el nombre del servidor va justo detrás de --env, la CLI lo toma por otra variable y lo rechaza. Pon otra opción, como --transport stdio, entre ambos.
  • Rutas relativas. La documentación no fija el directorio de trabajo. Para no depender de él, usa rutas absolutas o CLAUDE_PROJECT_DIR, la única variable que la documentación garantiza en el entorno del servidor stdio.

Así queda una entrada remota bien escrita:

{
  "mcpServers": {
    "nortia-soporte": {
      "type": "http",
      "url": "https://mcp.nortia.example/soporte/mcp"
    }
  }
}

Fuente: lección 12, configuración de Claude Code (.mcp.json).

Para el resto de la configuración en Claude Desktop, claude.ai y Claude Code, con sus errores típicos, tienes la guía conectar un servidor MCP a Claude.

¿Por qué la tool funciona pero el modelo no la usa bien?

Aquí el código no tiene ningún fallo. Lo que falla es lo que ve el modelo: de tu tool solo conoce el nombre, la description y el esquema de entrada que el host le pasa tras tools/list. Si la description no explica cuándo usar la tool ni qué límites tiene, el modelo lo adivina. Revisa la definición con el Inspector (--method tools/list), que muestra exactamente lo que recibe el cliente.

El segundo sitio donde mirar es el texto de los errores de ejecución, porque el modelo lo lee para corregirse. Con un texto genérico como «Error», no tiene nada a lo que agarrarse. Con uno que explica qué ha fallado y cuál es el siguiente paso, sí.

En nortia-pedidos, el patrón del pedido_id produce un error de validación que indica el formato esperado. El pedido inexistente responde pidiendo que se confirme el identificador con el usuario en lugar de probar otros. No pongas trazas ni nombres de sistemas internos en ese texto, porque todo lo que va en content entra en el contexto del modelo.

Medir si un modelo elige la tool correcta y rellena bien sus argumentos ya es evaluación, uno de los temas de la lección de Testing y depuración del nivel Avanzado.

¿Cómo fijo un fallo para que no vuelva?

Con un test que pase por el protocolo. En Python, el SDK oficial conecta el Client directamente con el objeto servidor, en el mismo proceso: sin subproceso, sin puerto y sin red, pero con validación, serialización y errores reales.

@pytest.fixture
async def client():
    async with Client(servidor.mcp, raise_exceptions=True) as c:  # en memoria: sin proceso ni red
        yield c
@pytest.mark.anyio
async def test_resource_inexistente_es_error_de_protocolo(client: Client):
    with pytest.raises(MCPError) as exc:
        await client.read_resource("almacen://MEX-01/existencias")
    assert exc.value.error.code == -32602

Fuente: lección 10, fragmentos de test_servidor.py (la fixture y uno de los cinco tests, sin cambios).

raise_exceptions=True hace que un fallo fuera del cuerpo de una tool llegue al test con su mensaje real, sin alterar los resultados con is_error=True. Como el test y el servidor comparten proceso, monkeypatch puede simular que falla el acceso a datos. Por stdio no funcionaría, porque el servidor sería otro proceso.

En TypeScript hay una trampa: InMemoryTransport siempre negocia 2025-11-25. Para probar 2026-07-28 en el mismo proceso, el curso pasa handler.fetch como fetch del transporte HTTP del cliente. Así no se abre ningún socket.

¿Por dónde sigo?

Si lo que te preocupa no es que el servidor falle, sino que alguien lo haga fallar a propósito, eso es otro tema: seguridad en servidores MCP.

En el curso, las lecciones 9 y 10 construyen nortia-pedidos en TypeScript y nortia-almacen en Python con el Inspector y los tests de este artículo. Después, Testing y depuración, del nivel Avanzado, reparte las pruebas en capas, depura con el Inspector una tool que el modelo no consigue usar y lo lleva todo a integración continua.

Aprende esto en el curso

La lección 1 la puedes leer ya, sin registro. Las otras 13 del Básico son gratis con una cuenta.

Empieza gratis

Fuentes

  1. Especificación MCP 2026-07-28: transportes
  2. Especificación MCP 2026-07-28: tools
  3. Especificación MCP 2026-07-28: versionado
  4. MCP Inspector (documentación 2026-07-28)
  5. MCP Inspector: eras del protocolo
  6. MCP Inspector: releases (2026-09-30)
  7. Claude Code: MCP
  8. Claude Code: changelog (2026-10-06)

Artículos relacionados