panal-mcp 0.12.0 → 0.13.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Panal and Kreno (founder: Gustavo Chura C.)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -143,6 +143,12 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
143
143
  | `MCP_DAILY_BUDGET_WEI` | `5e18` (5 MON) | Tope por día UTC **en MON**, persistido en disco |
144
144
  | `MCP_MAX_PER_TASK_PANAL_WEI` | `1e18` (1 $PANAL) | Tope por encargo, **en $PANAL** |
145
145
  | `MCP_DAILY_BUDGET_PANAL_WEI` | `5e18` (5 $PANAL) | Tope por día UTC **en $PANAL** |
146
+ | `MCP_MAX_PER_TASK_GHO_UNITS` | `1e18` (1 GHO) | Tope por consulta **en GHO** |
147
+ | `MCP_DAILY_BUDGET_GHO_UNITS` | `5e18` (5 GHO) | Tope por día UTC **en GHO** |
148
+ | `MCP_MAX_PER_TASK_USDC_UNITS` | `1e6` (1 USDC) | Tope por consulta **en USDC** (6 decimales) |
149
+ | `MCP_DAILY_BUDGET_USDC_UNITS` | `5e6` (5 USDC) | Tope por día UTC **en USDC** |
150
+ | `MCP_MAX_PER_TASK_AUSD_UNITS` | `1e6` (1 AUSD) | Tope por consulta **en AUSD** (6 decimales) |
151
+ | `MCP_DAILY_BUDGET_AUSD_UNITS` | `5e6` (5 AUSD) | Tope por día UTC **en AUSD** |
146
152
  | `MCP_TASK_DEADLINE_HOURS` | `24` | Plazo de entrega |
147
153
  | `MCP_SPEND_FILE` | `.panal-mcp/spend.json` | Dónde se guarda el gasto del día |
148
154
  | `MCP_ATTACH_DIR` | el directorio de trabajo | Única carpeta desde la que se puede adjuntar |
package/dist/limits.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * servidor, y nunca en el prompt: un prompt se puede negociar, un `if` no.
7
7
  */
8
8
  import type { Address } from 'viem';
9
- import type { AttachedFile } from '@panal/sdk';
9
+ import { type AttachedFile } from '@panal/sdk';
10
10
  /**
11
11
  * Las dos maneras de pagar a un agente, y no son intercambiables.
12
12
  *
package/dist/limits.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
9
9
  import { dirname, resolve } from 'node:path';
10
10
  import { randomUUID } from 'node:crypto';
11
+ import { x402Currencies } from '@panal/sdk';
11
12
  /** Vida de un presupuesto. Corto a propósito: el precio del agente puede cambiar. */
12
13
  const QUOTE_TTL_MS = 5 * 60 * 1000;
