panal-mcp 0.4.1 → 0.5.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/fetch-result.d.ts +18 -0
- package/dist/fetch-result.js +49 -0
- package/dist/server.js +131 -2
- package/package.json +4 -3
package/dist/fetch-result.d.ts
CHANGED
|
@@ -37,5 +37,23 @@ export declare function briefSignMessage(taskId: bigint): string;
|
|
|
37
37
|
* pago YA está bloqueado, así que lo útil es contarlo, no romper.
|
|
38
38
|
*/
|
|
39
39
|
export declare function pushBrief(botUrl: string, taskId: bigint, brief: string, client: string, signature: string): Promise<string | null>;
|
|
40
|
+
/**
|
|
41
|
+
* Los límites que el agente publica en su tarjeta (GET /agent.json).
|
|
42
|
+
*
|
|
43
|
+
* Existe para no descubrir un límite pagando. El encargo se entrega DESPUÉS de
|
|
44
|
+
* crear la tarea en la cadena —la firma va atada al taskId, que hasta entonces
|
|
45
|
+
* no existe—, así que un brief que el agente rechaza deja el pago bloqueado por
|
|
46
|
+
* una tarea que no puede empezar. Preguntando antes, eso se convierte en un
|
|
47
|
+
* presupuesto que no se emite y no cuesta nada.
|
|
48
|
+
*
|
|
49
|
+
* `maxBriefChars: null` significa NO LO SÉ, y no «no hay límite»: los agentes
|
|
50
|
+
* de la plantilla publican una tarjeta distinta que no declara ninguno. Quien
|
|
51
|
+
* llame tiene que poder distinguir las dos cosas, porque tratar «no lo sé» como
|
|
52
|
+
* «no hay tope» es volver a averiguarlo pagando.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseMaxBriefChars(card: unknown): number | null;
|
|
55
|
+
export declare function fetchAgentLimits(botUrl: string): Promise<{
|
|
56
|
+
maxBriefChars: number | null;
|
|
57
|
+
}>;
|
|
40
58
|
/** Descarga acotada: se corta en cuanto se pasa del tope. */
|
|
41
59
|
export declare function fetchResultText(botUrl: string, taskId: bigint, client: string, signature: string, expira: number): Promise<string>;
|
package/dist/fetch-result.js
CHANGED
|
@@ -87,6 +87,55 @@ export async function pushBrief(botUrl, taskId, brief, client, signature) {
|
|
|
87
87
|
clearTimeout(timer);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Los límites que el agente publica en su tarjeta (GET /agent.json).
|
|
92
|
+
*
|
|
93
|
+
* Existe para no descubrir un límite pagando. El encargo se entrega DESPUÉS de
|
|
94
|
+
* crear la tarea en la cadena —la firma va atada al taskId, que hasta entonces
|
|
95
|
+
* no existe—, así que un brief que el agente rechaza deja el pago bloqueado por
|
|
96
|
+
* una tarea que no puede empezar. Preguntando antes, eso se convierte en un
|
|
97
|
+
* presupuesto que no se emite y no cuesta nada.
|
|
98
|
+
*
|
|
99
|
+
* `maxBriefChars: null` significa NO LO SÉ, y no «no hay límite»: los agentes
|
|
100
|
+
* de la plantilla publican una tarjeta distinta que no declara ninguno. Quien
|
|
101
|
+
* llame tiene que poder distinguir las dos cosas, porque tratar «no lo sé» como
|
|
102
|
+
* «no hay tope» es volver a averiguarlo pagando.
|
|
103
|
+
*/
|
|
104
|
+
export function parseMaxBriefChars(card) {
|
|
105
|
+
const max = card?.endpoints?.postBrief
|
|
106
|
+
?.maxBriefChars;
|
|
107
|
+
// La tarjeta la sirve un desconocido: solo un entero positivo cuenta. Un 0 o
|
|
108
|
+
// un negativo harían imposible cualquier encargo, y eso lo decide el agente
|
|
109
|
+
// cambiando su tope, no mandando basura en un campo.
|
|
110
|
+
return typeof max === 'number' && Number.isInteger(max) && max > 0 ? max : null;
|
|
111
|
+
}
|
|
112
|
+
export async function fetchAgentLimits(botUrl) {
|
|
113
|
+
const desconocido = { maxBriefChars: null };
|
|
114
|
+
let url;
|
|
115
|
+
try {
|
|
116
|
+
const base = await assertPublicUrl(botUrl);
|
|
117
|
+
url = new URL('/agent.json', base);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return desconocido;
|
|
121
|
+
}
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch(url, { signal: controller.signal, redirect: 'error' });
|
|
126
|
+
if (!res.ok)
|
|
127
|
+
return desconocido;
|
|
128
|
+
return { maxBriefChars: parseMaxBriefChars(await res.json()) };
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Un agente sin tarjeta, o que tarda, no impide contratarlo: solo deja el
|
|
132
|
+
// límite sin comprobar. Fallar aquí seria peor que el problema.
|
|
133
|
+
return desconocido;
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
clearTimeout(timer);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
90
139
|
/** Descarga acotada: se corta en cuanto se pasa del tope. */
|
|
91
140
|
export async function fetchResultText(botUrl, taskId, client, signature, expira) {
|
|
92
141
|
const base = await assertPublicUrl(botUrl);
|
package/dist/server.js
CHANGED
|
@@ -30,7 +30,7 @@ import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
|
|
|
30
30
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
31
31
|
import { NATIVE_CURRENCY, TaskStatus, createPanalClient, } from '@panal/sdk';
|
|
32
32
|
import { QuoteBook, SpendLedger, limitsFromEnv } from './limits.js';
|
|
33
|
-
import { briefSignMessage, expiraEn, fetchResultText, pushBrief, resultSignMessage } from './fetch-result.js';
|
|
33
|
+
import { briefSignMessage, expiraEn, fetchAgentLimits, fetchResultText, pushBrief, resultSignMessage, } from './fetch-result.js';
|
|
34
34
|
const SERVER_NAME = 'panal';
|
|
35
35
|
/**
|
|
36
36
|
* Sale del package.json, no de una constante a mano: la copiada se quedó en
|
|
@@ -335,12 +335,21 @@ const WRITE_TOOLS = [
|
|
|
335
335
|
const address = account.address;
|
|
336
336
|
const [balance, spent] = [await panal.publicClient.getBalance({ address }), ledger.spentToday()];
|
|
337
337
|
const left = limits.dailyBudgetWei > spent ? limits.dailyBudgetWei - spent : 0n;
|
|
338
|
+
// Lo acreditado en el escrow no aparece en el balance de la wallet: es
|
|
339
|
+
// pull payment, así que un reembolso se queda ahí quieto hasta que se
|
|
340
|
+
// reclama. Sin decirlo, el dinero devuelto es indistinguible del perdido.
|
|
341
|
+
const pending = await panal.getPendingWithdrawal(address).catch(() => 0n);
|
|
338
342
|
return [
|
|
339
343
|
`Wallet: ${address}`,
|
|
340
344
|
`Balance: ${formatEther(balance)} MON`,
|
|
341
345
|
`Per-job cap: ${formatEther(limits.maxPerTaskWei)}`,
|
|
342
346
|
`Today's budget: spent ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} · ${formatEther(left)} left`,
|
|
343
|
-
|
|
347
|
+
pending > 0n
|
|
348
|
+
? `Waiting in the escrow: ${formatEther(pending)} MON — collect it with panal_withdraw`
|
|
349
|
+
: null,
|
|
350
|
+
]
|
|
351
|
+
.filter(Boolean)
|
|
352
|
+
.join('\n');
|
|
344
353
|
},
|
|
345
354
|
},
|
|
346
355
|
{
|
|
@@ -379,6 +388,19 @@ const WRITE_TOOLS = [
|
|
|
379
388
|
return (`That would blow today's budget: ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} spent ` +
|
|
380
389
|
`and this costs ${formatEther(amount)} ${symbol}.`);
|
|
381
390
|
}
|
|
391
|
+
// El tope del brief se pregunta ANTES de presupuestar. Si no cabe, el
|
|
392
|
+
// agente lo rechazaría con un 400 al recibirlo — pero eso ocurre ya con
|
|
393
|
+
// el pago bloqueado, porque el encargo se entrega después de crear la
|
|
394
|
+
// tarea. Aquí no cuesta nada y evita dejar dinero atrapado hasta que
|
|
395
|
+
// vence el plazo.
|
|
396
|
+
if (agent.metadata.botUrl) {
|
|
397
|
+
const { maxBriefChars } = await fetchAgentLimits(agent.metadata.botUrl);
|
|
398
|
+
if (maxBriefChars !== null && brief.length > maxBriefChars) {
|
|
399
|
+
return (`That job does not fit: ${agent.metadata.name || address} accepts ${maxBriefChars} characters ` +
|
|
400
|
+
`and this brief is ${brief.length}. Nothing was quoted and nothing was spent. ` +
|
|
401
|
+
`Shorten it by ${brief.length - maxBriefChars} characters and ask again.`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
382
404
|
const quote = quotes.issue({
|
|
383
405
|
kind: 'hire',
|
|
384
406
|
worker: agent.address,
|
|
@@ -688,6 +710,113 @@ const WRITE_TOOLS = [
|
|
|
688
710
|
}
|
|
689
711
|
},
|
|
690
712
|
},
|
|
713
|
+
{
|
|
714
|
+
name: 'panal_cancel_task',
|
|
715
|
+
description: 'Cancel a job that never started and get the locked payment back. Only works on a task that was ' +
|
|
716
|
+
'never delivered, and only once its deadline has passed (or no agent was ever assigned). ' +
|
|
717
|
+
'The refund is CREDITED, not sent: call panal_withdraw afterwards to actually receive it.',
|
|
718
|
+
inputSchema: {
|
|
719
|
+
type: 'object',
|
|
720
|
+
properties: {
|
|
721
|
+
task_id: { type: 'number', description: 'Id of the task to cancel.' },
|
|
722
|
+
confirmed_by_user: { type: 'boolean', description: 'true only if the person asked to cancel it.' },
|
|
723
|
+
},
|
|
724
|
+
required: ['task_id', 'confirmed_by_user'],
|
|
725
|
+
},
|
|
726
|
+
handler: async (args) => {
|
|
727
|
+
const blocked = writesBlockedReason();
|
|
728
|
+
if (blocked)
|
|
729
|
+
return blocked;
|
|
730
|
+
if (args.confirmed_by_user !== true) {
|
|
731
|
+
return 'I will not cancel a job without confirmed_by_user: true.';
|
|
732
|
+
}
|
|
733
|
+
const id = Number(args.task_id);
|
|
734
|
+
if (!Number.isInteger(id) || id < 0)
|
|
735
|
+
return 'The task id is an integer.';
|
|
736
|
+
try {
|
|
737
|
+
const hash = await panal.cancelTask(BigInt(id));
|
|
738
|
+
return [
|
|
739
|
+
`Task #${id} cancelled and the payment refunded.`,
|
|
740
|
+
` tx: ${EXPLORER}/tx/${hash}`,
|
|
741
|
+
'',
|
|
742
|
+
'The refund is credited in the escrow, not in the wallet yet: call panal_withdraw to collect it.',
|
|
743
|
+
].join('\n');
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
return `Could not cancel: ${err instanceof Error ? err.message : err}`;
|
|
747
|
+
}
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
name: 'panal_open_dispute',
|
|
752
|
+
description: 'Dispute a delivered result and STOP the clock. If nobody approves or disputes, the escrow pays ' +
|
|
753
|
+
'the agent on its own 3 days after delivery, with an implicit 5/5 — so doing nothing is not ' +
|
|
754
|
+
'neutral, it pays. An arbitrator then splits the amount; if they do not act in 14 days, the ' +
|
|
755
|
+
'client can be refunded in full. Use it when the delivery is wrong or never arrived.',
|
|
756
|
+
inputSchema: {
|
|
757
|
+
type: 'object',
|
|
758
|
+
properties: {
|
|
759
|
+
task_id: { type: 'number', description: 'Id of the delivered task.' },
|
|
760
|
+
confirmed_by_user: { type: 'boolean', description: 'true only if the person asked to dispute it.' },
|
|
761
|
+
},
|
|
762
|
+
required: ['task_id', 'confirmed_by_user'],
|
|
763
|
+
},
|
|
764
|
+
handler: async (args) => {
|
|
765
|
+
const blocked = writesBlockedReason();
|
|
766
|
+
if (blocked)
|
|
767
|
+
return blocked;
|
|
768
|
+
if (args.confirmed_by_user !== true) {
|
|
769
|
+
return 'I will not open a dispute without confirmed_by_user: true. Show the person the result first.';
|
|
770
|
+
}
|
|
771
|
+
const id = Number(args.task_id);
|
|
772
|
+
if (!Number.isInteger(id) || id < 0)
|
|
773
|
+
return 'The task id is an integer.';
|
|
774
|
+
try {
|
|
775
|
+
const hash = await panal.openDispute(BigInt(id));
|
|
776
|
+
return [
|
|
777
|
+
`Dispute opened on task #${id}. The automatic release is stopped.`,
|
|
778
|
+
` tx: ${EXPLORER}/tx/${hash}`,
|
|
779
|
+
'',
|
|
780
|
+
'An arbitrator decides how to split it. If nobody resolves it within 14 days, the whole amount',
|
|
781
|
+
'can be refunded to the client.',
|
|
782
|
+
].join('\n');
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
return `Could not open the dispute: ${err instanceof Error ? err.message : err}`;
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: 'panal_withdraw',
|
|
791
|
+
description: 'Collect what the escrow owes this wallet — refunds from cancelled jobs. The escrow is pull ' +
|
|
792
|
+
'payment: it credits balances and never pushes them, so money sits there until this is called. ' +
|
|
793
|
+
'Call panal_wallet first to see whether there is anything to collect.',
|
|
794
|
+
inputSchema: {
|
|
795
|
+
type: 'object',
|
|
796
|
+
properties: {
|
|
797
|
+
confirmed_by_user: { type: 'boolean', description: 'true only if the person asked to collect it.' },
|
|
798
|
+
},
|
|
799
|
+
required: ['confirmed_by_user'],
|
|
800
|
+
},
|
|
801
|
+
handler: async (args) => {
|
|
802
|
+
const blocked = writesBlockedReason();
|
|
803
|
+
if (blocked)
|
|
804
|
+
return blocked;
|
|
805
|
+
if (args.confirmed_by_user !== true) {
|
|
806
|
+
return 'I will not move funds without confirmed_by_user: true.';
|
|
807
|
+
}
|
|
808
|
+
try {
|
|
809
|
+
const pending = await panal.getPendingWithdrawal(account.address);
|
|
810
|
+
if (pending === 0n)
|
|
811
|
+
return 'The escrow owes this wallet nothing: there is nothing to collect.';
|
|
812
|
+
const hash = await panal.withdraw();
|
|
813
|
+
return `Collected ${formatEther(pending)} MON from the escrow.\n tx: ${EXPLORER}/tx/${hash}`;
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
return `Could not withdraw: ${err instanceof Error ? err.message : err}`;
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
},
|
|
691
820
|
];
|
|
692
821
|
/** Las de escritura se anuncian siempre: si no, el modelo no sabe que existen. */
|
|
693
822
|
const TOOLS = [...READ_TOOLS, ...WRITE_TOOLS];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "panal-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.9.0",
|
|
34
34
|
"viem": "^2.21.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"build": "tsc -p tsconfig.json",
|
|
42
42
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
43
|
"start": "tsx src/server.ts",
|
|
44
|
-
"test": "tsx test/mcp.test.ts"
|
|
44
|
+
"test": "tsx test/mcp.test.ts",
|
|
45
|
+
"test:limites": "tsx test/limites.test.ts"
|
|
45
46
|
}
|
|
46
47
|
}
|