Los MCPs (Model Context Protocol) han hecho un speedrun por el ciclo de hype de Gartner:
- Noviembre 2024: Anthropic anuncia la especificación MCP.
- Febrero 2025: Se crea el término Vibe Coding para programar sin tocar código usando la IA.
- Febrero 2025: Cursor añade soporte para MCPs y crece su popularidad.
- Marzo 2025: La gente empieza a usar muchos MCPs para Vibe Codear aplicaciones
- Mayo - Julio 2025: Twitter empeiza a arder de todos los agujeros de seguridad con los MCPs.
- Agosto 2025: Todo se calma, muchos de los MCPs que se habían creado quedan en desuso, pero los útil se usan más que nunca.
- Septiembre 2025: Sale el registry oficial de MCP y poco después GitHub lanza el suyo.

El ciclo de hype de Gartner aplicado a los MCPs
Y así es como en menos de un año, ya estamos entrando en la etapa final. Empezamos a entender:
- Cuándo tiene sentido usar MCPs (y cuándo no).
- Cómo implementarlos siguiendo buenas prácticas.
- Por qué el testing es fundamental para mantenerlos al largo plazo.
Por qué testear servidores MCP
Las primitivas de los MCPs (tools, resources, prompts y resource templates) son esencialmente puntos de entrada a nuestro sistema, exactamente igual que los controllers/rutas HTTP.
Cuando exponemos un endpoint HTTP, entendemos que estamos creando un contrato con nuestros clientes. Los testeamos para:
- Garantizar que funcionan como esperamos
- Evitar romper integraciones sin darnos cuenta
- Documentar el comportamiento esperado
- Detectar regresiones rápidamente
Con los MCPs pasa exactamente lo mismo, la única diferencia es que el cliente es un LLM.
Estrategia de testing para MCPs
¿Qué testeamos exactamente?
En nuestro caso, cuando creamos endpoints http, simplemente hacen de capa de traducción del contrato al caso de uso. Con los MCPs hacemos exactamente igual: Reciben la petición, llaman a un caso de uso y luego devuelve la respuesta de éste.
En nuestra arquitectura:
- Los controllers HTTP/MCP: Son meras capas de traducción del protocolo.
- Los casos de uso: Contienen la lógica de negocio (testeados unitariamente).
Por tanto, lo que necesitamos testear en los MCPs es el contrato, no la exhaustivamente la lógica de negocio, ya que ya está cubierta.
Por qué testearlos en formato End-to-End
Para verificar que todas las piezas encajan correctamente, optamos por tests end-to-end que:
- Invocan al servidor MCP exactamente como lo haría un cliente real.
- Verifican la integración completa del stack.
- Incluyen llamadas reales a base de datos cuando sea necesario.
- Validan que los contratos se mantienen estables.
Herramientas de testing: CLI vs Librería
Por qué no usar el MCP Inspector para testear
Mientras desarrollamos MCPs, el MCP Inspector es la herramienta más común para ir probando que todo lo que hacemos funciona.
Lo malo es que para hacer esas pruebas, hemos de ir haciéndolas manualmente todo el rato y se vuelve tedioso, además de que no tenemos todas las ventajas de los tests automatizados.
Para ello le añadieron soporte por CLI, pero de todas formas, como herramienta para testing, sigue teniendo problemas:
- Conexiones efímeras: Abre y cierra la conexión en cada llamada.
- Sin tipado: Las respuestas no están tipadas.
- API incompleta: No implementa toda la especificación MCP.
Por qué sí el cliente oficial de MCP
La forma más potente para poder hacer testing automatizado es utilizar el cliente oficial de MCP (TypeScript | Python), ya que:
- Tenemos conexiones persistentes.
- Implementación completa de la especificación.
- Es un cliente real y encima el oficial.
Implementación de los tests
Organización de los tests
En nuestro caso, para los tests, hacemos un paralelismo de cómo están organizado las primitivas de MCPs en nuestro código:
tests/
├── api/
└── mcp/
├── courses/
│ ├── prompts/
│ │ └── SearchSimilarCourseByCoursesNamesPrompt.test.ts
│ ├── resources/
│ │ ├── CourseResourceTemplate.test.ts
│ │ └── CoursesResource.test.ts
│ └── tools/
│ ├── SearchAllCoursesTool.test.ts
│ ├── SearchCourseByIdTool.test.ts
│ ├── SearchCourseBySimilarNameTool.test.ts
│ └── SearchSimilarCoursesByIdsTool.test.ts
├── users/
└── videos/
1. Configuración del cliente de test
Lo primero que hacemos en el test es instanciar el cliente y hacer que abra la conexión al empezar y la cierre al finalizar:
describe("SearchCoursesByQueryMcpTool should", () => {
// Configuración del cliente MCP
const mcpClient = new Client(
{
name: "mcp-client",
version: "1.0.0",
},
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
},
);
// Configuración del transporte STDIO
const transport = new StdioClientTransport({
command: "npx",
args: ["ts-node", "./src/app/mcp/server.ts"],
})
// Ciclo de vida del test
beforeAll(async () => {
await mcpClient.connect(transport);
});
afterAll(async () => {
await mcpClient.disconnect();
});
// Tests aquí...
});
2. Testear que hemos registrado las primitivas correctamente
El primer test que siempre hacemos es verificar que hemos registrado las primitivas correctamente, para ello llamamos al método de listar (tools, resources, prompts…) y nos aseguramos de que está el que esperamos:
it("list search course by query tool", async () => {
const toolsResponse = await mcpClient.listTools();
expect(toolsResponse.map((response) => response.name))
.toContain("courses-search_by_query");
});
💡 ProTip: Solo validamos el nombre, no la descripción. Las descripciones pueden cambiar frecuentemente y harían que tuviéramos un test frágil.
3. Testear el comportamiento de la primitiva
Aquí testeamos igual que haríamos con un endpoint HTTP. En este caso es una tool que dada una query te devuelve un listado de cursos que coinciden con lo buscado.
Lo más importante de estos tests, es que estamos testeando el contrato, por eso validamos que toda la respuesta que nos devuelve sea igual a lo que esperamos:
it("return an empty array when no courses are found", async () => {
const response = await mcpClient.callTool("courses-search_by_query", {
query: "non-existent-course",
languageCode: "en",
limit: 5,
});
const expectedResponse = { courses: [] };
expect(response).toEqual({
content: [
expect.objectContaining({
type: "text",
text: JSON.stringify(expectedResponse),
}),
],
structuredContent: expectedResponse,
isError: false,
});
});
En el caso que devuelve datos, antes hacemos un insert en la base de datos de los valores que esperamos que hayan:
it("return existing courses", async () => {
const query = "Node avanzado";
const relatedCourse = CourseMother.create({
title: "Node avanzado",
summary: "Aprende NodeJs y domina todas sus API",
});
const lessRelatedCourse = CourseMother.create({
title: "DDD",
summary: "Domina Domain-Driven Design para hacer un código más escalable",
languageCode: relatedCourse.languageCode.value,
});
await courseRepository.save(relatedCourse);
await courseRepository.save(lessRelatedCourse);
const response = await mcpClient.callTool("courses-search_by_query", {
query,
languageCode: relatedCourse.languageCode.value,
limit: 10,
});
const expectedResponse = {
courses: [relatedCourse.toPrimitives(), lessRelatedCourse.toPrimitives()]
};
expect(response).toEqual({
content: [
expect.objectContaining({
type: "text",
text: JSON.stringify(expectedResponse),
}),
],
structuredContent: expectedResponse,
isError: false,
});
});
Tests específicos de Resource Templates
Los Resource Templates tienen funcionalidades adicionales que también podemos testear:
Que al crear un resource template, se generen también sus resources:
it("list all available courses langs as resources", async () => {
const response = await mcpClient.listResources();
expect(response.map((response) => response.uri))).toEqual(expect.arrayContaining([`courses://all?lang=es`, `courses://all?lang=en`]));
});
Y el propio autocompletado de los parámetros del template:
it("complete the lang param", async () => {
const response = await mcpClient.completeResourceTemplateParam("courses://all?lang={lang}", "lang", "e");
expect(response).toEqual(["es", "en"]);
});
Gestión y testing de errores
La spec de MCP es relativamente nueva, pero está llean de inconsistencias. Sobretodo en el manejo de errores. En las tools los errores son esperados y la propia respuesta incluye un isError
, pero en Resources no existe esa opción.
Testing de errores en Resources
La forma más sencilal de testearlo, es hacer un try catch sobre el cliente y validar la respuesta:
it("return bad request error when course id is invalid", async () => {
const invalidId = CourseIdMother.invalid();
await expect(
mcpClient.readResource(`courses://${invalidId}`),
).rejects.toThrow(
`MCP error -32000: The id <${invalidId}> is not a valid nano id`,
);
});
Simplificando los tests: nuestro wrapper del cliente
El problema del cliente oficial
El cliente oficial está diseñado para ser una implementación transparente de la especificación (lo cual es perfecto para producción). Sin embargo, para testing necesitamos utilidades adicionales que simplifiquen nuestro trabajo.
Para simplificar el testing, hemos creado nuestro propio wrapper (dale una estrellita si te gusta 😊) que extiende el cliente oficial con funcionalidades específicas que nos son muy útiles para el testing.
Ventaja #1: Instanciación simplificada
const mcpClient = new McpClient("stdio", [
"npx",
"ts-node",
"./src/app/mcp/server.ts",
]);
Ventaja #2: Cambio de transporte sin modificar tests
Pasar de STDIO a HTTP es tan simple como cambiar la instanciación:
const mcpClient = new McpClient("http", ["http://localhost:3000/api/mcp"]);
Ventaja #3: Helpers específicos para testing
it("list search course by query tool", async () => {
const toolsResponse = await mcpClient.listTools();
expect(toolsResponse.names()).toContain("courses-search_by_query");
// ^^^^^^^ en lugar de tener que hacer el .map
});
Mejores prácticas y recomendaciones
Tests imprescindibles para cada primitiva
- Test de registro: Verifica que la primitiva está disponible.
- Test de caso vacío: Comportamiento sin datos.
- Test de caso exitoso: Flujo principal funcionando.
- Test de errores: Manejo correcto de excepciones.
- Test de reproducir bugs: Si aparece cualquier bug, la forma más fácil de reproducirlo es con los tests.
Al ser tests similares a los de la API, si ya los tenías, puedes correrlos todos juntos para asegurarte de que siempre se ejecutan en Integración Continua.
Conclusión
Lo esencial
- Los MCPs son contratos: Trátalos igual que tratas a tus endpoints.
- El cambio de transporte debe ser transparente: Tu suite de tests no debería cambiar por ello.
- Usa las herramientas adecuadas: Cliente oficial para tests, Inspector para desarrollo.
- No te saltes los tests de error: Son tan importantes como los casos de éxito.
Recursos adicionales
- 📚 Curso de MCP: Crea tu servidor: Aprende a crear servidores MCP siguiendo buenas prácticas
- 🔧 MCP Test Client: Nuestro wrapper para simplificar el testing.
- 📖 Ejemplos de tests a primitivas de MCP: Ejemplos del curso donde puedes ver cómo testear cada parte.