panal-mcp 0.13.1 → 0.15.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.
@@ -84,6 +84,11 @@ export interface LimitesDelAgente {
84
84
  * el agente caído y el único que nadie puede cambiar entre mirar y pagar.
85
85
  */
86
86
  niveles: Nivel[];
87
+ /**
88
+ * Los encargos de ejemplo de la ficha, si trae. Salen de la misma lectura,
89
+ * por lo mismo. La fuente es la cadena: ver `conEjemplosDeLaFicha`.
90
+ */
91
+ ejemplos?: string[];
87
92
  }
88
93
  export declare function fetchAgentLimits(botUrl: string): Promise<LimitesDelAgente>;
89
94
  /** Descarga acotada: se corta en cuanto se pasa del tope. */
@@ -14,7 +14,7 @@
14
14
  // implementaciones equivalentes de lo mismo —la del SDK y una aquí—, y dos
15
15
  // copias de un control de seguridad son una que se queda atrás sin que nadie
16
16
  // se entere el día que la otra mejora.
17
- import { assertPublicUrl, leerMaxBriefChars, leerNiveles, rutaDeAgente } from '@panal/sdk';
17
+ import { assertPublicUrl, leerEjemplos, leerMaxBriefChars, leerNiveles, rutaDeAgente } from '@panal/sdk';
18
18
  /** Tope de la respuesta. Sin esto, un endpoint hostil tumba el proceso. */
19
19
  const MAX_BYTES = 512 * 1024;
20
20
  const TIMEOUT_MS = 20_000;
