@prismnetwork/mcp 0.4.2 → 0.6.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 +12 -1
  2. package/package.json +2 -2
  3. package/server.mjs +231 -49
package/README.md CHANGED
@@ -18,10 +18,14 @@ real prices. Leasing spends money, so those tools ask for a wallet and say so.
18
18
  | Tool | Wallet |
19
19
  | --- | --- |
20
20
  | `prism_list_gpus` | no |
21
+ | `prism_price_index` | no |
22
+ | `prism_receipts` | no |
21
23
  | `prism_wallet` | yes |
24
+ | `prism_leases` | yes |
22
25
  | `prism_lease_and_run` | yes |
23
26
  | `prism_lease` | yes |
24
27
  | `prism_run` | yes |
28
+ | `prism_batch_run` | yes |
25
29
  | `prism_end_lease` | yes |
26
30
  | `prism_vault_store` | yes |
27
31
  | `prism_vault_list` | yes |
@@ -31,9 +35,16 @@ real prices. Leasing spends money, so those tools ask for a wallet and say so.
31
35
 
32
36
  - `prism_wallet`: the agent's address and USDG/ETH balances.
33
37
  - `prism_list_gpus`: GPUs available to lease, with price per second and per hour.
38
+ - `prism_price_index`: sourced and settled pricing per GPU model, for cost estimates.
39
+ - `prism_receipts`: recent settled receipts from the public proof feed, with the
40
+ settlement transaction on Robinhood Chain.
41
+ - `prism_leases`: this wallet's leases and their state.
34
42
  - `prism_lease_and_run`: lease a GPU, run a command, return the output (one shot).
35
43
  - `prism_lease`: lease a GPU and keep it; returns a `lease_id` and SSH access.
36
44
  - `prism_run`: run a command on an existing lease.
45
+ - `prism_batch_run`: fund a lease that runs one command with no interactive
46
+ access; the node reports the signed output. Matches only suppliers at trust
47
+ class `isolated` or above, so it can find no supplier when none is online.
37
48
  - `prism_end_lease`: release a lease.
38
49
  - `prism_vault_store`: seal private data under the wallet-derived key.
39
50
  - `prism_vault_list`: list sealed items; values are never returned.
@@ -49,7 +60,7 @@ derived from the agent's wallet inside this server process; Prism receives
49
60
  ciphertext and holds no way to read it.
50
61
 
51
62
  Each item names the weakest workspace trust class it may ever be released into,
52
- and new items default to `confidential` above anything the network serves
63
+ and new items default to `confidential`, above anything the network serves
53
64
  today. `prism_vault_release` is therefore refused on current capacity instead of
54
65
  handing a secret to a host that can read it. Lowering an item's floor is a
55
66
  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.4.2",
3
+ "version": "0.6.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,51 +9,82 @@ 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);
69
+ }
70
+
71
+ const PROOF_FEED = process.env.PRISM_PROOF_URL ?? "https://prismnetwork.tech/api/proof";
72
+
73
+ async function publicJson(url, what) {
74
+ const response = await fetch(url, {
75
+ headers: { accept: "application/json" },
76
+ signal: AbortSignal.timeout(10_000),
77
+ });
78
+ if (!response.ok) throw new Error(`prism ${what} unavailable (${response.status})`);
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;
53
82
  }
54
83
 
55
84
  const leases = new Map();
56
- const usdg = (micros) => `${(Number(micros) / 1e6).toFixed(6)} USDG`;
85
+ // null in, null out: a missing price must never render as 0.000000 USDG.
86
+ const usdg = (micros) =>
87
+ micros == null || !Number.isFinite(Number(micros)) ? null : `${(Number(micros) / 1e6).toFixed(6)} USDG`;
57
88
 
58
89
  function sweepExpiredLeases() {
59
90
  const now = Date.now();
@@ -92,6 +123,51 @@ const TOOLS = [
92
123
  },
93
124
  },
94
125
  },
