@prismnetwork/mcp 0.5.0 → 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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/package.json +2 -2
  3. package/server.mjs +206 -59
package/README.md CHANGED
@@ -22,6 +22,7 @@ real prices. Leasing spends money, so those tools ask for a wallet and say so.
22
22
  | `prism_receipts` | no |
23
23
  | `prism_wallet` | yes |
24
24
  | `prism_leases` | yes |
25
+ | `prism_infer` | yes |
25
26
  | `prism_lease_and_run` | yes |
26
27
  | `prism_lease` | yes |
27
28
  | `prism_run` | yes |
@@ -39,6 +40,10 @@ real prices. Leasing spends money, so those tools ask for a wallet and say so.
39
40
  - `prism_receipts`: recent settled receipts from the public proof feed, with the
40
41
  settlement transaction on Robinhood Chain.
41
42
  - `prism_leases`: this wallet's leases and their state.
43
+ - `prism_infer`: buy one LLM generation from the managed inference endpoint,
44
+ paying the quoted USDG price from this wallet (about 0.01 USDG). Waits
45
+ through a cold start; an unconsumed payment is kept and reused on the next
46
+ call instead of paying twice.
42
47
  - `prism_lease_and_run`: lease a GPU, run a command, return the output (one shot).
43
48
  - `prism_lease`: lease a GPU and keep it; returns a `lease_id` and SSH access.
44
49
  - `prism_run`: run a command on an existing lease.
@@ -60,7 +65,7 @@ derived from the agent's wallet inside this server process; Prism receives
60
65
  ciphertext and holds no way to read it.
61
66
 
62
67
  Each item names the weakest workspace trust class it may ever be released into,
63
- and new items default to `confidential` above anything the network serves
68
+ and new items default to `confidential`, above anything the network serves
64
69
  today. `prism_vault_release` is therefore refused on current capacity instead of
65
70
  handing a secret to a host that can read it. Lowering an item's floor is a
66
71
  deliberate act, and allowed releases are recorded against the account.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prismnetwork/mcp",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for leasing and running on Prism Network GPUs.",
5
5
  "mcpName": "io.github.prismnetwork-tech/mcp",
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@modelcontextprotocol/sdk": "^1.0.0",
19
- "@prismnetwork/agent-sdk": "^0.3.1",
19
+ "@prismnetwork/agent-sdk": "^0.5.0",
20
20
  "viem": "^2"
21
21
  },
