@nexusbloom/mcp-server 2.0.2 → 2.1.1
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 +169 -0
- package/index.js +13 -1
- package/package.json +5 -2
- package/src/config.js +5 -0
- package/src/handlers.js +368 -17
- package/src/history.js +316 -0
- package/src/local.js +164 -0
- package/src/manifests.js +44 -0
- package/src/progress.js +88 -0
- package/src/prompts.js +282 -0
- package/src/render.js +208 -9
- package/src/resources.js +322 -0
- package/src/server.js +106 -7
- package/src/validate.js +14 -1
package/src/handlers.js
CHANGED
|
@@ -18,9 +18,13 @@ import {
|
|
|
18
18
|
suggest,
|
|
19
19
|
} from "./discovery.js";
|
|
20
20
|
import { asNexusBloomError, ErrorCode, NexusBloomError } from "./errors.js";
|
|
21
|
-
import { ManifestCache, normaliseTool } from "./manifests.js";
|
|
21
|
+
import { ManifestCache, normaliseTool, partitionAdvertisable } from "./manifests.js";
|
|
22
22
|
import {
|
|
23
|
+
renderBatch,
|
|
23
24
|
renderConnectivityReport,
|
|
25
|
+
renderDiff,
|
|
26
|
+
renderHistory,
|
|
27
|
+
renderRun,
|
|
24
28
|
renderResult,
|
|
25
29
|
respond,
|
|
26
30
|
respondError,
|
|
@@ -30,6 +34,29 @@ import {
|
|
|
30
34
|
renderToolReportedError,
|
|
31
35
|
} from "./render.js";
|
|
32
36
|
import { assertValidInput, coerceParams } from "./validate.js";
|
|
37
|
+
import { RunHistory, summariseRun } from "./history.js";
|
|
38
|
+
import { runLocal } from "./local.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Unwrap the `{ success, data }` envelope the API wraps run results in.
|
|
42
|
+
*
|
|
43
|
+
* The check is for key *presence*, not truthiness. `body?.data ?? body` looks
|
|
44
|
+
* equivalent and is not: a tool whose result is legitimately `null` — an
|
|
45
|
+
* empty match set, a validator that found nothing — came back as
|
|
46
|
+
* `{ success: true, data: null }`, and `??` treats that null as "absent" and
|
|
47
|
+
* hands the agent the entire envelope as its result. It then renders
|
|
48
|
+
* `{"success":true,"data":null}` where the answer should have been `null`, which
|
|
49
|
+
* is the kind of wrong-but-plausible output an agent will build a conclusion on.
|
|
50
|
+
*
|
|
51
|
+
* @param {any} body the parsed response body
|
|
52
|
+
* @returns {any} the payload, or the body unchanged when it is not enveloped
|
|
53
|
+
*/
|
|
54
|
+
export function unwrapData(body) {
|
|
55
|
+
if (body && typeof body === "object" && !Array.isArray(body) && "data" in body) {
|
|
56
|
+
return body.data;
|
|
57
|
+
}
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
33
60
|
|
|
34
61
|
/**
|
|
35
62
|
* Assemble a handler set over a client and a manifest cache.
|
|
@@ -39,7 +66,10 @@ import { assertValidInput, coerceParams } from "./validate.js";
|
|
|
39
66
|
* @param {ManifestCache} deps.cache
|
|
40
67
|
* @param {object} deps.config
|
|
41
68
|
*/
|
|
42
|
-
export function createHandlers({ client, cache, config }) {
|
|
69
|
+
export function createHandlers({ client, cache, config, history: providedHistory }) {
|
|
70
|
+
// One log per handler set. Injected so a test can assert on a deterministic
|
|
71
|
+
// clock, and so a host that restarts the server does not inherit a stale log.
|
|
72
|
+
const history = providedHistory ?? new RunHistory();
|
|
43
73
|
/**
|
|
44
74
|
* Look up a tool by exact slug or unambiguous abbreviation.
|
|
45
75
|
*
|
|
@@ -89,7 +119,7 @@ export function createHandlers({ client, cache, config }) {
|
|
|
89
119
|
if (tools.length === 0) {
|
|
90
120
|
const result = await client
|
|
91
121
|
.request(`/run/${encodeURIComponent(slug.trim())}`, { method: "POST", body: params })
|
|
92
|
-
.then((body) => ({ success: true, data: body
|
|
122
|
+
.then((body) => ({ success: true, data: unwrapData(body) }))
|
|
93
123
|
.catch((err) => {
|
|
94
124
|
throw enrichNotFound(err, slug.trim(), []);
|
|
95
125
|
});
|
|
@@ -112,19 +142,80 @@ export function createHandlers({ client, cache, config }) {
|
|
|
112
142
|
assertValidInput(params, inputSchema, tool.slug);
|
|
113
143
|
|
|
114
144
|
const startedAt = Date.now();
|
|
145
|
+
|
|
146
|
+
// Local execution, when the operator has opted in. Decided before the request
|
|
147
|
+
// so a `local` run never touches /api/run at all, and an `auto` run falls
|
|
148
|
+
// through to the remote path below when no source is published.
|
|
149
|
+
const local = await runLocal({
|
|
150
|
+
client,
|
|
151
|
+
slug: tool.slug,
|
|
152
|
+
params,
|
|
153
|
+
mode: config?.execution ?? "remote",
|
|
154
|
+
config,
|
|
155
|
+
deps: opts.deps,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (local.executed) {
|
|
159
|
+
const durationMs = Date.now() - startedAt;
|
|
160
|
+
history.record({
|
|
161
|
+
tool: tool.slug,
|
|
162
|
+
params,
|
|
163
|
+
data: local.data,
|
|
164
|
+
ok: true,
|
|
165
|
+
durationMs,
|
|
166
|
+
batch: Boolean(opts.batch),
|
|
167
|
+
});
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
data: local.data,
|
|
171
|
+
tool: tool.slug,
|
|
172
|
+
execution: "local",
|
|
173
|
+
durationMs,
|
|
174
|
+
rateLimit: null,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
115
178
|
// The run endpoint records usage itself, so attribution rides on a header
|
|
116
179
|
// rather than a second tracking call — a separate POST would either be a
|
|
117
180
|
// no-op anonymously or double-count once authenticated.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
body
|
|
121
|
-
|
|
122
|
-
|
|
181
|
+
let body;
|
|
182
|
+
try {
|
|
183
|
+
body = await client.request(`/run/${encodeURIComponent(tool.slug)}`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
body: params,
|
|
186
|
+
headers: { "X-NexusBloom-Source": "mcp" },
|
|
187
|
+
});
|
|
188
|
+
} catch (err) {
|
|
189
|
+
// Failures are recorded too: a log that only holds successes cannot explain
|
|
190
|
+
// why an agent is retrying, which is the main reason to keep one.
|
|
191
|
+
const nxb = asNexusBloomError(err);
|
|
192
|
+
history.record({
|
|
193
|
+
tool: tool.slug,
|
|
194
|
+
params,
|
|
195
|
+
ok: false,
|
|
196
|
+
durationMs: Date.now() - startedAt,
|
|
197
|
+
error: nxb.message,
|
|
198
|
+
code: nxb.code,
|
|
199
|
+
retryable: nxb.retryable ?? false,
|
|
200
|
+
batch: Boolean(opts.batch),
|
|
201
|
+
});
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
123
204
|
const durationMs = Date.now() - startedAt;
|
|
205
|
+
const data = unwrapData(body);
|
|
206
|
+
|
|
207
|
+
history.record({
|
|
208
|
+
tool: tool.slug,
|
|
209
|
+
params,
|
|
210
|
+
data,
|
|
211
|
+
ok: true,
|
|
212
|
+
durationMs,
|
|
213
|
+
batch: Boolean(opts.batch),
|
|
214
|
+
});
|
|
124
215
|
|
|
125
216
|
return {
|
|
126
217
|
success: true,
|
|
127
|
-
data
|
|
218
|
+
data,
|
|
128
219
|
tool: tool.slug,
|
|
129
220
|
execution: "remote",
|
|
130
221
|
durationMs,
|
|
@@ -133,7 +224,7 @@ export function createHandlers({ client, cache, config }) {
|
|
|
133
224
|
}
|
|
134
225
|
|
|
135
226
|
/** Meta-tool: discover, inspect, or run. */
|
|
136
|
-
async function meta(args) {
|
|
227
|
+
async function meta(args, { onProgress } = {}) {
|
|
137
228
|
const command = args?.command;
|
|
138
229
|
|
|
139
230
|
if (!command) {
|
|
@@ -193,6 +284,36 @@ export function createHandlers({ client, cache, config }) {
|
|
|
193
284
|
return respond(renderSchema(schema, tools));
|
|
194
285
|
}
|
|
195
286
|
|
|
287
|
+
if (command === "batch") {
|
|
288
|
+
const batch = await runBatch(args?.runs, { onProgress });
|
|
289
|
+
return respond(renderBatch(batch));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (command === "history") {
|
|
293
|
+
// `show` narrows to one run; otherwise this is an index of what ran.
|
|
294
|
+
if (args?.show) {
|
|
295
|
+
const run = history.find(args.show);
|
|
296
|
+
if (!run) throw unknownRun(history, args.show);
|
|
297
|
+
return respond(renderRun(run));
|
|
298
|
+
}
|
|
299
|
+
const limit = Number.isInteger(args?.limit) && args.limit > 0 ? Math.min(args.limit, 50) : 10;
|
|
300
|
+
const all = history.list();
|
|
301
|
+
return respond(renderHistory(history.recent(limit), { total: all.length, scope: history.scope() }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (command === "diff") {
|
|
305
|
+
const from = args?.from ?? "-2";
|
|
306
|
+
const to = args?.to ?? "-1";
|
|
307
|
+
if (!history.list().length) {
|
|
308
|
+
throw new NexusBloomError(
|
|
309
|
+
"No runs are recorded yet, so there is nothing to compare. Run a tool first — " +
|
|
310
|
+
'history is in memory only and starts empty on every server start.',
|
|
311
|
+
ErrorCode.INVALID_ARGS,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return respond(renderDiff(history.diff(from, to)));
|
|
315
|
+
}
|
|
316
|
+
|
|
196
317
|
// run
|
|
197
318
|
const slug = (args?.slug || "").trim();
|
|
198
319
|
if (!slug) {
|
|
@@ -205,6 +326,113 @@ export function createHandlers({ client, cache, config }) {
|
|
|
205
326
|
return respondResult(result);
|
|
206
327
|
}
|
|
207
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Run several tools in one call.
|
|
331
|
+
*
|
|
332
|
+
* Two properties make this worth having over N direct calls:
|
|
333
|
+
*
|
|
334
|
+
* 1. **Resolution happens up front.** Every slug is resolved and every schema
|
|
335
|
+
* validated before the first run, so a typo in item four costs zero quota
|
|
336
|
+
* rather than three runs. A batch is one intent, so a malformed batch is an
|
|
337
|
+
* error, not eight partial results.
|
|
338
|
+
* 2. **Failures are isolated.** Once past resolution, each run is independent:
|
|
339
|
+
* one tool erroring must not discard the seven results that worked, because
|
|
340
|
+
* the whole point of batching is getting all of them in one turn.
|
|
341
|
+
*/
|
|
342
|
+
async function runBatch(runs, { onProgress } = {}) {
|
|
343
|
+
const specs = normaliseBatchInput(runs);
|
|
344
|
+
const total = specs.length;
|
|
345
|
+
|
|
346
|
+
// Phase 1 — resolve and validate everything.
|
|
347
|
+
const tools = await cache.tools();
|
|
348
|
+
const planned = [];
|
|
349
|
+
const unknown = [];
|
|
350
|
+
|
|
351
|
+
for (const spec of specs) {
|
|
352
|
+
const { tool, ambiguous } = resolveSlugStrict(tools, spec.slug);
|
|
353
|
+
if (!tool) {
|
|
354
|
+
// An ambiguous prefix is a different mistake from a typo, and the
|
|
355
|
+
// recovery differs: candidates the agent must choose between, versus a
|
|
356
|
+
// near match it can just use. Both are recorded so one message can carry
|
|
357
|
+
// the right hint for each.
|
|
358
|
+
const candidates = [
|
|
359
|
+
...new Set([
|
|
360
|
+
...ambiguous.map((t) => t.slug),
|
|
361
|
+
...suggest(tools, spec.slug).map((t) => t.slug),
|
|
362
|
+
]),
|
|
363
|
+
].slice(0, 3);
|
|
364
|
+
unknown.push({ slug: spec.slug, candidates });
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
planned.push({ spec, tool });
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (unknown.length) {
|
|
371
|
+
const parts = unknown.map(({ slug, candidates }) =>
|
|
372
|
+
candidates.length ? `"${slug}" → ${candidates.join(" or ")}` : `"${slug}" (no close match)`,
|
|
373
|
+
);
|
|
374
|
+
throw new NexusBloomError(
|
|
375
|
+
`Nothing in the batch ran. Unresolvable: ${parts.join("; ")}. ` +
|
|
376
|
+
`Call {"command":"search","query":"<what you need>"} or {"command":"list"}.`,
|
|
377
|
+
ErrorCode.INVALID_ARGS,
|
|
378
|
+
{ details: { unknown: unknown.map((u) => u.slug) } },
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Coerce and validate before spending anything, matching the single-call path.
|
|
383
|
+
const validated = [];
|
|
384
|
+
for (const { spec, tool } of planned) {
|
|
385
|
+
const params = coerceParams(spec.params, tool.slug);
|
|
386
|
+
let inputSchema = tool.input_schema;
|
|
387
|
+
try {
|
|
388
|
+
const manifest = await cache.manifest(tool.slug);
|
|
389
|
+
inputSchema = manifest.input_schema || inputSchema;
|
|
390
|
+
} catch {
|
|
391
|
+
/* the list schema is good enough; the API is authoritative anyway */
|
|
392
|
+
}
|
|
393
|
+
assertValidInput(params, inputSchema, tool.slug);
|
|
394
|
+
validated.push({ slug: tool.slug, params });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Phase 2 — run, isolating failures.
|
|
398
|
+
const startedAt = Date.now();
|
|
399
|
+
const results = [];
|
|
400
|
+
|
|
401
|
+
await onProgress?.({ progress: 0, total, message: `Running ${total} tool${total === 1 ? "" : "s"}…` });
|
|
402
|
+
|
|
403
|
+
for (const [index, run] of validated.entries()) {
|
|
404
|
+
try {
|
|
405
|
+
const result = await execute(run.slug, run.params, { batch: true });
|
|
406
|
+
const data = result.data;
|
|
407
|
+
const reported = data && typeof data === "object" && (data.success === false || (data.error && !data.data));
|
|
408
|
+
results.push({
|
|
409
|
+
slug: run.slug,
|
|
410
|
+
success: !reported,
|
|
411
|
+
data,
|
|
412
|
+
durationMs: result.durationMs,
|
|
413
|
+
...(reported ? { error: data.error || data.message || "The tool reported an error.", code: data.code } : {}),
|
|
414
|
+
});
|
|
415
|
+
} catch (err) {
|
|
416
|
+
const nxb = asNexusBloomError(err);
|
|
417
|
+
results.push({
|
|
418
|
+
slug: run.slug,
|
|
419
|
+
success: false,
|
|
420
|
+
error: nxb.message,
|
|
421
|
+
code: nxb.code,
|
|
422
|
+
retryable: nxb.retryable ?? false,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
await onProgress?.({
|
|
427
|
+
progress: index + 1,
|
|
428
|
+
total,
|
|
429
|
+
message: `Finished ${run.slug} (${index + 1}/${total})`,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return { runs: validated.length, results, durationMs: Date.now() - startedAt };
|
|
434
|
+
}
|
|
435
|
+
|
|
208
436
|
/** Direct invocation by tool slug. */
|
|
209
437
|
async function callDirect(name, args) {
|
|
210
438
|
const result = await execute(name, args ?? {});
|
|
@@ -221,8 +449,22 @@ export function createHandlers({ client, cache, config }) {
|
|
|
221
449
|
}
|
|
222
450
|
|
|
223
451
|
return {
|
|
224
|
-
|
|
452
|
+
// Withheld tools are reported once, on stderr, and not behind a debug flag:
|
|
453
|
+
// a slug the catalogue cannot express as an MCP tool name is a data problem
|
|
454
|
+
// somebody has to fix, and silence leaves the tool quietly unreachable.
|
|
455
|
+
listTools: buildToolList(cache, {
|
|
456
|
+
onWithheld(withheld) {
|
|
457
|
+
for (const w of withheld) {
|
|
458
|
+
process.stderr.write(
|
|
459
|
+
`[catalogue] "${w.slug}" is not advertised as an MCP tool — ${w.reason}. ` +
|
|
460
|
+
'It is still reachable via {"command":"run"} and nexusbloom://catalogue.\n',
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
225
465
|
meta,
|
|
466
|
+
batch: runBatch,
|
|
467
|
+
history,
|
|
226
468
|
callDirect,
|
|
227
469
|
execute,
|
|
228
470
|
findTool,
|
|
@@ -231,7 +473,69 @@ export function createHandlers({ client, cache, config }) {
|
|
|
231
473
|
}
|
|
232
474
|
|
|
233
475
|
/** The meta-tool's accepted commands, used in errors and tests. */
|
|
234
|
-
export const META_COMMANDS = ["list", "search", "schema", "run"];
|
|
476
|
+
export const META_COMMANDS = ["list", "search", "schema", "run", "batch", "history", "diff"];
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* How many runs one batch may contain.
|
|
480
|
+
*
|
|
481
|
+
* Bounded because anonymous execution is capped at 30 requests/minute: a batch of
|
|
482
|
+
* fifty would spend the whole budget in one turn and 429 the user's next ten
|
|
483
|
+
* calls. Ten is enough for the real tasks (validate, lint, transform a set of
|
|
484
|
+
* files) and leaves headroom.
|
|
485
|
+
*/
|
|
486
|
+
export const MAX_BATCH_SIZE = 10;
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Validate the batch argument and return `{slug, params}` specs.
|
|
490
|
+
*
|
|
491
|
+
* A single malformed entry rejects the whole batch, because a batch is one
|
|
492
|
+
* intent — silently dropping item three would return a result set the caller
|
|
493
|
+
* cannot reconcile with what it asked for.
|
|
494
|
+
*/
|
|
495
|
+
export function normaliseBatchInput(runs) {
|
|
496
|
+
if (!Array.isArray(runs) || runs.length === 0) {
|
|
497
|
+
throw new NexusBloomError(
|
|
498
|
+
`Missing "runs" for batch. Example: ` +
|
|
499
|
+
`{"command":"batch","runs":[{"slug":"env-validator","params":{"env_content":"DEBUG=true"}}]}`,
|
|
500
|
+
ErrorCode.INVALID_ARGS,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
if (runs.length > MAX_BATCH_SIZE) {
|
|
504
|
+
throw new NexusBloomError(
|
|
505
|
+
`A batch may contain at most ${MAX_BATCH_SIZE} runs; got ${runs.length}. ` +
|
|
506
|
+
`Split it — or run them as direct tool calls, which do not share this cap.`,
|
|
507
|
+
ErrorCode.INVALID_ARGS,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return runs.map((entry, i) => {
|
|
512
|
+
// `{slug}` alone is accepted; params defaults to {} for a no-argument tool.
|
|
513
|
+
const spec = typeof entry === "string" ? { slug: entry } : entry;
|
|
514
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec)) {
|
|
515
|
+
throw new NexusBloomError(
|
|
516
|
+
`runs[${i}] must be an object like {"slug":"…","params":{…}}, or a slug string.`,
|
|
517
|
+
ErrorCode.INVALID_ARGS,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
const slug = typeof spec.slug === "string" ? spec.slug.trim() : "";
|
|
521
|
+
if (!slug) {
|
|
522
|
+
throw new NexusBloomError(`runs[${i}] is missing a "slug".`, ErrorCode.INVALID_ARGS);
|
|
523
|
+
}
|
|
524
|
+
return { slug, params: spec.params ?? {} };
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** A run reference that is not in the log, with the ids that are. */
|
|
529
|
+
export function unknownRun(history, ref) {
|
|
530
|
+
const known = history.list().map((r) => r.id);
|
|
531
|
+
return new NexusBloomError(
|
|
532
|
+
`No run recorded for "${ref}". ` +
|
|
533
|
+
`Known run ids: ${known.join(", ") || "(none yet)"}. ` +
|
|
534
|
+
`Pass an id, or -1 for the most recent run.`,
|
|
535
|
+
ErrorCode.NOT_FOUND,
|
|
536
|
+
{ details: { known } },
|
|
537
|
+
);
|
|
538
|
+
}
|
|
235
539
|
|
|
236
540
|
/**
|
|
237
541
|
* Build a not-found error that tells the agent what to do next.
|
|
@@ -277,7 +581,7 @@ function enrichNotFound(err, slug, tools) {
|
|
|
277
581
|
* meta-tool is advertised too — it is how an agent recovers when a direct call
|
|
278
582
|
* is not what it wanted.
|
|
279
583
|
*/
|
|
280
|
-
export function buildToolList(cache) {
|
|
584
|
+
export function buildToolList(cache, { onWithheld } = {}) {
|
|
281
585
|
return async function listTools() {
|
|
282
586
|
// A catalogue failure must not fail the whole request. If the API is
|
|
283
587
|
// unreachable, a host that receives a JSON-RPC error considers the *server*
|
|
@@ -290,7 +594,16 @@ export function buildToolList(cache) {
|
|
|
290
594
|
tools = [];
|
|
291
595
|
}
|
|
292
596
|
|
|
293
|
-
|
|
597
|
+
// A slug that is not a legal MCP tool name is held back rather than
|
|
598
|
+
// emitted. A strict host validates every name in this array and rejects the
|
|
599
|
+
// entire response over one bad entry, so a single odd slug in the catalogue
|
|
600
|
+
// would cost the agent every tool — not just that one. The meta-tool still
|
|
601
|
+
// reaches the withheld tools, and the reason is surfaced rather than
|
|
602
|
+
// swallowed, so the catalogue can be fixed at source.
|
|
603
|
+
const { advertisable, withheld } = partitionAdvertisable(tools);
|
|
604
|
+
if (withheld.length) onWithheld?.(withheld);
|
|
605
|
+
|
|
606
|
+
const entries = advertisable.map((t) => ({
|
|
294
607
|
name: t.slug,
|
|
295
608
|
description: buildToolDescription(t),
|
|
296
609
|
inputSchema: t.input_schema,
|
|
@@ -303,9 +616,15 @@ export function buildToolList(cache) {
|
|
|
303
616
|
"Use this instead of guessing a slug:\n" +
|
|
304
617
|
'- {"command":"search","query":"validate environment file"} — rank tools by intent\n' +
|
|
305
618
|
'- {"command":"list"} — every published tool, one line each\n' +
|
|
306
|
-
'- {"command":"schema","slug":"<slug>"} — exact parameters plus a ready-to-send example\n
|
|
619
|
+
'- {"command":"schema","slug":"<slug>"} — exact parameters plus a ready-to-send example\n' +
|
|
620
|
+
'- {"command":"batch","runs":[{"slug":"<slug>","params":{…}},…]} — run up to 10 tools in one call\n' +
|
|
621
|
+
'- {"command":"history"} / {"command":"history","show":"<id>"} — what this server process has run\n' +
|
|
622
|
+
'- {"command":"diff","from":"<id>","to":"<id>"} — compare two runs field by field\n\n' +
|
|
307
623
|
"Any published tool is also callable directly by its slug, with its own " +
|
|
308
|
-
"parameters as arguments — this tool is for when you do not yet know which one to use
|
|
624
|
+
"parameters as arguments — this tool is for when you do not yet know which one to use.\n\n" +
|
|
625
|
+
"If your host supports MCP resources, the same information is readable without a " +
|
|
626
|
+
'call: "nexusbloom://catalogue" lists every tool, "nexusbloom://tools/{slug}" ' +
|
|
627
|
+
'carries one full manifest, and "nexusbloom://guide" explains the recovery paths.',
|
|
309
628
|
inputSchema: {
|
|
310
629
|
type: "object",
|
|
311
630
|
properties: {
|
|
@@ -313,7 +632,9 @@ export function buildToolList(cache) {
|
|
|
313
632
|
type: "string",
|
|
314
633
|
enum: META_COMMANDS,
|
|
315
634
|
description:
|
|
316
|
-
"list: all tools. search: find by intent. schema: a tool's parameters.
|
|
635
|
+
"list: all tools. search: find by intent. schema: a tool's parameters. " +
|
|
636
|
+
"run: execute one tool. batch: execute up to 10 tools in one call. " +
|
|
637
|
+
"history: what this server process has run, and since when. diff: compare two recorded runs.",
|
|
317
638
|
},
|
|
318
639
|
slug: {
|
|
319
640
|
type: "string",
|
|
@@ -329,6 +650,36 @@ export function buildToolList(cache) {
|
|
|
329
650
|
type: "object",
|
|
330
651
|
description: "Input parameters for 'run', matching the tool's input schema.",
|
|
331
652
|
},
|
|
653
|
+
show: {
|
|
654
|
+
type: "string",
|
|
655
|
+
description:
|
|
656
|
+
"Run id for 'history' — a single run with its input and result. " +
|
|
657
|
+
'-1 is the most recent, -2 the one before it.',
|
|
658
|
+
},
|
|
659
|
+
from: {
|
|
660
|
+
type: "string",
|
|
661
|
+
description: "Run id or negative index for 'diff'. Default \"-2\".",
|
|
662
|
+
},
|
|
663
|
+
to: {
|
|
664
|
+
type: "string",
|
|
665
|
+
description: "Run id or negative index for 'diff'. Default \"-1\".",
|
|
666
|
+
},
|
|
667
|
+
runs: {
|
|
668
|
+
type: "array",
|
|
669
|
+
description:
|
|
670
|
+
"Up to 10 entries for 'batch', each {\"slug\":\"…\",\"params\":{…}}. " +
|
|
671
|
+
"A bare slug string is accepted for a tool that takes no arguments. " +
|
|
672
|
+
"All slugs are resolved and validated before anything runs, so a typo costs no quota.",
|
|
673
|
+
maxItems: MAX_BATCH_SIZE,
|
|
674
|
+
items: {
|
|
675
|
+
type: "object",
|
|
676
|
+
properties: {
|
|
677
|
+
slug: { type: "string", description: "Tool slug." },
|
|
678
|
+
params: { type: "object", description: "That tool's input parameters." },
|
|
679
|
+
},
|
|
680
|
+
required: ["slug"],
|
|
681
|
+
},
|
|
682
|
+
},
|
|
332
683
|
limit: {
|
|
333
684
|
type: "integer",
|
|
334
685
|
description: "Maximum results for 'search'. Default 10.",
|