¿Te resulta más fácil leer con fondo crema, un tono de color, más contraste o un texto más grande? Elige tus colores y tu texto una sola vez, para todas las páginas.

Lección gratuita

Probar un servidor MCP

Después de esta lección: Puedo probar un servidor con clientes con guion.

Lección de 80 min13 comandosLaboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

Probar un servidor MCP con clientes con guion

Un cliente con guion es un programa corto que envía una lista fija de mensajes a un servidor y comprueba cada respuesta. Nunca se cansa y nunca se salta un paso.

Importa porque un servidor se rompe fácilmente sin querer. Un cambio de un solo carácter puede alterar una respuesta, y nadie se entera hasta que falla un cliente real.

Al final de esta lección

Nota

Puedo probar un servidor con clientes con guion.

Palabras clave

PalabraQué significaEjemplo
Guionuna lista de mensajes por enviar, cada uno con la respuesta esperada
Esperadala respuesta que quiere la prueba
Realla respuesta que el servidor envió de verdad
Diferenciael primer punto en que las dos no coinciden
Comodínuna marca que encaja con cualquier valor<any>

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Un guion empareja cada mensaje con su respuesta

Un guion es una lista de pasos. Cada paso tiene un mensaje que enviar y la respuesta esperada. Un paso de notificación no espera respuesta.

Algunos valores cambian, como un número de versión. La prueba los marca con un comodín, así que un cambio ahí no es un fallo. Todo lo demás debe coincidir exactamente.

Ejemplo resuelto: un guion para sumar

PasoSe envíaRespuesta esperada
1initializeLa versión pedida, tools anunciadas, cualquier versión del servidor
2La notificación de listoNinguna respuesta
3tools/listUna herramienta llamada add
4add 2 y 3El texto 5, isError false
5add "2" y 3Un error de herramienta, isError true

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Informa de la primera diferencia

Una prueba que solo dice «falló» hace perder tiempo. Una buena dice dónde. La ruta result.content[0].text señala un valor de la respuesta.

El comprobador recorre la respuesta esperada en un orden fijo y se detiene en la primera diferencia. Dice qué esperaba y qué obtuvo.

Un error frecuente

Advertencia

Mucha gente ejecuta una prueba solo contra un servidor que funciona. Una prueba que nunca ha fallado nunca ha demostrado que detecte algo. Rompe el servidor a propósito y comprueba que la prueba falla. Luego deshaz el cambio.

