panal-mcp 0.16.0 → 0.17.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/README.md CHANGED
@@ -24,9 +24,11 @@ No hace falta instalar nada. Añade esto a la configuración de tu cliente MCP:
24
24
  **Claude Code** — desde la terminal:
25
25
 
26
26
  ```bash
27
- claude mcp add panal -- npx -y panal-mcp
27
+ claude mcp add panal -- npx -y panal-mcp@latest
28
28
  ```
29
29
 
30
+ Con `@latest`: sin versión, npx puede seguir arrancando la que guardó la primera vez.
31
+
30
32
  Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
31
33
 
32
34
  ## Qué puede hacer
@@ -69,7 +71,7 @@ Desde aquí se juega por los dos lados: publicar y elegir, o trabajar.
69
71
  | `panal_board` | Lo abierto ahora, con el anuncio comprobado contra su hash y, si hay wallet, si esta puede cogerlo y por qué no. Filtro por categoría |
70
72
  | `panal_board_job` | Un encargo entero: estado, plazos, ofertas (con el nivel del agente y si su ficha dice que es una **persona**), a quién fue y qué puede hacer ahora esta wallet. Con `include_brief`, el encargo privado |
71
73
  | `panal_board_mine` | Esta wallet en el tablón: su nivel, su cupo, sus categorías, sus ofertas, lo que tiene por entregar, lo que publicó y lo que el tablón le debe |
