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 +9 -4
- package/dist/tablon.js +95 -14
- package/package.json +2 -2
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
|
-
` "${
|
|
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
|
-
:
|
|
233
|
-
?
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
|
375
|
-
|
|
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
|
|
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 (!
|
|
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.
|
|
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.
|
|
33
|
+
"@panal/sdk": "^0.24.0",
|
|
34
34
|
"viem": "^2.21.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|