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
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Esquema | una descripción de cómo debe ser la entrada | |
| Argumento | un valor con nombre en una llamada a una herramienta | |
| Validar | comprobar la entrada antes de usarla | |
| Enum | una lista fija de valores permitidos | bocadillo, tortilla, ensalada |
| Error de herramienta | un 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
| Argumento | Regla | Pasa | Falla |
|---|---|---|---|
| item | Obligatorio. Uno de bocadillo, tortilla, ensalada | tortilla | sopa |
| quantity | Obligatorio. Un entero, de 1 a 5 | 2 | 9, o "2" como texto |
| note | Texto opcional, como máximo 20 caracteres | sin cebolla | Una nota de 21 caracteres |
| cualquier otro nombre | No se permite | Ninguno | colour |
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
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
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
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
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
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
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
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
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
docker exec -w /home/node/project agent node client3.jsSe 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
| Llamada | Respuesta |
|---|---|
| tortilla, cantidad 2 | Pedido: 2 tortilla, isError false |
| sopa, cantidad 9, colour rojo | Tres problemas en una frase, isError true |
| sin argumentos | falta: item; falta: quantity |
| cantidad como texto "2" | quantity: se esperaba integer |
| delete_menu | Error de protocolo -32602 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.