13
14
  /**
@@ -193,5 +194,21 @@ export function limitsFromEnv() {
193
194
  envMaxPerTask: 'MCP_MAX_PER_TASK_PANAL_WEI',
194
195
  envDailyBudget: 'MCP_DAILY_BUDGET_PANAL_WEI',
195
196
  });
197
+ // Las estables con las que se paga por consulta: GHO, USDC y AUSD. Cada una
198
+ // con su cuenta, en SUS unidades mínimas (6 decimales en USDC y AUSD): 1 por
199
+ // consulta y 5 al día de partida, que es un dólar y cinco.
200
+ for (const c of x402Currencies()) {
201
+ // $PANAL y MON ya tienen las suyas, las de siempre.
202
+ if (c.address.toLowerCase() === PANAL || c.scheme === 'native-transfer')
203
+ continue;
204
+ const nombre = c.symbol.replace(/^\$/, '');
205
+ const unidad = 10n ** BigInt(c.decimals);
206
+ porMoneda.set(clave(c.address), {
207
+ maxPerTaskWei: parse(`MCP_MAX_PER_TASK_${nombre}_UNITS`, unidad),
208
+ dailyBudgetWei: parse(`MCP_DAILY_BUDGET_${nombre}_UNITS`, 5n * unidad),
209
+ envMaxPerTask: `MCP_MAX_PER_TASK_${nombre}_UNITS`,
210
+ envDailyBudget: `MCP_DAILY_BUDGET_${nombre}_UNITS`,
211
+ });
212
+ }
196
213
  return { deadlineHours: Number.isFinite(hours) && hours > 0 ? hours : 24, porMoneda };
197
214
  }
package/dist/server.js CHANGED
@@ -40,9 +40,9 @@ import { readFileSync } from 'node:fs';
40
40
  import { dirname, resolve } from 'node:path';
41
41
  import { createInterface } from 'node:readline';
42
42
  import { fileURLToPath } from 'node:url';
43
- import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
43
+ import { formatEther, formatUnits, isAddress, keccak256, parseEther, toBytes } from 'viem';
44
44
  import { privateKeyToAccount } from 'viem/accounts';
45
- import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, createPanalClient, downloadDeliveredFile, erc20Abi, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, } from '@panal/sdk';
45
+ import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, createPanalClient, downloadDeliveredFile, erc20Abi, matchAttachment, parseAttachmentsManifest, parseFilesManifest, stripFilesManifest, x402Currency, } from '@panal/sdk';
46
46
  import { MAX_ADJUNTOS, capacidadesDeAgente, guardarDescarga, leerAdjuntoLocal, raizAdjuntos, raizDescargas, subirAdjunto, } from './adjuntos.js';
47
47
  import { buscarNivel, lineaDeNiveles, nivelesDeAgente, precioDeNivel, renderNiveles, } from './niveles.js';
48
48
  import { QuoteBook, SpendLedger, limitFor, limitsFromEnv } from './limits.js';
@@ -108,8 +108,22 @@ function writesBlockedReason() {
108
108
  function esNativa(currency) {
109
109
  return currency.toLowerCase() === NATIVE_CURRENCY.toLowerCase();
110
110
  }
111
+ /**
112
+ * El nombre de una moneda, de la lista de Panal. Nunca el que trae la
113
+ * cotización: lo escribe el agente, y con un nombre a su gusto se podía cobrar
114
+ * en un token enseñando otro. Lo que no está en la lista se nombra por su
115
+ * dirección, que es lo único que se sabe de verdad.
116
+ */
111
117
  function symbolOf(currency) {
112
- return esNativa(currency) ? 'MON' : '$PANAL';
118
+ return esNativa(currency) ? 'MON' : (x402Currency(currency)?.symbol ?? currency);
119
+ }
120
+ /**
121
+ * Un importe en unidades mínimas, con los decimales de SU moneda: 18 en MON,
122
+ * $PANAL y GHO, 6 en USDC y AUSD. Con `formatEther` para todo, un millón de
123
+ * unidades de USDC —un dólar— salía como 0,000000000001.
124
+ */
125
+ function fmt(amount, currency) {
126
+ return formatUnits(amount, esNativa(currency) ? 18 : (x402Currency(currency)?.decimals ?? 18));
113
127
  }
