Un servidor MCP puede aparecer conectado en un host y aun así publicar el schema equivocado, fallar en el transporte o devolver un resultado que el cliente no puede interpretar. La prueba que importa no consiste solo en abrir Inspector. Consiste en iniciar el proceso como lo haría el host y comprobar el contrato de principio a fin.
Este tutorial construye una suite pequeña para un servidor MCP en TypeScript. Descubre herramientas, comprueba el schema, ejecuta una llamada válida y cubre una entrada inválida mediante el mismo transporte stdio que usan las integraciones locales. Al terminar, tendrás un comando que falla en el CI cuando cambia la superficie del servidor sin actualizar la prueba.

Resultado del tutorial
- Un servidor MCP mínimo con una herramienta de solo lectura.
- Un cliente TypeScript que prueba descubrimiento, llamada válida y entrada inválida.
- Verificación manual en MCP Inspector para depurar lo que encontró la prueba automatizada.
- Una rutina de CI que separa errores de herramienta y errores de protocolo.
¿Qué necesitas antes de empezar?
Necesitas Node.js 20 o posterior, TypeScript, npm y familiaridad con funciones asíncronas. La documentación del primer servidor del SDK MCP usa este entorno, el paquete tsx y paquetes separados para servidor y cliente. El ejemplo sigue esa organización actual.
El proyecto también usa Zod para el schema de la herramienta y Vitest para las pruebas. Puedes adaptar los comandos a pnpm o Bun, pero conserva el mismo contrato: la prueba debe iniciar el proceso compilado y hablar con él mediante el transporte real. Una prueba que solo importa una función interna no detecta errores de JSON-RPC, stdout o negociación de capacidades.
¿Qué debe demostrar la prueba?
Un contrato MCP tiene tres capas que deben aparecer en la suite. La primera es el descubrimiento: el cliente encuentra el nombre de la herramienta y el schema que publicó el servidor. La segunda es la ejecución: una llamada válida devuelve una respuesta que el cliente puede leer. La tercera es el fallo: una entrada inválida no llega al efecto de la herramienta y aparece de una forma verificable.
La documentación del cliente TypeScript de MCP separa listTools() de callTool() y también distingue los errores de herramienta de los errores de protocolo. Esa diferencia sirve para las pruebas. isError: true significa que la llamada llegó al servidor y la herramienta informó un fallo; una excepción del cliente suele apuntar al transporte, al protocolo o a un proceso que terminó.
Este ejemplo expone find-ci-failures. Lee datos locales solo para mantener la prueba determinista. Si necesitas crear primero el servidor, consulta el tutorial de un servidor MCP en TypeScript, y luego compáralo con la validación de tool calls en TypeScript. Aquí el foco está en el proceso MCP completo. En un proyecto real, sustituye la lista fija por un adaptador de base de datos o API, pero conserva los casos del contrato y no conviertas la prueba en una copia frágil de la implementación.
Paso 1: prepara el proyecto y las dependencias
Crea un proyecto vacío, instala los paquetes y haz que el script de pruebas compile antes de ejecutar Vitest. La guía oficial de servidor MCP en TypeScript también subraya que stdout pertenece al protocolo stdio; los logs de diagnóstico deben ir a stderr.
mkdir mcp-contract-tests
cd mcp-contract-tests
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server @modelcontextprotocol/client zod
npm install --save-dev @types/node typescript tsx vitest
mkdir -p src tests
Crea tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "build",
"rootDir": ".",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src", "tests"]
}
Añade estos scripts a package.json:
{
"scripts": {
"build": "tsc",
"test": "npm run build && vitest run"
}
}
Si tu proyecto todavía usa el paquete heredado @modelcontextprotocol/sdk, no mezcles imports de las dos generaciones. La estrategia es la misma, pero cambian los nombres de los paquetes y algunos helpers. Consulta la guía de migración del SDK TypeScript antes de copiar el código.
Paso 2: separa la fábrica del proceso stdio
Coloca la implementación en src/factory.ts. Separar la fábrica del punto de entrada deja claro qué es código del servidor y qué es código del proceso. La prueba de contrato no importa la fábrica para simular una llamada; inicia build/server.js como lo haría un host local.
import { McpServer } from "@modelcontextprotocol/server";
import * as z from "zod/v4";
const failures = [
{ id: "ci-1042", suite: "auth", summary: "token refresh returned 401" },
{ id: "ci-1043", suite: "billing", summary: "webhook retry exceeded the limit" },
];
const resultSchema = z.object({
matches: z.array(
z.object({
id: z.string(),
suite: z.string(),
summary: z.string(),
}),
),
});
export function createServer() {
const server = new McpServer({
name: "ci-evidence",
version: "0.1.0",
});
server.registerTool(
"find-ci-failures",
{
description: "Find a small set of CI failures by suite or message.",
inputSchema: z.object({ query: z.string().min(1) }),
outputSchema: resultSchema,
},
async ({ query }) => {
const normalized = query.toLowerCase();
const matches = failures.filter((failure) =>
`${failure.suite} ${failure.summary}`
.toLowerCase()
.includes(normalized),
);
const structuredContent = resultSchema.parse({ matches });
return {
content: [
{ type: "text", text: JSON.stringify(structuredContent) },
],
structuredContent,
};
},
);
return server;
}
Ahora crea src/server.ts:
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { createServer } from "./factory.js";
void serveStdio(createServer);
console.error("servidor MCP de evidencias iniciado");
La llamada console.error() no es decorativa. Con stdio, stdout transporta los mensajes del protocolo. Un console.log() suelto puede convertir una conexión aparentemente correcta en JSON inválido. La guía de depuración de MCP recomienda revisar los logs y probar el proceso de forma aislada cuando falla la conexión.
Paso 3: escribe el cliente de contrato
Crea tests/mcp.contract.test.ts e inicia el proceso compilado con StdioClientTransport. La prueba comienza por el descubrimiento, porque una herramienta puede seguir respondiendo en una prueba antigua después de desaparecer de la lista pública.
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
describe("contrato del servidor MCP", () => {
let client: Client;
beforeEach(async () => {
client = new Client({ name: "contract-test", version: "0.1.0" });
const transport = new StdioClientTransport({
command: "node",
args: ["build/server.js"],
});
await client.connect(transport);
});
afterEach(async () => {
await client.close();
});
it("descubre la herramienta y el schema públicos", async () => {
const { tools } = await client.listTools();
const tool = tools.find(({ name }) => name === "find-ci-failures");
expect(tool).toBeDefined();
expect(tool?.inputSchema).toMatchObject({ type: "object" });
expect(tool?.inputSchema.properties).toHaveProperty("query");
});
it("ejecuta una llamada válida mediante el transporte real", async () => {
const result = await client.callTool({
name: "find-ci-failures",
arguments: { query: "auth" },
});
expect(result.isError).not.toBe(true);
expect(JSON.stringify(result)).toContain("ci-1042");
});
it("rechaza una entrada inválida antes del efecto de la herramienta", async () => {
await expect(
client.callTool({
name: "find-ci-failures",
arguments: { query: "" },
}),
).rejects.toThrow();
});
});
El último caso necesita una nota. Algunas versiones del SDK exponen el rechazo del schema como una excepción del cliente; otras superficies pueden devolver un resultado de error. El contrato de tu proyecto debe elegir una forma y fijarla en una prueba. Si tu versión devuelve isError: true, cambia la aserción por expect(result.isError).toBe(true) y confirma que el handler no se ejecutó.
Paso 4: ejecuta e interpreta la suite
Ejecuta el build y el comando completo de pruebas:
npm test
Una ejecución sana debe mostrar tres pruebas aprobadas. La primera demuestra que el servidor publica la superficie esperada. La segunda confirma la negociación, el transporte stdio, la llamada y el resultado. La tercera protege la frontera de entrada. Juntas cubren un nombre de herramienta cambiado, un proceso que no inicia y un schema demasiado permisivo.
Si la prueba falla con spawn node ENOENT, el runner no encontró el ejecutable en el entorno de CI. Usa la ruta de Node configurada por el job o el gestor de versiones del proyecto. Si falla con JSON inválido, busca logs escritos en stdout y mueve los diagnósticos a stderr. Si listTools() llega vacío, el servidor puede haber terminado antes de completar initialize.
Paso 5: usa Inspector para depurar el caso que falló
El cliente automatizado protege el CI, pero Inspector es más rápido para entender un fallo local. La documentación oficial de MCP Inspector lo presenta como una interfaz para conectar, descubrir schemas y ejecutar herramientas con entradas de prueba.
Después del build, ejecuta:
npx @modelcontextprotocol/inspector node build/server.js
En el panel, conecta el proceso, abre Tools, comprueba find-ci-failures y envía auth. Repite con una cadena vacía y observa si el cliente muestra un rechazo de argumentos. Inspector muestra lo que ocurrió en esa ejecución; no sustituye la prueba que debe ejecutarse sin navegador en el CI.
Este flujo también ayuda a separar capas. Si Inspector no conecta, investiga el proceso, stdout, la versión del protocolo y el transporte. Si conecta, pero la llamada devuelve isError, investiga la regla de la herramienta. Si llega la respuesta, pero la prueba no encuentra el dato esperado, el contrato de salida o la aserción son demasiado débiles.
Cuando el servidor gane más herramientas, conserva una tabla de casos por intención. Incluye una llamada válida para cada herramienta crítica, una entrada obligatoria ausente, un enum desconocido, un resultado vacío válido y un fallo de dependencia. No conviertas cada combinación de campos en una prueba de integración; cubre la lógica detallada con pruebas unitarias y reserva el proceso MCP para la frontera.
¿Cómo conectarlo al CI sin crear ruido?
El job debe instalar dependencias, compilar, ejecutar la suite y guardar logs solo cuando falla. No necesita llamar a un modelo. La elección de la herramienta y los argumentos ya forman parte del contrato, y la verificación determinista evita que una variación de generación oculte regresiones del servidor.
En un pipeline de agentes, publica juntos el nombre de la herramienta, la versión del servidor, el resultado del contrato y la revisión probada. Eso encaja con la observabilidad de agentes de código en CI y las evals de regresión para agentes de código: la primera muestra qué ocurrió; la segunda comprueba si el agente usó la evidencia correctamente.
Cuando muchos handoffs repiten los mismos errores y el historial empieza a consumir la ventana de contexto, uso RemoteCode para llevar Claude Code y Codex más lejos en flujos agentic con menos contexto repetido. Es una herramienta mía, mencionada aquí porque las pruebas de contrato producen evidencia breve que puede pasar por varias etapas sin reenviar toda la ejecución.
Errores comunes y límites de la prueba
Una prueba de contrato puede demostrar que el servidor publica una herramienta y responde a una llamada. No demuestra que la herramienta tenga autorización para acceder a una base de datos, que el resultado cumpla una regla de negocio o que el agente elija la herramienta correcta ante una pregunta ambigua.
| Síntoma | Causa probable | Corrección |
|---|---|---|
| Inspector conecta, pero la prueba no | La prueba inicia otro directorio u otro build | Usa el mismo comando del CI y confirma build/server.js |
| La prueba pasa aunque falte una herramienta | La suite solo llama a una función conocida | Comprueba listTools() y el schema publicado |
| Un error de validación aparece como éxito | La prueba ignora isError o captura cualquier excepción |
Separa errores de herramienta y de protocolo |
| El servidor se cierra sin un mensaje claro | Se escribió un log en stdout | Envía diagnósticos a stderr y conserva el código de salida |
| El resultado cambia en cada ejecución | Una dependencia externa está dentro de la prueba de contrato | Usa un fixture determinista y prueba el adaptador por separado |
El repositorio de pruebas de conformidad de MCP cubre una conformidad más amplia con la especificación. Úsalo cuando mantengas una implementación de cliente o servidor que necesite demostrar comportamiento de protocolo. Para una herramienta de negocio normal, la suite local sigue siendo necesaria porque solo ella conoce los nombres, schemas, permisos y efectos esperados.
Preguntas frecuentes
¿MCP Inspector sustituye las pruebas automatizadas?
No. Inspector es excelente para explorar schemas, observar logs y reproducir una llamada manual. Una prueba automatizada se ejecuta en el CI, repite el mismo contrato y falla cuando cambia la superficie pública. Usa Inspector para diagnosticar y el cliente TypeScript para proteger la integración.
¿Necesito llamar a un modelo durante la prueba?
No para probar el contrato MCP. El descubrimiento, los schemas, el transporte y las respuestas se pueden comprobar con un cliente determinista. Añade una eval con modelo solo cuando la pregunta sea si el agente elige la herramienta correcta, interpreta la respuesta o respeta la política del sistema.
¿Debo probar el servidor por stdio o por HTTP?
Prueba el transporte usado en producción. Para un servidor local iniciado por Claude Code, Codex u otro host, stdio reproduce el ciclo de vida del proceso. Para un servidor remoto, usa el cliente HTTP correspondiente e incluye autenticación, sesiones, tiempos de espera y cierre en el contrato.
¿La prueba necesita todas las combinaciones del schema?
No. El contrato debe cubrir las fronteras y los casos que cambian la decisión del cliente. La validación detallada y las combinaciones del dominio pertenecen a las pruebas unitarias del handler. La prueba MCP debe ser lo bastante pequeña para ejecutarse con cada cambio sin convertirse en una segunda implementación del servidor.
¿Qué cambia cuando se actualiza el SDK de MCP?
Vuelve a comprobar los imports, el transporte y el formato de error. Conserva las pruebas de comportamiento y actualiza solo el adaptador necesario. La intención sigue siendo la misma: descubrir, llamar, validar y distinguir fallos. Fijar la versión en el lockfile ayuda a impedir que el CI cambie el contrato sin una revisión explícita.
El servidor está listo para un agente cuando la prueba puede decir algo más que "conectado". Debe demostrar qué herramientas existen, qué argumentos entran, qué respuesta sale y cómo un fallo detiene el camino. Esa evidencia es pequeña, reproducible y más útil en el CI que una captura de Inspector.