¿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

Leer y escribir mensajes MCP

Después de esta lección: Puedo leer y escribir los mensajes JSON que se intercambian un servidor y un cliente.

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

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

PalabraQué significaEjemplo
Búfertexto guardado hasta que llega una línea entera
Tramaun mensaje entero cortado del flujo en un salto de línea
stdoutel flujo de salida: aquí solo van mensajes
stderrel flujo de errores: aquí van los registros
Parse errorla línea no era JSON-32700
Invalid Requestes 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 trozoBúfer despuésMensajes listos
{"id":1} {"id{"id1 mensaje: {"id":1}
":2} (vacío)1 mensaje: {"id":2}
{"i{"iNinguno 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íneaRespuesta
no es jsonError -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

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' '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

Comando
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

Comando
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

Comando
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

Comando
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

Comando
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

Comando
docker exec -w /home/node/project agent node 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.

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 10La espera corta acabó primero
id 3, un resultado vacíoEl ping quedó entero al llegar su segunda mitad
id null, error -32700La línea "no es json"
id 5, error -32600Un método que es un número
id 1, waited 80La espera larga acabó la última

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.