22
22
  "keywords": [
package/server.mjs CHANGED
@@ -9,59 +9,85 @@ import { DEFAULT_IMAGE, DEFAULT_TRUST_FLOOR, PrismAgent, TRUST_CLASSES } from "@
9
9
 
10
10
  const IMAGE = process.env.PRISM_DEFAULT_IMAGE ?? DEFAULT_IMAGE;
11
11
 
12
- function requireEnv(name) {
13
- const value = process.env[name];
14
- if (!value) throw new Error(`${name} is required`);
15
- return value;
16
- }
17
-
18
12
  const PUBLIC_API = process.env.PRISM_PUBLIC_API ?? "https://api.prismnetwork.tech";
13
+ // The live lease escrow. Overridable, but its absence must not silently
14
+ // disable the wallet the way a missing key does.
15
+ const DEFAULT_ESCROW = "0x62C042265991bEa17B07229322A01850974626dA";
16
+ // Matches the limit the SDK and the control plane enforce, so a command that
17
+ // cannot run is rejected before an escrow is funded.
18
+ const MAX_COMMAND_BYTES = 8 * 1024;
19
19
 
20
20
  // Refusing to start without a wallet meant nobody could ask what a GPU costs
21
21
  // without first producing a private key, which is a strange thing to demand of
22
22
  // someone deciding whether to use you at all.
23
23
  let agent = null;
24
- try {
25
- agent = new PrismAgent({
26
- privateKey: requireEnv("PRISM_AGENT_KEY"),
27
- escrow: requireEnv("PRISM_ESCROW"),
28
- apiBase: process.env.PRISM_API_BASE ?? "https://prismnetwork.tech",
29
- rpcUrl: process.env.PRISM_RPC_URL,
30
- });
31
- } catch {
24
+ let walletProblem = "PRISM_AGENT_KEY is not set";
25
+ if (process.env.PRISM_AGENT_KEY) {
26
+ try {
27
+ agent = new PrismAgent({
28
+ privateKey: process.env.PRISM_AGENT_KEY,
29
+ escrow: process.env.PRISM_ESCROW ?? DEFAULT_ESCROW,
30
+ apiBase: process.env.PRISM_API_BASE ?? "https://prismnetwork.tech",
31
+ rpcUrl: process.env.PRISM_RPC_URL,
32
+ });
33
+ walletProblem = null;
34
+ } catch (err) {
35
+ walletProblem = `PRISM_AGENT_KEY is set but unusable: ${err?.message ?? err}`;
36
+ }
37
+ }
38
+ if (!agent) {
32
39
  console.error(
33
- "prism mcp: no wallet configured, so capacity and pricing are readable and leasing is not. " +
34
- "Set PRISM_AGENT_KEY and PRISM_ESCROW to lease.",
40
+ `prism mcp: no wallet configured (${walletProblem}), so capacity and pricing are readable and leasing is not.`,
35
41
  );
36
42
  }
37
43
 
38
- function requireWallet(tool) {
44
+ function requireWallet(tool, reason = "spends money") {
39
45
  if (!agent) {
40
46
  throw new Error(
41
- `${tool} spends money, so it needs a wallet. Set PRISM_AGENT_KEY and PRISM_ESCROW in this server's environment and restart it.`,
47
+ `${tool} ${reason} and needs a wallet. None is configured: ${walletProblem}. Fix the environment and restart the server.`,
42
48
  );
43
49
  }
44
50
  return agent;
45
51
  }
46
52
 
47
- async function publicOffers(minTrust) {
48
- const url = new URL("/v1/offers", PUBLIC_API);
49
- if (minTrust) url.searchParams.set("min_trust", minTrust);
50
- const response = await fetch(url, { headers: { accept: "application/json" } });
51
- if (!response.ok) throw new Error(`prism offers unavailable (${response.status})`);
52
- return response.json();
53
+ function requireCommand(value) {
54
+ if (typeof value !== "string" || value.trim() === "") {
55
+ throw new Error("command is required: the shell command to run on the GPU, e.g. 'nvidia-smi'.");
56
+ }
57
+ if (Buffer.byteLength(value, "utf8") > MAX_COMMAND_BYTES) {
58
+ throw new Error(`command exceeds the ${MAX_COMMAND_BYTES / 1024} KiB limit.`);
59
+ }
60
+ return value;
61
+ }
62
+
63
+ function maxDeposit(args) {
64
+ const cap = args.max_usdg ?? 1;
65
+ if (typeof cap !== "number" || !Number.isFinite(cap) || cap <= 0) {
66
+ throw new Error("max_usdg must be a positive number of USDG.");
67
+ }
68
+ return Math.round(cap * 1e6);
53
69
  }
54
70
 
55
71
  const PROOF_FEED = process.env.PRISM_PROOF_URL ?? "https://prismnetwork.tech/api/proof";
56
72
 
57
73
  async function publicJson(url, what) {
58
- const response = await fetch(url, { headers: { accept: "application/json" } });
74
+ const response = await fetch(url, {
75
+ headers: { accept: "application/json" },
76
+ signal: AbortSignal.timeout(10_000),
77
+ });
59
78
  if (!response.ok) throw new Error(`prism ${what} unavailable (${response.status})`);
60
- return response.json();
79
+ const body = await response.json().catch(() => null);
80
+ if (body === null) throw new Error(`prism ${what} answered with something that is not JSON`);
81
+ return body;
61
82
  }
62
83
 
63
84
  const leases = new Map();
64
- const usdg = (micros) => `${(Number(micros) / 1e6).toFixed(6)} USDG`;
85
+ // Unconsumed inference payments, keyed by endpoint and price, so a failed
86
+ // generation is retried with the same paid header instead of paying again.
87
+ const pendingInference = new Map();
88
+ // null in, null out: a missing price must never render as 0.000000 USDG.
89
+ const usdg = (micros) =>
90
+ micros == null || !Number.isFinite(Number(micros)) ? null : `${(Number(micros) / 1e6).toFixed(6)} USDG`;
65
91
 
66
92
  function sweepExpiredLeases() {
67
93
  const now = Date.now();
@@ -111,7 +137,7 @@ const TOOLS = [
111
137
  inputSchema: {
112
138
  type: "object",
113
139
  properties: {
114
- limit: { type: "integer", description: "Max receipts to return (default 10)." },
140
+ limit: { type: "integer", description: "Max receipts to return (default 10, max 50)." },
115
141
  },
116
142
  },
117
143
  },
@@ -122,17 +148,42 @@ const TOOLS = [
122
148
  },
123
149
  {
124
150
  name: "prism_batch_run",
125
- description: "Fund a lease that runs one command with no interactive access at all: the node executes it and reports the signed output. Matches only suppliers at trust class 'isolated' or above, so it can find no supplier when none is online prefer prism_lease_and_run for broad availability. Output is capped at 64 KiB per stream.",
151
+ description: "Fund a lease that runs one command with no interactive access at all: the node executes it and reports the signed output. Matches only suppliers at trust class 'isolated' or above, so it can find no supplier when none is online; prefer prism_lease_and_run for broad availability. Output is capped at 64 KiB per stream.",
126
152
  inputSchema: {
127
153
  type: "object",
128
154
  properties: {
129
155
  command: { type: "string", description: "Shell command to run (max 8 KiB)." },
130
156
  duration_seconds: { type: "integer", description: "Paid window in seconds (default 900, max 21600). A command still running at the end is killed and reported exit 124." },
131
157
  min_vram_mib: { type: "integer", description: "Minimum GPU memory in MiB (default 16000)." },
158
+ max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
132
159
  },
133
160
  required: ["command"],
134
161
  },
135
162
  },
163
+ {
164
+ name: "prism_batch_result",
165
+ description: "Read the output of a batch lease by lease_id, once its node has reported. Use it to recover a result after prism_batch_run timed out.",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ lease_id: { type: "integer", description: "The lease_id from prism_batch_run's output or error." },
170
+ },
171
+ required: ["lease_id"],
172
+ },
173
+ },
174
+ {
175
+ name: "prism_infer",
176
+ description: "Buy one LLM generation from Prism's managed inference endpoint. Pays the quoted USDG price from this wallet (about 0.01 USDG), waits through a cold start when no box is warm (up to a few minutes), and returns the generation with token usage. Cheaper and simpler than leasing when all you need is a completion.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: {
180
+ prompt: { type: "string", description: "The prompt to generate from (max 32 KiB)." },
181
+ model: { type: "string", description: "Model to use; defaults to the endpoint's first offered model." },
182
+ max_usdg: { type: "number", description: "Refuse if the quoted price exceeds this (default 0.05)." },
183
+ },
184
+ required: ["prompt"],
185
+ },
186
+ },
136
187
  {
137
188
  name: "prism_lease_and_run",
138
189
  description: "Lease a GPU, run one shell command on it, and return the output. The lease stays alive (use prism_run for more commands, prism_end_lease to release). Prefer this for a single command; use prism_lease when you'll run several.",
@@ -147,6 +198,7 @@ const TOOLS = [
147
198
  enum: TRUST_CLASSES,
148
199
  description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
149
200
  },
201
+ max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
150
202
  },
151
203
  required: ["command"],
152
204
  },