126
+ {
127
+ name: "prism_price_index",
128
+ description: "Current GPU pricing on Prism Network by model: sourced low/median/high and settled mean, in USDG per hour. Needs no wallet. Use it to estimate what an analysis job will cost before leasing.",
129
+ inputSchema: { type: "object", properties: {} },
130
+ },
131
+ {
132
+ name: "prism_receipts",
133
+ description: "Recent settled lease receipts from the public proof feed: GPU model, runtime, what was charged and refunded, and the settlement transaction hash on Robinhood Chain. Needs no wallet. Every Prism lease ends in one of these.",
134
+ inputSchema: {
135
+ type: "object",
136
+ properties: {
137
+ limit: { type: "integer", description: "Max receipts to return (default 10, max 50)." },
138
+ },
139
+ },
140
+ },
141
+ {
142
+ name: "prism_leases",
143
+ description: "List this wallet's leases on Prism Network with their current state.",
144
+ inputSchema: { type: "object", properties: {} },
145
+ },
146
+ {
147
+ name: "prism_batch_run",
148
+ 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.",
149
+ inputSchema: {
150
+ type: "object",
151
+ properties: {
152
+ command: { type: "string", description: "Shell command to run (max 8 KiB)." },
153
+ 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." },
154
+ min_vram_mib: { type: "integer", description: "Minimum GPU memory in MiB (default 16000)." },
155
+ max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
156
+ },
157
+ required: ["command"],
158
+ },
159
+ },
160
+ {
161
+ name: "prism_batch_result",
162
+ 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.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ lease_id: { type: "integer", description: "The lease_id from prism_batch_run's output or error." },
167
+ },
168
+ required: ["lease_id"],
169
+ },
170
+ },
95
171
  {
96
172
  name: "prism_lease_and_run",
97
173
  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.",
@@ -106,6 +182,7 @@ const TOOLS = [
106
182
  enum: TRUST_CLASSES,
107
183
  description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
108
184
  },
185
+ max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
109
186
  },
110
187
  required: ["command"],
111
188
  },
@@ -123,6 +200,7 @@ const TOOLS = [
123
200
  enum: TRUST_CLASSES,
124
201
  description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
125
202
  },
203
+ max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
126
204
  },
127
205
  },
128
206
  },
@@ -150,7 +228,7 @@ const TOOLS = [
150
228
  },
151
229
  {
152
230
  name: "prism_vault_store",
153
- 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.",
231
+ 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.",
154
232
  inputSchema: {
155
233
  type: "object",
156
234
  properties: {
@@ -172,7 +250,7 @@ const TOOLS = [
172
250
  },
173
251
  {
174
252
  name: "prism_vault_read",
175
- 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.",
253
+ 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.",
176
254
  inputSchema: {
177
255
  type: "object",
178
256
  properties: { item_id: { type: "string", description: "The item_id from prism_vault_store or prism_vault_list." } },
@@ -209,12 +287,16 @@ async function handle(name, args) {
209
287
  }
210
288
  if (name === "prism_list_gpus") {
211
289
  const minTrust = args.min_trust ?? "open";
290
+ if (!TRUST_CLASSES.includes(minTrust)) {
291
+ throw new Error(`min_trust must be one of ${TRUST_CLASSES.join(", ")}`);
292
+ }
212
293
  let offers;
213
294
  if (agent) {
214
- await ensureAuth();
215
295
  offers = await agent.offers({ minTrust });
216
296
  } else {
217
- offers = await publicOffers(minTrust);
297
+ const url = new URL("/v1/offers", PUBLIC_API);
298
+ url.searchParams.set("min_trust", minTrust);
299
+ offers = await publicJson(url, "offers");
218
300
  }
219
301
  return {
220
302
  available: offers.length,
@@ -224,38 +306,136 @@ async function handle(name, args) {
224
306
  price_per_second: usdg(o.rate_per_second),
225
307
  price_per_hour: usdg(o.rate_per_second * 3600),
226
308
  trust: o.trust_class,
309
+ // A staker-only offer will not match a wallet without the stake, so an
310
+ // unstaked renter should budget from the non-staker rows.
311
+ ...(o.staker_only ? { staker_only: true } : {}),
227
312
  })),
228
313
  };
229
314
  }
315
+ if (name === "prism_price_index") {
316
+ const index = await publicJson(new URL("/v1/price-index", PUBLIC_API), "price index");
317
+ return {
318
+ currency: index.currency,
319
+ generated_at: index.generated_at,
320
+ gpus: (index.gpus ?? []).map((g) => ({
321
+ model: g.gpu_model,
322
+ sourced_low_per_hour: usdg(g.sourced_low_micros_per_hour),
323
+ sourced_median_per_hour: usdg(g.sourced_median_micros_per_hour),
324
+ sourced_high_per_hour: usdg(g.sourced_high_micros_per_hour),
325
+ settled_mean_per_hour: usdg(g.settled_mean_micros_per_hour),
326
+ settled_leases: g.settled_leases,
327
+ })),
328
+ };
329
+ }
330
+ if (name === "prism_receipts") {
331
+ const feed = await publicJson(PROOF_FEED, "proof feed");
332
+ if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit <= 0)) {
333
+ throw new Error("limit must be a positive integer (max 50)");
334
+ }
335
+ const limit = Math.min(args.limit ?? 10, 50);
336
+ return {
337
+ generated_at: feed.generated_at,
338
+ receipts: (feed.receipts ?? []).slice(0, limit).map((r) => ({
339
+ // The feed numbers leases per escrow deployment, so lease_id repeats
340
+ // across deployments; receipt_id is the unique handle.
341
+ receipt_id: r.receipt_id,
342
+ lease_id: r.lease_id,
343
+ outcome: r.outcome,
344
+ gpu_model: r.gpu_model,
345
+ trust: r.trust_class ?? null,
346
+ runtime_seconds: r.runtime_seconds,
347
+ charged: usdg(r.charged_base_units),
348
+ refunded: usdg(r.refunded_base_units),
349
+ settlement_tx: r.transaction_hash,
350
+ })),
351
+ };
352
+ }
353
+ if (name === "prism_leases") {
354
+ requireWallet(name);
355
+ const all = await agent.leases();
356
+ return {
357
+ total: all.length,
358
+ showing: Math.min(all.length, 20),
359
+ leases: all.slice(0, 20).map((l) => ({
360
+ lease_id: l.lease_id,
361
+ state: l.state,
362
+ image: l.image,
363
+ duration_seconds: l.duration_seconds,
364
+ max_escrow: usdg(l.maximum_escrow),
365
+ trust: l.trust_class,
366
+ funding_tx: l.funding_transaction_hash,
367
+ created_at: l.created_at,
368
+ })),
369
+ };
370
+ }
371
+ if (name === "prism_batch_run") {
372
+ requireCommand(args.command);
373
+ requireWallet(name);
374
+ const cap = maxDeposit(args);
375
+ const batch = await agent.lease({
376
+ image: IMAGE,
377
+ durationSeconds: args.duration_seconds ?? 900,
378
+ minVramMib: args.min_vram_mib ?? 16000,
379
+ maxDeposit: cap,
380
+ command: args.command,
381
+ });
382
+ return {
383
+ lease_id: batch.leaseId,
384
+ funding_tx: batch.fundingHash,
385
+ exit_code: batch.result?.exit_code,
386
+ stdout: batch.result?.stdout,
387
+ stderr: batch.result?.stderr,
388
+ truncated: batch.result?.truncated ?? false,
389
+ };
390
+ }
391
+ if (name === "prism_batch_result") {
392
+ requireWallet(name, "reads this wallet's leases");
393
+ const id = leaseId(args.lease_id);
394
+ return { lease_id: id, result: await agent.result(id) };
395
+ }
230
396
  if (name === "prism_lease_and_run" || name === "prism_lease") {
231
- if (name === "prism_lease_and_run" && !args.command) throw new Error("command is required");
397
+ if (name === "prism_lease_and_run") requireCommand(args.command);
232
398
  requireWallet(name);
233
- await ensureAuth();
399
+ const cap = maxDeposit(args);
234
400
  sweepExpiredLeases();
235
401
  const lease = await agent.lease({
236
402
  image: IMAGE,
237
403
  durationSeconds: args.duration_seconds ?? 900,
238
404
  minVramMib: args.min_vram_mib ?? 16000,
405
+ maxDeposit: cap,
239
406
  minTrustClass: args.min_trust_class ?? "open",
240
407
  });
241
408
  leases.set(lease.leaseId, lease);
242
409
  const summary = {
243
410
  lease_id: lease.leaseId,
411
+ funding_tx: lease.fundingHash,
244
412
  ssh: { host: lease.access.ssh_host, port: lease.access.ssh_port, user: lease.access.ssh_user },
245
413
  trust: lease.quote?.trust_class ?? "open",
246
414
  expires_at: lease.access.expires_at,
247
415
  };
248
416
  if (name === "prism_lease") return summary;
249
- const out = await agent.run(lease, args.command);
250
- return { ...summary, command: args.command, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
417
+ // The lease is paid for by this point; an SSH failure must still hand the
418
+ // caller everything it bought.
419
+ try {
420
+ const out = await agent.run(lease, args.command);
421
+ return { ...summary, command: args.command, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
422
+ } catch (err) {
423
+ return {
424
+ ...summary,
425
+ error: `the lease is funded but the command could not run: ${err?.message ?? err}`,
426
+ next: `the lease stays open; try prism_run with lease_id ${lease.leaseId}, or prism_end_lease`,
427
+ };
428
+ }
251
429
  }
252
430
  if (name === "prism_run") {
253
- if (!args.command) throw new Error("command is required");
431
+ requireCommand(args.command);
254
432
  const id = leaseId(args.lease_id);
255
433
  const lease = leases.get(id);
256
434
  if (!lease) throw new Error(`no active lease ${id} in this session`);
435
+ const timeoutSeconds = args.timeout_seconds ?? 120;
257
436
  const out = await agent.run(lease, args.command, {
258
- timeoutMs: (args.timeout_seconds ?? 120) * 1000,
437
+ timeoutMs: timeoutSeconds * 1000,
438
+ connectRetries: 6,
259
439
  });
260
440
  return { lease_id: id, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
261
441
  }
@@ -269,14 +449,13 @@ async function handle(name, args) {
269
449
  return { lease_id: id, released: Boolean(lease) };
270
450
  }
271
451
  if (name.startsWith("prism_vault_")) return handleVault(name, args);
272
- throw new Error(`unknown tool ${name}`);
452
+ throw new Error(`unknown tool ${name}. Valid tools: ${TOOLS.map((t) => t.name).join(", ")}`);
273
453
  }
274
454
 
275
455
  // The vault key is derived here from the wallet signature and stays in this
276
456
  // process. Nothing in this function sends a key or a plaintext to Prism.
277
457
  async function handleVault(name, args) {
278
- const vault = requireWallet(name).vault;
279
- await ensureAuth();
458
+ const vault = requireWallet(name, "derives the vault key from the wallet").vault;
280
459
  if (!vault.unlocked) await vault.unlock();
281
460
 
282
461
  if (name === "prism_vault_store") {
@@ -322,23 +501,26 @@ async function handleVault(name, args) {
322
501
  throw new Error(`unknown tool ${name}`);
323
502
  }
324
503
 
325
- let authPromise = null;
326
- function ensureAuth() {
327
- authPromise ??= agent.authenticate().catch((err) => {
328
- authPromise = null;
329
- throw err;
330
- });
331
- return authPromise;
332
- }
333
-
334
- const server = new Server({ name: "prism", version: "0.4.1" }, { capabilities: { tools: {} } });
504
+ const server = new Server({ name: "prism", version: "0.6.0" }, { capabilities: { tools: {} } });
335
505
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
336
506
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
337
507
  try {
338
508
  const result = await handle(request.params.name, request.params.arguments ?? {});
339
509
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
340
510
  } catch (err) {
341
- return { isError: true, content: [{ type: "text", text: `error: ${err.message ?? err}` }] };
511
+ const body = err?.body ?? {};
512
+ const detail = [
513
+ body.cause,
514
+ body.hint,
515
+ body.required != null ? `required ${body.required}` : null,
516
+ body.max != null ? `cap ${body.max}` : null,
517
+ body.lease_id != null ? `lease_id ${body.lease_id}` : null,
518
+ body.funding_hash ? `funding_tx ${body.funding_hash}` : null,
519
+ ]
520
+ .filter(Boolean)
521
+ .join("; ");
522
+ const text = `error: ${err?.message ?? err}${detail ? ` (${detail})` : ""}`;
523
+ return { isError: true, content: [{ type: "text", text }] };
342
524
  }
343
525
  });
344
526