@@ -129,7 +129,12 @@ export async function fetchAgentLimits(botUrl) {
129
129
  return { alcanzable: true, maxBriefChars: null, niveles: [] };
130
130
  try {
131
131
  const card = await res.json();
132
- return { alcanzable: true, maxBriefChars: parseMaxBriefChars(card), niveles: leerNiveles(card) };
132
+ return {
133
+ alcanzable: true,
134
+ maxBriefChars: parseMaxBriefChars(card),
135
+ niveles: leerNiveles(card),
136
+ ejemplos: leerEjemplos(card),
137
+ };
133
138
  }
134
139
  catch {
135
140
  return { alcanzable: true, maxBriefChars: null, niveles: [] };
package/dist/niveles.d.ts CHANGED
@@ -41,7 +41,7 @@ export declare function precioDeNivel(nivel: Nivel, symbol: string): string;
41
41
  * agente que vende tres tamaños se veía con un precio suelto —el más barato— y
42
42
  * el encargo grande se contrataba al precio del pequeño.
43
43
  */
44
- export declare function renderNiveles(niveles: Nivel[], symbol: string): string[];
44
+ export declare function renderNiveles(niveles: Nivel[], symbol: string, enUsd?: (wei: bigint) => string): string[];
45
45
  /**
46
46
  * Los niveles en UNA línea, para las listas.
47
47
  *
package/dist/niveles.js CHANGED
@@ -47,13 +47,13 @@ export function precioDeNivel(nivel, symbol) {
47
47
  * agente que vende tres tamaños se veía con un precio suelto —el más barato— y
48
48
  * el encargo grande se contrataba al precio del pequeño.
49
49
  */
50
- export function renderNiveles(niveles, symbol) {
50
+ export function renderNiveles(niveles, symbol, enUsd = () => '') {
51
51
  if (niveles.length === 0)
52
52
  return [];
53
53
  return [
54
54
  ` Sizes (${niveles.length}) — pass the one the person picks as \`tier\` to panal_quote_hire:`,
55
55
  ...niveles.map((n) => {
56
- const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}`];
56
+ const partes = [` · ${n.name ?? '(unnamed)'} — ${precioDeNivel(n, symbol)}${enUsd(n.wei)}`];
57
57
  if (n.description)
58
58
  partes.push(n.description);
59
59
  if (n.maxBriefChars)
package/dist/server.js CHANGED
@@ -42,7 +42,7 @@ import { createInterface } from 'node:readline';
42
42
  import { fileURLToPath } from 'node:url';
43
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, x402Currency, } from '@panal/sdk';
45
+ import { NATIVE_CURRENCY, TaskStatus, appendAttachmentsManifest, conEjemplosDeLaFicha, createPanalClient, downloadDeliveredFile, enDolares, formatearDolares, erc20Abi, leerEjemplosDeMetadata, 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';
@@ -125,6 +125,31 @@ function symbolOf(currency) {
125
125
  function fmt(amount, currency) {
126
126
  return formatUnits(amount, esNativa(currency) ? 18 : (x402Currency(currency)?.decimals ?? 18));
127
127
  }
128
+ /**
129
+ * Los precios en dólares, leídos como mucho cada 30 s.
130
+ *
131
+ * Solo para enseñar: lo que se firma y se bloquea sigue siendo la cantidad en
132
+ * su moneda. MON sale de Pyth y $PANAL de la curva de nad.fun, los dos en la
133
+ * cadena (ver precios.ts del SDK). Si no se pueden leer, las respuestas salen
134
+ * sin dólares, igual que antes.
135
+ */
136
+ let preciosCache = { leidos: 0, valor: null };
137
+ async function refrescarPrecios() {
138
+ if (Date.now() - preciosCache.leidos < 30_000)
139
+ return;
140
+ preciosCache = { leidos: Date.now(), valor: await panal.precios().catch(() => null) };
141
+ }
142
+ /**
143
+ * « (≈ $0.60)» si hay un precio de menos de cinco minutos para esa moneda;
144
+ * nada si no. Las estables ya son dólares y no llevan nada.
145
+ */
146
+ function usd(amount, currency) {
147
+ const sym = symbolOf(currency);
148
+ if (sym !== 'MON' && sym !== '$PANAL')
149
+ return '';
150
+ const d = enDolares(amount, sym, preciosCache.valor);
151
+ return d ? ` (≈ ${formatearDolares(d.usd)})` : '';
152
+ }
128
153
  /** Las monedas que este servidor sabe manejar: las que tienen presupuesto. */
129
154
  function monedasConocidas() {
130
155
  return [...limits.porMoneda.keys()];
@@ -234,7 +259,7 @@ function trustLine(agent) {
234
259
  return ` Trust: ${partes.join('; ')}`;
235
260
  }
236
261
  function renderAgent(agent, niveles = [], detallado = false) {
237
- const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}`;
262
+ const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}${usd(agent.pricePerTask, agent.currency)}`;
238
263
  const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(no skills declared)';
239
264
  return [
240
265
  `${agent.metadata.name || '(no name)'} — ${agent.address}`,
@@ -247,7 +272,9 @@ function renderAgent(agent, niveles = [], detallado = false) {
247
272
  // Los niveles van DEBAJO del precio de tarea y no en su lugar: ese precio
248
273
  // sigue siendo el que se bloquea si nadie elige tamaño, y el que cobran
249
274
  // los agentes —casi todos— que no venden ninguno.
250
- ...(detallado ? renderNiveles(niveles, symbolOf(agent.currency)) : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
275
+ ...(detallado
276
+ ? renderNiveles(niveles, symbolOf(agent.currency), (wei) => usd(wei, agent.currency))
277
+ : [lineaDeNiveles(niveles, symbolOf(agent.currency))]),
251
278
  ` Skills: ${skills}`,
252
279
  agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
253
280
  agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
@@ -262,7 +289,7 @@ function renderTask(task) {
262
289
  `Task #${task.id} — ${TaskStatus[task.status] ?? task.status}`,
263
290
  ` Client: ${task.client}`,
264
291
  ` Agent: ${task.worker}`,
265
- ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}`,
292
+ ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}${usd(task.amount, task.currency)}`,
266
293
  ` Deadline: ${deadline}`,
267
294
  ` Job hash: ${task.taskHash}`,
268
295
  task.resultHash && !/^0x0+$/.test(task.resultHash) ? ` Result hash: ${task.resultHash}` : null,
@@ -335,6 +362,7 @@ const READ_TOOLS = [
335
362
  },
336
363
  },
337
364
  handler: async (args) => {
365
+ await refrescarPrecios();
338
366
  const found = await panal.searchAgents(str(args.query), {
339
367
  includeInactive: args.include_inactive === true,
340
368
  });
@@ -352,7 +380,8 @@ const READ_TOOLS = [
352
380
  },
353
381
  {
354
382
  name: 'panal_get_agent',
355
- description: 'Full profile of a Panal agent, by address: price, the sizes it sells if it sells several, skills, status and its on-chain name.',
383
+ description: 'Full profile of a Panal agent, by address: price, the sizes it sells if it sells several, skills, status, ' +
384
+ 'its on-chain name and the example jobs its owner suggests.',
356
385
  inputSchema: {
357
386
  type: 'object',
358
387
  properties: { address: { type: 'string', description: 'The agent 0x… address.' } },
@@ -362,7 +391,7 @@ const READ_TOOLS = [
362
391
  const address = str(args.address);
363
392
  if (!address || !isAddress(address))
364
393
  return 'I need a valid 0x address.';
365
- const agent = await panal.getAgent(address);
394
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
366
395
  // El perfil es donde alguien mira el precio, así que aquí sí se paga la
367
396
  // llamada al manifiesto del agente: enseñar solo el de tarea era la
368
397
  // razón de que el precio por consulta fuese invisible desde aquí. En
@@ -382,7 +411,15 @@ const READ_TOOLS = [
382
411
  ? ` Per question: ${fmt(BigInt(q.amount), q.asset)} ${symbolOf(q.asset)} (x402, answered on the spot)`
383
412
  : ' Per question: not offered (this agent only takes jobs through escrow)';
384
413
  const niveles = nivelesDeAgente(agent.metadataURI, ficha?.niveles ?? []);
385
- return `${renderAgent(agent, niveles, true)}\n${porLlamada}`;
414
+ // Los encargos de ejemplo que propone su dueño: ayudan a redactar el
415
+ // encargo con lo que este agente hace de verdad. Son una idea, no un
416
+ // encargo listo — al que pide le falta añadir su material.
417
+ const ejemplos = conEjemplosDeLaFicha(leerEjemplosDeMetadata(agent.metadataURI), ficha?.ejemplos ?? []);
418
+ const lineaEjemplos = ejemplos.length > 0
419
+ ? `\n Example jobs its owner suggests (a starting point; the brief still needs the actual material): ` +
420
+ ejemplos.map((e) => `"${e}"`).join(' · ')
421
+ : '';
422
+ return `${renderAgent(agent, niveles, true)}\n${porLlamada}${lineaEjemplos}`;
386
423
  },
387
424
  },
388
425
  {
@@ -400,7 +437,8 @@ const READ_TOOLS = [
400
437
  const count = await panal.getTaskCount();
401
438
  if (BigInt(id) >= count)
402
439
  return `Task #${id} does not exist: there are ${count} so far.`;
403
- return renderTask(await panal.getTask(BigInt(id)));
440
+ const [task] = await Promise.all([panal.getTask(BigInt(id)), refrescarPrecios()]);
441
+ return renderTask(task);
404
442
  },
405
443
  },
406
444
  {
@@ -424,7 +462,7 @@ const READ_TOOLS = [
424
462
  return 'I need a valid 0x address for the agent.';
425
463
  if (!prompt)
426
464
  return 'I need the question.';
427
- const agent = await panal.getAgent(address);
465
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
428
466
  const name = agent.metadata.name || address;
429
467
  let quote;
430
468
  try {
@@ -458,7 +496,7 @@ const READ_TOOLS = [
458
496
  });
459
497
  return [
460
498
  `${name} charges per question:`,
461
- ` Price: ${fmt(amount, quote.asset)} ${symbol} for this one question`,
499
+ ` Price: ${fmt(amount, quote.asset)} ${symbol}${usd(amount, quote.asset)} for this one question`,
462
500
  ` Paid to: ${quote.payTo}`,
463
501
  quote.description ? ` Covers: ${quote.description}` : null,
464
502
  '',
@@ -506,7 +544,7 @@ const READ_TOOLS = [
506
544
  inputSchema: { type: 'object', properties: {} },
507
545
  handler: async () => {
508
546
  try {
509
- const lista = await panal.listBoard();
547
+ const [lista] = await Promise.all([panal.listBoard(), refrescarPrecios()]);
510
548
  if (!lista.length)
511
549
  return 'The board is empty: no job is waiting to be taken right now.';
512
550
  const ahora = Math.floor(Date.now() / 1000);
@@ -514,7 +552,7 @@ const READ_TOOLS = [
514
552
  `${lista.length} job(s) on the board:`,
515
553
  ...lista.map((e) => {
516
554
  const horas = Math.max(0, Math.floor((Number(e.deadline) - ahora) / 3600));
517
- return (`\n#${e.taskId} — ${formatEther(e.amount)} ${symbolOf(e.currency)}, ${horas} h left\n` +
555
+ return (`\n#${e.taskId} — ${formatEther(e.amount)} ${symbolOf(e.currency)}${usd(e.amount, e.currency)}, ${horas} h left\n` +
518
556
  ` "${e.anuncio}"\n posted by ${e.cliente}`);
519
557
  }),
520
558
  '\nTo take one, use panal_claim_task with its number. Only an active registered agent can claim.',
@@ -552,7 +590,8 @@ const WRITE_TOOLS = [
552
590
  // perdido — y también hay que preguntarlo moneda a moneda.
553
591
  Promise.all(monedas.map((m) => panal.getPendingWithdrawal(address, m).catch(() => null))),
554
592
  ]);
555
- const importe = (v, m) => (v === null ? 'could not be read' : fmt(v, m));
593
+ await refrescarPrecios();
594
+ const importe = (v, m) => v === null ? 'could not be read' : `${fmt(v, m)}${v > 0n ? usd(v, m) : ''}`;
556
595
  const balances = monedas.map((m, i) => ` ${symbolOf(m)}: ${importe(saldos[i] ?? null, m)}`);
557
596
  // Un presupuesto por moneda: MON y $PANAL no valen lo mismo y no hay
558
597
  // tipo de cambio, así que se enseñan por separado o no se entienden.
@@ -621,7 +660,7 @@ const WRITE_TOOLS = [
621
660
  if (rutas.length > MAX_ADJUNTOS) {
622
661
  return `That is ${rutas.length} attachments and the limit is ${MAX_ADJUNTOS} per job.`;
623
662
  }
624
- const agent = await panal.getAgent(address);
663
+ const [agent] = await Promise.all([panal.getAgent(address), refrescarPrecios()]);
625
664
  if (!agent.active)
626
665
  return `${agent.metadata.name || address} is delisted and does not take jobs.`;
627
666
  // ---- Adjuntos -------------------------------------------------------
@@ -766,7 +805,7 @@ const WRITE_TOOLS = [
766
805
  });
767
806
  return [
768
807
  `Quote for ${quote.agentName}:`,
769
- ` Price: ${formatEther(amount)} ${symbol}${elegido ? ` — size "${elegido.name}"` : ''}`,
808
+ ` Price: ${formatEther(amount)} ${symbol}${usd(amount, agent.currency)}${elegido ? ` — size "${elegido.name}"` : ''}`,
770
809
  // Los otros tamaños se enseñan SIEMPRE que existan, también cuando ya
771
810
  // se eligió uno: quien está leyendo esto es quien paga, y enterarse de
772
811
  // que había uno más grande después de contratar no sirve de nada.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.13.1",
3
+ "version": "0.15.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.19.0",
33
+ "@panal/sdk": "^0.21.0",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {