panal-mcp 0.8.0 → 0.8.1

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.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
37
37
  | `panal_get_agent` | Ficha completa: precio, skills, estado y nombre único leído de la cadena |
38
38
  | `panal_get_task` | Estado de un encargo en el escrow |
39
39
  | `panal_marketplace_stats` | Cifras del marketplace |
40
- | `panal_wallet` | Saldo y presupuesto restante *(escritura)* |
40
+ | `panal_wallet` | Saldo en cada moneda (MON y $PANAL), presupuesto restante y lo que el escrow te debe *(escritura)* |
41
41
  | `panal_quote_hire` | Presupuesta un encargo sin pagar, y comprueba que el agente responde *(escritura)* |
42
42
  | `panal_hire` | Contrata y bloquea el pago *(escritura)* |
43
43
  | `panal_send_brief` | Reenvía el encargo si no llegó al contratar *(escritura)* |
@@ -46,7 +46,7 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
46
46
  | `panal_quote_ask` / `panal_ask` | Pregunta suelta a un agente, sin abrir un encargo *(escritura)* |
47
47
  | `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
48
48
  | `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
49
- | `panal_withdraw` | Retira lo que el escrow te debe *(escritura)* |
49
+ | `panal_withdraw` | Retira lo que el escrow te debe, en todas las monedas *(escritura)* |
50
50
 
51
51
  Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
52
52
  hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
package/dist/server.js CHANGED
@@ -34,7 +34,7 @@ import { createInterface } from 'node:readline';
34
34
  import { fileURLToPath } from 'node:url';
35
35
  import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
36
36
  import { privateKeyToAccount } from 'viem/accounts';
37
- import { NATIVE_CURRENCY, TaskStatus, createPanalClient, } from '@panal/sdk';
37
+ import { NATIVE_CURRENCY, TaskStatus, createPanalClient, erc20Abi, } from '@panal/sdk';
38
38
  import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
39
39
  import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
40
40
  const SERVER_NAME = 'panal';
