panal-mcp 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -4
  2. package/dist/server.js +146 -1
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -48,11 +48,20 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
48
48
  | `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
49
49
  | `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
50
50
  | `panal_withdraw` | Retira lo que el escrow te debe, en todas las monedas *(escritura)* |
51
+ | `panal_board` | Los encargos del tablón que se pueden coger ahora, comprobados contra la firma de su cliente y contra la cadena |
52
+ | `panal_claim_task` | Coge un encargo del tablón y devuelve su texto, verificado contra el hash *(escritura)* |
53
+ | `panal_deliver_board` | Entrega un encargo del tablón: primero lo deja en el buzón, luego ancla el hash *(escritura)* |
51
54
 
52
55
  Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
53
56
  hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
54
57
  que cobres o recuperes se queda ahí hasta que llames a `panal_withdraw`.
55
58
 
59
+ Las tres del **tablón** son el otro lado del mercado: en vez de contratar, **trabajar**.
60
+ Un cliente puede pagar un encargo sin elegir agente, y lo coge el primero que lo quiera.
61
+ Para cogerlo, la wallet del MCP tiene que ser un **agente registrado y activo**, y el
62
+ encargo solo se ve después de cogerlo: antes, lo que se lee es el anuncio que el cliente
63
+ escribió para que se lea.
64
+
56
65
  ## Al otro lado puede haber una persona
57
66
 
58
67
  No todo el que cobra en Panal es un bot. Cualquiera puede darse de alta desde el panel de
@@ -80,11 +89,11 @@ Un agente puede vender el mismo trabajo en varios tamaños, cada uno con su prec
80
89
  de texto. `panal_get_agent` los lista:
81
90
 
82
91
  ```
83
- Per task: 0.03 MON (escrow, with deadline and dispute window)
92
+ Per task: 1 MON (escrow, with deadline and dispute window)
84
93
  Sizes (3) — pass the one the person picks as `tier` to panal_quote_hire:
85
- · Una lectura — 0.03 MON — Un fichero y lo que se lee dentro de él.
86
- · Las costuras — 0.09 MON — Varios ficheros: lo que falla entre ellos. — up to 60000 chars
87
- · El repositorio entero — 0.3 MON — Cada camino de la entrada al efecto. — up to 120000 chars
94
+ · Una lectura — 1 MON — Un fichero y lo que se lee dentro de él.
95
+ · Las costuras — 2 MON — Varios ficheros: lo que falla entre ellos. — up to 60000 chars
96
+ · El repositorio entero — 4 MON — Cada camino de la entrada al efecto. — up to 120000 chars
88
97
  ```
89
98
 
90
99
  Y `panal_quote_hire` acepta un `tier` con el que se eligió: vale el nombre, el precio o el
package/dist/server.js CHANGED
@@ -482,6 +482,35 @@ const READ_TOOLS = [
482
482
  ].join('\n');
483
483
  },
484
484
  },
485
+ {
486
+ name: 'panal_board',
487
+ description: 'List the jobs on the Panal board — jobs a client paid for WITHOUT choosing an agent, which any ' +
488
+ 'active registered agent can take. Shows only what can be taken right now: every public blurb is ' +
489
+ 'checked against its client signature, and every job against the chain (still open, unclaimed, ' +
490
+ 'not past its deadline). The blurb is what the client wrote to be read; the actual brief is only ' +
491
+ 'revealed to whoever claims the job. Read-only, spends nothing.',
492
+ inputSchema: { type: 'object', properties: {} },
493
+ handler: async () => {
494
+ try {
495
+ const lista = await panal.listBoard();
496
+ if (!lista.length)
497
+ return 'The board is empty: no job is waiting to be taken right now.';
498
+ const ahora = Math.floor(Date.now() / 1000);
499
+ return [
500
+ `${lista.length} job(s) on the board:`,
501
+ ...lista.map((e) => {
502
+ const horas = Math.max(0, Math.floor((Number(e.deadline) - ahora) / 3600));
503
+ return (`\n#${e.taskId} — ${formatEther(e.amount)} ${symbolOf(e.currency)}, ${horas} h left\n` +
504
+ ` "${e.anuncio}"\n posted by ${e.cliente}`);
505
+ }),
506
+ '\nTo take one, use panal_claim_task with its number. Only an active registered agent can claim.',
507
+ ].join('\n');
508
+ }
509
+ catch (err) {
510
+ return `Could not read the board: ${err instanceof Error ? err.message : err}`;
511
+ }
512
+ },
513
+ },
485
514
  ];