Escribe el ayudante del modelo

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", { method: \"POST\", headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" }, body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 80 }), signal: AbortSignal.timeout(30000) });' '    if (res.ok === false) throw new Error(\"estado \" + res.status);' '    const data = await res.json();' '    return { text: data.choices[0].message.content, recorded: false };' '  } catch (error) {' '    console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' '    return { text: sample, recorded: true };' '  }' '}' 'module.exports = { ask };' > model.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el servidor, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'const SCHEMA = { type: \"object\", properties: { a: { type: \"integer\" }, b: { type: \"integer\" } }, required: [\"a\", \"b\"] };' 'function words(text, bad) { return { content: [{ type: \"text\", text: text }], isError: bad }; }' > server6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el servidor, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function handle(msg) {' '  if (msg.id === undefined) return;' '  const p = msg.params || {};' '  const args = p.arguments || {};' '  if (msg.method === \"initialize\") return send({ id: msg.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"sumador\", version: \"1.0\" } } });' >> server6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el servidor, parte 3

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  if (msg.method === \"tools/list\") return send({ id: msg.id, result: { tools: [{ name: \"add\", description: \"Suma dos enteros\", inputSchema: SCHEMA }] } });' '  if (msg.method === \"tools/call\" && p.name === \"add\") {' '    if (Number.isInteger(args.a) && Number.isInteger(args.b)) return send({ id: msg.id, result: words(String(args.a + args.b), false) });' '    return send({ id: msg.id, result: words(\"a y b deben ser enteros\", true) });' '  }' '  if (msg.method === \"tools/call\") return send({ id: msg.id, error: { code: -32602, message: \"Unknown tool: \" + p.name } });' '  send({ id: msg.id, error: { code: -32601, message: \"Method not found: \" + msg.method } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) { handle(JSON.parse(line)); });' >> server6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el comprobador, parte 1

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const server = require(\"child_process\").spawn(\"node\", [process.argv[2] || \"server6.js\"]);' 'const any = \"<any>\";' 'const one = function (id, method, params) { return { jsonrpc: \"2.0\", id: id, method: method, params: params }; };' 'const add = function (id, args) { return one(id, \"tools/call\", { name: \"add\", arguments: args }); };' 'const ok = function (id, result) { return { jsonrpc: \"2.0\", id: id, result: result }; };' 'const text = function (t, bad) { return { content: [{ type: \"text\", text: t }], isError: bad }; };' > check6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el comprobador, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const steps = [' '  [one(1, \"initialize\", { protocolVersion: \"2025-03-26\" }), ok(1, { protocolVersion: \"2025-03-26\", capabilities: { tools: {} }, serverInfo: { name: \"sumador\", version: any } })],' '  [{ jsonrpc: \"2.0\", method: \"notifications/initialized\" }, null],' '  [one(2, \"tools/list\"), ok(2, { tools: [{ name: \"add\", description: any, inputSchema: any }] })],' '  [add(3, { a: 2, b: 3 }), ok(3, text(\"5\", false))],' '  [add(4, { a: \"2\", b: 3 }), ok(4, text(\"a y b deben ser enteros\", true))],' '  [one(5, \"tools/call\", { name: \"nope\" }), { jsonrpc: \"2.0\", id: 5, error: { code: -32602, message: \"Unknown tool: nope\" } }],' '];' >> check6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el comprobador, parte 3

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function isObj(x) { return Boolean(x) && typeof x === \"object\"; }' 'function name(path, k, list) { return list ? path + \"[\" + k + \"]\" : (path ? path + \".\" + k : k); }' 'function diff(want, got, path) {' '  if (want === any) return null;' '  const both = isObj(want) && isObj(got) && Array.isArray(want) === Array.isArray(got);' '  const said = \" se esperaba \" + JSON.stringify(want) + \", se obtuvo \" + JSON.stringify(got);' '  if (both === false) return JSON.stringify(want) === JSON.stringify(got) ? null : (path || \"(raíz)\") + \":\" + said;' >> check6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el comprobador, parte 4

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  const list = Array.isArray(want);' '  if (list && (want.length === got.length) === false) return (path || \"(raíz)\") + \": se esperaban \" + want.length + \" elementos, se obtuvieron \" + got.length;' '  const keys = Object.keys(want).sort();' '  for (const k of keys) {' '    if ((k in got) === false) return name(path, k, list) + \": falta\";' '    const d = diff(want[k], got[k], name(path, k, list));' '    if (d) return d;' '  }' '  const extra = Object.keys(got).sort().filter(function (k) { return keys.indexOf(k) < 0; });' '  return extra.length > 0 ? name(path, extra[0], list) + \": sobra\" : null;' '}' >> check6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el comprobador, parte 5

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const lines = [];' 'const waiting = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (l) {' '  if (waiting.length > 0) waiting.shift()(JSON.parse(l)); else lines.push(JSON.parse(l));' '});' 'function next() { return lines.length > 0 ? Promise.resolve(lines.shift()) : new Promise(function (r) { waiting.push(r); }); }' >> check6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Escribe el comprobador, parte 6

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  let passed = 0, total = 0, first = null;' '  for (let i = 0; i < steps.length; i++) {' '    server.stdin.write(JSON.stringify(steps[i][0]) + \"\n\");' '    if (steps[i][1] === null) continue;' '    total += 1;' '    const d = diff(steps[i][1], await next(), \"\");' '    console.log((d ? \"FAIL paso \" + (i + 1) + \" en \" + d : \"PASS paso \" + (i + 1)));' '    if (d === null) passed += 1; else if (first === null) first = d;' '  }' '  server.stdin.end();' '  console.log(\"pasaron \" + passed + \" de \" + total);' '  if (first === null) return;' '  const r = await ask([{ role: \"user\", content: \"Una prueba de mi servidor falló con esta diferencia. En una frase, ¿cuál es la causa probable? \" + first }], \"El servidor dio un texto distinto del que esperaba la prueba.\", 60);' '  console.log(\"el modelo dijo: \" + r.text);' '}' 'main();' >> check6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Ejecuta el comprobador con el servidor bueno

Comando
docker exec -w /home/node/project agent node check6.js

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Haz una copia con un cambio malo

Comando
docker exec -w /home/node/project agent sh -c "sed 's/args.a + /args.a - /' server6.js > bad6.js"

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Ejecuta el comprobador con la copia mala

Comando
docker exec -w /home/node/project agent node check6.js bad6.js

Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.

Las dos ejecuciones, lado a lado

EjecuciónPasos comprobadosResultado
Servidor bueno1, 3, 4, 5 y 6pasaron 5 de 5
Copia mala1, 3, 4, 5 y 6pasaron 4 de 5
Copia mala, paso 4add 2 y 3result.content[0].text: se esperaba "5", se obtuvo "-1"

Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.

Sobre este laboratorio y su código: Laboratorio de agente de programación con IA: usa opencode para explicar y corregir código, y comprueba su trabajo

¿Enseñas a una clase? Asigna este módulo a tus alumnos con guía del profesor, tareas con corrección automática y un laboratorio para cada alumno.