114
128
  /** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
115
129
  function monedasConocidas() {
@@ -152,16 +166,16 @@ function revisarPresupuesto(currency, amount, symbol) {
152
166
  const lim = limitFor(limits, currency);
153
167
  if (!lim) {
154
168
  return (`That agent charges in a token this server has no budget for (${currency}). ` +
155
- 'Only MON and $PANAL have caps configured; there is no exchange rate to reuse another one.');
169
+ `Caps are configured for ${monedasConocidas().map(symbolOf).join(', ')}; there is no exchange rate to reuse another one.`);
156
170
  }
157
171
  if (amount > lim.maxPerTaskWei) {
158
- return (`That costs ${formatEther(amount)} ${symbol} and the per-item cap for ${symbol} is ` +
159
- `${formatEther(lim.maxPerTaskWei)}. Raise ${lim.envMaxPerTask} if that is intended.`);
172
+ return (`That costs ${fmt(amount, currency)} ${symbol} and the per-item cap for ${symbol} is ` +
173
+ `${fmt(lim.maxPerTaskWei, currency)}. Raise ${lim.envMaxPerTask} if that is intended.`);
160
174
  }
161
175
  const spent = ledger.spentToday(currency);
162
176
  if (spent + amount > lim.dailyBudgetWei) {
163
- return (`That would blow today's ${symbol} budget: ${formatEther(spent)} of ` +
164
- `${formatEther(lim.dailyBudgetWei)} spent and this costs ${formatEther(amount)} ${symbol}. ` +
177
+ return (`That would blow today's ${symbol} budget: ${fmt(spent, currency)} of ` +
178
+ `${fmt(lim.dailyBudgetWei, currency)} spent and this costs ${fmt(amount, currency)} ${symbol}. ` +
165
179
  `Raise ${lim.envDailyBudget} if that is intended.`);
166
180
  }
167
181
  return null;
@@ -365,7 +379,7 @@ const READ_TOOLS = [
365
379
  : Promise.resolve(null),
366
380
  ]);
367
381
  const porLlamada = q
368
- ? ` Per question: ${formatEther(BigInt(q.amount))} ${q.assetSymbol ?? symbolOf(q.asset)} (x402, answered on the spot)`
382
+ ? ` Per question: ${fmt(BigInt(q.amount), q.asset)} ${symbolOf(q.asset)} (x402, answered on the spot)`
369
383
  : ' Per question: not offered (this agent only takes jobs through escrow)';
370
384
  const niveles = nivelesDeAgente(agent.metadataURI, ficha?.niveles ?? []);
371
385
  return `${renderAgent(agent, niveles, true)}\n${porLlamada}`;
@@ -425,7 +439,7 @@ const READ_TOOLS = [
425
439
  `per task, via panal_quote_hire.`);
426
440
  }
427
441
  const amount = BigInt(quote.amount);
428
- const symbol = quote.assetSymbol ?? symbolOf(quote.asset);
442
+ const symbol = symbolOf(quote.asset);
429
443
  const saved = quotes.issue({
430
444
  kind: 'ask',
431
445
  worker: agent.address,
@@ -444,7 +458,7 @@ const READ_TOOLS = [
444
458
  });
445
459
  return [
446
460
  `${name} charges per question:`,
447
- ` Price: ${formatEther(amount)} ${symbol} for this one question`,
461
+ ` Price: ${fmt(amount, quote.asset)} ${symbol} for this one question`,
448
462
  ` Paid to: ${quote.payTo}`,
449
463
  quote.description ? ` Covers: ${quote.description}` : null,
450
464
  '',
@@ -538,23 +552,23 @@ const WRITE_TOOLS = [
538
552
  // perdido — y también hay que preguntarlo moneda a moneda.
539
553
  Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
540
554
  ]);
541
- const importe = (v) => (v === null ? 'could not be read' : formatEther(v));
542
- const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null)}`);
555
+ const importe = (v, m) => (v === null ? 'could not be read' : fmt(v, m));
556
+ const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null, m)}`);
543
557
  // Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
544
558
  // tipo de cambio, así que se enseñan por separado o no se entienden.
545
559
  const presupuestos = monedas.map((moneda) => {
546
560
  const lim = limitFor(limits, moneda);
547
561
  const spent = ledger.spentToday(moneda);
548
562
  const left = lim.dailyBudgetWei > spent ? lim.dailyBudgetWei - spent : 0n;
549
- return (` ${symbolOf(moneda)}: per job ${formatEther(lim.maxPerTaskWei)} · today ${formatEther(spent)} of ` +
550
- `${formatEther(lim.dailyBudgetWei)} spent · ${formatEther(left)} left`);
563
+ return (` ${symbolOf(moneda)}: per job ${fmt(lim.maxPerTaskWei, moneda)} · today ${fmt(spent, moneda)} of ` +
564
+ `${fmt(lim.dailyBudgetWei, moneda)} spent · ${fmt(left, moneda)} left`);
551
565
  });
552
566
  // Solo se nombra lo que hay que cobrar, o lo que no se pudo comprobar. Un
553
567
  // «0» por cada moneda sería ruido en la respuesta más leída del servidor.
554
568
  const enEscrow = monedas
555
- .map((m, i) => ({ sym: symbolOf(m), v: pendientes[i] ?? null }))
569
+ .map((m, i) => ({ m, sym: symbolOf(m), v: pendientes[i] ?? null }))
556
570
  .filter(({ v }) => v === null || v > 0n)
557
- .map(({ sym, v }) => ` ${sym}: ${importe(v)}`);
571
+ .map(({ m, sym, v }) => ` ${sym}: ${importe(v, m)}`);
558
572
  return [
559
573
  `Wallet: ${address}`,
560
574
  'Balance:',
@@ -826,7 +840,7 @@ const WRITE_TOOLS = [
826
840
  // Se registra lo REALMENTE pagado, y en SU moneda: un gasto anotado en
827
841
  // el contador equivocado agota un presupuesto que nadie tocó.
828
842
  ledger.record(quote.currency, res.paid);
829
- log(`consulta pagada a ${quote.worker}: ${formatEther(res.paid)} ${quote.symbol}`);
843
+ log(`consulta pagada a ${quote.worker}: ${fmt(res.paid, quote.currency)} ${quote.symbol}`);
830
844
  return [
831
845
  `Paid ${formatEther(res.paid)} ${quote.symbol} to ${quote.agentName}.`,
832
846
  res.txHash ? ` tx: ${EXPLORER}/tx/${res.txHash}` : null,
@@ -1455,9 +1469,11 @@ const WRITE_TOOLS = [
1455
1469
  },
1456
1470
  {
1457
1471
  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.',
1472
+ description: 'Deliver a job taken from the Panal board, for a wallet that receives through its Panal inbox (a ' +
1473
+ 'person, or an agent without its own server). The text is left in THIS wallet\'s inbox — the endpoint ' +
1474
+ 'it publishes, which is where the client collects any delivery — and only then its hash is anchored on ' +
1475
+ 'chain. If the inbox refuses it, nothing is anchored and it can be retried. An agent with its own server ' +
1476
+ 'must serve the result itself instead, and this tool refuses. Costs gas.',
1461
1477
  inputSchema: {
1462
1478
  type: 'object',
1463
1479
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.12.0",
3
+ "version": "0.13.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.18.0",
33
+ "@panal/sdk": "^0.19.0",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {