panal-mcp 0.2.0 → 0.3.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/dist/limits.d.ts +19 -2
- package/dist/limits.js +15 -3
- package/dist/server.js +152 -3
- package/package.json +2 -2
package/dist/limits.d.ts
CHANGED
|
@@ -6,8 +6,19 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* Las dos maneras de pagar a un agente, y no son intercambiables.
|
|
11
|
+
*
|
|
12
|
+
* `hire` bloquea el pago en el escrow y abre una tarea con plazo, entrega y
|
|
13
|
+
* disputa. `ask` paga una consulta suelta por x402 y se resuelve en la misma
|
|
14
|
+
* llamada HTTP. Los precios son distintos —Spec cobra 100 $PANAL por tarea y
|
|
15
|
+
* 0,5 por consulta—, así que canjear un presupuesto por el camino que no es
|
|
16
|
+
* cobraría doscientas veces de más.
|
|
17
|
+
*/
|
|
18
|
+
export type QuoteKind = 'hire' | 'ask';
|
|
9
19
|
export interface Quote {
|
|
10
20
|
id: string;
|
|
21
|
+
kind: QuoteKind;
|
|
11
22
|
worker: Address;
|
|
12
23
|
agentName: string;
|
|
13
24
|
brief: string;
|
|
@@ -26,8 +37,14 @@ export interface Quote {
|
|
|
26
37
|
export declare class QuoteBook {
|
|
27
38
|
private readonly quotes;
|
|
28
39
|
issue(data: Omit<Quote, 'id' | 'expiresAt'>): Quote;
|
|
29
|
-
/**
|
|
30
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Devuelve el presupuesto, o el motivo por el que no vale.
|
|
42
|
+
*
|
|
43
|
+
* `kind` no es decorativo: obliga a canjear cada presupuesto por el camino
|
|
44
|
+
* para el que se pidió. Sin esto, el id de una consulta de 0,5 $PANAL se
|
|
45
|
+
* podría canjear como contratación y bloquear los 100 $PANAL de la tarea.
|
|
46
|
+
*/
|
|
47
|
+
redeem(id: string, kind: QuoteKind): {
|
|
31
48
|
quote: Quote;
|
|
32
49
|
} | {
|
|
33
50
|
error: string;
|
package/dist/limits.js
CHANGED
|
@@ -24,14 +24,26 @@ export class QuoteBook {
|
|
|
24
24
|
this.quotes.set(quote.id, quote);
|
|
25
25
|
return quote;
|
|
26
26
|
}
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Devuelve el presupuesto, o el motivo por el que no vale.
|
|
29
|
+
*
|
|
30
|
+
* `kind` no es decorativo: obliga a canjear cada presupuesto por el camino
|
|
31
|
+
* para el que se pidió. Sin esto, el id de una consulta de 0,5 $PANAL se
|
|
32
|
+
* podría canjear como contratación y bloquear los 100 $PANAL de la tarea.
|
|
33
|
+
*/
|
|
34
|
+
redeem(id, kind) {
|
|
29
35
|
this.prune();
|
|
30
36
|
const quote = this.quotes.get(id);
|
|
31
37
|
if (!quote) {
|
|
32
38
|
return {
|
|
33
39
|
error: 'That quote does not exist or has expired (they last 5 minutes). ' +
|
|
34
|
-
'Ask for a new one
|
|
40
|
+
'Ask for a new one and show it to the person before spending.',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (quote.kind !== kind) {
|
|
44
|
+
return {
|
|
45
|
+
error: `That quote_id is for ${quote.kind === 'ask' ? 'a per-call question (panal_ask)' : 'hiring a job (panal_hire)'}, ` +
|
|
46
|
+
`not for this. The two have different prices; ask for the right quote instead of reusing this id.`,
|
|
35
47
|
};
|
|
36
48
|
}
|
|
37
49
|
// De un solo uso: sin esto, un reintento del modelo contrataría dos veces.
|
package/dist/server.js
CHANGED
|
@@ -97,7 +97,10 @@ function renderAgent(agent) {
|
|
|
97
97
|
return [
|
|
98
98
|
`${agent.metadata.name || '(no name)'} — ${agent.address}`,
|
|
99
99
|
` Status: ${agent.active ? 'active' : 'DELISTED (does not take jobs)'}`,
|
|
100
|
-
|
|
100
|
+
// "Per task" y no "Price" a secas: hay dos precios (tarea por escrow y
|
|
101
|
+
// consulta por x402) y llamar "el precio" a uno de ellos fue justo lo que
|
|
102
|
+
// hizo invisible al otro.
|
|
103
|
+
` Per task: ${price} (escrow, with deadline and dispute window)`,
|
|
101
104
|
` Skills: ${skills}`,
|
|
102
105
|
agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
|
|
103
106
|
agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
|
|
@@ -157,7 +160,21 @@ const READ_TOOLS = [
|
|
|
157
160
|
const address = str(args.address);
|
|
158
161
|
if (!address || !isAddress(address))
|
|
159
162
|
return 'I need a valid 0x address.';
|
|
160
|
-
|
|
163
|
+
const agent = await panal.getAgent(address);
|
|
164
|
+
// El perfil es donde alguien mira el precio, así que aquí sí se paga la
|
|
165
|
+
// llamada al manifiesto del agente: enseñar solo el de tarea era la
|
|
166
|
+
// razón de que el precio por consulta fuese invisible desde aquí. En
|
|
167
|
+
// `panal_search_agents` no se hace, porque serían N llamadas HTTP a
|
|
168
|
+
// servidores ajenos por cada búsqueda.
|
|
169
|
+
let porLlamada;
|
|
170
|
+
try {
|
|
171
|
+
const q = await panal.quoteAgent(address, 'Price check.');
|
|
172
|
+
porLlamada = ` Per question: ${formatEther(BigInt(q.amount))} ${q.assetSymbol ?? symbolOf(q.asset)} (x402, answered on the spot)`;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
porLlamada = ' Per question: not offered (this agent only takes jobs through escrow)';
|
|
176
|
+
}
|
|
177
|
+
return `${renderAgent(agent)}\n${porLlamada}`;
|
|
161
178
|
},
|
|
162
179
|
},
|
|
163
180
|
{
|
|
@@ -178,6 +195,71 @@ const READ_TOOLS = [
|
|
|
178
195
|
return renderTask(await panal.getTask(BigInt(id)));
|
|
179
196
|
},
|
|
180
197
|
},
|
|
198
|
+
{
|
|
199
|
+
name: 'panal_quote_ask',
|
|
200
|
+
description: 'Ask an agent what it charges for ONE question, answered on the spot (x402 per-call pricing). ' +
|
|
201
|
+
'Costs nothing and needs no wallet. This is a DIFFERENT price from the per-task price in the ' +
|
|
202
|
+
'agent profile: per-task means hiring a job through escrow, with a deadline and a dispute window. ' +
|
|
203
|
+
'The two can differ by a lot, so quote the one you actually mean to use.',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
agent: { type: 'string', description: 'The agent 0x… address.' },
|
|
208
|
+
prompt: { type: 'string', description: 'The question, so the agent can price it if it prices per question.' },
|
|
209
|
+
},
|
|
210
|
+
required: ['agent', 'prompt'],
|
|
211
|
+
},
|
|
212
|
+
handler: async (args) => {
|
|
213
|
+
const address = str(args.agent);
|
|
214
|
+
const prompt = str(args.prompt);
|
|
215
|
+
if (!address || !isAddress(address))
|
|
216
|
+
return 'I need a valid 0x address for the agent.';
|
|
217
|
+
if (!prompt)
|
|
218
|
+
return 'I need the question.';
|
|
219
|
+
const agent = await panal.getAgent(address);
|
|
220
|
+
const name = agent.metadata.name || address;
|
|
221
|
+
let quote;
|
|
222
|
+
try {
|
|
223
|
+
quote = await panal.quoteAgent(address, prompt);
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
// Cobrar por llamada es opcional: un agente puede trabajar solo por
|
|
227
|
+
// escrow. Se dice cuál es la alternativa en vez de dejarlo en error.
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
return (`${name} does not answer per-question requests (${msg}).\n` +
|
|
230
|
+
`It can still be hired for a job: ${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)} ` +
|
|
231
|
+
`per task, via panal_quote_hire.`);
|
|
232
|
+
}
|
|
233
|
+
const amount = BigInt(quote.amount);
|
|
234
|
+
const symbol = quote.assetSymbol ?? symbolOf(quote.asset);
|
|
235
|
+
const saved = quotes.issue({
|
|
236
|
+
kind: 'ask',
|
|
237
|
+
worker: agent.address,
|
|
238
|
+
agentName: name,
|
|
239
|
+
brief: prompt,
|
|
240
|
+
amount,
|
|
241
|
+
currency: quote.asset,
|
|
242
|
+
symbol,
|
|
243
|
+
botUrl: agent.metadata.botUrl,
|
|
244
|
+
});
|
|
245
|
+
return [
|
|
246
|
+
`${name} charges per question:`,
|
|
247
|
+
` Price: ${formatEther(amount)} ${symbol} for this one question`,
|
|
248
|
+
` Paid to: ${quote.payTo}`,
|
|
249
|
+
quote.description ? ` Covers: ${quote.description}` : null,
|
|
250
|
+
'',
|
|
251
|
+
`For comparison, hiring it for a job costs ${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)} ` +
|
|
252
|
+
'per task (escrow, with deadline and dispute window).',
|
|
253
|
+
'',
|
|
254
|
+
`quote_id: ${saved.id} (valid for 5 minutes)`,
|
|
255
|
+
'',
|
|
256
|
+
'Show this price to the person. Only if they say yes, call panal_ask with that quote_id and ' +
|
|
257
|
+
'confirmed_by_user: true. This moves real money.',
|
|
258
|
+
]
|
|
259
|
+
.filter((l) => l !== null)
|
|
260
|
+
.join('\n');
|
|
261
|
+
},
|
|
262
|
+
},
|
|
181
263
|
{
|
|
182
264
|
name: 'panal_marketplace_stats',
|
|
183
265
|
description: 'Panal marketplace figures: how many agents exist, how many are active, how many jobs so far.',
|
|
@@ -255,6 +337,7 @@ const WRITE_TOOLS = [
|
|
|
255
337
|
`and this costs ${formatEther(amount)} ${symbol}.`);
|
|
256
338
|
}
|
|
257
339
|
const quote = quotes.issue({
|
|
340
|
+
kind: 'hire',
|
|
258
341
|
worker: agent.address,
|
|
259
342
|
agentName: agent.metadata.name || agent.address,
|
|
260
343
|
brief,
|
|
@@ -276,6 +359,72 @@ const WRITE_TOOLS = [
|
|
|
276
359
|
].join('\n');
|
|
277
360
|
},
|
|
278
361
|
},
|
|
362
|
+
{
|
|
363
|
+
name: 'panal_ask',
|
|
364
|
+
description: 'Pay an agent for ONE question and get its answer in the same call (x402). Requires a quote_id from ' +
|
|
365
|
+
'panal_quote_ask and an explicit yes from the person. This spends real money. Unlike panal_hire ' +
|
|
366
|
+
'there is no escrow, no deadline and no dispute: once paid, it is paid.',
|
|
367
|
+
inputSchema: {
|
|
368
|
+
type: 'object',
|
|
369
|
+
properties: {
|
|
370
|
+
quote_id: { type: 'string', description: 'The id returned by panal_quote_ask.' },
|
|
371
|
+
confirmed_by_user: {
|
|
372
|
+
type: 'boolean',
|
|
373
|
+
description: 'true only if the person has seen the price and approved it.',
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
required: ['quote_id', 'confirmed_by_user'],
|
|
377
|
+
},
|
|
378
|
+
handler: async (args) => {
|
|
379
|
+
const blocked = writesBlockedReason();
|
|
380
|
+
if (blocked)
|
|
381
|
+
return blocked;
|
|
382
|
+
if (args.confirmed_by_user !== true) {
|
|
383
|
+
return 'I will not pay without confirmed_by_user: true. Show the quote to the person and ask first.';
|
|
384
|
+
}
|
|
385
|
+
const id = str(args.quote_id);
|
|
386
|
+
if (!id)
|
|
387
|
+
return 'The quote_id is missing. Ask for a quote with panal_quote_ask first.';
|
|
388
|
+
const redeemed = quotes.redeem(id, 'ask');
|
|
389
|
+
if ('error' in redeemed)
|
|
390
|
+
return redeemed.error;
|
|
391
|
+
const quote = redeemed.quote;
|
|
392
|
+
// Los mismos topes que contratar. Una consulta es barata, pero nada
|
|
393
|
+
// impide encadenar mil: el presupuesto del día es lo que lo impide.
|
|
394
|
+
if (quote.amount > limits.maxPerTaskWei) {
|
|
395
|
+
return (`That question costs ${formatEther(quote.amount)} ${quote.symbol} and this server per-item cap is ` +
|
|
396
|
+
`${formatEther(limits.maxPerTaskWei)}. Raise MCP_MAX_PER_TASK_WEI if that is intended.`);
|
|
397
|
+
}
|
|
398
|
+
const spent = ledger.spentToday();
|
|
399
|
+
if (spent + quote.amount > limits.dailyBudgetWei) {
|
|
400
|
+
return (`That would blow today's budget: ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} spent ` +
|
|
401
|
+
`and this costs ${formatEther(quote.amount)} ${quote.symbol}.`);
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
// `maxSpend` va atado al importe del presupuesto que la persona
|
|
405
|
+
// aprobó, no al tope del servidor: si el agente sube el precio entre
|
|
406
|
+
// el presupuesto y el sí, la firma no se produce.
|
|
407
|
+
const res = await panal.askAgent(quote.worker, quote.brief, { maxSpend: quote.amount });
|
|
408
|
+
// Se registra lo REALMENTE pagado, que es lo que devuelve el SDK.
|
|
409
|
+
ledger.record(res.paid);
|
|
410
|
+
log(`consulta pagada a ${quote.worker}: ${formatEther(res.paid)} ${quote.symbol}`);
|
|
411
|
+
return [
|
|
412
|
+
`Paid ${formatEther(res.paid)} ${quote.symbol} to ${quote.agentName}.`,
|
|
413
|
+
res.txHash ? ` tx: ${EXPLORER}/tx/${res.txHash}` : null,
|
|
414
|
+
'',
|
|
415
|
+
'Answer:',
|
|
416
|
+
res.answer,
|
|
417
|
+
]
|
|
418
|
+
.filter((l) => l !== null)
|
|
419
|
+
.join('\n');
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
423
|
+
log(`fallo al pagar la consulta: ${msg}`);
|
|
424
|
+
return `Could not pay for the question: ${msg}`;
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
},
|
|
279
428
|
{
|
|
280
429
|
name: 'panal_hire',
|
|
281
430
|
description: 'Hire the agent and LOCK THE REAL PAYMENT in escrow. Requires a quote_id from panal_quote_hire and ' +
|
|
@@ -301,7 +450,7 @@ const WRITE_TOOLS = [
|
|
|
301
450
|
const id = str(args.quote_id);
|
|
302
451
|
if (!id)
|
|
303
452
|
return 'The quote_id is missing. Ask for a quote with panal_quote_hire first.';
|
|
304
|
-
const redeemed = quotes.redeem(id);
|
|
453
|
+
const redeemed = quotes.redeem(id, 'hire');
|
|
305
454
|
if ('error' in redeemed)
|
|
306
455
|
return redeemed.error;
|
|
307
456
|
const quote = redeemed.quote;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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.6.1",
|
|
34
34
|
"viem": "^2.21.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|