¿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

Herramientas MCP con validación

Después de esta lección: Puedo ofrecer una herramienta cuya entrada se comprueba antes de ejecutarse.

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

Cómo comprueba su entrada una herramienta MCP

Una herramienta MCP tiene un nombre, una descripción y un esquema de entrada. El esquema dice qué aspecto deben tener sus argumentos. El servidor los comprueba contra él antes de ejecutar nada.

Importa porque un modelo rellena los argumentos, y a veces se equivoca. Un buen rechazo le dice qué corregir.

Al final de esta lección

Nota

Puedo ofrecer una herramienta cuya entrada se comprueba antes de ejecutarse.

Palabras clave

PalabraQué significaEjemplo
Esquemauna descripción de cómo debe ser la entrada
Argumentoun valor con nombre en una llamada a una herramienta
Validarcomprobar la entrada antes de usarla
Enumuna lista fija de valores permitidosbocadillo, tortilla, ensalada
Error de herramientaun resultado con isError true que dice qué falló

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

Un esquema lista las reglas de cada argumento

Cada argumento tiene un tipo. También puede tener una enumeración, un rango de números o una longitud máxima. El esquema dice qué argumentos son obligatorios, y puede decir que no se permite ninguno más.

El servidor comprueba cada regla en un orden fijo y reúne todos los problemas, no solo el primero.

Ejemplo resuelto: la herramienta order_lunch

ArgumentoReglaPasaFalla
itemObligatorio. Uno de bocadillo, tortilla, ensaladatortillasopa
quantityObligatorio. Un entero, de 1 a 529, o "2" como texto
noteTexto opcional, como máximo 20 caracteressin cebollaUna nota de 21 caracteres
cualquier otro nombreNo se permiteNingunocolour

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

Un rechazo dice al modelo qué corregir

Una llamada con argumentos malos no es una petición rota. La petición estaba bien. Por eso el servidor envía un resultado normal con isError true y una frase que lista los problemas.

Una llamada a una herramienta que no existe es distinta. Eso es un error de protocolo, código -32602.

Un error frecuente

Advertencia

Mucha gente piensa que el modelo siempre sigue el esquema, así que el servidor no necesita comprobar. Un modelo puede olvidar un nombre, enviar texto en vez de un número o añadir uno inventado. Puede que el cliente tampoco compruebe. El servidor es el último sitio donde comprobar, así que siempre lo hace.

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' 'const SCHEMA = { type: \"object\", properties: { item: { type: \"string\", enum: [\"bocadillo\", \"tortilla\", \"ensalada\"] }, quantity: { type: \"integer\", minimum: 1, maximum: 5 }, note: { type: \"string\", maxLength: 20 } }, required: [\"item\", \"quantity\"], additionalProperties: false };' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function typeOk(r, v) { return r.type === \"string\" ? typeof v === \"string\" : Number.isInteger(v); }' > server3.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 problems(args) {' '  const out = [];' '  SCHEMA.required.forEach(function (n) { if ((n in args) === false) out.push(\"falta: \" + n); });' '  Object.keys(SCHEMA.properties).forEach(function (n) {' '    if ((n in args) === false) return;' '    const r = SCHEMA.properties[n], v = args[n];' '    if (typeOk(r, v) === false) return out.push(n + \": se esperaba \" + r.type);' '    if (r.enum && r.enum.indexOf(v) < 0) out.push(n + \": debe ser uno de \" + r.enum.join(\", \"));' '    if (typeof r.minimum === \"number\" && (v < r.minimum || v > r.maximum)) out.push(n + \": debe estar entre \" + r.minimum + \" y \" + r.maximum);' '    if (typeof r.maxLength === \"number\" && v.length > r.maxLength) out.push(n + \": como máximo \" + r.maxLength + \" caracteres\");' '  });' >> server3.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' '  Object.keys(args).sort().forEach(function (n) { if ((n in SCHEMA.properties) === false) out.push(\"sobra: \" + n); });' '  return out;' '}' 'function words(text, bad) { return { content: [{ type: \"text\", text: text }], isError: bad }; }' 'function handle(msg) {' '  if (msg.id === undefined) return;' '  const p = msg.params || {};' '  if (msg.method === \"initialize\") return send({ id: msg.id, result: { protocolVersion: p.protocolVersion, capabilities: { tools: {} }, serverInfo: { name: \"comedor\", version: \"0.3\" } } });' '  if (msg.method === \"tools/list\") return send({ id: msg.id, result: { tools: [{ name: \"order_lunch\", description: \"Pedir el almuerzo en el comedor\", inputSchema: SCHEMA }] } });' >> server3.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 4

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  if (p.name === \"order_lunch\") {' '    const args = p.arguments || {};' '    const found = problems(args);' '    if (found.length > 0) return send({ id: msg.id, result: words(\"No se pudo ejecutar order_lunch: \" + found.join(\"; \"), true) });' '    return send({ id: msg.id, result: words(\"Pedido: \" + args.quantity + \" \" + args.item, false) });' '  }' '  send({ id: msg.id, error: { code: -32602, message: \"Unknown tool: \" + p.name } });' '}' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) { handle(JSON.parse(line)); });' >> server3.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 cliente con guion, 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\", [\"server3.js\"]);' 'const order = function (id, args) { return { id: id, method: \"tools/call\", params: { name: \"order_lunch\", arguments: args } }; };' 'const script = [' '  { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' '  { method: \"notifications/initialized\" },' '  order(2, { item: \"tortilla\", quantity: 2 }),' '  order(3, { item: \"sopa\", quantity: 9, colour: \"rojo\" }),' '  order(4, {}),' '  order(5, { item: \"ensalada\", quantity: \"2\" }),' '  { id: 6, method: \"tools/call\", params: { name: \"delete_menu\", arguments: {} } },' '];' > client3.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 cliente con guion, parte 2

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const seen = [];' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' '  const m = JSON.parse(line);' '  console.log(\"<- \" + (m.result && m.result.content ? m.result.content[0].text + \" (isError \" + m.result.isError + \")\" : line));' '  seen.push(m);' '  if (seen.length === 6) server.stdin.end();' '});' 'script.forEach(function (m) { server.stdin.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); });' >> client3.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 cliente con guion, parte 3

Comando
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'server.on(\"close\", async function () {' '  const shown = seen[2].result.content[0].text;' '  const r = await ask([{ role: \"user\", content: \"Una herramienta rechazó una llamada. En una frase, ¿qué debe cambiar quien llama? \" + shown }], \"Usar bocadillo, tortilla o ensalada, una cantidad de 1 a 5 y ningún color.\", 60);' '  console.log(\"el modelo dijo: \" + r.text);' '});' >> client3.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 cliente contra el servidor

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

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

A qué llama el guion

El guion abre una sesión y luego llama cinco veces a order_lunch y una vez a una herramienta que no existe. Una llamada es buena. Las otras rompen una regla, varias reglas, o todos los nombres obligatorios.

Una llamada corta al modelo al final lee un rechazo y dice qué cambiar. Sin modelo, imprime una Muestra grabada.

Qué recibe cada llamada

LlamadaRespuesta
tortilla, cantidad 2Pedido: 2 tortilla, isError false
sopa, cantidad 9, colour rojoTres problemas en una frase, isError true
sin argumentosfalta: item; falta: quantity
cantidad como texto "2"quantity: se esperaba integer
delete_menuError de protocolo -32602

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.