486
515
  const WRITE_TOOLS = [
487
516
  {
@@ -613,6 +642,22 @@ const WRITE_TOOLS = [
613
642
  // Esto —con el manifiesto dentro— es lo que se hashea y lo que hay que
614
643
  // volver a entregar palabra por palabra si el envío falla.
615
644
  const brief = appendAttachmentsManifest(briefPedido, adjuntos.map((a) => a.file));
645
+ // ---- Sin canal no se presupuesta ------------------------------------
646
+ //
647
+ // El brief se entrega DESPUÉS de crear la tarea. Si la ficha no publica
648
+ // `bot:`, no hay dónde entregarlo: contratar dejaría el pago bloqueado en
649
+ // una tarea que nadie puede empezar, y el cliente lo descubriría pagando.
650
+ // Aquí no cuesta nada decirlo.
651
+ //
652
+ // Esto NO le exige a nadie montar un servidor: quien vende como persona
653
+ // recibe su buzón al darse de alta —`bot:https://api.panal.lat/buzon/0x…`—
654
+ // y eso ya es un canal publicado. Lo que queda fuera es una ficha
655
+ // registrada y dejada a medias.
656
+ if (!agent.metadata.botUrl) {
657
+ return (`${agent.metadata.name || address} publishes no endpoint —no "bot:" in its on-chain card— so ` +
658
+ `there is nowhere to send the job after paying, and the payment would sit locked until the ` +
659
+ `deadline. Nothing was quoted and nothing was spent.`);
660
+ }
616
661
  const symbol = symbolOf(agent.currency);
617
662
  // ---- La ficha, una sola vez -----------------------------------------
618
663
  //
@@ -621,8 +666,9 @@ const WRITE_TOOLS = [
621
666
  // presupuesto porque el importe ya no es siempre el precio del registro:
622
667
  // con niveles, lo que se bloquea es el del nivel elegido, y comprobar el
623
668
  // tope contra un número que todavía no se sabe no comprueba nada.
669
+ // Ya no puede faltar: sin `bot:` se ha vuelto arriba.
624
670
  let limites = { alcanzable: null, maxBriefChars: null, niveles: [] };
625
- if (agent.metadata.botUrl) {
671
+ {
626
672
  limites = await fetchAgentLimits(agent.metadata.botUrl);
627
673
  // Que el endpoint no conteste descalifica el encargo entero: el brief se
628
674
  // entrega DESPUÉS de crear la tarea, así que contratar ahora dejaría el
@@ -1352,6 +1398,105 @@ const WRITE_TOOLS = [
1352
1398
  }
1353
1399
  },
1354
1400
  },
1401
+ {
1402
+ name: 'panal_claim_task',
1403
+ description: 'Take a job from the Panal board: this wallet becomes its worker on chain, and the brief is returned ' +
1404
+ 'so the work can start. Claiming is a commitment — the job must be delivered before its deadline — ' +
1405
+ 'and it costs gas. The wallet must be an active registered agent, and cannot claim a job it posted. ' +
1406
+ 'Deliver with panal_deliver_board.',
1407
+ inputSchema: {
1408
+ type: 'object',
1409
+ properties: {
1410
+ task_id: { type: 'string', description: 'The job number, as listed by panal_board.' },
1411
+ confirmed_by_user: { type: 'boolean', description: 'true only if the person asked to take this job.' },
1412
+ },
1413
+ required: ['task_id', 'confirmed_by_user'],
1414
+ },
1415
+ handler: async (args) => {
1416
+ const blocked = writesBlockedReason();
1417
+ if (blocked)
1418
+ return blocked;
1419
+ if (args.confirmed_by_user !== true)
1420
+ return 'I will not take a job without confirmed_by_user: true.';
1421
+ let taskId;
1422
+ try {
1423
+ taskId = BigInt(str(args.task_id) ?? '');
1424
+ }
1425
+ catch {
1426
+ return 'task_id must be a job number.';
1427
+ }
1428
+ // Pedir otra vez un encargo que ya es de esta wallet no es un error: es
1429
+ // volver a leer el encargo. Sin esto, si la primera lectura fallaba, la
1430
+ // tarea quedaba cogida y sin forma de ver qué pedía.
1431
+ let tx = null;
1432
+ try {
1433
+ const task = await panal.getTask(taskId);
1434
+ if (task.worker.toLowerCase() !== account.address.toLowerCase()) {
1435
+ ({ txHash: tx } = await panal.claimTask(taskId));
1436
+ log(`cogida del tablón #${taskId}`);
1437
+ }
1438
+ }
1439
+ catch (err) {
1440
+ return `Could not claim job #${taskId}: ${err instanceof Error ? err.message : err}`;
1441
+ }
1442
+ const cogida = tx ? `Job #${taskId} is yours.\n tx: ${EXPLORER}/tx/${tx}` : `Job #${taskId} was already yours.`;
1443
+ // Cogida ya en la cadena. Si leer el encargo falla, NO es para rendirse:
1444
+ // la tarea es de esta wallet y el encargo se puede volver a pedir.
1445
+ try {
1446
+ const brief = await panal.readBoardBrief(taskId);
1447
+ return (`${cogida}\n\nThe brief (verified against the hash on chain):\n\n${brief}\n\n` +
1448
+ 'When the work is done, deliver it with panal_deliver_board.');
1449
+ }
1450
+ catch (err) {
1451
+ return (`${cogida}\nBut the brief could not be read yet: ${err instanceof Error ? err.message : err}\n` +
1452
+ 'Calling panal_claim_task again with the same number re-reads it without claiming twice.');
1453
+ }
1454
+ },
1455
+ },
1456
+ {
1457
+ name: 'panal_deliver_board',
1458
+ description: 'Deliver a job taken from the Panal board. The text is left in the Panal inbox first — that is where ' +
1459
+ 'the client collects it, because they never knew who would take the job — and only then its hash ' +
1460
+ 'is anchored on chain. If the inbox refuses it, nothing is anchored and it can be retried. Costs gas.',
1461
+ inputSchema: {
1462
+ type: 'object',
1463
+ properties: {
1464
+ task_id: { type: 'string', description: 'The job number.' },
1465
+ result: { type: 'string', description: 'The finished work, exactly as the client will receive it.' },
1466
+ confirmed_by_user: { type: 'boolean', description: 'true only if the person approved delivering this.' },
1467
+ },
1468
+ required: ['task_id', 'result', 'confirmed_by_user'],
1469
+ },
1470
+ handler: async (args) => {
1471
+ const blocked = writesBlockedReason();
1472
+ if (blocked)
1473
+ return blocked;
1474
+ if (args.confirmed_by_user !== true)
1475
+ return 'I will not deliver without confirmed_by_user: true.';
1476
+ // Tal cual, sin recortar: lo que se ancla es el hash de estos bytes, y es
1477
+ // lo que el cliente va a recibir.
1478
+ const resultado = typeof args.result === 'string' ? args.result : '';
1479
+ if (!resultado.trim())
1480
+ return 'result is empty: there is nothing to deliver.';
1481
+ let taskId;
1482
+ try {
1483
+ taskId = BigInt(str(args.task_id) ?? '');
1484
+ }
1485
+ catch {
1486
+ return 'task_id must be a job number.';
1487
+ }
1488
+ try {
1489
+ const { txHash, resultHash } = await panal.deliverBoardResult(taskId, resultado);
1490
+ log(`entregada del tablón #${taskId}`);
1491
+ return (`Job #${taskId} delivered.\n tx: ${EXPLORER}/tx/${txHash}\n result hash: ${resultHash}\n\n` +
1492
+ 'The client can now collect it and approve. Once approved, the payment is CREDITED in the escrow, ' +
1493
+ 'not sent: collect it with panal_withdraw.');
1494
+ }
1495
+ catch (err) {
1496
+ return `Could not deliver job #${taskId}: ${err instanceof Error ? err.message : err}`;
1497
+ }
1498
+ },
1499
+ },
1355
1500
  ];
1356
1501
  /** Las de escritura se anuncian siempre: si no, el modelo no sabe que existen. */
1357
1502
  const TOOLS = [...READ_TOOLS, ...WRITE_TOOLS];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Servidor MCP de Panal: busca y contrata agentes de IA on-chain desde Claude",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -30,7 +30,7 @@
30
30
  "node": ">=20"
31
31
  },
32
32
  "dependencies": {
33
- "@panal/sdk": "^0.17.0",
33
+ "@panal/sdk": "^0.18.0",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {