Lo que envía un servidor de herramientas
El Model Context Protocol (MCP) es una forma estándar de que un agente encuentre y llame a las herramientas de otro programa. Sus mensajes son JSON sencillo, uno por línea, por la entrada y la salida estándar. Importa porque, si sabes leer los mensajes, puedes depurar cualquier servidor de herramientas.
Un servidor de herramientas que funciona solo pero no con tu agente suele enviar un mensaje con la forma equivocada.
Al final de esta lección
Nota
Puedo leer y escribir los mensajes que se intercambian un servidor de herramientas y su cliente.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| JSON-RPC | un formato de mensaje para llamar a métodos, en JSON | |
| Petición | un mensaje con id que espera respuesta | |
| Notificación | un mensaje sin id y sin respuesta | |
| stdio | la entrada y la salida estándar de un proceso | |
| Código de error | un número que dice qué tipo de fallo es | -32601 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
JSON-RPC tiene tres clases de mensaje
Una petición tiene un id, un nombre de método y parámetros opcionales. Una respuesta copia ese id y trae un resultado o un error. Una notificación no tiene id, así que nada la responde.
Por stdio, cada mensaje es una línea de JSON. El servidor escribe solo mensajes en su salida y manda sus registros por el flujo de errores.
Ejemplo resuelto: tres mensajes
| Clase | Ejemplo | Respuesta |
|---|---|---|
| Petición | {"jsonrpc":"2.0","id":2,"method":"tools/list"} | Una respuesta con id 2 |
| Respuesta | {"jsonrpc":"2.0","id":2,"result":{"tools":[]}} | Ninguna |
| Notificación | {"jsonrpc":"2.0","method":"notifications/initialized"} | Ninguna |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Es la forma, no el protocolo entero
Importante
El servidor de abajo tiene la forma del protocolo real, en pequeño. Un servidor real usa el SDK oficial, que también gestiona versiones, cancelación, más tipos de contenido y muchos más métodos. Aquí no se instala nada.
Escribir el servidor de herramientas, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const PRICES = { pan: 90, leche: 110 };' 'const TOOLS = [{ name: \"get_price\", description: \"Precio de un producto de la tienda en céntimos\", inputSchema: { type: \"object\", properties: { item: { type: \"string\" } }, required: [\"item\"] } }];' 'function send(message) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, message)) + \"\n\"); }' 'function reply(id, result) { send({ id: id, result: result }); }' 'function fail(id, code, text) { send({ id: id, error: { code: code, message: text } }); }' 'function text(words, isError) { return { content: [{ type: \"text\", text: words }], isError: isError }; }' > server.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el servidor de herramientas, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function call(id, params) {' ' if (params.name === \"get_price\") {' ' const price = PRICES[(params.arguments || {}).item];' ' if (price === undefined) return reply(id, text(\"Producto desconocido\", true));' ' return reply(id, text(price + \" céntimos\", false));' ' }' ' return fail(id, -32602, \"Herramienta desconocida: \" + params.name);' '}' 'function handle(msg) {' ' if (msg.id === undefined) return;' ' if (msg.method === \"initialize\") return reply(msg.id, { protocolVersion: \"2024-11-05\", capabilities: { tools: {} }, serverInfo: { name: \"tienda\", version: \"0.1\" } });' ' if (msg.method === \"tools/list\") return reply(msg.id, { tools: TOOLS });' ' if (msg.method === \"tools/call\") return call(msg.id, msg.params || {});' ' return fail(msg.id, -32601, \"Método no encontrado: \" + msg.method);' '}' >> server.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el servidor de herramientas, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'require(\"readline\").createInterface({ input: process.stdin }).on(\"line\", function (line) {' ' try { handle(JSON.parse(line)); } catch (error) { fail(null, -32700, \"Error de análisis\"); }' '});' >> server.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el cliente con guion, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const server = require(\"child_process\").spawn(\"node\", [\"server.js\"]);' 'const call = function (n, name, args) { return { id: n, method: \"tools/call\", params: { name: name, arguments: args } }; };' 'const script = [' ' { id: 1, method: \"initialize\", params: { protocolVersion: \"2024-11-05\", capabilities: {}, clientInfo: { name: \"demo\", version: \"0.1\" } } },' ' { method: \"notifications/initialized\" },' ' { id: 2, method: \"tools/list\" },' ' call(3, \"get_price\", { item: \"leche\" }),' ' call(4, \"get_price\", { item: \"kiwi\" }),' ' call(5, \"delete_all\", {}),' ' { id: 6, method: \"resources/list\" },' '];' > client.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribir el cliente con guion, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'let waiting = 6;' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) {' ' console.log(\"<- \" + line);' ' waiting -= 1;' ' if (waiting === 0) server.stdin.end();' '});' 'script.forEach(function (m) {' ' const full = Object.assign({ jsonrpc: \"2.0\" }, m);' ' console.log(\"-> \" + JSON.stringify(full));' ' server.stdin.write(JSON.stringify(full) + \"\n\");' '});' >> client.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecutar el cliente contra el servidor
docker exec -w /home/node/project agent node client.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Una salida posible
| Petición | Respuesta |
|---|---|
| initialize | Un resultado con la versión del protocolo y el nombre del servidor |
| tools/list | Un resultado que lista get_price |
| get_price para la leche | Un resultado con el texto 110 céntimos |
| get_price para un kiwi | Un resultado con isError true y Producto desconocido |
| delete_all | Un error con código -32602 |
| resources/list | Un error con código -32601 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una sesión sigue un orden
El cliente envía primero initialize y el servidor responde con lo que admite. Después el cliente envía la notificación de que está listo. Solo entonces pide tools/list y usa tools/call.
El agente enseña al modelo las herramientas listadas. Cuando el modelo pide una, el cliente envía tools/call.
Dos clases de fallo
Un error de protocolo es una respuesta de error JSON-RPC. La petición misma estaba mal: método desconocido, herramienta desconocida o JSON ilegible. Cada uno tiene un código, como -32601 para un método que falta.
Un error de herramienta es un resultado normal con isError en true. La llamada era correcta, pero la herramienta no pudo hacerlo. El modelo lee el texto y puede probar otra cosa.
Un error frecuente
Advertencia
Mucha gente cree que todo fallo debe ser un error JSON-RPC. Una respuesta de error dice que la petición está mal formada. Una herramienta que se ejecutó y no encontró el producto debe dar un resultado con isError en true. Así el modelo ve el motivo y puede recuperarse.