¿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

Limitar herramientas MCP por alcance

Después de esta lección: Puedo limitar las herramientas de un servidor según quién las pide.

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 limita permisos un servidor MCP

Una herramienta que hace un pedido y otra que cancela todos los pedidos no deben estar abiertas a la misma persona. Un servidor lo limita con alcances: una etiqueta corta para cada cosa que puede hacer quien llama.

Un servidor que se ejecuta por entrada y salida estándar no tiene página de inicio de sesión. Quien lo arranca dice qué alcances tiene quien llama, y el servidor los comprueba en cada llamada.

Al final de esta lección

Nota

Puedo limitar las herramientas de un servidor según quién las pide.

Palabras clave

PalabraQué significaEjemplo
Alcanceuna etiqueta para una cosa que puede hacer quien llamaorders:write
Permisoun alcance que tiene quien llama
Denegarrechazar una llamada porque falta un alcance
Comodínun permiso que cubre todos los alcances con el mismo principioorders:*
Registro de auditoríaun apunte de cada llamada y de lo que se decidió

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

Cada herramienta nombra el alcance que necesita

El servidor guarda una tabla: cada herramienta y el único alcance que necesita. Quien llama tiene una lista de permisos. Una llamada se permite cuando un permiso cubre el alcance.

Un permiso comodín como orders: cubre todos los alcances que empiezan por orders: y nada más. El permiso lo cubre todo.

Ejemplo resuelto: quien llama tiene dos permisos

HerramientaNecesitaQuien llama tiene menu:read, orders:writeDecisión
read_menumenu:readCubiertoPermitir
place_orderorders:writeCubiertoPermitir
cancel_allorders:adminNo cubiertoDenegar
burn_kitchenNo existeNo se aplicaDesconocida

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

Ocultar una herramienta no es rechazarla

El servidor puede dejar fuera de tools/list las herramientas que quien llama no puede usar. Así el modelo no las intenta. Pero quien llama aún puede enviar una llamada a una herramienta que nunca se le mostró.

Por eso la comprobación se hace en cada tools/call, siempre. Un alcance que falta da un error de herramienta que nombra el alcance. Una herramienta desconocida sigue siendo un error de protocolo.

Un error frecuente

Advertencia

Mucha gente oculta de la lista la herramienta peligrosa y lo llama seguridad. Una herramienta oculta sigue ahí. Quien sepa su nombre puede llamarla. Comprueba el alcance en cada llamada, y registra tanto las llamadas rechazadas como las que se ejecutan.

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 NEEDS = { read_menu: \"menu:read\", place_order: \"orders:write\", cancel_all: \"orders:admin\" };' 'const HAVE = (process.env.SCOPES || \"\").split(\",\");' 'let count = 0;' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function words(text, bad) { return { content: [{ type: \"text\", text: text }], isError: bad }; }' 'function note(tool, decision) {' '  count += 1;' '  process.stderr.write(JSON.stringify({ n: count, tool: tool, decision: decision, needs: NEEDS[tool] || null }) + \"\n\");' '}' > server5.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 call(msg, p) {' '  if (Object.keys(NEEDS).indexOf(p.name) < 0) {' '    note(p.name, \"unknown\");' '    return send({ id: msg.id, error: { code: -32602, message: \"Unknown tool: \" + p.name } });' '  }' '  if (HAVE.indexOf(NEEDS[p.name]) < 0) {' '    note(p.name, \"deny\");' '    return send({ id: msg.id, result: words(\"No permitido: \" + p.name + \" necesita \" + NEEDS[p.name], true) });' '  }' '  note(p.name, \"allow\");' '  send({ id: msg.id, result: words(\"hecho: \" + p.name, false) });' '}' >> server5.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' 'function listed() {' '  return Object.keys(NEEDS).filter(function (n) { return HAVE.indexOf(NEEDS[n]) >= 0; }).map(function (n) {' '    return { name: n, description: \"Herramienta del comedor \" + n, inputSchema: { type: \"object\" } };' '  });' '}' >> server5.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' '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.5\" } } });' '  if (msg.method === \"tools/list\") return send({ id: msg.id, result: { tools: listed() } });' '  if (msg.method === \"tools/call\") return call(msg, p);' '  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)); });' >> server5.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 env = Object.assign({}, process.env, { SCOPES: \"menu:read,orders:write\" });' 'const server = require(\"child_process\").spawn(\"node\", [\"server5.js\"], { env: env });' 'const call = function (id, name) { return { id: id, method: \"tools/call\", params: { name: name, arguments: {} } }; };' 'const script = [' '  { id: 1, method: \"initialize\", params: { protocolVersion: \"2025-03-26\" } },' '  { id: 2, method: \"tools/list\" },' '  call(3, \"read_menu\"), call(4, \"place_order\"), call(5, \"cancel_all\"), call(6, \"burn_kitchen\"),' '];' > client5.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 = [];' 'const log = [];' 'require(\"readline\").createInterface({ input: server.stderr }).on(\"line\", function (line) { log.push(line); });' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' '  const m = JSON.parse(line);' '  const names = m.result && m.result.tools ? \"herramientas: \" + m.result.tools.map(function (t) { return t.name; }).join(\", \") : null;' '  console.log(\"<- \" + (names || (m.result && m.result.content ? m.result.content[0].text : JSON.stringify(m.error || m.result))));' '  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\"); });' >> client5.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 () {' '  log.forEach(function (l) { console.log(\"log \" + l); });' '  const refusal = seen[4].result.content[0].text;' '  const r = await ask([{ role: \"user\", content: \"En una frase corta, explica este rechazo a un estudiante: \" + refusal }], \"Tu cuenta no tiene permiso para cancelar todos los pedidos.\", 60);' '  console.log(\"el modelo dijo: \" + r.text);' '});' >> client5.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 client5.js

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

Qué hace el guion

El cliente arranca el servidor con los alcances menu:read y orders:write en su entorno. Lista las herramientas y luego llama a tres que existen y a una que no.

Imprime el registro del servidor al final. Una llamada corta al modelo explica un rechazo. Sin modelo, imprime una Muestra grabada.

Qué devuelve y qué registra cada llamada

LlamadaRespuestaDecisión en el registro
tools/listSolo read_menu y place_orderNo es una llamada a herramienta, no se registra
read_menuhecho: read_menuallow
place_orderhecho: place_orderallow
cancel_allNo permitido: cancel_all necesita orders:admindeny
burn_kitchenError de protocolo -32602unknown

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.