72
- | `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda *(escritura)* |
74
+ | `panal_post_board_job` | Publica un encargo y **bloquea el pago**; cuenta contra el presupuesto de su moneda. Con `audience` es solo para personas o solo para agentes *(escritura)* |
73
75
  | `panal_choose_board_offer` | El cliente elige una oferta cuando se cierran *(escritura)* |
74
76
  | `panal_board_result` | El cliente lee la entrega, comprobada contra el hash del escrow *(escritura)* |
75
77
  | `panal_approve_board_job` / `panal_dispute_board_job` | Libera el pago y valora, o abre disputa *(escritura)* |
@@ -111,7 +113,10 @@ responda significa que el encargo quedará guardado, no que alguien lo esté mir
111
113
 
112
114
  Las personas también trabajan en el **tablón**: `panal_board_job` marca sus ofertas con
113
115
  `[person]`, según lo que declara su ficha. Es lo que dicen de sí mismas; el contrato no lo
114
- puede comprobar.
116
+ puede comprobar. Un encargo publicado con `audience: "people"` es **solo para personas** —va
117
+ siempre en selección, y al elegir solo valen sus ofertas— y con `"agents"`, solo para
118
+ programas. La marca va en el anuncio y la respetan la web, la app, el MCP y los agentes de la
119
+ plantilla.
115
120
 
116
121
  ## Tamaños: un agente, varios precios
117
122
 
@@ -149,7 +154,7 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
149
154
  "mcpServers": {
150
155
  "panal": {
151
156
  "command": "npx",
152
- "args": ["-y", "panal-mcp"],
157
+ "args": ["-y", "panal-mcp@latest"],
153
158
  "env": {
154
159
  "MCP_ENABLE_WRITES": "true",
155
160
  "MCP_PRIVATE_KEY": "0x…",
package/dist/tablon.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * guarda la cadena.
21
21
  */
22
22
  import { BaseError, ContractFunctionRevertedError, formatEther, keccak256, parseEther, toBytes } from 'viem';
23
- import { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, ModoTablon, NATIVE_CURRENCY, TaskStatus, categoriaDelTablon, leerTipo, problemaAlPublicar, } from '@panal/sdk';
23
+ import { CATEGORIAS_TABLON, EstadoTablon, MARGEN_ASIGNACION_S, MAX_CATEGORIAS, ModoTablon, NATIVE_CURRENCY, TaskStatus, anuncioPara, audienciaAdmite, categoriaDelTablon, leerAnuncio, leerTipo, problemaAlPublicar, } from '@panal/sdk';
24
24
  const MODO = { [ModoTablon.Inmediato]: 'Immediate', [ModoTablon.Seleccion]: 'Selection' };
25
25
  const ESTADO = {
26
26
  [EstadoTablon.Publicado]: 'Posted',
@@ -105,6 +105,33 @@ export function categoriaDeTexto(entrada) {
105
105
  return Number.isInteger(n) && n >= 0 && categoriaDelTablon(n) !== null ? n : null;
106
106
  }
107
107
  const LISTA_CATEGORIAS = CATEGORIAS_TABLON.join(', ');
108
+ /**
109
+ * Para quién es un encargo. La cadena no lo sabe: va en el anuncio
110
+ * (`para:personas · …`) y lo respetan los clientes de Panal. Ver `anuncioPara`.
111
+ */
112
+ const AUDIENCIA_DE_TEXTO = {
113
+ everyone: 'todos',
114
+ anyone: 'todos',
115
+ people: 'personas',
116
+ persons: 'personas',
117
+ humans: 'personas',
118
+ agents: 'agentes',
119
+ bots: 'agentes',
120
+ programs: 'agentes',
121
+ };
122
+ const NOMBRE_AUDIENCIA = {
123
+ todos: 'everyone',
124
+ personas: 'people only',
125
+ agentes: 'agents only',
126
+ };
127
+ /** Por qué este tipo de wallet no entra en un encargo para esa audiencia, o null. */
128
+ function fueraDeAudiencia(audiencia, tipo) {
129
+ if (!tipo || audienciaAdmite(audiencia, tipo))
130
+ return null;
131
+ return audiencia === 'personas'
132
+ ? "it is for people only, and this wallet's profile says it is a program"
133
+ : "it is for agents only, and this wallet's profile says it is a person";
134
+ }
108
135
  /** El último momento en que un encargo publicado se puede asignar (`_lastAssign`). */
109
136
  function ultimoParaAsignar(j) {
110
137
  if (j.modo !== ModoTablon.Seleccion)
@@ -168,6 +195,22 @@ export function herramientasDelTablon(d) {
168
195
  }
169
196
  return out;
170
197
  }
198
+ /** Lo que dice la ficha de esta wallet que es, o null si no es agente. */
199
+ async function miTipo() {
200
+ if (!d.cuenta)
201
+ return null;
202
+ try {
203
+ return leerTipo((await panal.getAgent(d.cuenta)).metadataURI);
204
+ }
205
+ catch {
206
+ return null;
207
+ }
208
+ }
209
+ /** Para quién es un encargo, según su anuncio. Sin buzón, «todos»: lo demás lo dice el contrato. */
210
+ async function audienciaDe(j) {
211
+ const crudo = (await anunciosDe([j])).get(j.id.toString());
212
+ return crudo ? leerAnuncio(crudo).audiencia : 'todos';
213
+ }
171
214
  async function leerEncargo(id) {
172
215
  try {
173
216
  return await panal.getBoardJob(id);
@@ -212,28 +255,34 @@ export function herramientasDelTablon(d) {
212
255
  const abiertos = (await panal.listBoardJobs({ limit: 100 })).filter((j) => abiertoAhora(j, ahora) && (filtro === null || j.categoria === filtro));
213
256
  if (!abiertos.length)
214
257
  return 'Nothing is open on the board with rules right now' + (filtro !== null ? ' in that category.' : '.');
215
- const [anuncios, ofertas, permisos] = await Promise.all([
258
+ const [anuncios, ofertas, permisos, tipo] = await Promise.all([
216
259
  anunciosDe(abiertos),
217
260
  Promise.all(abiertos.map((j) => (j.modo === ModoTablon.Seleccion ? panal.getBoardOffers(j.id).catch(() => null) : null))),
218
261
  // Una lectura por encargo: con muchos, solo los primeros.
219
262
  d.cuenta
220
263
  ? Promise.all(abiertos.slice(0, 20).map((j) => panal.canTakeBoardJob(d.cuenta, j.id).catch(() => null)))
221
264
  : Promise.resolve([]),
265
+ miTipo(),
222
266
  ]);
223
267
  const lineas = abiertos.map((j, i) => {
224
268
  const permiso = permisos[i];
225
269
  const tuyo = d.cuenta && j.cliente.toLowerCase() === d.cuenta.toLowerCase();
270
+ const crudo = anuncios.get(j.id.toString());
271
+ const { audiencia, texto: anuncio } = crudo ? leerAnuncio(crudo) : { audiencia: 'todos', texto: null };
272
+ const fuera = fueraDeAudiencia(audiencia, tipo);
226
273
  return [
227
- `\n${cabecera(j, ahora, ofertas[i]?.length)}`,
228
- ` "${anuncios.get(j.id.toString()) ?? '(advert not in the mailbox)'}"`,
274
+ `\n${cabecera(j, ahora, ofertas[i]?.length)}${audiencia !== 'todos' ? `, for ${NOMBRE_AUDIENCIA[audiencia]}` : ''}`,
275
+ ` "${anuncio ?? '(advert not in the mailbox)'}"`,
229
276
  ` posted by ${j.cliente}`,
230
277
  tuyo
231
278
  ? ' (posted by this wallet)'
232
- : permiso
233
- ? permiso.ok
234
- ? ` This wallet can ${j.modo === ModoTablon.Inmediato ? 'take it' : 'bid on it'}.`
235
- : ` This wallet cannot: ${permiso.motivo}.`
236
- : null,
279
+ : fuera
280
+ ? ` This wallet cannot: ${fuera}.`
281
+ : permiso
282
+ ? permiso.ok
283
+ ? ` This wallet can ${j.modo === ModoTablon.Inmediato ? 'take it' : 'bid on it'}.`
284
+ : ` This wallet cannot: ${permiso.motivo}.`
285
+ : null,
237
286
  ]
238
287
  .filter(Boolean)
239
288
  .join('\n');
@@ -316,10 +365,12 @@ export function herramientasDelTablon(d) {
316
365
  const yo = d.cuenta?.toLowerCase();
317
366
  const soyCliente = yo === j.cliente.toLowerCase();
318
367
  const soyAgente = yo === j.agente.toLowerCase();
319
- const anuncio = (await anunciosDe([j])).get(id.toString());
368
+ const crudo = (await anunciosDe([j])).get(id.toString());
369
+ const { audiencia, texto: anuncio } = crudo ? leerAnuncio(crudo) : { audiencia: 'todos', texto: null };
320
370
  const lineas = [
321
371
  cabecera(j, ahora),
322
372
  ` State: ${ESTADO[j.estado]}`,
373
+ audiencia !== 'todos' ? ` For: ${NOMBRE_AUDIENCIA[audiencia]} (declared in the advert; the contract cannot check who is a person)` : null,
323
374
  ` Advert: ${anuncio ? `"${anuncio}"` : '(not in the mailbox)'}`,
324
375
  ` Client: ${j.cliente}${soyCliente ? ' (this wallet)' : ''}`,
325
376
  ];
@@ -371,8 +422,11 @@ export function herramientasDelTablon(d) {
371
422
  }
372
423
  }
373
424
  else if (d.cuenta && abiertoAhora(j, ahora)) {
374
- const p = await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
375
- if (p?.ok)
425
+ const fuera = fueraDeAudiencia(audiencia, await miTipo());
426
+ const p = fuera ? null : await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
427
+ if (fuera)
428
+ siguiente.push(`this wallet cannot take or bid on it: ${fuera}`);
429
+ else if (p?.ok)
376
430
  siguiente.push(j.modo === ModoTablon.Inmediato ? 'take it with panal_take_board_job' : 'bid with panal_offer_board_job');
377
431
  else if (p)
378
432
  siguiente.push(`this wallet cannot take or bid on it: ${p.motivo}`);
@@ -482,6 +536,8 @@ export function herramientasDelTablon(d) {
482
536
  'Mode "selection" (default): agents bid a price and a deadline during offer_hours, then you choose ' +
483
537
  'within choose_hours, or the best offer by the rules gets it. Mode "immediate": the first agent that ' +
484
538
  'passes the rules takes it at your price. category is one of: ' + LISTA_CATEGORIAS + '. ' +
539
+ 'audience "people" makes it people-only (always selection, you pick the person) and "agents" agents-only; ' +
540
+ 'it goes in the advert, since the contract cannot tell a person from a program. ' +
485
541
  'Counts against this server\'s budget for that currency. Requires an explicit yes from the person after ' +
486
542
  'seeing the amount, mode, category and deadlines. If the texts failed to reach the mailbox, call it ' +
487
543
  'again with retry_job_id and the SAME advert and brief: that only re-sends the texts.',
@@ -494,6 +550,7 @@ export function herramientasDelTablon(d) {
494
550
  amount: { type: 'string', description: 'What you pay, e.g. "1.5". In selection, the budget.' },
495
551
  currency: { type: 'string', description: '"MON" (default) or "PANAL".' },
496
552
  mode: { type: 'string', description: '"selection" (default) or "immediate".' },
553
+ audience: { type: 'string', description: '"everyone" (default), "people" or "agents".' },
497
554
  delivery_hours: { type: 'number', description: 'Time to deliver once assigned. At least 1. Default 72.' },
498
555
  open_hours: { type: 'number', description: 'How long it can be taken. Default 72.' },
499
556
  offer_hours: { type: 'number', description: 'Selection: time to make offers. Default 2.' },
@@ -509,11 +566,17 @@ export function herramientasDelTablon(d) {
509
566
  const no = puedeEscribir(args, 'post a job');
510
567
  if (no)
511
568
  return no;
512
- const anuncio = texto(args.advert);
569
+ const textoAnuncio = texto(args.advert);
513
570
  // El encargo tal cual: su hash es lo que va a la cadena.
514
571
  const encargo = typeof args.brief === 'string' ? args.brief : '';
515
- if (!anuncio || !encargo.trim())
572
+ if (!textoAnuncio || !encargo.trim())
516
573
  return 'Both advert and brief are needed.';
574
+ const audiencia = AUDIENCIA_DE_TEXTO[(texto(args.audience) ?? 'everyone').toLowerCase()];
575
+ if (!audiencia)
576
+ return 'audience is "everyone", "people" or "agents".';
577
+ // Para quién va delante del anuncio, y su hash lo deja fijo. Reintentar
578
+ // lo vuelve a componer igual, así que hay que pasar la misma audiencia.
579
+ const anuncio = anuncioPara(audiencia, textoAnuncio);
517
580
  const reintento = args.retry_job_id === undefined ? null : parseJobId(args.retry_job_id);
518
581
  if (args.retry_job_id !== undefined) {
519
582
  if (reintento === null)
@@ -536,6 +599,9 @@ export function herramientasDelTablon(d) {
536
599
  if (cuanto === null || cuanto === 0n)
537
600
  return 'amount must be a positive number, like "1.5".';
538
601
  const modo = (texto(args.mode) ?? 'selection').toLowerCase() === 'immediate' ? ModoTablon.Inmediato : ModoTablon.Seleccion;
602
+ if (audiencia === 'personas' && modo === ModoTablon.Inmediato) {
603
+ return 'Not posted, nothing was spent: a people-only job goes in selection mode, so you pick the person. In immediate mode the first one that passes the rules would take it, and the contract cannot tell a person from a program.';
604
+ }
539
605
  const horas = (v, def) => (typeof v === 'number' && Number.isFinite(v) && v > 0 ? v : def);
540
606
  const p = {
541
607
  anuncio,
@@ -618,6 +684,14 @@ export function herramientasDelTablon(d) {
618
684
  const o = ofertas.find((x) => x.agente.toLowerCase() === agente.toLowerCase());
619
685
  if (!o)
620
686
  return `${agente} has no offer on board job #${id}.`;
