@prismnetwork/mcp 0.5.0 → 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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/server.mjs +130 -59
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ derived from the agent's wallet inside this server process; Prism receives
|
|
|
60
60
|
ciphertext and holds no way to read it.
|
|
61
61
|
|
|
62
62
|
Each item names the weakest workspace trust class it may ever be released into,
|
|
63
|
-
and new items default to `confidential
|
|
63
|
+
and new items default to `confidential`, above anything the network serves
|
|
64
64
|
today. `prism_vault_release` is therefore refused on current capacity instead of
|
|
65
65
|
handing a secret to a host that can read it. Lowering an item's floor is a
|
|
66
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.
|
|
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.
|
|
19
|
+
"@prismnetwork/agent-sdk": "^0.5.0",
|
|
20
20
|
"viem": "^2"
|
|
21
21
|
},
|
|
22
22
|
"keywords": [
|
package/server.mjs
CHANGED
|
@@ -9,59 +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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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`;
|
|
65
88
|
|
|
66
89
|
function sweepExpiredLeases() {
|
|
67
90
|
const now = Date.now();
|
|
@@ -111,7 +134,7 @@ const TOOLS = [
|
|
|
111
134
|
inputSchema: {
|
|
112
135
|
type: "object",
|
|
113
136
|
properties: {
|
|
114
|
-
limit: { type: "integer", description: "Max receipts to return (default 10)." },
|
|
137
|
+
limit: { type: "integer", description: "Max receipts to return (default 10, max 50)." },
|
|
115
138
|
},
|
|
116
139
|
},
|
|
117
140
|
},
|
|
@@ -122,17 +145,29 @@ const TOOLS = [
|
|
|
122
145
|
},
|
|
123
146
|
{
|
|
124
147
|
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
|
|
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.",
|
|
126
149
|
inputSchema: {
|
|
127
150
|
type: "object",
|
|
128
151
|
properties: {
|
|
129
152
|
command: { type: "string", description: "Shell command to run (max 8 KiB)." },
|
|
130
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." },
|
|
131
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." },
|
|
132
156
|
},
|
|
133
157
|
required: ["command"],
|
|
134
158
|
},
|
|
135
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
|
+
},
|
|
136
171
|
{
|
|
137
172
|
name: "prism_lease_and_run",
|
|
138
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.",
|
|
@@ -147,6 +182,7 @@ const TOOLS = [
|
|
|
147
182
|
enum: TRUST_CLASSES,
|
|
148
183
|
description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
|
|
149
184
|
},
|
|
185
|
+
max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
|
|
150
186
|
},
|
|
151
187
|
required: ["command"],
|
|
152
188
|
},
|
|
@@ -164,6 +200,7 @@ const TOOLS = [
|
|
|
164
200
|
enum: TRUST_CLASSES,
|
|
165
201
|
description: "Refuse suppliers below this trust class (default 'open'). Raise it for anything the host operator must not read.",
|
|
166
202
|
},
|
|
203
|
+
max_usdg: { type: "number", description: "Hard cap on the USDG this lease may cost (default 1). Raise it deliberately for longer leases." },
|
|
167
204
|
},
|
|
168
205
|
},
|
|
169
206
|
},
|
|
@@ -191,7 +228,7 @@ const TOOLS = [
|
|
|
191
228
|
},
|
|
192
229
|
{
|
|
193
230
|
name: "prism_vault_store",
|
|
194
|
-
description: "Store private data
|
|
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.",
|
|
195
232
|
inputSchema: {
|
|
196
233
|
type: "object",
|
|
197
234
|
properties: {
|
|
@@ -213,7 +250,7 @@ const TOOLS = [
|
|
|
213
250
|
},
|
|
214
251
|
{
|
|
215
252
|
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
|
|
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.",
|
|
217
254
|
inputSchema: {
|
|
218
255
|
type: "object",
|
|
219
256
|
properties: { item_id: { type: "string", description: "The item_id from prism_vault_store or prism_vault_list." } },
|
|
@@ -250,12 +287,16 @@ async function handle(name, args) {
|
|
|
250
287
|
}
|
|
251
288
|
if (name === "prism_list_gpus") {
|
|
252
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
|
+
}
|
|
253
293
|
let offers;
|
|
254
294
|
if (agent) {
|
|
255
|
-
await ensureAuth();
|
|
256
295
|
offers = await agent.offers({ minTrust });
|
|
257
296
|
} else {
|
|
258
|
-
|
|
297
|
+
const url = new URL("/v1/offers", PUBLIC_API);
|
|
298
|
+
url.searchParams.set("min_trust", minTrust);
|
|
299
|
+
offers = await publicJson(url, "offers");
|
|
259
300
|
}
|
|
260
301
|
return {
|
|
261
302
|
available: offers.length,
|
|
@@ -265,6 +306,9 @@ async function handle(name, args) {
|
|
|
265
306
|
price_per_second: usdg(o.rate_per_second),
|
|
266
307
|
price_per_hour: usdg(o.rate_per_second * 3600),
|
|
267
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 } : {}),
|
|
268
312
|
})),
|
|
269
313
|
};
|
|
270
314
|
}
|
|
@@ -278,21 +322,27 @@ async function handle(name, args) {
|
|
|
278
322
|
sourced_low_per_hour: usdg(g.sourced_low_micros_per_hour),
|
|
279
323
|
sourced_median_per_hour: usdg(g.sourced_median_micros_per_hour),
|
|
280
324
|
sourced_high_per_hour: usdg(g.sourced_high_micros_per_hour),
|
|
281
|
-
settled_mean_per_hour:
|
|
325
|
+
settled_mean_per_hour: usdg(g.settled_mean_micros_per_hour),
|
|
282
326
|
settled_leases: g.settled_leases,
|
|
283
327
|
})),
|
|
284
328
|
};
|
|
285
329
|
}
|
|
286
330
|
if (name === "prism_receipts") {
|
|
287
331
|
const feed = await publicJson(PROOF_FEED, "proof feed");
|
|
288
|
-
|
|
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);
|
|
289
336
|
return {
|
|
290
337
|
generated_at: feed.generated_at,
|
|
291
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,
|
|
292
342
|
lease_id: r.lease_id,
|
|
293
343
|
outcome: r.outcome,
|
|
294
344
|
gpu_model: r.gpu_model,
|
|
295
|
-
trust: r.trust_class,
|
|
345
|
+
trust: r.trust_class ?? null,
|
|
296
346
|
runtime_seconds: r.runtime_seconds,
|
|
297
347
|
charged: usdg(r.charged_base_units),
|
|
298
348
|
refunded: usdg(r.refunded_base_units),
|
|
@@ -302,7 +352,6 @@ async function handle(name, args) {
|
|
|
302
352
|
}
|
|
303
353
|
if (name === "prism_leases") {
|
|
304
354
|
requireWallet(name);
|
|
305
|
-
await ensureAuth();
|
|
306
355
|
const all = await agent.leases();
|
|
307
356
|
return {
|
|
308
357
|
total: all.length,
|
|
@@ -320,13 +369,14 @@ async function handle(name, args) {
|
|
|
320
369
|
};
|
|
321
370
|
}
|
|
322
371
|
if (name === "prism_batch_run") {
|
|
323
|
-
|
|
372
|
+
requireCommand(args.command);
|
|
324
373
|
requireWallet(name);
|
|
325
|
-
|
|
374
|
+
const cap = maxDeposit(args);
|
|
326
375
|
const batch = await agent.lease({
|
|
327
376
|
image: IMAGE,
|
|
328
377
|
durationSeconds: args.duration_seconds ?? 900,
|
|
329
378
|
minVramMib: args.min_vram_mib ?? 16000,
|
|
379
|
+
maxDeposit: cap,
|
|
330
380
|
command: args.command,
|
|
331
381
|
});
|
|
332
382
|
return {
|
|
@@ -338,35 +388,54 @@ async function handle(name, args) {
|
|
|
338
388
|
truncated: batch.result?.truncated ?? false,
|
|
339
389
|
};
|
|
340
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
|
+
}
|
|
341
396
|
if (name === "prism_lease_and_run" || name === "prism_lease") {
|
|
342
|
-
if (name === "prism_lease_and_run"
|
|
397
|
+
if (name === "prism_lease_and_run") requireCommand(args.command);
|
|
343
398
|
requireWallet(name);
|
|
344
|
-
|
|
399
|
+
const cap = maxDeposit(args);
|
|
345
400
|
sweepExpiredLeases();
|
|
346
401
|
const lease = await agent.lease({
|
|
347
402
|
image: IMAGE,
|
|
348
403
|
durationSeconds: args.duration_seconds ?? 900,
|
|
349
404
|
minVramMib: args.min_vram_mib ?? 16000,
|
|
405
|
+
maxDeposit: cap,
|
|
350
406
|
minTrustClass: args.min_trust_class ?? "open",
|
|
351
407
|
});
|
|
352
408
|
leases.set(lease.leaseId, lease);
|
|
353
409
|
const summary = {
|
|
354
410
|
lease_id: lease.leaseId,
|
|
411
|
+
funding_tx: lease.fundingHash,
|
|
355
412
|
ssh: { host: lease.access.ssh_host, port: lease.access.ssh_port, user: lease.access.ssh_user },
|
|
356
413
|
trust: lease.quote?.trust_class ?? "open",
|
|
357
414
|
expires_at: lease.access.expires_at,
|
|
358
415
|
};
|
|
359
416
|
if (name === "prism_lease") return summary;
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
}
|
|
362
429
|
}
|
|
363
430
|
if (name === "prism_run") {
|
|
364
|
-
|
|
431
|
+
requireCommand(args.command);
|
|
365
432
|
const id = leaseId(args.lease_id);
|
|
366
433
|
const lease = leases.get(id);
|
|
367
434
|
if (!lease) throw new Error(`no active lease ${id} in this session`);
|
|
435
|
+
const timeoutSeconds = args.timeout_seconds ?? 120;
|
|
368
436
|
const out = await agent.run(lease, args.command, {
|
|
369
|
-
timeoutMs:
|
|
437
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
438
|
+
connectRetries: 6,
|
|
370
439
|
});
|
|
371
440
|
return { lease_id: id, exit_code: out.code, stdout: out.stdout, stderr: out.stderr };
|
|
372
441
|
}
|
|
@@ -380,14 +449,13 @@ async function handle(name, args) {
|
|
|
380
449
|
return { lease_id: id, released: Boolean(lease) };
|
|
381
450
|
}
|
|
382
451
|
if (name.startsWith("prism_vault_")) return handleVault(name, args);
|
|
383
|
-
throw new Error(`unknown tool ${name}`);
|
|
452
|
+
throw new Error(`unknown tool ${name}. Valid tools: ${TOOLS.map((t) => t.name).join(", ")}`);
|
|
384
453
|
}
|
|
385
454
|
|
|
386
455
|
// The vault key is derived here from the wallet signature and stays in this
|
|
387
456
|
// process. Nothing in this function sends a key or a plaintext to Prism.
|
|
388
457
|
async function handleVault(name, args) {
|
|
389
|
-
const vault = requireWallet(name).vault;
|
|
390
|
-
await ensureAuth();
|
|
458
|
+
const vault = requireWallet(name, "derives the vault key from the wallet").vault;
|
|
391
459
|
if (!vault.unlocked) await vault.unlock();
|
|
392
460
|
|
|
393
461
|
if (name === "prism_vault_store") {
|
|
@@ -433,23 +501,26 @@ async function handleVault(name, args) {
|
|
|
433
501
|
throw new Error(`unknown tool ${name}`);
|
|
434
502
|
}
|
|
435
503
|
|
|
436
|
-
|
|
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: {} } });
|
|
504
|
+
const server = new Server({ name: "prism", version: "0.6.0" }, { capabilities: { tools: {} } });
|
|
446
505
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
447
506
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
448
507
|
try {
|
|
449
508
|
const result = await handle(request.params.name, request.params.arguments ?? {});
|
|
450
509
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
451
510
|
} catch (err) {
|
|
452
|
-
|
|
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 }] };
|
|
453
524
|
}
|
|
454
525
|
});
|
|
455
526
|
|