Cómo lee sus mensajes un servidor MCP
Un servidor MCP es un programa que lee mensajes JSON de su entrada y escribe mensajes JSON en su salida, uno por línea. Si los maneja mal, funciona solo en tu prueba y falla con cualquier cliente real.
El módulo 67, lección 5, mostró el lado del cliente y los tipos de mensaje. Aquí escribes el bucle de lectura de un servidor.
Al final de esta lección
Nota
Puedo leer y escribir los mensajes JSON que se intercambian un servidor y un cliente.
Palabras clave
| Palabra | Qué significa | Ejemplo |
|---|---|---|
| Búfer | texto guardado hasta que llega una línea entera | |
| Trama | un mensaje entero cortado del flujo en un salto de línea | |
| stdout | el flujo de salida: aquí solo van mensajes | |
| stderr | el flujo de errores: aquí van los registros | |
| Parse error | la línea no era JSON | -32700 |
| Invalid Request | es JSON, pero no una petición | -32600 |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un flujo se corta en líneas
La entrada no llega en mensajes enteros. Llega en trozos. Un trozo puede traer medio mensaje, uno entero o dos y medio.
El servidor guarda un búfer. Añade cada trozo, corta cada línea completa y se queda con el resto. Un mensaje nunca contiene un salto de línea suelto, así que un salto siempre marca el final de uno.
Ejemplo resuelto: tres trozos
| Llega el trozo | Búfer después | Mensajes listos |
|---|---|---|
| {"id":1} {"id | {"id | 1 mensaje: {"id":1} |
| ":2} | (vacío) | 1 mensaje: {"id":2} |
| {"i | {"i | Ninguno aún |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una línea mala también recibe respuesta
Una línea que no es JSON recibe un parse error. El servidor no puede leer ningún id, así que el id es null.
Una línea que es JSON pero no una petición recibe Invalid Request. Si se puede leer un id, se copia. Una petición sin id es una notificación, y ni siquiera una notificación de un método desconocido recibe respuesta.
Lo que devuelve el servidor
| Línea | Respuesta |
|---|---|
| no es json | Error -32700, id null |
| {"jsonrpc":"2.0","id":5,"method":5} | Error -32600, id 5 |
| [1,2] | Error -32600, id null |
| {"jsonrpc":"2.0","method":"nonsense"} | Nada: es una notificación |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Las respuestas pueden volver en cualquier orden
Una petición lenta no debe frenar a una rápida. El servidor responde a cada petición cuando su respuesta está lista.
Por eso un cliente no puede emparejar las respuestas por posición. Las empareja por id. Por eso el servidor copia el id tal cual, sea número o texto, igual que lo recibió.
Un error frecuente
Advertencia
Mucha gente depura con una línea de impresión. En stdout esa línea no es un mensaje, y el cliente falla con ella. Solo los mensajes van a stdout. Escribe los registros en stderr, que el cliente puede mostrar o ignorar.
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' 'let buffer = \"\";' 'function send(m) { process.stdout.write(JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, m)) + \"\n\"); }' 'function fail(id, code, text) { send({ id: id, error: { code: code, message: text } }); }' 'function log(text) { process.stderr.write(\"log: \" + text + \"\n\"); }' > server1.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 handle(line) {' ' let msg;' ' try { msg = JSON.parse(line); } catch (error) { return fail(null, -32700, \"Parse error\"); }' ' const seen = msg && (typeof msg.id === \"number\" || typeof msg.id === \"string\") ? msg.id : null;' ' const good = msg && typeof msg === \"object\" && msg.jsonrpc === \"2.0\" && typeof msg.method === \"string\";' ' if (Boolean(good) === false) return fail(seen, -32600, \"Invalid Request\");' ' log(\"recibido \" + msg.method);' ' if (msg.id === undefined) return;' ' const ms = (msg.params || {}).ms || 0;' ' if (msg.method === \"ping\") return send({ id: msg.id, result: {} });' ' if (msg.method === \"wait\") return setTimeout(function () { send({ id: msg.id, result: { waited: ms } }); }, ms);' ' fail(msg.id, -32601, \"Method not found: \" + msg.method);' '}' >> server1.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' 'process.stdin.on(\"data\", function (chunk) {' ' buffer += chunk.toString();' ' let at = buffer.indexOf(\"\n\");' ' while (at >= 0) {' ' const line = buffer.slice(0, at);' ' buffer = buffer.slice(at + 1);' ' if (line.trim().length > 0) handle(line);' ' at = buffer.indexOf(\"\n\");' ' }' '});' 'log(\"listo\");' >> server1.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\", [\"server1.js\"]);' 'const m = function (obj) { return JSON.stringify(Object.assign({ jsonrpc: \"2.0\" }, obj)); };' 'const wait = function (id, ms) { return m({ id: id, method: \"wait\", params: { ms: ms } }) + \"\n\"; };' 'const ping = m({ id: 3, method: \"ping\" }) + \"\n\";' 'const bad = \"no es json\n\" + m({ id: 5, method: 5 }) + \"\n\" + m({ method: \"notifications/x\" }) + \"\n\";' 'const steps = [[0, wait(1, 80)], [10, wait(2, 10)], [20, ping.slice(0, 12)], [40, ping.slice(12) + bad]];' > client1.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' 'function start() {' ' steps.forEach(function (s) {' ' setTimeout(function () { console.log(\"-> \" + s[1].trim().split(\"\n\").join(\" | \")); server.stdin.write(s[1]); }, s[0]);' ' });' ' setTimeout(function () { server.stdin.end(); }, 200);' '}' 'require(\"readline\").createInterface({ input: server.stderr }).on(\"line\", function (line) {' ' console.log(\"stderr \" + line);' ' if (line === \"log: listo\") start();' '});' 'require(\"readline\").createInterface({ input: server.stdout }).on(\"line\", function (line) { console.log(\"<- \" + line); });' >> client1.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 q = \"En una frase, ¿por qué una respuesta debe llevar el id de su petición?\";' ' const r = await ask([{ role: \"user\", content: q }], \"Para que el cliente sepa a qué petición pertenece cada respuesta, porque pueden llegar en cualquier orden.\", 60);' ' console.log(\"el modelo dijo: \" + r.text);' '});' >> client1.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 client1.jsSe 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 cliente
El cliente arranca el servidor y espera su línea de listo en stderr. Envía dos peticiones lentas, medio ping y luego el resto del ping con tres líneas más.
Una llamada en vivo al final pide al modelo una frase. Sin modelo, el Run imprime una Muestra grabada para esa línea.
Las respuestas, en el orden en que llegan
| Línea impresa con <- | Por qué llegó entonces |
|---|---|
| id 2, waited 10 | La espera corta acabó primero |
| id 3, un resultado vacío | El ping quedó entero al llegar su segunda mitad |
| id null, error -32700 | La línea "no es json" |
| id 5, error -32600 | Un método que es un número |
| id 1, waited 80 | La espera larga acabó la última |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.