panal-mcp 0.1.3 → 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.
@@ -12,6 +12,21 @@
12
12
  */
13
13
  /** El mensaje que firma el cliente. Debe coincidir con el bot y el frontend. */
14
14
  export declare function resultSignMessage(taskId: bigint): string;
15
+ /** El mensaje que firma el cliente para MANDAR el encargo. Mismo pacto. */
16
+ export declare function briefSignMessage(taskId: bigint): string;
17
+ /**
18
+ * Le entrega el encargo al agente: POST /brief/<taskId>, firmado.
19
+ *
20
+ * Existe porque contratar sin esto deja la tarea a medias. On-chain solo queda
21
+ * el hash del encargo; el texto tiene que llegarle al agente por su endpoint o
22
+ * el agente se queda mirando una tarea pagada sin saber qué se le pide. Antes
23
+ * el MCP contrataba y le decía al usuario "ahora hazle llegar el texto tú",
24
+ * que en una conversación con un modelo no lo hace nadie.
25
+ *
26
+ * Devuelve null si fue bien, o el motivo si no. No lanza: cuando esto falla el
27
+ * pago YA está bloqueado, así que lo útil es contarlo, no romper.
28
+ */
29
+ export declare function pushBrief(botUrl: string, taskId: bigint, brief: string, client: string, signature: string): Promise<string | null>;
15
30
  /**
16
31
  * Rechaza una URL que apunte a algo que no sea internet pública.
17
32
  *
@@ -19,6 +19,59 @@ const TIMEOUT_MS = 20_000;
19
19
  export function resultSignMessage(taskId) {
20
20
  return `Panal resultado #${taskId.toString()}`;
21
21
  }
22
+ /** El mensaje que firma el cliente para MANDAR el encargo. Mismo pacto. */
23
+ export function briefSignMessage(taskId) {
24
+ return `Panal brief #${taskId.toString()}`;
25
+ }
26
+ /**
27
+ * Le entrega el encargo al agente: POST /brief/<taskId>, firmado.
28
+ *
29
+ * Existe porque contratar sin esto deja la tarea a medias. On-chain solo queda
30
+ * el hash del encargo; el texto tiene que llegarle al agente por su endpoint o
31
+ * el agente se queda mirando una tarea pagada sin saber qué se le pide. Antes
32
+ * el MCP contrataba y le decía al usuario "ahora hazle llegar el texto tú",
33
+ * que en una conversación con un modelo no lo hace nadie.
34
+ *
35
+ * Devuelve null si fue bien, o el motivo si no. No lanza: cuando esto falla el
36
+ * pago YA está bloqueado, así que lo útil es contarlo, no romper.
37
+ */
38
+ export async function pushBrief(botUrl, taskId, brief, client, signature) {
39
+ let url;
40
+ try {
41
+ const base = await assertPublicUrl(botUrl);
42
+ url = new URL(`/brief/${taskId}`, base);
43
+ }
44
+ catch (err) {
45
+ return err instanceof Error ? err.message : String(err);
46
+ }
47
+ const controller = new AbortController();
48
+ const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
49
+ try {
50
+ const res = await fetch(url, {
51
+ method: 'POST',
52
+ headers: { 'content-type': 'application/json' },
53
+ body: JSON.stringify({ brief, address: client, signature }),
54
+ signal: controller.signal,
55
+ redirect: 'error',
56
+ });
57
+ if (res.ok)
58
+ return null;
59
+ // El cuerpo del error dice cuál de las comprobaciones del agente falló
60
+ // —firma, estado, o que el texto no cuadra con el hash—, y eso es
61
+ // exactamente lo que hay que enseñar para poder arreglarlo.
62
+ const detalle = (await res.text().catch(() => '')).slice(0, 300);
63
+ return `the agent replied ${res.status}${detalle ? `: ${detalle}` : ''}`;
64
+ }
65
+ catch (err) {
66
+ if (err instanceof Error && err.name === 'AbortError') {
67
+ return `the endpoint did not answer within ${TIMEOUT_MS / 1000} s`;
68
+ }
69
+ return err instanceof Error ? err.message : String(err);
70
+ }
71
+ finally {
72
+ clearTimeout(timer);
73
+ }
74
+ }
22
75
  /** ¿Esta IP apunta dentro de nuestra propia red? */
23
76
  function isPrivateIp(ip) {
24
77
  if (isIP(ip) === 6) {
@@ -59,24 +112,24 @@ export async function assertPublicUrl(raw) {
59
112
  url = new URL(raw);
60
113
  }
61
114
  catch {
62
- throw new Error(`El endpoint del agente no es una URL válida: ${raw}`);
115
+ throw new Error(`The agent endpoint is not a valid URL: ${raw}`);
63
116
  }
64
117
  if (url.protocol !== 'https:') {
65
118
  // La petición lleva una firma del cliente en la query: en claro la lee cualquiera.
66
- throw new Error(`El endpoint del agente tiene que ser https, y es ${url.protocol}//`);
119
+ throw new Error(`The agent endpoint must be https, and it is ${url.protocol}//`);
67
120
  }
68
121
  if (url.username || url.password)
69
- throw new Error('El endpoint lleva credenciales embebidas: se rechaza.');
122
+ throw new Error('The endpoint carries embedded credentials: rejected.');
70
123
  const host = url.hostname.replace(/^\[|\]$/g, '');
71
124
  if (/^(localhost|.*\.local|.*\.internal)$/i.test(host)) {
72
- throw new Error(`El endpoint apunta a un nombre local (${host}): se rechaza.`);
125
+ throw new Error(`The endpoint points at a local name (${host}): rejected.`);
73
126
  }
74
127
  const ips = isIP(host) ? [host] : (await lookup(host, { all: true })).map((r) => r.address);
75
128
  if (!ips.length)
76
- throw new Error(`No se pudo resolver ${host}.`);
129
+ throw new Error(`Could not resolve ${host}.`);
77
130
  for (const ip of ips) {
78
131
  if (isPrivateIp(ip))
79
- throw new Error(`El endpoint apunta a una dirección interna (${ip}): se rechaza.`);
132
+ throw new Error(`The endpoint points at an internal address (${ip}): rejected.`);
80
133
  }
81
134
  return url;
82
135
  }
@@ -91,11 +144,11 @@ export async function fetchResultText(botUrl, taskId, client, signature) {
91
144
  try {
92
145
  const res = await fetch(url, { signal: controller.signal, headers: { accept: 'application/json' } });
93
146
  if (!res.ok) {
94
- throw new Error(`El endpoint del agente respondió ${res.status}. ¿Sigue en pie y la firma es la del cliente?`);
147
+ throw new Error(`The agent endpoint replied ${res.status}. Is it still up, and is the signature the client's?`);
95
148
  }
96
149
  const reader = res.body?.getReader();
97
150
  if (!reader)
98
- throw new Error('El endpoint del agente respondió sin cuerpo.');
151
+ throw new Error('The agent endpoint replied with no body.');
99
152
  const chunks = [];
100
153
  let total = 0;
101
154
  for (;;) {
@@ -105,14 +158,14 @@ export async function fetchResultText(botUrl, taskId, client, signature) {
105
158
  total += value.byteLength;
106
159
  if (total > MAX_BYTES) {
107
160
  await reader.cancel();
108
- throw new Error(`El resultado pasa de ${MAX_BYTES / 1024} KB: se corta por seguridad.`);
161
+ throw new Error(`The result exceeds ${MAX_BYTES / 1024} KB: cut off for safety.`);
109
162
  }
110
163
  chunks.push(value);
111
164
  }
112
165
  const body = Buffer.concat(chunks).toString('utf8');
113
166
  const parsed = JSON.parse(body);
114
167
  if (typeof parsed.resultText !== 'string')
115
- throw new Error('La respuesta del agente no trae `resultText`.');
168
+ throw new Error('The agent response has no `resultText`.');
116
169
  return parsed.resultText;
117
170
  }
118
171
  finally {
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
- /** Devuelve el presupuesto, o el motivo por el que no vale. */
30
- redeem(id: string): {
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
- /** Devuelve el presupuesto, o el motivo por el que no vale. */
28
- redeem(id) {
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
- error: 'Ese presupuesto no existe o ya caducó (duran 5 minutos). ' +
34
- 'Pide uno nuevo con panal_quote_hire y enséñaselo a la persona antes de contratar.',
39
+ error: 'That quote does not exist or has expired (they last 5 minutes). ' +
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
@@ -22,14 +22,30 @@
22
22
  * RESERVADO para los mensajes del protocolo —un `console.log` suelto ahí
23
23
  * corrompe la sesión entera—, así que todo el registro va por stderr.
24
24
  */
25
+ import { readFileSync } from 'node:fs';
26
+ import { dirname, resolve } from 'node:path';
25
27
  import { createInterface } from 'node:readline';
28
+ import { fileURLToPath } from 'node:url';
26
29
  import { formatEther, isAddress, keccak256, parseEther, toBytes } from 'viem';
27
30
  import { privateKeyToAccount } from 'viem/accounts';
28
31
  import { NATIVE_CURRENCY, TaskStatus, createPanalClient, } from '@panal/sdk';
29
32
  import { QuoteBook, SpendLedger, limitsFromEnv } from './limits.js';
30
- import { fetchResultText, resultSignMessage } from './fetch-result.js';
33
+ import { briefSignMessage, fetchResultText, pushBrief, resultSignMessage } from './fetch-result.js';
31
34
  const SERVER_NAME = 'panal';
32
- const SERVER_VERSION = '0.1.0';
35
+ /**
36
+ * Sale del package.json, no de una constante a mano: la copiada se quedó en
37
+ * 0.1.0 mientras el paquete iba por 0.1.3, y un servidor que se anuncia con
38
+ * una versión falsa hace imposible saber qué está corriendo el usuario.
39
+ */
40
+ const SERVER_VERSION = (() => {
41
+ try {
42
+ const aqui = dirname(fileURLToPath(import.meta.url));
43
+ return JSON.parse(readFileSync(resolve(aqui, '..', 'package.json'), 'utf8')).version;
44
+ }
45
+ catch {
46
+ return '0.0.0';
47
+ }
48
+ })();
33
49
  /** Versiones del protocolo que sabemos hablar; la primera es la preferida. */
34
50
  const SUPPORTED_PROTOCOLS = ['2025-06-18', '2025-03-26', '2024-11-05'];
35
51
  const EXPLORER = 'https://monadvision.com';
@@ -61,12 +77,11 @@ const panal = createPanalClient({
61
77
  /** Motivo por el que no se puede escribir, o null si sí se puede. */
62
78
  function writesBlockedReason() {
63
79
  if (!writesRequested) {
64
- return ('Este servidor está en SOLO LECTURA. Para contratar hay que arrancarlo con ' +
65
- 'MCP_ENABLE_WRITES=true y una MCP_PRIVATE_KEY con fondos. Dile a la persona que lo configure ella: ' +
66
- 'no intentes rodearlo.');
80
+ return ('This server is READ-ONLY. To hire, it must be started with MCP_ENABLE_WRITES=true and an ' +
81
+ 'MCP_PRIVATE_KEY with funds. Tell the person to configure it themselves: do not try to work around it.');
67
82
  }
68
83
  if (!account) {
69
- return 'MCP_ENABLE_WRITES está activo pero no hay una MCP_PRIVATE_KEY válida, así que no hay wallet con la que pagar.';
84
+ return 'MCP_ENABLE_WRITES is on but there is no valid MCP_PRIVATE_KEY, so there is no wallet to pay with.';
70
85
  }
71
86
  return null;
72
87
  }
@@ -78,15 +93,18 @@ function symbolOf(currency) {
78
93
  }
79
94
  function renderAgent(agent) {
80
95
  const price = `${formatEther(agent.pricePerTask)} ${symbolOf(agent.currency)}`;
81
- const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(sin skills declaradas)';
96
+ const skills = agent.metadata.skills.length ? agent.metadata.skills.join(', ') : '(no skills declared)';
82
97
  return [
83
- `${agent.metadata.name || '(sin nombre)'} — ${agent.address}`,
84
- ` Estado: ${agent.active ? 'activo' : 'DADO DE BAJA (no acepta encargos)'}`,
85
- ` Precio: ${price} por tarea`,
98
+ `${agent.metadata.name || '(no name)'} — ${agent.address}`,
99
+ ` Status: ${agent.active ? 'active' : 'DELISTED (does not take jobs)'}`,
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)`,
86
104
  ` Skills: ${skills}`,
87
- agent.metadata.description ? ` Descripción: ${agent.metadata.description}` : null,
105
+ agent.metadata.description ? ` Description: ${agent.metadata.description}` : null,
88
106
  agent.metadata.botUrl ? ` Endpoint: ${agent.metadata.botUrl}` : null,
89
- ` Ficha: ${EXPLORER}/address/${agent.address}`,
107
+ ` Profile: ${EXPLORER}/address/${agent.address}`,
90
108
  ]
91
109
  .filter(Boolean)
92
110
  .join('\n');
@@ -94,13 +112,13 @@ function renderAgent(agent) {
94
112
  function renderTask(task) {
95
113
  const deadline = new Date(Number(task.deadline) * 1000).toISOString();
96
114
  return [
97
- `Tarea #${task.id} — ${TaskStatus[task.status] ?? task.status}`,
98
- ` Cliente: ${task.client}`,
99
- ` Agente: ${task.worker}`,
100
- ` Importe: ${formatEther(task.amount)} ${symbolOf(task.currency)}`,
101
- ` Plazo: ${deadline}`,
102
- ` Hash del encargo: ${task.taskHash}`,
103
- task.resultHash && !/^0x0+$/.test(task.resultHash) ? ` Hash del resultado: ${task.resultHash}` : null,
115
+ `Task #${task.id} — ${TaskStatus[task.status] ?? task.status}`,
116
+ ` Client: ${task.client}`,
117
+ ` Agent: ${task.worker}`,
118
+ ` Amount: ${formatEther(task.amount)} ${symbolOf(task.currency)}`,
119
+ ` Deadline: ${deadline}`,
120
+ ` Job hash: ${task.taskHash}`,
121
+ task.resultHash && !/^0x0+$/.test(task.resultHash) ? ` Result hash: ${task.resultHash}` : null,
104
122
  ]
105
123
  .filter(Boolean)
106
124
  .join('\n');
@@ -109,13 +127,13 @@ const str = (v) => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
109
127
  const READ_TOOLS = [
110
128
  {
111
129
  name: 'panal_search_agents',
112
- description: 'Busca agentes de IA en el marketplace Panal (Monad mainnet) por texto libre sobre su nombre, ' +
113
- 'descripción y skills. Sin búsqueda devuelve todos los activos. Datos leídos de la cadena en vivo.',
130
+ description: 'Search AI agents on the Panal marketplace (Monad mainnet) by free text over their name, ' +
131
+ 'description and skills. With no query it returns every active agent. Read live from the chain.',
114
132
  inputSchema: {
115
133
  type: 'object',
116
134
  properties: {
117
- query: { type: 'string', description: 'Qué buscas, p. ej. "traducción" o "resúmenes legales".' },
118
- include_inactive: { type: 'boolean', description: 'Incluir agentes dados de baja. Por defecto no.' },
135
+ query: { type: 'string', description: 'What you are looking for, e.g. "translation" or "legal summaries".' },
136
+ include_inactive: { type: 'boolean', description: 'Include delisted agents. Off by default.' },
119
137
  },
120
138
  },
121
139
  handler: async (args) => {
@@ -124,60 +142,139 @@ const READ_TOOLS = [
124
142
  });
125
143
  if (!found.length) {
126
144
  return str(args.query)
127
- ? `Ningún agente activo encaja con "${str(args.query)}". Prueba con un término más general o mira todos sin búsqueda.`
128
- : 'Ahora mismo no hay agentes activos en el marketplace.';
145
+ ? `No active agent matches "${str(args.query)}". Try a broader term, or list them all with no query.`
146
+ : 'There are no active agents on the marketplace right now.';
129
147
  }
130
- return `${found.length} agente(s):\n\n${found.map(renderAgent).join('\n\n')}`;
148
+ return `${found.length} agent(s):\n\n${found.map(renderAgent).join('\n\n')}`;
131
149
  },
132
150
  },
133
151
  {
134
152
  name: 'panal_get_agent',
135
- description: 'Ficha completa de un agente de Panal por su dirección.',
153
+ description: 'Full profile of a Panal agent, by address.',
136
154
  inputSchema: {
137
155
  type: 'object',
138
- properties: { address: { type: 'string', description: 'Dirección 0x… del agente.' } },
156
+ properties: { address: { type: 'string', description: 'The agent 0x… address.' } },
139
157
  required: ['address'],
140
158
  },
141
159
  handler: async (args) => {
142
160
  const address = str(args.address);
143
161
  if (!address || !isAddress(address))
144
- return 'Necesito una dirección 0x válida.';
145
- return renderAgent(await panal.getAgent(address));
162
+ return 'I need a valid 0x address.';
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}`;
146
178
  },
147
179
  },
148
180
  {
149
181
  name: 'panal_get_task',
150
- description: 'Estado de un encargo en el escrow de Panal: importe, plazo, agente y si ya se entregó.',
182
+ description: 'State of a job in the Panal escrow: amount, deadline, agent, and whether it was delivered.',
151
183
  inputSchema: {
152
184
  type: 'object',
153
- properties: { task_id: { type: 'number', description: 'Id numérico de la tarea.' } },
185
+ properties: { task_id: { type: 'number', description: 'Numeric task id.' } },
154
186
  required: ['task_id'],
155
187
  },
156
188
  handler: async (args) => {
157
189
  const id = Number(args.task_id);
158
190
  if (!Number.isInteger(id) || id < 0)
159
- return 'El id de la tarea es un entero mayor o igual que cero.';
191
+ return 'The task id is an integer greater than or equal to zero.';
160
192
  const count = await panal.getTaskCount();
161
193
  if (BigInt(id) >= count)
162
- return `La tarea #${id} no existe: ahora mismo hay ${count}.`;
194
+ return `Task #${id} does not exist: there are ${count} so far.`;
163
195
  return renderTask(await panal.getTask(BigInt(id)));
164
196
  },
165
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
+ },
166
263
  {
167
264
  name: 'panal_marketplace_stats',
168
- description: 'Cifras del marketplace Panal: cuántos agentes hay, cuántos activos y cuántos encargos van.',
265
+ description: 'Panal marketplace figures: how many agents exist, how many are active, how many jobs so far.',
169
266
  inputSchema: { type: 'object', properties: {} },
170
267
  handler: async () => {
171
268
  const [agents, taskCount] = await Promise.all([panal.listAgents(), panal.getTaskCount()]);
172
269
  const active = agents.filter((a) => a.active);
173
270
  return [
174
- `Agentes registrados: ${agents.length}`,
175
- `Agentes activos: ${active.length}`,
176
- `Encargos creados: ${taskCount}`,
271
+ `Registered agents: ${agents.length}`,
272
+ `Active agents: ${active.length}`,
273
+ `Jobs created: ${taskCount}`,
177
274
  `Escrow: ${panal.addresses.escrow}`,
178
275
  `Registry: ${panal.addresses.registry}`,
179
276
  '',
180
- active.length ? `Activos ahora:\n${active.map(renderAgent).join('\n\n')}` : 'No hay agentes activos.',
277
+ active.length ? `Active now:\n${active.map(renderAgent).join('\n\n')}` : 'No active agents.',
181
278
  ].join('\n');
182
279
  },
183
280
  },
@@ -185,8 +282,8 @@ const READ_TOOLS = [
185
282
  const WRITE_TOOLS = [
186
283
  {
187
284
  name: 'panal_wallet',
188
- description: 'Estado de la wallet con la que este servidor pagaría: dirección, saldo y cuánto queda del ' +
189
- 'presupuesto de hoy. Úsala antes de contratar para saber si hay fondos.',
285
+ description: 'State of the wallet this server would pay with: address, balance, and how much of today\'s budget ' +
286
+ 'is left. Call it before hiring to know whether there are funds.',
190
287
  inputSchema: { type: 'object', properties: {} },
191
288
  handler: async () => {
192
289
  const blocked = writesBlockedReason();
@@ -197,21 +294,21 @@ const WRITE_TOOLS = [
197
294
  const left = limits.dailyBudgetWei > spent ? limits.dailyBudgetWei - spent : 0n;
198
295
  return [
199
296
  `Wallet: ${address}`,
200
- `Saldo: ${formatEther(balance)} MON`,
201
- `Tope por encargo: ${formatEther(limits.maxPerTaskWei)}`,
202
- `Presupuesto de hoy: gastado ${formatEther(spent)} de ${formatEther(limits.dailyBudgetWei)} · queda ${formatEther(left)}`,
297
+ `Balance: ${formatEther(balance)} MON`,
298
+ `Per-job cap: ${formatEther(limits.maxPerTaskWei)}`,
299
+ `Today's budget: spent ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} · ${formatEther(left)} left`,
203
300
  ].join('\n');
204
301
  },
205
302
  },
206
303
  {
207
304
  name: 'panal_quote_hire',
208
- description: 'Presupuesta un encargo SIN pagar nada. Devuelve el precio real del agente y un quote_id. ' +
209
- 'Enseña siempre el precio a la persona y espera su visto bueno antes de llamar a panal_hire.',
305
+ description: 'Quote a job WITHOUT paying anything. Returns the agent real price and a quote_id. ' +
306
+ 'Always show the price to the person and wait for their approval before calling panal_hire.',
210
307
  inputSchema: {
211
308
  type: 'object',
212
309
  properties: {
213
- agent: { type: 'string', description: 'Dirección 0x… del agente.' },
214
- brief: { type: 'string', description: 'El encargo, redactado con todo el detalle necesario.' },
310
+ agent: { type: 'string', description: 'The agent 0x… address.' },
311
+ brief: { type: 'string', description: 'The job, written with all the detail the agent will need.' },
215
312
  },
216
313
  required: ['agent', 'brief'],
217
314
  },
@@ -222,24 +319,25 @@ const WRITE_TOOLS = [
222
319
  const address = str(args.agent);
223
320
  const brief = str(args.brief);
224
321
  if (!address || !isAddress(address))
225
- return 'Necesito una dirección 0x válida del agente.';
322
+ return 'I need a valid 0x address for the agent.';
226
323
  if (!brief)
227
- return 'Necesito el texto del encargo.';
324
+ return 'I need the text of the job.';
228
325
  const agent = await panal.getAgent(address);
229
326
  if (!agent.active)
230
- return `${agent.metadata.name || address} está dado de baja y no acepta encargos.`;
327
+ return `${agent.metadata.name || address} is delisted and does not take jobs.`;
231
328
  const amount = agent.pricePerTask;
232
329
  const symbol = symbolOf(agent.currency);
233
330
  if (amount > limits.maxPerTaskWei) {
234
- return (`Ese agente cobra ${formatEther(amount)} ${symbol} y el tope por encargo de este servidor es ` +
235
- `${formatEther(limits.maxPerTaskWei)}. No se puede contratar sin que la persona suba MCP_MAX_PER_TASK_WEI.`);
331
+ return (`That agent charges ${formatEther(amount)} ${symbol} and this server per-job cap is ` +
332
+ `${formatEther(limits.maxPerTaskWei)}. It cannot be hired unless the person raises MCP_MAX_PER_TASK_WEI.`);
236
333
  }
237
334
  const spent = ledger.spentToday();
238
335
  if (spent + amount > limits.dailyBudgetWei) {
239
- return (`Se pasaría del presupuesto de hoy: llevas ${formatEther(spent)} de ${formatEther(limits.dailyBudgetWei)} ` +
240
- `y esto cuesta ${formatEther(amount)} ${symbol}.`);
336
+ return (`That would blow today's budget: ${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)} spent ` +
337
+ `and this costs ${formatEther(amount)} ${symbol}.`);
241
338
  }
242
339
  const quote = quotes.issue({
340
+ kind: 'hire',
243
341
  worker: agent.address,
244
342
  agentName: agent.metadata.name || agent.address,
245
343
  brief,
@@ -249,29 +347,95 @@ const WRITE_TOOLS = [
249
347
  botUrl: agent.metadata.botUrl,
250
348
  });
251
349
  return [
252
- `Presupuesto para ${quote.agentName}:`,
253
- ` Precio: ${formatEther(amount)} ${symbol}`,
254
- ` Plazo: ${limits.deadlineHours} h desde que se contrate`,
255
- ` Encargo: ${brief.length > 200 ? `${brief.slice(0, 200)}…` : brief}`,
350
+ `Quote for ${quote.agentName}:`,
351
+ ` Price: ${formatEther(amount)} ${symbol}`,
352
+ ` Deadline: ${limits.deadlineHours} h from the moment it is hired`,
353
+ ` Job: ${brief.length > 200 ? `${brief.slice(0, 200)}…` : brief}`,
256
354
  '',
257
- `quote_id: ${quote.id} (vale 5 minutos)`,
355
+ `quote_id: ${quote.id} (valid for 5 minutes)`,
258
356
  '',
259
- 'Enséñale este precio a la persona. Solo si dice que sí, llama a panal_hire con ese quote_id ' +
260
- 'y confirmed_by_user: true. Esto mueve dinero real.',
357
+ 'Show this price to the person. Only if they say yes, call panal_hire with that quote_id ' +
358
+ 'and confirmed_by_user: true. This moves real money.',
261
359
  ].join('\n');
262
360
  },
263
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
+ },
264
428
  {
265
429
  name: 'panal_hire',
266
- description: 'Contrata al agente y BLOQUEA EL PAGO REAL en el escrow. Exige un quote_id de panal_quote_hire y ' +
267
- 'que la persona haya dicho que sí de forma explícita. No lo llames por iniciativa propia.',
430
+ description: 'Hire the agent and LOCK THE REAL PAYMENT in escrow. Requires a quote_id from panal_quote_hire and ' +
431
+ 'an explicit yes from the person. Never call it on your own initiative.',
268
432
  inputSchema: {
269
433
  type: 'object',
270
434
  properties: {
271
- quote_id: { type: 'string', description: 'El id devuelto por panal_quote_hire.' },
435
+ quote_id: { type: 'string', description: 'The id returned by panal_quote_hire.' },
272
436
  confirmed_by_user: {
273
437
  type: 'boolean',
274
- description: 'true solo si la persona ha visto el precio y ha dado su visto bueno.',
438
+ description: 'true only if the person has seen the price and approved it.',
275
439
  },
276
440
  },
277
441
  required: ['quote_id', 'confirmed_by_user'],
@@ -281,12 +445,12 @@ const WRITE_TOOLS = [
281
445
  if (blocked)
282
446
  return blocked;
283
447
  if (args.confirmed_by_user !== true) {
284
- return 'No contrato sin confirmed_by_user: true. Enséñale el presupuesto a la persona y pregúntale primero.';
448
+ return 'I will not hire without confirmed_by_user: true. Show the quote to the person and ask first.';
285
449
  }
286
450
  const id = str(args.quote_id);
287
451
  if (!id)
288
- return 'Falta el quote_id. Pide antes un presupuesto con panal_quote_hire.';
289
- const redeemed = quotes.redeem(id);
452
+ return 'The quote_id is missing. Ask for a quote with panal_quote_hire first.';
453
+ const redeemed = quotes.redeem(id, 'hire');
290
454
  if ('error' in redeemed)
291
455
  return redeemed.error;
292
456
  const quote = redeemed.quote;
@@ -294,63 +458,83 @@ const WRITE_TOOLS = [
294
458
  // pueden haber pasado minutos y otros encargos.
295
459
  const spent = ledger.spentToday();
296
460
  if (spent + quote.amount > limits.dailyBudgetWei) {
297
- return `Mientras tanto se agotó el presupuesto de hoy (${formatEther(spent)} de ${formatEther(limits.dailyBudgetWei)}).`;
461
+ return `Today's budget ran out in the meantime (${formatEther(spent)} of ${formatEther(limits.dailyBudgetWei)}).`;
298
462
  }
299
463
  const deadline = BigInt(Math.floor(Date.now() / 1000) + limits.deadlineHours * 3600);
300
464
  try {
301
465
  const result = await panal.hire({ agent: quote.worker, brief: quote.brief, deadline });
302
466
  ledger.record(result.amount);
303
467
  log(`contratada #${result.taskId} a ${quote.worker} por ${formatEther(result.amount)} ${quote.symbol}`);
468
+ // Contratar sin entregar el encargo deja la tarea a medias: el pago
469
+ // bloqueado y el agente esperando un texto que nadie le manda. Se hace
470
+ // aquí, no se le pide al usuario, porque quien está leyendo esto es un
471
+ // modelo en mitad de una conversación y no va a abrir una terminal.
472
+ let entregaBrief;
473
+ if (!quote.botUrl) {
474
+ entregaBrief = 'That agent publishes no endpoint: get the job to them through whatever channel you use.';
475
+ }
476
+ else {
477
+ const firma = await account.signMessage({ message: briefSignMessage(result.taskId) });
478
+ const fallo = await pushBrief(quote.botUrl, result.taskId, quote.brief, account.address, firma);
479
+ if (fallo === null) {
480
+ entregaBrief = `Job delivered to ${quote.botUrl}: the agent is already working on it.`;
481
+ log(`brief #${result.taskId} entregado en ${quote.botUrl}`);
482
+ }
483
+ else {
484
+ entregaBrief =
485
+ `WARNING: the payment is locked but the job did NOT arrive (${fallo}).\n` +
486
+ `The agent cannot start. Retry with panal_send_brief ${result.taskId}, ` +
487
+ `or the payment returns on its own when the deadline passes.`;
488
+ log(`brief #${result.taskId} NO entregado: ${fallo}`);
489
+ }
490
+ }
304
491
  return [
305
- `Contratado. Tarea #${result.taskId} creada y pago bloqueado en el escrow.`,
306
- ` Agente: ${quote.agentName}`,
307
- ` Importe: ${formatEther(result.amount)} ${quote.symbol}`,
492
+ `Hired. Task #${result.taskId} created and payment locked in escrow.`,
493
+ ` Agent: ${quote.agentName}`,
494
+ ` Amount: ${formatEther(result.amount)} ${quote.symbol}`,
308
495
  ` tx: ${EXPLORER}/tx/${result.txHash}`,
309
496
  '',
310
- quote.botUrl
311
- ? `El agente publica endpoint (${quote.botUrl}): el resultado se podrá descargar de ahí.`
312
- : 'Ese agente no publica endpoint: pídele el resultado por el canal que uses con él.',
497
+ entregaBrief,
313
498
  '',
314
- 'IMPORTANTE: el encargo NO viaja on-chain, solo su hash. Hazle llegar el texto al agente.',
315
- `Cuando entregue, revisa con panal_get_task ${result.taskId} y aprueba con panal_approve_task.`,
499
+ `When it delivers, check with panal_get_task ${result.taskId} and approve with panal_approve_task.`,
316
500
  ].join('\n');
317
501
  }
318
502
  catch (err) {
319
503
  const msg = err instanceof Error ? err.message : String(err);
320
504
  log(`fallo al contratar: ${msg}`);
321
- return `No se pudo contratar: ${msg}`;
505
+ return `Could not hire: ${msg}`;
322
506
  }
323
507
  },
324
508
  },
325
509
  {
326
510
  name: 'panal_get_result',
327
- description: 'Recoge el resultado de una tarea ya entregada. El texto vive fuera de la cadena: se le pide al ' +
328
- 'endpoint del agente firmando como cliente, y se comprueba que su hash coincide con el anclado ' +
329
- 'on-chain. Solo funciona para tareas que contrató la wallet de este servidor.',
511
+ description: 'Collect the result of a delivered task. The text lives off-chain: it is requested from the agent ' +
512
+ 'endpoint by signing as the client, and its hash is checked against the one anchored on-chain. ' +
513
+ 'Only works for tasks hired by this server wallet.',
330
514
  inputSchema: {
331
515
  type: 'object',
332
- properties: { task_id: { type: 'number', description: 'Id de la tarea entregada.' } },
516
+ properties: { task_id: { type: 'number', description: 'Id of the delivered task.' } },
333
517
  required: ['task_id'],
334
518
  },
335
519
  handler: async (args) => {
336
520
  if (!account)
337
- return 'Sin MCP_PRIVATE_KEY no se puede firmar la petición del resultado.';
521
+ return 'Without MCP_PRIVATE_KEY the request for the result cannot be signed.';
338
522
  const id = Number(args.task_id);
339
523
  if (!Number.isInteger(id) || id < 0)
340
524
  return 'El id de la tarea es un entero mayor o igual que cero.';
341
525
  const taskId = BigInt(id);
342
526
  const task = await panal.getTask(taskId);
343
527
  if (task.client.toLowerCase() !== account.address.toLowerCase()) {
344
- return `La tarea #${id} la contrató ${task.client}, no esta wallet. Solo el cliente puede recoger el resultado.`;
528
+ return `Task #${id} was hired by ${task.client}, not this wallet. Only the client can collect the result.`;
345
529
  }
346
530
  if (/^0x0+$/.test(task.resultHash)) {
347
- return `La tarea #${id} todavía no tiene resultado entregado (estado: ${TaskStatus[task.status] ?? task.status}).`;
531
+ return `Task #${id} has no delivered result yet (status: ${TaskStatus[task.status] ?? task.status}).`;
348
532
  }
349
533
  const agent = await panal.getAgent(task.worker);
350
534
  if (!agent.metadata.botUrl) {
351
- return (`El agente ${agent.metadata.name || task.worker} no publica endpoint, así que el resultado no se puede ` +
352
- `descargar por aquí. En la cadena consta su hash (${task.resultHash}): pídeselo por vuestro canal y ` +
353
- 'comprueba que el hash coincide.');
535
+ return (`Agent ${agent.metadata.name || task.worker} publishes no endpoint, so the result cannot be downloaded ` +
536
+ `here. Its hash is on-chain (${task.resultHash}): ask them through your channel and check that the ` +
537
+ 'hash matches.');
354
538
  }
355
539
  try {
356
540
  const signature = await account.signMessage({ message: resultSignMessage(taskId) });
@@ -360,28 +544,80 @@ const WRITE_TOOLS = [
360
544
  // on-chain y enseñarte otra distinta.
361
545
  const actual = keccak256(toBytes(text));
362
546
  if (actual.toLowerCase() !== task.resultHash.toLowerCase()) {
363
- return (`⚠️ El resultado que devuelve el agente NO coincide con el anclado on-chain.\n` +
364
- ` esperado: ${task.resultHash}\n recibido: ${actual}\n\n` +
365
- 'No lo apruebes: o el agente cambió el texto después de entregar, o alguien alteró la respuesta. ' +
366
- 'Puedes abrir una disputa desde https://panal.lat/dashboard.');
547
+ return (`⚠️ The result the agent serves does NOT match the one anchored on-chain.\n` +
548
+ ` expected: ${task.resultHash}\n received: ${actual}\n\n` +
549
+ 'Do not approve it: either the agent changed the text after delivering, or someone altered the ' +
550
+ 'response. You can open a dispute at https://panal.lat/dashboard.');
367
551
  }
368
- return `Resultado de la tarea #${id} (hash verificado contra la cadena):\n\n${text}`;
552
+ return `Result of task #${id} (hash verified against the chain):\n\n${text}`;
369
553
  }
370
554
  catch (err) {
371
- return `No se pudo recoger el resultado: ${err instanceof Error ? err.message : err}`;
555
+ return `Could not collect the result: ${err instanceof Error ? err.message : err}`;
556
+ }
557
+ },
558
+ },
559
+ {
560
+ name: 'panal_send_brief',
561
+ description: 'Deliver the job again to an already-hired agent, when the automatic send from panal_hire did not ' +
562
+ 'arrive. It costs NO money: the payment is already locked, this only repeats the HTTP delivery. ' +
563
+ 'The text must be EXACTLY the one hired, because its hash is on the chain.',
564
+ inputSchema: {
565
+ type: 'object',
566
+ properties: {
567
+ task_id: { type: 'number', description: 'Id of the already-created task.' },
568
+ brief: { type: 'string', description: 'The job, word for word as it was when hired.' },
569
+ },
570
+ required: ['task_id', 'brief'],
571
+ },
572
+ handler: async (args) => {
573
+ const blocked = writesBlockedReason();
574
+ if (blocked)
575
+ return blocked;
576
+ const id = Number(args.task_id);
577
+ if (!Number.isInteger(id) || id < 0)
578
+ return 'I need a valid task id.';
579
+ const brief = str(args.brief);
580
+ if (!brief)
581
+ return 'I need the text of the job.';
582
+ const task = await panal.getTask(BigInt(id));
583
+ if (task.client.toLowerCase() !== account.address.toLowerCase()) {
584
+ return `Task #${id} was not hired by this wallet, so its signature is not valid for delivering the job.`;
585
+ }
586
+ if (task.status !== TaskStatus.Open) {
587
+ return `Task #${id} is ${TaskStatus[task.status]}: it no longer accepts the job.`;
588
+ }
589
+ // Se comprueba aquí antes de molestar al agente: así el error dice que el
590
+ // texto no es el mismo, en vez de un 409 sin contexto desde el endpoint.
591
+ if (keccak256(toBytes(brief)) !== task.taskHash) {
592
+ return [
593
+ 'That text is NOT the one hired: its hash does not match the one on the chain.',
594
+ ` hired hash: ${task.taskHash}`,
595
+ ` your text hash: ${keccak256(toBytes(brief))}`,
596
+ 'It must be identical, character for character. One extra space already changes it.',
597
+ ].join('\n');
598
+ }
599
+ const agent = await panal.getAgent(task.worker);
600
+ if (!agent.metadata.botUrl)
601
+ return 'That agent publishes no endpoint: there is nowhere to send the job.';
602
+ const firma = await account.signMessage({ message: briefSignMessage(BigInt(id)) });
603
+ const fallo = await pushBrief(agent.metadata.botUrl, BigInt(id), brief, account.address, firma);
604
+ if (fallo === null) {
605
+ log(`brief #${id} entregado (reintento) en ${agent.metadata.botUrl}`);
606
+ return `Job delivered. The agent is already working on task #${id}.`;
372
607
  }
608
+ return `It still does not arrive (${fallo}). The payment stays locked and returns on its own when the deadline passes.`;
373
609
  },
374
610
  },
375
611
  {
376
612
  name: 'panal_approve_task',
377
- description: 'Aprueba el resultado entregado, LIBERA EL PAGO al agente y le deja una valoración de 1 a 5. ' +
378
- 'Solo tras enseñarle el resultado a la persona y que ella decida la nota.',
613
+ description: 'Approve the delivered result, RELEASE THE PAYMENT to the agent and leave a 1-5 rating. ' +
614
+ 'Only after showing the result to the person and letting them decide the score.',
379
615
  inputSchema: {
380
616
  type: 'object',
381
617
  properties: {
382
618
  task_id: { type: 'number', description: 'Id de la tarea entregada.' },
383
- rating: { type: 'number', description: 'Valoración de 1 a 5 que ha decidido la persona.' },
384
- confirmed_by_user: { type: 'boolean', description: 'true solo si la persona lo ha aprobado.' },
619
+ rating: { type: 'number', description: 'The 1-5 rating the person decided.' },
620
+ confirmed_by_user: { type: 'boolean', description: 'true only if the person approved it.' },
385
621
  },
386
622
  required: ['task_id', 'rating', 'confirmed_by_user'],
387
623
  },
@@ -390,20 +626,20 @@ const WRITE_TOOLS = [
390
626
  if (blocked)
391
627
  return blocked;
392
628
  if (args.confirmed_by_user !== true) {
393
- return 'No libero el pago sin confirmed_by_user: true. Enséñale el resultado a la persona primero.';
629
+ return 'I will not release the payment without confirmed_by_user: true. Show the result to the person first.';
394
630
  }
395
631
  const id = Number(args.task_id);
396
632
  const rating = Number(args.rating);
397
633
  if (!Number.isInteger(id) || id < 0)
398
- return 'El id de la tarea es un entero.';
634
+ return 'The task id is an integer.';
399
635
  if (!Number.isInteger(rating) || rating < 1 || rating > 5)
400
- return 'La valoración es un entero de 1 a 5.';
636
+ return 'The rating is an integer from 1 to 5.';
401
637
  try {
402
638
  const hash = await panal.approveTask(BigInt(id), rating);
403
- return `Pago liberado para la tarea #${id} con valoración ${rating}/5.\n tx: ${EXPLORER}/tx/${hash}`;
639
+ return `Payment released for task #${id} with a ${rating}/5 rating.\n tx: ${EXPLORER}/tx/${hash}`;
404
640
  }
405
641
  catch (err) {
406
- return `No se pudo aprobar: ${err instanceof Error ? err.message : err}`;
642
+ return `Could not approve: ${err instanceof Error ? err.message : err}`;
407
643
  }
408
644
  },
409
645
  },
@@ -450,7 +686,7 @@ async function handle(req) {
450
686
  const name = typeof req.params?.name === 'string' ? req.params.name : '';
451
687
  const tool = TOOLS.find((t) => t.name === name);
452
688
  if (!tool) {
453
- replyError(req.id, -32602, `No existe la herramienta "${name}".`);
689
+ replyError(req.id, -32602, `There is no tool named "${name}".`);
454
690
  return;
455
691
  }
456
692
  const args = req.params?.arguments ?? {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.1.3",
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.4.0",
33
+ "@panal/sdk": "^0.6.1",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {