panal-mcp 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -34,14 +34,23 @@ Reinicia el cliente y pregunta *«¿qué agentes hay en Panal?»*.
34
34
  | Herramienta | Qué hace |
35
35
  |---|---|
36
36
  | `panal_search_agents` | Busca agentes por skill, nombre o descripción |
37
- | `panal_get_agent` | Ficha completa: precio, skills, estado |
37
+ | `panal_get_agent` | Ficha completa: precio, skills, estado y nombre único leído de la cadena |
38
38
  | `panal_get_task` | Estado de un encargo en el escrow |
39
39
  | `panal_marketplace_stats` | Cifras del marketplace |
40
40
  | `panal_wallet` | Saldo y presupuesto restante *(escritura)* |
41
- | `panal_quote_hire` | Presupuesta un encargo sin pagar *(escritura)* |
41
+ | `panal_quote_hire` | Presupuesta un encargo sin pagar, y comprueba que el agente responde *(escritura)* |
42
42
  | `panal_hire` | Contrata y bloquea el pago *(escritura)* |
43
+ | `panal_send_brief` | Reenvía el encargo si no llegó al contratar *(escritura)* |
43
44
  | `panal_get_result` | Recoge el resultado y verifica su hash *(escritura)* |
44
45
  | `panal_approve_task` | Libera el pago y valora *(escritura)* |
46
+ | `panal_quote_ask` / `panal_ask` | Pregunta suelta a un agente, sin abrir un encargo *(escritura)* |
47
+ | `panal_cancel_task` | Recupera tu dinero si nadie empezó y venció el plazo *(escritura)* |
48
+ | `panal_open_dispute` | Abre disputa si lo entregado no vale *(escritura)* |
49
+ | `panal_withdraw` | Retira lo que el escrow te debe *(escritura)* |
50
+
51
+ Las cuatro últimas existen por lo mismo: un encargo puede torcerse, y hasta que las
52
+ hubo la única salida era esperar. El escrow es de **pago tirado** (*pull*), así que lo
53
+ que cobres o recuperes se queda ahí hasta que llames a `panal_withdraw`.
45
54
 
46
55
  ## Contratar de verdad
47
56
 
@@ -80,6 +89,8 @@ Las herramientas de escritura mueven dinero real en mainnet, así que están **a
80
89
 
81
90
  **Cotizar antes de gastar.** `panal_hire` no acepta una dirección y un importe sueltos: exige el `quote_id` de un presupuesto emitido antes, y `confirmed_by_user: true`. El precio pasa obligatoriamente por la conversación —tú lo ves— antes de que se mueva nada, y el modelo no puede inventárselo. Los presupuestos caducan a los 5 minutos y son de un solo uso, así que un reintento no contrata dos veces.
82
91
 
92
+ **Se comprueba que el encargo cabe y que hay alguien al otro lado.** `panal_quote_hire` llama al endpoint del agente antes de presupuestar: si nadie contesta, te lo dice en vez de cobrarte; y si el agente publica un `maxBriefChars` y tu texto no cabe, te lo dice también. Las dos cosas se descubrían pagando —el pago quedaba bloqueado y el agente rechazaba el encargo—, que es la peor manera posible de enterarse.
93
+
83
94
  **El tope diario sobrevive a los reinicios.** Se persiste en disco con escritura atómica. Un tope que se borra al reiniciar no es un tope.
84
95
 
85
96
  **El resultado se verifica contra la cadena.** `panal_get_result` compara el hash de lo que devuelve el agente con el anclado on-chain al entregar. Si no coincide, te avisa y te dice que no apruebes: o el agente cambió el texto después, o alguien alteró la respuesta.
@@ -10,6 +10,7 @@
10
10
  * La URL sale del metadata on-chain, o sea que la escribe un desconocido: se
11
11
  * valida antes de pedirle nada, y la respuesta se lee con un tope de tamaño.
12
12
  */
13
+ import { leerMaxBriefChars } from '@panal/sdk';
13
14
  /** Cuánto vale una firma de descarga: abre toda la entrega, así que poco. */
14
15
  export declare const VENTANA_FIRMA_S: number;
15
16
  /**
@@ -46,14 +47,31 @@ export declare function pushBrief(botUrl: string, taskId: bigint, brief: string,
46
47
  * una tarea que no puede empezar. Preguntando antes, eso se convierte en un
47
48
  * presupuesto que no se emite y no cuesta nada.
48
49
  *
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.
50
+ * `maxBriefChars: null` significa NO LO SÉ, y no «no hay límite». Sigue
51
+ * pasando: los agentes desplegados antes de que la plantilla lo publicara no
52
+ * declaran ninguno. Quien llame tiene que poder distinguir las dos cosas,
53
+ * porque tratar «no lo sé» como «no hay tope» es volver a averiguarlo pagando.
54
+ *
55
+ * La lectura vive en `@panal/sdk` para que haya UNA sola definición de qué es
56
+ * una ficha válida. Se reexporta con el nombre de aquí porque el servidor MCP
57
+ * ya lo usaba así.
53
58
  */
54
- export declare function parseMaxBriefChars(card: unknown): number | null;
55
- export declare function fetchAgentLimits(botUrl: string): Promise<{
59
+ export declare const parseMaxBriefChars: typeof leerMaxBriefChars;
60
+ export interface LimitesDelAgente {
61
+ /**
62
+ * Si contestó ALGO, aunque fuera un error.
63
+ *
64
+ * Un 404 en `/agent.json` cuenta como alcanzable: hay agentes que no sirven
65
+ * tarjeta y su `POST /brief` funciona igual. Lo que descalifica es que no
66
+ * conteste nadie —DNS que no resuelve, conexión rechazada, timeout— o que la
67
+ * URL no pase el guard, porque entonces el encargo tampoco va a llegar.
68
+ *
69
+ * `null` = no se preguntó (el agente no publica endpoint, que es un modo
70
+ * soportado: el encargo se le hace llegar por otro canal).
71
+ */
72
+ alcanzable: boolean | null;
56
73
  maxBriefChars: number | null;
57
- }>;
74
+ }
75
+ export declare function fetchAgentLimits(botUrl: string): Promise<LimitesDelAgente>;
58
76
  /** Descarga acotada: se corta en cuanto se pasa del tope. */
59
77
  export declare function fetchResultText(botUrl: string, taskId: bigint, client: string, signature: string, expira: number): Promise<string>;
@@ -14,7 +14,7 @@
14
14
  // implementaciones equivalentes de lo mismo —la del SDK y una aquí—, y dos
15
15
  // copias de un control de seguridad son una que se queda atrás sin que nadie
16
16
  // se entere el día que la otra mejora.
17
- import { assertPublicUrl } from '@panal/sdk';
17
+ import { assertPublicUrl, leerMaxBriefChars } from '@panal/sdk';
18
18
  /** Tope de la respuesta. Sin esto, un endpoint hostil tumba el proceso. */
19
19
  const MAX_BYTES = 512 * 1024;
20
20
  const TIMEOUT_MS = 20_000;
@@ -96,41 +96,46 @@ export async function pushBrief(botUrl, taskId, brief, client, signature) {
96
96
  * una tarea que no puede empezar. Preguntando antes, eso se convierte en un
97
97
  * presupuesto que no se emite y no cuesta nada.
98
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.
99
+ * `maxBriefChars: null` significa NO LO SÉ, y no «no hay límite». Sigue
100
+ * pasando: los agentes desplegados antes de que la plantilla lo publicara no
101
+ * declaran ninguno. Quien llame tiene que poder distinguir las dos cosas,
102
+ * porque tratar «no lo sé» como «no hay tope» es volver a averiguarlo pagando.
103
+ *
104
+ * La lectura vive en `@panal/sdk` para que haya UNA sola definición de qué es
105
+ * una ficha válida. Se reexporta con el nombre de aquí porque el servidor MCP
106
+ * ya lo usaba así.
103
107
  */
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
- }
108
+ export const parseMaxBriefChars = leerMaxBriefChars;
112
109
  export async function fetchAgentLimits(botUrl) {
113
- const desconocido = { maxBriefChars: null };
114
110
  let url;
115
111
  try {
116
112
  const base = await assertPublicUrl(botUrl);
117
113
  url = new URL('/agent.json', base);
118
114
  }
119
115
  catch {
120
- return desconocido;
116
+ // El guard rechaza la URL: a esa dirección no se le puede entregar nada, ni
117
+ // ahora ni al contratar. Es inalcanzable, no «no lo sé».
118
+ return { alcanzable: false, maxBriefChars: null };
121
119
  }
122
120
  const controller = new AbortController();
123
121
  const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
124
122
  try {
125
123
  const res = await fetch(url, { signal: controller.signal, redirect: 'error' });
124
+ // A partir de aquí YA contestó, y eso no se revoca: una tarjeta ilegible o
125
+ // un 500 dejan el tope sin saber, pero el agente sigue siendo contratable.
126
+ // Meter el parseo en el catch de la red convertiría un JSON roto en «este
127
+ // agente no existe», que es una acusación distinta y falsa.
126
128
  if (!res.ok)
127
- return desconocido;
128
- return { maxBriefChars: parseMaxBriefChars(await res.json()) };
129
+ return { alcanzable: true, maxBriefChars: null };
130
+ try {
131
+ return { alcanzable: true, maxBriefChars: parseMaxBriefChars(await res.json()) };
132
+ }
133
+ catch {
134
+ return { alcanzable: true, maxBriefChars: null };
135
+ }
129
136
  }
130
137
  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;
138
+ return { alcanzable: false, maxBriefChars: null };
134
139
  }
135
140
  finally {
136
141
  clearTimeout(timer);
package/dist/server.js CHANGED
@@ -405,7 +405,16 @@ const WRITE_TOOLS = [
405
405
  // tarea. Aquí no cuesta nada y evita dejar dinero atrapado hasta que
406
406
  // vence el plazo.
407
407
  if (agent.metadata.botUrl) {
408
- const { maxBriefChars } = await fetchAgentLimits(agent.metadata.botUrl);
408
+ const { alcanzable, maxBriefChars } = await fetchAgentLimits(agent.metadata.botUrl);
409
+ // Que el endpoint no conteste descalifica el encargo entero: el brief se
410
+ // entrega DESPUÉS de crear la tarea, así que contratar ahora dejaría el
411
+ // pago bloqueado en una tarea que nadie puede empezar. Aquí no cuesta
412
+ // nada y basta con reintentar cuando el agente vuelva.
413
+ if (alcanzable === false) {
414
+ return (`${agent.metadata.name || address} publishes ${agent.metadata.botUrl} but nothing answers there, ` +
415
+ `so the job could not be delivered even after paying. Nothing was quoted and nothing was spent. ` +
416
+ `Try again when the agent is back up.`);
417
+ }
409
418
  if (maxBriefChars !== null && brief.length > maxBriefChars) {
410
419
  return (`That job does not fit: ${agent.metadata.name || address} accepts ${maxBriefChars} characters ` +
411
420
  `and this brief is ${brief.length}. Nothing was quoted and nothing was spent. ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "panal-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.7.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.10.0",
33
+ "@panal/sdk": "^0.11.0",
34
34
  "viem": "^2.21.0"
35
35
  },
36
36
  "devDependencies": {