@@ -164,6 +216,7 @@ const TOOLS = [
164
216
  enum: TRUST_CLASSES,
165
217
  description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
166
218
  },
219
+ max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
167
220
  },
168
221
  },
169
222
  },
@@ -191,7 +244,7 @@ const TOOLS = [
191
244
  },
192
245
  {
193
246
  name: "prism_vault_store",
194
- description: "Store private data a card, an identity document, an API credential encrypted under a key derived from this agent's wallet on this machine. Prism receives ciphertext only and cannot read it. Use this instead of writing a secret into a workspace or a file. Returns an item_id; the value is not recoverable without the wallet.",
247
+ description: "Store private data (a card, an identity document, an API credential) encrypted under a key derived from this agent's wallet on this machine. Prism receives ciphertext only and cannot read it. Use this instead of writing a secret into a workspace or a file. Returns an item_id; the value is not recoverable without the wallet.",
195
248
  inputSchema: {
196
249
  type: "object",
197
250
  properties: {
@@ -213,7 +266,7 @@ const TOOLS = [
213
266
  },
214
267
  {
215
268
  name: "prism_vault_read",
216
- description: "Decrypt and return one vault item, in this process, using the wallet-derived key. The plaintext exists only here do not echo it into a leased workspace, a log, or a message.",
269
+ description: "Decrypt and return one vault item, in this process, using the wallet-derived key. The plaintext exists only here; do not echo it into a leased workspace, a log, or a message.",
217
270
  inputSchema: {
218
271
  type: "object",
219
272
  properties: { item_id: { type: "string", description: "The item_id from prism_vault_store or prism_vault_list." } },
@@ -250,12 +303,16 @@ async function handle(name, args) {
250
303
  }
251
304
  if (name === "prism_list_gpus") {
252
305
  const minTrust = args.min_trust ?? "open";
306
+ if (!TRUST_CLASSES.includes(minTrust)) {
307
+ throw new Error(`min_trust must be one of ${TRUST_CLASSES.join(", ")}`);
308
+ }
253
309
  let offers;
254
310
  if (agent) {
255
- await ensureAuth();
256
311
  offers = await agent.offers({ minTrust });
257
312
  } else {
258
- offers = await publicOffers(minTrust);
313
+ const url = new URL("/v1/offers", PUBLIC_API);
314
+ url.searchParams.set("min_trust", minTrust);
315
+ offers = await publicJson(url, "offers");
259
316
  }
260
317
  return {
261
318
  available: offers.length,
@@ -265,6 +322,9 @@ async function handle(name, args) {
265
322
  price_per_second: usdg(o.rate_per_second),
266
323
  price_per_hour: usdg(o.rate_per_second * 3600),
267
324
  trust: o.trust_class,
325
+ // A staker-only offer will not match a wallet without the stake, so an
326
+ // unstaked renter should budget from the non-staker rows.
327
+ ...(o.staker_only ? { staker_only: true } : {}),
268
328
  })),
269
329
  };
270
330
  }
@@ -278,21 +338,27 @@ async function handle(name, args) {
278
338
  sourced_low_per_hour: usdg(g.sourced_low_micros_per_hour),
279
339
  sourced_median_per_hour: usdg(g.sourced_median_micros_per_hour),
280
340
  sourced_high_per_hour: usdg(g.sourced_high_micros_per_hour),
281
- settled_mean_per_hour: g.settled_mean_micros_per_hour == null ? null : usdg(g.settled_mean_micros_per_hour),
341
+ settled_mean_per_hour: usdg(g.settled_mean_micros_per_hour),
282
342
  settled_leases: g.settled_leases,
283
343
  })),
284
344
  };
285
345
  }
286
346
  if (name === "prism_receipts") {
287
347
  const feed = await publicJson(PROOF_FEED, "proof feed");
288
- const limit = Number.isInteger(args.limit) && args.limit > 0 ? args.limit : 10;
348
+ if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit <= 0)) {
349
+ throw new Error("limit must be a positive integer (max 50)");
350
+ }
351
+ const limit = Math.min(args.limit ?? 10, 50);
289
352
  return {
290
353
  generated_at: feed.generated_at,
291
354
  receipts: (feed.receipts ?? []).slice(0, limit).map((r) => ({
355
+ // The feed numbers leases per escrow deployment, so lease_id repeats
356
+ // across deployments; receipt_id is the unique handle.
357
+ receipt_id: r.receipt_id,
292
358
  lease_id: r.lease_id,
293
359
  outcome: r.outcome,
294
360
  gpu_model: r.gpu_model,
295
- trust: r.trust_class,
361
+ trust: r.trust_class ?? null,
296
362
  runtime_seconds: r.runtime_seconds,
297
363
  charged: usdg(r.charged_base_units),
298
364
  refunded: usdg(r.refunded_base_units),
@@ -302,7 +368,6 @@ async function handle(name, args) {
302
368
  }
303
369
  if (name === "prism_leases") {
304
370
  requireWallet(name);
305
- await ensureAuth();
306
371
  const all = await agent.leases();
307
372
  return {
308
373
  total: all.length,
@@ -320,13 +385,14 @@ async function handle(name, args) {
320
385
  };
321
386
  }
322
387
  if (name === "prism_batch_run") {
323
- if (!args.command) throw new Error("command is required");
388
+ requireCommand(args.command);
324
389
  requireWallet(name);
325
- await ensureAuth();
390
+ const cap = maxDeposit(args);
326
391
  const batch = await agent.lease({
327
392
  image: IMAGE,
328
393
  durationSeconds: args.duration_seconds ?? 900,
329
394
  minVramMib: args.min_vram_mib ?? 16000,
395
+ maxDeposit: cap,
330
396
  command: args.command,
331
397
  });
332
398
  return {
@@ -338,35 +404,114 @@ async function handle(name, args) {
338
404
  truncated: batch.result?.truncated ?? false,
339
405
  };
340
406
  }
407
+ if (name === "prism_batch_result") {
408
+ requireWallet(name, "reads this wallet's leases");
409
+ const id = leaseId(args.lease_id);
410
+ return { lease_id: id, result: await agent.result(id) };
411
+ }
412
+ if (name === "prism_infer") {
413
+ if (typeof args.prompt !== "string" || args.prompt.trim() === "") {
414
+ throw new Error("prompt is required.");
415
+ }
416
+ requireWallet(name);
417
+ const base = (process.env.PRISM_INFERENCE_URL ?? "https://api.prismnetwork.tech/inference").replace(/\/$/, "");
418
+ const offer = await publicJson(`${base}/v1/models`, "inference endpoint");
419
+ const model = args.model ?? offer.models?.[0];
420
+ if (!model || (Array.isArray(offer.models) && !offer.models.includes(model))) {
421
+ throw new Error(`model must be one of ${offer.models?.join(", ") ?? "(endpoint offered none)"}`);
422
+ }
423
+ const price = BigInt(offer.price_micros ?? 0);
424
+ const cap = args.max_usdg ?? 0.05;
425
+ if (typeof cap !== "number" || !Number.isFinite(cap) || cap <= 0) {
426
+ throw new Error("max_usdg must be a positive number of USDG.");
427
+ }
428
+ if (price <= 0n || price > BigInt(Math.round(cap * 1e6))) {
429
+ throw new Error(`the endpoint quotes ${usdg(price)} per generation, past the ${cap} USDG cap.`);
430
+ }
431
+ const pendingKey = `${base}:${price}`;
432
+ let pending = pendingInference.get(pendingKey);
433
+ if (!pending) {
434
+ const paymentTx = await agent.transferUsdg(offer.pay_to, price);
435
+ const signature = await agent.account.signMessage({ message: paymentTx });
436
+ pending = {
437
+ tx: paymentTx,
438
+ header: Buffer.from(JSON.stringify({ txHash: paymentTx, signature })).toString("base64"),
439
+ };
440
+ pendingInference.set(pendingKey, pending);
441
+ }
442
+ // A cold endpoint holds the request through provisioning; when it answers
443
+ // 503 instead, the payment is not consumed and the same header retries.
444
+ const deadline = Date.now() + 600_000;
445
+ for (;;) {
446
+ const res = await fetch(`${base}/v1/inference`, {
447
+ method: "POST",
448
+ headers: { "content-type": "application/json", "x-payment": pending.header },
449
+ body: JSON.stringify({ model, prompt: args.prompt }),
450
+ signal: AbortSignal.timeout(620_000),
451
+ });
452
+ const body = await res.json().catch(() => null);
453
+ if (res.status === 200 && body) {
454
+ pendingInference.delete(pendingKey);
455
+ return { ...body, paid: usdg(price), payment_tx: pending.tx };
456
+ }
457
+ const last = body?.detail ?? body?.error ?? `status ${res.status}`;
458
+ // 503 means the box is warming; a 402 for a payment that is merely too
459
+ // young (confirmations still landing, receipt not yet visible) heals by
460
+ // itself. Everything else is final.
461
+ const retryable =
462
+ res.status === 503 ||
463
+ (res.status === 402 && ["insufficient_confirmations", "tx_not_found"].includes(body?.error));
464
+ if (!retryable || Date.now() > deadline) {
465
+ throw new Error(
466
+ `inference failed: ${last}. The payment (tx ${pending.tx}) was not consumed and is kept; the next prism_infer call retries with it instead of paying again.`,
467
+ );
468
+ }
469
+ await new Promise((r) => setTimeout(r, 15_000));
470
+ }
471
+ }
341
472
  if (name === "prism_lease_and_run" || name === "prism_lease") {
342
- if (name === "prism_lease_and_run" && !args.command) throw new Error("command is required");
473
+ if (name === "prism_lease_and_run") requireCommand(args.command);
343
474
  requireWallet(name);
344
- await ensureAuth();
475
+ const cap = maxDeposit(args);
345
476
  sweepExpiredLeases();
346
477
  const lease = await agent.lease({
347
478
  image: IMAGE,
348
479
  durationSeconds: args.duration_seconds ?? 900,
349
480
  minVramMib: args.min_vram_mib ?? 16000,
481
+ maxDeposit: cap,
350
482
  minTrustClass: args.min_trust_class ?? "open",
351
483
  });
352
484
  leases.set(lease.leaseId, lease);
353
485
  const summary = {
354
486
  lease_id: lease.leaseId,
487
+ funding_tx: lease.fundingHash,
355
488
  ssh: { host: lease.access.ssh_host, port: lease.access.ssh_port, user: lease.access.ssh_user },
356
489
  trust: lease.quote?.trust_class ?? "open",
357
490
  expires_at: lease.access.expires_at,
358
491
  };
359
492
  if (name === "prism_lease") return summary;
360
- const out = await agent.run(lease, args.command);
361
- return { ...summary, command: args.command, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
493
+ // The lease is paid for by this point; an SSH failure must still hand the
494
+ // caller everything it bought.
495
+ try {
496
+ const out = await agent.run(lease, args.command);
497
+ return { ...summary, command: args.command, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
498
+ } catch (err) {
499
+ return {
500
+ ...summary,
501
+ error: `the lease is funded but the command could not run: ${err?.message ?? err}`,
502
+ next: `the lease stays open; try prism_run with lease_id ${lease.leaseId}, or prism_end_lease`,
503
+ };
504
+ }
362
505
  }
363
506
  if (name === "prism_run") {
364
- if (!args.command) throw new Error("command is required");
507
+ requireCommand(args.command);
365
508
  const id = leaseId(args.lease_id);
366
509
  const lease = leases.get(id);
367
510
  if (!lease) throw new Error(`no active lease ${id} in this session`);
511
+ const timeoutSeconds = args.timeout_seconds ?? 120;
368
512
  const out = await agent.run(lease, args.command, {
369
- timeoutMs: (args.timeout_seconds ?? 120) * 1000,
513
+ timeoutMs: timeoutSeconds * 1000,
514
+ connectRetries: 6,
370
515
  });
371
516
  return { lease_id: id, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
372
517
  }
@@ -380,14 +525,13 @@ async function handle(name, args) {
380
525
  return { lease_id: id, released: Boolean(lease) };
381
526
  }
382
527
  if (name.startsWith("prism_vault_")) return handleVault(name, args);
383
- throw new Error(`unknown tool ${name}`);
528
+ throw new Error(`unknown tool ${name}. Valid tools: ${TOOLS.map((t) => t.name).join(", ")}`);
384
529
  }
385
530
 
386
531
  // The vault key is derived here from the wallet signature and stays in this
387
532
  // process. Nothing in this function sends a key or a plaintext to Prism.
388
533
  async function handleVault(name, args) {
389
- const vault = requireWallet(name).vault;
390
- await ensureAuth();
534
+ const vault = requireWallet(name, "derives the vault key from the wallet").vault;
391
535
  if (!vault.unlocked) await vault.unlock();
392
536
 
393
537
  if (name === "prism_vault_store") {
@@ -433,23 +577,26 @@ async function handleVault(name, args) {
433
577
  throw new Error(`unknown tool ${name}`);
434
578
  }
435
579
 
436
- let authPromise = null;
437
- function ensureAuth() {
438
- authPromise ??= agent.authenticate().catch((err) => {
439
- authPromise = null;
440
- throw err;
441
- });
442
- return authPromise;
443
- }
444
-
445
- const server = new Server({ name: "prism", version: "0.5.0" }, { capabilities: { tools: {} } });
580
+ const server = new Server({ name: "prism", version: "0.7.0" }, { capabilities: { tools: {} } });
446
581
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
447
582
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
448
583
  try {
449
584
  const result = await handle(request.params.name, request.params.arguments ?? {});
450
585
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
451
586
  } catch (err) {
452
- return { isError: true, content: [{ type: "text", text: `error: ${err.message ?? err}` }] };
587
+ const body = err?.body ?? {};
588
+ const detail = [
589
+ body.cause,
590
+ body.hint,
591
+ body.required != null ? `required ${body.required}` : null,
592
+ body.max != null ? `cap ${body.max}` : null,
593
+ body.lease_id != null ? `lease_id ${body.lease_id}` : null,
594
+ body.funding_hash ? `funding_tx ${body.funding_hash}` : null,
595
+ ]
596
+ .filter(Boolean)
597
+ .join("; ");
598
+ const text = `error: ${err?.message ?? err}${detail ? ` (${detail})` : ""}`;
599
+ return { isError: true, content: [{ type: "text", text }] };
453
600
  }
454
601
  });
455
602