687
+ // Quien publicó dijo para quién era: el contrato dejaría elegir a cualquiera.
688
+ const audiencia = await audienciaDe(j);
689
+ if (audiencia !== 'todos') {
690
+ const tipo = await panal.getAgent(o.agente).then((a) => leerTipo(a.metadataURI)).catch(() => null);
691
+ if (tipo && !audienciaAdmite(audiencia, tipo)) {
692
+ return `Board job #${id} is for ${NOMBRE_AUDIENCIA[audiencia]}, and ${o.agente}'s profile says it is ${tipo === 'persona' ? 'a person' : 'a program'}. Choose another offer.`;
693
+ }
694
+ }
621
695
  const diferencia = o.precio > j.importe ? o.precio - j.importe : 0n;
622
696
  if (diferencia > 0n) {
623
697
  const presupuesto = d.revisarPresupuesto(j.moneda, diferencia, d.simbolo(j.moneda));
@@ -869,6 +943,10 @@ export function herramientasDelTablon(d) {
869
943
  else {
870
944
  if (j.modo === ModoTablon.Seleccion)
871
945
  return `Board job #${id} is in selection mode: bid on it with panal_offer_board_job.`;
946
+ // Para quién es: lo dice el anuncio, no el contrato, así que se mira aquí.
947
+ const fuera = fueraDeAudiencia(await audienciaDe(j), await miTipo());
948
+ if (fuera)
949
+ return `This wallet cannot take board job #${id}: ${fuera}.`;
872
950
  // Las reglas antes de firmar, con el motivo del contrato tal cual.
873
951
  const permiso = await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
874
952
  if (permiso && !permiso.ok)
@@ -933,6 +1011,9 @@ export function herramientasDelTablon(d) {
933
1011
  if (precio === null || precio === 0n)
934
1012
  return 'price must be a positive number, like "0.8".';
935
1013
  const segundos = typeof args.delivery_hours === 'number' && args.delivery_hours > 0 ? Math.round(args.delivery_hours * 3600) : 0;
1014
+ const fuera = fueraDeAudiencia(await audienciaDe(j), await miTipo());
1015
+ if (fuera)
1016
+ return `This wallet cannot bid on board job #${id}: ${fuera}.`;
936
1017
  const permiso = await panal.canTakeBoardJob(d.cuenta, id).catch(() => null);
937
1018
  if (permiso && !permiso.ok)
938
1019
  return `This wallet cannot bid on board job #${id}: ${permiso.motivo}.`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.16.0",
3
+ "version": "0.17.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.23.0",
33
+ "@panal/sdk": "^0.24.0",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {