Módulo 7: Testing, Debugging e Integración
Debugging: Herramientas para MCP Servers
Debugging: Herramientas para MCP Servers
Descripción de la cápsula
Los tests te dicen qué está roto. Las herramientas de debugging te dicen por qué. Un test que falla con "expected 3, received 0" te dice que algo salió mal, pero no te dice dónde exactamente en tu cadena de código se perdieron esos 3 resultados. Para eso necesitas herramientas de debugging.
En el mundo MCP, debugging tiene desafíos únicos. Tu server se comunica via JSON-RPC sobre stdio — no puedes simplemente poner un console.log porque eso rompe el protocolo. Las herramientas estándar de debugging de Node.js o Python no entienden el protocolo MCP. Y cuando Claude Code te dice "Error calling tool," no te da suficiente información para diagnosticar el problema.
Esta cápsula te enseña tres herramientas que resuelven estos problemas: MCP Inspector para debugging visual e interactivo, logging para registrar qué pasa dentro de tu server sin romper stdio, y tracing para seguir un request a través de todo el flujo.
MCP Inspector: tu herramienta de debugging principal
Qué es MCP Inspector
MCP Inspector es una herramienta visual que se conecta a tu MCP server y te permite interactuar con él directamente — invocar tools, leer resources, listar capabilities — todo desde una interfaz web en tu navegador. Es el equivalente a Postman para APIs REST, pero para MCP servers.
Ya usaste MCP Inspector en módulos anteriores para probar tus servers. Ahora vas a usarlo como herramienta de debugging, no solo de testing.
Cómo ejecutar MCP Inspector
# Para un server TypeScript
npx @modelcontextprotocol/inspector node dist/index.js
# Para un server Python
npx @modelcontextprotocol/inspector python server.py
# Con argumentos adicionales
npx @modelcontextprotocol/inspector node dist/index.js -- --config ./config.json
# En un puerto específico
CLIENT_PORT=8080 SERVER_PORT=9000 npx @modelcontextprotocol/inspector node dist/index.js
MCP Inspector abre una interfaz web (por defecto en http://localhost:6274) con tres paneles principales.
Panel de Tools
┌─────────────────────────────────────────────────────────┐
│ MCP Inspector │
├──────────────┬──────────────────────────────────────────┤
│ │ │
│ Tools │ Tool: read_file │
│ ────────── │ ────────────── │
│ > read_file │ Parámetros: │
│ > list_files│ ┌──────────────────────────────────┐ │
│ │ │ filePath: [/tmp/test.txt ] │ │
│ Resources │ └──────────────────────────────────┘ │
│ ────────── │ │
│ > status:// │ [Ejecutar Tool] │
│ │ │
│ │ Resultado: │
│ │ ┌──────────────────────────────────┐ │
│ │ │ { │ │
│ │ │ "type": "text", │ │
│ │ │ "text": "Hello, MCP!" │ │
│ │ │ } │ │
│ │ └──────────────────────────────────┘ │
└──────────────┴──────────────────────────────────────────┘
En este panel puedes:
- Ver todos los tools registrados y sus schemas
- Llenar los parámetros con valores de prueba
- Ejecutar el tool y ver el resultado completo
- Ver si el resultado incluye
isError: true - Verificar el formato del output (text, JSON, etc.)
Panel de Resources
Similar al de tools, pero para resources:
- Listar todos los resources disponibles
- Leer cada resource y ver su contenido
- Verificar el
mimeTypey formato de datos - Probar resource templates con diferentes parámetros
Panel de mensajes JSON-RPC
Este es el panel más útil para debugging. Muestra los mensajes JSON-RPC crudos entre el Inspector (client) y tu server:
┌─────────────────────────────────────────────┐
│ Messages │
├─────────────────────────────────────────────┤
│ → Request: tools/call │
│ { │
│ "method": "tools/call", │
│ "params": { │
│ "name": "read_file", │
│ "arguments": { │
│ "filePath": "/tmp/test.txt" │
│ } │
│ } │
│ } │
│ │
│ ← Response: │
│ { │
│ "content": [{ │
│ "type": "text", │
│ "text": "Hello, MCP!" │
│ }] │
│ } │
└─────────────────────────────────────────────┘
Este panel te muestra exactamente qué envió el client y qué respondió tu server. Si algo falla, aquí ves el error exacto.
Flujo de debugging con MCP Inspector
Cuando algo no funciona, sigue este proceso:
1. Conecta tu server a MCP Inspector
npx @modelcontextprotocol/inspector node dist/index.js
2. Verifica capabilities
¿Aparecen todos tus tools y resources?
Si no → el server no los registra correctamente
3. Invoca el tool problemático
¿Retorna el resultado esperado?
Si no → revisa el handler del tool
4. Revisa los mensajes JSON-RPC
¿El request tiene los parámetros correctos?
¿El response tiene el formato correcto?
5. Prueba con inputs edge case
Strings vacíos, números negativos, caracteres especiales
¿Tu server los maneja sin crash?
Logging en MCP servers
El problema: stdio vs logging
En un MCP server que usa stdio transport, stdout está reservado para el protocolo MCP. Cada byte que envías a stdout debe ser un mensaje JSON-RPC válido. Si pones un console.log("debug: procesando archivo"), ese texto se mezcla con los mensajes del protocolo y causa un parse error.
// ❌ ESTO ROMPE EL PROTOCOLO STDIO
console.log("Procesando request...");
// El client recibe:
// Procesando request...
// {"jsonrpc":"2.0","result":...}
// ^ Error: "Procesando request..." no es JSON-RPC válido
Solución: loguear a stderr
La solución estándar es enviar logs a stderr, no a stdout. stderr no interfiere con el protocolo MCP:
// ✅ CORRECTO: loguear a stderr
console.error("[INFO] Procesando request...");
console.error("[DEBUG] filePath:", filePath);
console.error("[ERROR] Archivo no encontrado:", path);
// stdout sigue limpio para JSON-RPC
// stderr muestra tus logs
Implementar un logger para MCP servers (TypeScript)
// src/logger.ts
type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR";
const LOG_LEVELS: Record<LogLevel, number> = {
DEBUG: 0,
INFO: 1,
WARN: 2,
ERROR: 3,
};
const currentLevel: LogLevel = (process.env.LOG_LEVEL as LogLevel) || "INFO";
function shouldLog(level: LogLevel): boolean {
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
}
export const logger = {
debug: (msg: string, data?: unknown) => {
if (shouldLog("DEBUG")) {
console.error(`[DEBUG] ${new Date().toISOString()} ${msg}`, data ?? "");
}
},
info: (msg: string, data?: unknown) => {
if (shouldLog("INFO")) {
console.error(`[INFO] ${new Date().toISOString()} ${msg}`, data ?? "");
}
},
warn: (msg: string, data?: unknown) => {
if (shouldLog("WARN")) {
console.error(`[WARN] ${new Date().toISOString()} ${msg}`, data ?? "");
}
},
error: (msg: string, data?: unknown) => {
if (shouldLog("ERROR")) {
console.error(`[ERROR] ${new Date().toISOString()} ${msg}`, data ?? "");
}
},
};
Usar el logger en tu MCP server
import { logger } from "./logger.js";
server.tool(
"read_file",
"Lee el contenido de un archivo",
{ filePath: z.string().describe("Ruta al archivo") },
async ({ filePath }) => {
logger.info("read_file invocado", { filePath });
try {
const content = await fs.readFile(filePath, "utf-8");
logger.debug("Archivo leído", { size: content.length });
return { content: [{ type: "text" as const, text: content }] };
} catch (error) {
logger.error("Error leyendo archivo", { filePath, error: (error as Error).message });
return {
content: [{ type: "text" as const, text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
Ver los logs
Cuando ejecutas tu server con MCP Inspector, los logs de stderr aparecen en la terminal donde ejecutaste el comando:
# Terminal donde ejecutas el server
$ npx @modelcontextprotocol/inspector node dist/index.js
# Output en stderr (tus logs):
[INFO] 2026-03-13T10:30:00.000Z read_file invocado { filePath: '/tmp/test.txt' }
[DEBUG] 2026-03-13T10:30:00.015Z Archivo leído { size: 42 }
[INFO] 2026-03-13T10:30:05.000Z list_files invocado { directory: '/tmp' }
[ERROR] 2026-03-13T10:30:10.000Z Error leyendo archivo { filePath: '/no/existe', error: 'ENOENT' }
Implementar logging en Python
# logger.py
import sys
import logging
from datetime import datetime
logger = logging.getLogger("mcp-server")
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter(
"[%(levelname)s] %(asctime)s %(message)s",
datefmt="%Y-%m-%dT%H:%M:%S"
))
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
# server.py
from logger import logger
@mcp.tool()
async def read_file(file_path: str) -> str:
"""Lee el contenido de un archivo."""
logger.info(f"read_file invocado: {file_path}")
try:
with open(file_path, "r") as f:
content = f.read()
logger.debug(f"Archivo leído: {len(content)} bytes")
return content
except FileNotFoundError:
logger.error(f"Archivo no encontrado: {file_path}")
raise ValueError(f"Archivo no encontrado: {file_path}")
Qué loguear (y qué no)
| Loguear | No loguear |
|---|---|
| Cada invocación de tool (nombre + params) | Contenido completo de archivos grandes |
| Errores con contexto | Datos sensibles (passwords, tokens) |
| Tiempos de ejecución de operaciones lentas | Cada línea de código ejecutada |
| Conexión/desconexión del client | Mensajes JSON-RPC completos (usa Inspector) |
| Resultados resumidos (count, size) | Stack traces completos en producción |
Tracing de requests
Medir tiempos de respuesta
Cuando un tool es lento, necesitas saber dónde se gasta el tiempo. Implementa tracing con timestamps:
server.tool(
"search_files",
"Busca archivos por patrón",
{ pattern: z.string(), directory: z.string() },
async ({ pattern, directory }) => {
const start = Date.now();
logger.info("search_files inicio", { pattern, directory });
const readDirStart = Date.now();
const entries = await fs.readdir(directory, { recursive: true, withFileTypes: true });
logger.debug(`readdir completado en ${Date.now() - readDirStart}ms`, { entries: entries.length });
const filterStart = Date.now();
const matches = entries
.filter((e) => e.isFile() && e.name.includes(pattern))
.map((e) => path.join(e.parentPath || e.path, e.name));
logger.debug(`filter completado en ${Date.now() - filterStart}ms`, { matches: matches.length });
const total = Date.now() - start;
logger.info(`search_files completado en ${total}ms`, { matches: matches.length });
return {
content: [{ type: "text" as const, text: JSON.stringify({ matches, count: matches.length, timeMs: total }, null, 2) }],
};
}
);
Output de tracing
[INFO] 2026-03-13T10:30:00.000Z search_files inicio { pattern: '.ts', directory: '/proyecto' }
[DEBUG] 2026-03-13T10:30:00.250Z readdir completado en 250ms { entries: 1500 }
[DEBUG] 2026-03-13T10:30:00.255Z filter completado en 5ms { matches: 87 }
[INFO] 2026-03-13T10:30:00.256Z search_files completado en 256ms { matches: 87 }
Con este output, sabes exactamente que el 98% del tiempo se gasta en readdir, no en el filtrado. Eso te dice dónde optimizar.
Request ID para tracing distribuido
Cuando debuggeas problemas de conexión, es útil asignar un ID único a cada request:
import crypto from "crypto";
function withTracing(handler: Function) {
return async (...args: unknown[]) => {
const requestId = crypto.randomUUID().slice(0, 8);
logger.info(`[${requestId}] Request inicio`);
try {
const result = await handler(...args);
logger.info(`[${requestId}] Request completado`);
return result;
} catch (error) {
logger.error(`[${requestId}] Request falló`, { error: (error as Error).message });
throw error;
}
};
}
Debugging de problemas comunes
Problema 1: "Tool no aparece en MCP Inspector"
Síntomas: Ejecutas MCP Inspector, pero tu tool no está en la lista.
Diagnóstico:
# 1. Verifica que el server compila sin errores
npx tsc --noEmit
# 2. Verifica que el build está actualizado
npm run build
# 3. Ejecuta el server standalone y verifica stderr
node dist/index.js 2>&1 | head -5
Causas comunes:
- El build no está actualizado (olvidaste
npm run build) - Error de sintaxis que impide el registro del tool
- El tool se registra condicionalmente y la condición falla
Problema 2: "El tool retorna resultado vacío"
Síntomas: MCP Inspector muestra un resultado pero content está vacío o text es vacío.
Diagnóstico: Agrega logging en el handler del tool:
async ({ filePath }) => {
logger.debug("Handler ejecutado", { filePath });
const content = await fs.readFile(filePath, "utf-8");
logger.debug("Contenido leído", { length: content.length, preview: content.slice(0, 50) });
// ... return
}
Causas comunes:
- La variable tiene el valor pero el return no la incluye
JSON.stringifyde un objeto con propiedadesundefined- Path incorrecto al archivo
Problema 3: "El server se congela al recibir un request"
Síntomas: Envías un request y nunca recibes respuesta.
Diagnóstico:
// Agrega timeout a operaciones async
const withTimeout = <T>(promise: Promise<T>, ms: number): Promise<T> => {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
return Promise.race([promise, timeout]);
};
// Uso
const content = await withTimeout(fs.readFile(filePath, "utf-8"), 5000);
Causas comunes:
- Operación de I/O que nunca resuelve (archivo en filesystem de red)
- Deadlock en código async
- Await de una Promise que nunca se resuelve
Ejercicios
Ejercicio 1: Agregar logging a un tool (Fácil)
Toma uno de los tools de tu MCP server (de módulos 4 o 5) y agrega logging con el patrón de stderr. Logguea: invocación con parámetros, resultado exitoso (resumido), y errores con contexto.
Ver solución
server.tool(
"count_lines",
"Cuenta las líneas de un archivo",
{ filePath: z.string() },
async ({ filePath }) => {
console.error(`[INFO] count_lines invocado: ${filePath}`);
try {
const content = await fs.readFile(filePath, "utf-8");
const lines = content.split("\n").length;
console.error(`[INFO] count_lines resultado: ${lines} líneas`);
return { content: [{ type: "text" as const, text: `${lines} líneas` }] };
} catch (error) {
console.error(`[ERROR] count_lines falló: ${(error as Error).message}`);
return {
content: [{ type: "text" as const, text: `Error: ${(error as Error).message}` }],
isError: true,
};
}
}
);
Ejercicio 2: Implementar el logger configurable (Fácil)
Implementa el módulo logger.ts mostrado en esta cápsula, y configúralo para que use LOG_LEVEL=DEBUG en desarrollo y LOG_LEVEL=WARN en producción. Verifica que funciona ejecutando tu server con diferentes niveles.
Ver solución
# Desarrollo — ver todos los logs
LOG_LEVEL=DEBUG npx @modelcontextprotocol/inspector node dist/index.js
# Producción — solo warnings y errores
LOG_LEVEL=WARN node dist/index.js
Verifica que con LOG_LEVEL=WARN, los mensajes de logger.debug() y logger.info() no aparecen en stderr.
Ejercicio 3: Debugging con MCP Inspector (Medio)
Introduce un bug intencional en uno de tus tools (e.g., cambia el nombre de un parámetro en el schema pero no en el handler). Usa MCP Inspector para diagnosticar el problema siguiendo el flujo de debugging de 5 pasos descrito en esta cápsula.
Ver solución
// Bug intencional: schema dice "filePath", handler usa "path"
server.tool(
"read_file",
"Lee un archivo",
{ filePath: z.string() },
async (args) => {
// Bug: args.filePath tiene el valor, pero accedemos a args.path (undefined)
const content = await fs.readFile((args as any).path, "utf-8");
return { content: [{ type: "text" as const, text: content }] };
}
);
// En MCP Inspector:
// 1. El tool aparece ✅
// 2. Llenas filePath con una ruta válida ✅
// 3. Ejecutas → Error: "The argument 'path' must be a string" ❌
// 4. Revisas JSON-RPC: el request tiene filePath, pero el error dice "path"
// 5. Diagnóstico: mismatch entre schema y handler
Ejercicio 4: Tracing de performance (Medio)
Agrega tracing de tiempos a un tool que haga I/O (leer archivos, llamar API). Ejecuta el tool 5 veces y reporta el tiempo promedio. Identifica qué operación consume más tiempo.
Ver solución
server.tool(
"analyze_directory",
"Analiza un directorio",
{ directory: z.string() },
async ({ directory }) => {
const timings: Record<string, number> = {};
const totalStart = Date.now();
let start = Date.now();
const entries = await fs.readdir(directory, { withFileTypes: true });
timings.readdir = Date.now() - start;
start = Date.now();
const stats = await Promise.all(
entries.filter(e => e.isFile()).map(async (e) => {
const stat = await fs.stat(path.join(directory, e.name));
return { name: e.name, size: stat.size };
})
);
timings.stats = Date.now() - start;
timings.total = Date.now() - totalStart;
console.error(`[TRACE] analyze_directory timings:`, JSON.stringify(timings));
return {
content: [{ type: "text" as const, text: JSON.stringify({ files: stats.length, timings }, null, 2) }],
};
}
);
Ejercicio 5: Logger con output a archivo (Difícil)
Extiende el logger para que, además de escribir a stderr, escriba a un archivo de log rotativo. Configura la ruta del archivo via variable de entorno LOG_FILE.
Ver solución
import fs from "fs";
import path from "path";
const logFile = process.env.LOG_FILE;
let logStream: fs.WriteStream | null = null;
if (logFile) {
const dir = path.dirname(logFile);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
logStream = fs.createWriteStream(logFile, { flags: "a" });
}
function writeLog(level: string, msg: string, data?: unknown) {
const line = `[${level}] ${new Date().toISOString()} ${msg} ${data ? JSON.stringify(data) : ""}`;
console.error(line);
if (logStream) {
logStream.write(line + "\n");
}
}
export const logger = {
debug: (msg: string, data?: unknown) => writeLog("DEBUG", msg, data),
info: (msg: string, data?: unknown) => writeLog("INFO", msg, data),
warn: (msg: string, data?: unknown) => writeLog("WARN", msg, data),
error: (msg: string, data?: unknown) => writeLog("ERROR", msg, data),
};
# Uso
LOG_FILE=./logs/mcp-server.log LOG_LEVEL=DEBUG node dist/index.js
Troubleshooting rápido
"MCP Inspector no conecta"
Verifica que el path al ejecutable es correcto y que el server arranca sin errores:
# Prueba que el server arranca
node dist/index.js < /dev/null
# Si hay errores, los verás en stderr
"Los logs no aparecen"
Verifica que usas console.error (stderr), no console.log (stdout). En MCP Inspector, los logs de stderr aparecen en la terminal, no en la UI.
"El server crashea al conectar"
Asegúrate de que npm run build está actualizado. Un mismatch entre tu código fuente y el build compilado causa errores silenciosos.
Resumen
En esta cápsula aprendiste:
- MCP Inspector es tu herramienta principal de debugging — muestra tools, resources, y mensajes JSON-RPC crudos
- Logging a stderr es obligatorio en MCP servers con stdio transport —
console.logrompe el protocolo - Un logger configurable con niveles (DEBUG/INFO/WARN/ERROR) te permite controlar la verbosidad
- Tracing con timestamps te muestra dónde se gasta el tiempo en operaciones lentas
- Request IDs facilitan seguir un request específico a través de los logs
- El flujo de debugging es: conectar Inspector → verificar capabilities → invocar tool → revisar JSON-RPC → probar edge cases
- Los 3 problemas más comunes (tool invisible, resultado vacío, server congelado) tienen diagnósticos y soluciones específicas
La combinación de MCP Inspector (visual) + logging (automático) + tracing (performance) te da visibilidad completa sobre qué hace tu server.
Recursos adicionales
- MCP Inspector — Herramienta oficial de debugging visual
- Node.js console.error — Escribir a stderr en Node.js
- Python logging module — Logging estándar de Python
- JSON-RPC 2.0 Specification — El protocolo subyacente de MCP
- Winston Logger — Logger avanzado para Node.js (alternativa)
- MCP Specification — Error Handling — Cómo MCP maneja errores
Siguiente cápsula: Configurar Claude Code — conectar tu MCP server a Claude Code con settings, permisos, y verificación paso a paso.