@@ -94,8 +94,39 @@ function writesBlockedReason() {
94
94
  // ---------------------------------------------------------------------------
95
95
  // Presentación
96
96
  // ---------------------------------------------------------------------------
97
+ /** ¿Es la moneda nativa de la cadena, o un token? */
98
+ function esNativa(currency) {
99
+ return currency.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
100
+ }
97
101
  function symbolOf(currency) {
98
- return currency.toLowerCase() === NATIVE_CURRENCY.toLowerCase() ? 'MON' : '$PANAL';
102
+ return esNativa(currency) ? 'MON' : '$PANAL';
103
+ }
104
+ /** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
105
+ function monedasConocidas() {
106
+ return [...limits.porMoneda.keys()];
107
+ }
108
+ /**
109
+ * Saldo de una wallet en una moneda: a la cadena si es la nativa, al contrato
110
+ * del token si es un ERC-20.
111
+ *
112
+ * `null` es «no se pudo leer», y se distingue de cero a propósito. Enseñar 0
113
+ * cuando la consulta falla es afirmar que no hay fondos, que es una respuesta
114
+ * distinta: llevaría a no intentar un pago que sí se podía hacer.
115
+ */
116
+ async function saldoDe(owner, currency) {
117
+ try {
118
+ if (esNativa(currency))
119
+ return await panal.publicClient.getBalance({ address: owner });
120
+ return (await panal.publicClient.readContract({
121
+ address: currency,
122
+ abi: erc20Abi,
123
+ functionName: 'balanceOf',
124
+ args: [owner],
125
+ }));
126
+ }
127
+ catch {
128
+ return null;
129
+ }
99
130
  }
100
131
  /**
101
132
  * ¿Cabe este gasto en el presupuesto de SU moneda? Devuelve el motivo, o null.
@@ -370,36 +401,54 @@ const READ_TOOLS = [
370
401
  const WRITE_TOOLS = [
371
402
  {
372
403
  name: 'panal_wallet',
373
- description: 'State of the wallet this server would pay with: address, balance, and how much of today\'s budget ' +
374
- 'is left. Call it before hiring to know whether there are funds.',
404
+ description: 'State of the wallet this server would pay with: address, the balance of EVERY currency Panal ' +
405
+ 'takes (MON and $PANAL), and how much of today\'s budget is left. Call it before hiring to know ' +
406
+ 'whether there are funds.',
375
407
  inputSchema: { type: 'object', properties: {} },
376
408
  handler: async () => {
377
409
  const blocked = writesBlockedReason();
378
410
  if (blocked)
379
411
  return blocked;
380
412
  const address = account.address;
381
- const balance = await panal.publicClient.getBalance({ address });
413
+ const monedas = monedasConocidas();
414
+ // Un saldo por moneda, no solo el nativo. Antes se leía únicamente MON y
415
+ // justo debajo se listaban los topes de $PANAL: quien lo leía daba por
416
+ // comprobado un saldo que nadie había mirado, y podía contratar en $PANAL
417
+ // sin tener con qué pagar. Las lecturas son independientes, así que van
418
+ // en paralelo.
419
+ const [saldos, pendientes] = await Promise.all([
420
+ Promise.all(monedas.map((m) => saldoDe(address, m))),
421
+ // Lo acreditado en el escrow no aparece en el balance de la wallet: es
422
+ // pull payment, así que un reembolso se queda ahí quieto hasta que se
423
+ // reclama. Sin decirlo, el dinero devuelto es indistinguible del
424
+ // perdido — y también hay que preguntarlo moneda a moneda.
425
+ Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
426
+ ]);
427
+ const importe = (v) => (v === null ? 'could not be read' : formatEther(v));
428
+ const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null)}`);
382
429
  // Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
383
430
  // tipo de cambio, así que se enseñan por separado o no se entienden.
384
- const presupuestos = [...limits.porMoneda.entries()].map(([moneda, lim]) => {
431
+ const presupuestos = monedas.map((moneda) => {
432
+ const lim = limitFor(limits, moneda);
385
433
  const spent = ledger.spentToday(moneda);
386
434
  const left = lim.dailyBudgetWei > spent ? lim.dailyBudgetWei - spent : 0n;
387
- const sym = symbolOf(moneda);
388
- return (` ${sym}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
435
+ return (` ${symbolOf(moneda)}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
389
436
  `${formatEther(lim.dailyBudgetWei)} spent · ${formatEther(left)} left`);
390
437
  });
391
- // Lo acreditado en el escrow no aparece en el balance de la wallet: es
392
- // pull payment, así que un reembolso se queda ahí quieto hasta que se
393
- // reclama. Sin decirlo, el dinero devuelto es indistinguible del perdido.
394
- const pending = await panal.getPendingWithdrawal(address).catch(() => 0n);
438
+ // Solo se nombra lo que hay que cobrar, o lo que no se pudo comprobar. Un
439
+ // «0» por cada moneda sería ruido en la respuesta más leída del servidor.
440
+ const enEscrow = monedas
441
+ .map((m, i) => ({ sym: symbolOf(m), v: pendientes[i] ?? null }))
442
+ .filter(({ v }) => v === null || v > 0n)
443
+ .map(({ sym, v }) => ` ${sym}: ${importe(v)}`);
395
444
  return [
396
445
  `Wallet: ${address}`,
397
- `Balance: ${formatEther(balance)} MON`,
446
+ 'Balance:',
447
+ ...balances,
398
448
  'Caps and budgets (each currency is counted on its own):',
399
449
  ...presupuestos,
400
- pending > 0n
401
- ? `Waiting in the escrow: ${formatEther(pending)} MON — collect it with panal_withdraw`
402
- : null,
450
+ enEscrow.length ? 'Waiting in the escrow — collect it with panal_withdraw:' : null,
451
+ ...enEscrow,
403
452
  ]
404
453
  .filter(Boolean)
405
454
  .join('\n');
@@ -838,9 +887,9 @@ const WRITE_TOOLS = [
838
887
  },
839
888
  {
840
889
  name: 'panal_withdraw',
841
- description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs. The escrow is pull ' +
842
- 'payment: it credits balances and never pushes them, so money sits there until this is called. ' +
843
- 'Call panal_wallet first to see whether there is anything to collect.',
890
+ description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs — in EVERY currency it ' +
891
+ 'holds, one transaction each. The escrow is pull payment: it credits balances and never pushes ' +
892
+ 'them, so money sits there until this is called. Call panal_wallet first to see what is waiting.',
844
893
  inputSchema: {
845
894
  type: 'object',
846
895
  properties: {
@@ -856,11 +905,36 @@ const WRITE_TOOLS = [
856
905
  return 'I will not move funds without confirmed_by_user: true.';
857
906
  }
858
907
  try {
859
- const pending = await panal.getPendingWithdrawal(account.address);
860
- if (pending === 0n)
908
+ // Se retira MONEDA A MONEDA. El escrow lleva un saldo por cada una y
909
+ // `withdraw` solo vacía la que se le pide; preguntando únicamente por
910
+ // la nativa, un reembolso en $PANAL quedaba invisible y esta misma
911
+ // herramienta contestaba que no había nada que cobrar. El dinero seguía
912
+ // en el contrato, pero desde aquí no había forma de sacarlo.
913
+ const monedas = monedasConocidas();
914
+ const pendientes = [];
915
+ for (const moneda of monedas) {
916
+ const importe = await panal.getPendingWithdrawal(account.address, moneda);
917
+ if (importe > 0n)
918
+ pendientes.push({ moneda, importe });
919
+ }
920
+ if (!pendientes.length)
861
921
  return 'The escrow owes this wallet nothing: there is nothing to collect.';
862
- const hash = await panal.withdraw();
863
- return `Collected ${formatEther(pending)} MON from the escrow.\n tx: ${EXPLORER}/tx/${hash}`;
922
+ // Cada moneda es una transacción aparte, y si una falla se sigue con
923
+ // las demás: quedarse sin cobrar el $PANAL porque el MON se quedó sin
924
+ // gas es perder dos veces.
925
+ const lineas = [];
926
+ for (const { moneda, importe } of pendientes) {
927
+ const sym = symbolOf(moneda);
928
+ try {
929
+ const hash = await panal.withdraw(moneda);
930
+ lineas.push(`Collected ${formatEther(importe)} ${sym} from the escrow.\n tx: ${EXPLORER}/tx/${hash}`);
931
+ }
932
+ catch (err) {
933
+ lineas.push(`Could not collect the ${formatEther(importe)} ${sym}: ${err instanceof Error ? err.message : err}` +
934
+ '\n It is still credited in the escrow; calling this again will retry it.');
935
+ }
936
+ }
937
+ return lineas.join('\n');
864
938
  }
865
939
  catch (err) {
866
940
  return `Could not withdraw: ${err instanceof Error ? err.message : err}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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",