@sriramadari/mcp-devkit 0.1.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/dist/cli.cjs ADDED
@@ -0,0 +1,619 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var util = require('util');
5
+ var index_js = require('@modelcontextprotocol/sdk/client/index.js');
6
+ var inMemory_js = require('@modelcontextprotocol/sdk/inMemory.js');
7
+ var stdio_js = require('@modelcontextprotocol/sdk/client/stdio.js');
8
+
9
+ async function openConnection(target, options = {}) {
10
+ const client = new index_js.Client({
11
+ name: options.clientName ?? "mcp-devkit",
12
+ version: options.clientVersion ?? "0.1.0"
13
+ });
14
+ if ("server" in target && target.server) {
15
+ const server = typeof target.server === "function" ? await target.server() : target.server;
16
+ const [clientTransport, serverTransport] = inMemory_js.InMemoryTransport.createLinkedPair();
17
+ await server.connect(serverTransport);
18
+ await client.connect(clientTransport);
19
+ return {
20
+ client,
21
+ close: async () => {
22
+ await client.close().catch(() => {
23
+ });
24
+ await server.close().catch(() => {
25
+ });
26
+ }
27
+ };
28
+ }
29
+ if ("command" in target && target.command) {
30
+ const transport = new stdio_js.StdioClientTransport({
31
+ command: target.command,
32
+ args: target.args ?? [],
33
+ env: target.env,
34
+ cwd: target.cwd
35
+ });
36
+ await client.connect(transport);
37
+ return {
38
+ client,
39
+ // Closing the client closes the transport, which terminates the child.
40
+ close: async () => {
41
+ await client.close().catch(() => {
42
+ });
43
+ }
44
+ };
45
+ }
46
+ throw new Error("openConnection: target must provide either `server` or `command`");
47
+ }
48
+
49
+ // src/schema.ts
50
+ var TYPEOF = {
51
+ string: "string",
52
+ number: "number",
53
+ boolean: "boolean"
54
+ };
55
+ function typeOf(value) {
56
+ if (value === null) return "null";
57
+ if (Array.isArray(value)) return "array";
58
+ const t = typeof value;
59
+ if (t === "object") return "object";
60
+ return TYPEOF[t] ?? "string";
61
+ }
62
+ function matchesType(value, type) {
63
+ switch (type) {
64
+ case "integer":
65
+ return typeof value === "number" && Number.isInteger(value);
66
+ case "number":
67
+ return typeof value === "number";
68
+ case "null":
69
+ return value === null;
70
+ default:
71
+ return typeOf(value) === type;
72
+ }
73
+ }
74
+ function deepEqual(a, b) {
75
+ if (a === b) return true;
76
+ if (typeof a !== typeof b || a === null || b === null) return false;
77
+ if (typeof a !== "object") return false;
78
+ const ka = Object.keys(a);
79
+ const kb = Object.keys(b);
80
+ if (ka.length !== kb.length) return false;
81
+ return ka.every(
82
+ (k) => deepEqual(a[k], b[k])
83
+ );
84
+ }
85
+ function validateAgainstSchema(value, schema) {
86
+ const errors = [];
87
+ walk(value, schema, "", errors);
88
+ return { valid: errors.length === 0, errors };
89
+ }
90
+ function walk(value, schema, path, errors) {
91
+ if (!schema || typeof schema !== "object") return;
92
+ if ("const" in schema && !deepEqual(value, schema.const)) {
93
+ errors.push({ path: path || "/", message: `must equal ${JSON.stringify(schema.const)}` });
94
+ }
95
+ if (schema.enum && !schema.enum.some((e) => deepEqual(e, value))) {
96
+ errors.push({ path: path || "/", message: `must be one of ${JSON.stringify(schema.enum)}` });
97
+ }
98
+ if (schema.allOf) {
99
+ for (const sub of schema.allOf) walk(value, sub, path, errors);
100
+ }
101
+ if (schema.anyOf && !schema.anyOf.some((sub) => validateAgainstSchema(value, sub).valid)) {
102
+ errors.push({ path: path || "/", message: "did not match any schema in anyOf" });
103
+ }
104
+ if (schema.oneOf) {
105
+ const hits = schema.oneOf.filter((sub) => validateAgainstSchema(value, sub).valid).length;
106
+ if (hits !== 1) {
107
+ errors.push({ path: path || "/", message: `must match exactly one schema in oneOf (matched ${hits})` });
108
+ }
109
+ }
110
+ const types = schema.type === void 0 ? [] : Array.isArray(schema.type) ? schema.type : [schema.type];
111
+ if (types.length > 0 && !types.some((t) => matchesType(value, t))) {
112
+ errors.push({ path: path || "/", message: `expected type ${types.join(" | ")}, got ${typeOf(value)}` });
113
+ return;
114
+ }
115
+ const actual = typeOf(value);
116
+ if (actual === "string" && typeof value === "string") {
117
+ if (schema.minLength !== void 0 && value.length < schema.minLength) {
118
+ errors.push({ path: path || "/", message: `shorter than minLength ${schema.minLength}` });
119
+ }
120
+ if (schema.maxLength !== void 0 && value.length > schema.maxLength) {
121
+ errors.push({ path: path || "/", message: `longer than maxLength ${schema.maxLength}` });
122
+ }
123
+ }
124
+ if (actual === "number" && typeof value === "number") {
125
+ if (schema.minimum !== void 0 && value < schema.minimum) {
126
+ errors.push({ path: path || "/", message: `less than minimum ${schema.minimum}` });
127
+ }
128
+ if (schema.maximum !== void 0 && value > schema.maximum) {
129
+ errors.push({ path: path || "/", message: `greater than maximum ${schema.maximum}` });
130
+ }
131
+ }
132
+ if (actual === "object" && value && typeof value === "object") {
133
+ const obj = value;
134
+ for (const key of schema.required ?? []) {
135
+ if (!(key in obj)) {
136
+ errors.push({ path: `${path}/${key}`, message: "is required" });
137
+ }
138
+ }
139
+ if (schema.properties) {
140
+ for (const [key, sub] of Object.entries(schema.properties)) {
141
+ if (key in obj) walk(obj[key], sub, `${path}/${key}`, errors);
142
+ }
143
+ }
144
+ if (schema.additionalProperties === false && schema.properties) {
145
+ const allowed = new Set(Object.keys(schema.properties));
146
+ for (const key of Object.keys(obj)) {
147
+ if (!allowed.has(key)) {
148
+ errors.push({ path: `${path}/${key}`, message: "is not an allowed property" });
149
+ }
150
+ }
151
+ } else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
152
+ const declared = new Set(Object.keys(schema.properties ?? {}));
153
+ for (const key of Object.keys(obj)) {
154
+ if (!declared.has(key)) {
155
+ walk(obj[key], schema.additionalProperties, `${path}/${key}`, errors);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ if (actual === "array" && Array.isArray(value)) {
161
+ if (Array.isArray(schema.items)) {
162
+ schema.items.forEach((sub, i) => {
163
+ if (i < value.length) walk(value[i], sub, `${path}/${i}`, errors);
164
+ });
165
+ } else if (schema.items) {
166
+ value.forEach((item, i) => walk(item, schema.items, `${path}/${i}`, errors));
167
+ }
168
+ }
169
+ }
170
+
171
+ // src/client.ts
172
+ var TestClient = class {
173
+ /** The underlying SDK client — escape hatch for anything not wrapped here. */
174
+ raw;
175
+ _close;
176
+ defaultTimeoutMs;
177
+ _tools;
178
+ _resources;
179
+ _prompts;
180
+ /** @internal — use {@link createTestClient}. */
181
+ constructor(raw, close, defaultTimeoutMs) {
182
+ this.raw = raw;
183
+ this._close = close;
184
+ this.defaultTimeoutMs = defaultTimeoutMs;
185
+ }
186
+ // ── server identity / capabilities ─────────────────────────────────────────
187
+ serverInfo() {
188
+ return this.raw.getServerVersion();
189
+ }
190
+ capabilities() {
191
+ return this.raw.getServerCapabilities();
192
+ }
193
+ // ── tools ───────────────────────────────────────────────────────────────────
194
+ /** List the server's tools (cached after the first call — see {@link refresh}). */
195
+ async tools() {
196
+ if (!this._tools) {
197
+ const res = await this.raw.listTools();
198
+ this._tools = res.tools ?? [];
199
+ }
200
+ return this._tools;
201
+ }
202
+ async toolNames() {
203
+ return (await this.tools()).map((t) => t.name);
204
+ }
205
+ async hasTool(name) {
206
+ return (await this.tools()).some((t) => t.name === name);
207
+ }
208
+ async tool(name) {
209
+ return (await this.tools()).find((t) => t.name === name);
210
+ }
211
+ /**
212
+ * Call a tool. By default the args are validated against the tool's declared
213
+ * `inputSchema` first; set `{ validate: false }` to send them as-is (e.g. to
214
+ * test how the server handles bad input).
215
+ */
216
+ async call(name, args = {}, options = {}) {
217
+ if (options.validate !== false) {
218
+ const info = await this.tool(name);
219
+ if (info?.inputSchema) {
220
+ const { valid, errors } = validateAgainstSchema(args, info.inputSchema);
221
+ if (!valid) {
222
+ const detail = errors.map((e) => ` ${e.path} ${e.message}`).join("\n");
223
+ throw new Error(`Arguments for tool "${name}" do not match its inputSchema:
224
+ ${detail}`);
225
+ }
226
+ }
227
+ }
228
+ const result = await this.raw.callTool(
229
+ { name, arguments: args },
230
+ void 0,
231
+ { timeout: options.timeoutMs ?? this.defaultTimeoutMs }
232
+ );
233
+ return result;
234
+ }
235
+ // ── resources ─────────────────────────────────────────────────────────────
236
+ async resources() {
237
+ if (!this._resources) {
238
+ try {
239
+ const res = await this.raw.listResources();
240
+ this._resources = res.resources ?? [];
241
+ } catch {
242
+ this._resources = [];
243
+ }
244
+ }
245
+ return this._resources;
246
+ }
247
+ async readResource(uri) {
248
+ return await this.raw.readResource({ uri });
249
+ }
250
+ // ── prompts ───────────────────────────────────────────────────────────────
251
+ async prompts() {
252
+ if (!this._prompts) {
253
+ try {
254
+ const res = await this.raw.listPrompts();
255
+ this._prompts = res.prompts ?? [];
256
+ } catch {
257
+ this._prompts = [];
258
+ }
259
+ }
260
+ return this._prompts;
261
+ }
262
+ async getPrompt(name, args = {}) {
263
+ return this.raw.getPrompt({ name, arguments: args });
264
+ }
265
+ // ── lifecycle ───────────────────────────────────────────────────────────────
266
+ /** Drop cached listings so the next `tools()`/`resources()`/`prompts()` re-fetches. */
267
+ refresh() {
268
+ this._tools = void 0;
269
+ this._resources = void 0;
270
+ this._prompts = void 0;
271
+ }
272
+ /** Liveness check — resolves if the server answers a ping. */
273
+ async ping() {
274
+ await this.raw.ping();
275
+ }
276
+ /** Tear down the connection (and any spawned child process). Idempotent. */
277
+ async close() {
278
+ await this._close();
279
+ }
280
+ };
281
+ async function createTestClient(target, options = {}) {
282
+ const { client, close } = await openConnection(target, options);
283
+ return new TestClient(client, close, options.timeoutMs ?? 1e4);
284
+ }
285
+
286
+ // src/conformance.ts
287
+ var TOOL_NAME_RE = /^[a-zA-Z0-9_-]{1,128}$/;
288
+ var CHECKS = [
289
+ {
290
+ id: "server-identity",
291
+ title: "Server reports a name and version",
292
+ severity: "warning",
293
+ run: async ({ client }) => {
294
+ const info = client.serverInfo();
295
+ if (info?.name && info?.version) return { status: "pass" };
296
+ return { status: "fail", detail: `server info is ${JSON.stringify(info)}` };
297
+ }
298
+ },
299
+ {
300
+ id: "tools-listable",
301
+ title: "tools/list responds",
302
+ severity: "error",
303
+ run: async ({ tools }) => {
304
+ return { status: "pass", detail: `${tools.length} tool(s)` };
305
+ }
306
+ },
307
+ {
308
+ id: "unique-tool-names",
309
+ title: "Tool names are unique",
310
+ severity: "error",
311
+ run: async ({ tools }) => {
312
+ const seen = /* @__PURE__ */ new Set();
313
+ const dupes = /* @__PURE__ */ new Set();
314
+ for (const t of tools) {
315
+ if (seen.has(t.name)) dupes.add(t.name);
316
+ seen.add(t.name);
317
+ }
318
+ return dupes.size === 0 ? { status: "pass" } : { status: "fail", detail: "duplicate tool names", findings: [...dupes] };
319
+ }
320
+ },
321
+ {
322
+ id: "valid-tool-names",
323
+ title: "Tool names use a safe charset and length",
324
+ severity: "error",
325
+ run: async ({ tools }) => {
326
+ const bad = tools.filter((t) => !TOOL_NAME_RE.test(t.name)).map((t) => t.name);
327
+ return bad.length === 0 ? { status: "pass" } : { status: "fail", detail: "names must match /^[a-zA-Z0-9_-]{1,128}$/", findings: bad };
328
+ }
329
+ },
330
+ {
331
+ id: "tool-descriptions",
332
+ title: "Every tool has a description",
333
+ severity: "warning",
334
+ run: async ({ tools }) => {
335
+ const missing = tools.filter((t) => !t.description || !t.description.trim()).map((t) => t.name);
336
+ return missing.length === 0 ? { status: "pass" } : { status: "fail", detail: "an LLM picks tools by their description", findings: missing };
337
+ }
338
+ },
339
+ {
340
+ id: "description-quality",
341
+ title: "Descriptions are substantive (\u2265 12 chars)",
342
+ severity: "info",
343
+ run: async ({ tools }) => {
344
+ const thin = tools.filter((t) => t.description && t.description.trim().length > 0 && t.description.trim().length < 12).map((t) => `${t.name} ("${t.description.trim()}")`);
345
+ return thin.length === 0 ? { status: "pass" } : { status: "fail", detail: "very short descriptions hurt tool selection", findings: thin };
346
+ }
347
+ },
348
+ {
349
+ id: "tool-input-schema",
350
+ title: "Every tool declares an object inputSchema",
351
+ severity: "warning",
352
+ run: async ({ tools }) => {
353
+ const bad = tools.filter((t) => {
354
+ const s = t.inputSchema;
355
+ return !s || typeof s !== "object" || s.type !== "object";
356
+ }).map((t) => t.name);
357
+ return bad.length === 0 ? { status: "pass" } : { status: "fail", detail: 'inputSchema should be a JSON Schema with type:"object"', findings: bad };
358
+ }
359
+ },
360
+ {
361
+ id: "required-props-exist",
362
+ title: "Every `required` entry names a declared property",
363
+ severity: "error",
364
+ run: async ({ tools }) => {
365
+ const findings = [];
366
+ for (const t of tools) {
367
+ const s = t.inputSchema;
368
+ if (!s?.required || !s.properties) continue;
369
+ const props = new Set(Object.keys(s.properties));
370
+ for (const r of s.required) {
371
+ if (!props.has(r)) findings.push(`${t.name}: required "${r}" has no matching property`);
372
+ }
373
+ }
374
+ return findings.length === 0 ? { status: "pass" } : { status: "fail", findings };
375
+ }
376
+ },
377
+ {
378
+ id: "unknown-tool-handled",
379
+ title: "Calling an unknown tool fails gracefully (no crash/hang)",
380
+ severity: "error",
381
+ run: async ({ client, options }) => {
382
+ if (options.probeUnknownTool === false) {
383
+ return { status: "skip", detail: "probeUnknownTool disabled" };
384
+ }
385
+ const phantom = "__mcp_devkit_nonexistent_tool__";
386
+ try {
387
+ const res = await client.call(phantom, {}, { validate: false, timeoutMs: 5e3 });
388
+ return res.isError === true ? { status: "pass", detail: "returned isError result" } : { status: "fail", detail: "server returned a success result for a non-existent tool" };
389
+ } catch {
390
+ return { status: "pass", detail: "rejected with a protocol error" };
391
+ }
392
+ }
393
+ }
394
+ ];
395
+ async function conformanceCheck(client, options = {}) {
396
+ const skip = new Set(options.skip ?? []);
397
+ const tools = await client.tools();
398
+ const checks = [];
399
+ for (const check of CHECKS) {
400
+ if (skip.has(check.id)) {
401
+ checks.push({ id: check.id, title: check.title, severity: check.severity, status: "skip", detail: "skipped by caller" });
402
+ continue;
403
+ }
404
+ try {
405
+ const outcome = await check.run({ client, tools, options });
406
+ checks.push({ id: check.id, title: check.title, severity: check.severity, ...outcome });
407
+ } catch (e) {
408
+ checks.push({
409
+ id: check.id,
410
+ title: check.title,
411
+ severity: check.severity,
412
+ status: "fail",
413
+ detail: `check threw: ${e.message}`
414
+ });
415
+ }
416
+ }
417
+ const summary = {
418
+ passed: checks.filter((c2) => c2.status === "pass").length,
419
+ failed: checks.filter((c2) => c2.status === "fail").length,
420
+ warnings: checks.filter((c2) => c2.status === "fail" && c2.severity === "warning").length,
421
+ skipped: checks.filter((c2) => c2.status === "skip").length
422
+ };
423
+ const ok = !checks.some((c2) => c2.status === "fail" && c2.severity === "error");
424
+ return { ok, checks, summary };
425
+ }
426
+
427
+ // src/cli.ts
428
+ var useColor = process.stdout.isTTY && !process.env.NO_COLOR;
429
+ var c = {
430
+ dim: (s) => useColor ? `\x1B[2m${s}\x1B[0m` : s,
431
+ bold: (s) => useColor ? `\x1B[1m${s}\x1B[0m` : s,
432
+ green: (s) => useColor ? `\x1B[32m${s}\x1B[0m` : s,
433
+ yellow: (s) => useColor ? `\x1B[33m${s}\x1B[0m` : s,
434
+ red: (s) => useColor ? `\x1B[31m${s}\x1B[0m` : s,
435
+ cyan: (s) => useColor ? `\x1B[36m${s}\x1B[0m` : s
436
+ };
437
+ var USAGE = `${c.bold("mcp-devkit")} \u2014 testing & inspection toolkit for MCP servers
438
+
439
+ ${c.bold("Usage:")}
440
+ mcp-devkit inspect -- <command...>
441
+ mcp-devkit check -- <command...>
442
+ mcp-devkit call --tool <name> [--args <json>] [--no-validate] -- <command...>
443
+
444
+ ${c.bold("Examples:")}
445
+ mcp-devkit inspect -- node dist/server.js
446
+ mcp-devkit check -- npx -y @modelcontextprotocol/server-everything
447
+ mcp-devkit call --tool search --args '{"q":"mcp"}' -- node dist/server.js
448
+
449
+ ${c.bold("Options:")}
450
+ --tool <name> tool to call (call command)
451
+ --args <json> JSON arguments object for the tool (default {})
452
+ --no-validate skip client-side inputSchema validation on call
453
+ --json machine-readable JSON output (inspect / check)
454
+ --timeout <ms> per-request timeout (default 15000)
455
+ -h, --help show this help
456
+
457
+ Everything after ${c.bold("--")} is the MCP server command to launch over stdio.
458
+ `;
459
+ function splitOnDoubleDash(argv) {
460
+ const i = argv.indexOf("--");
461
+ if (i === -1) return { flags: argv, command: [] };
462
+ return { flags: argv.slice(0, i), command: argv.slice(i + 1) };
463
+ }
464
+ async function main() {
465
+ const rawArgv = process.argv.slice(2);
466
+ const sub = rawArgv[0];
467
+ if (!sub || sub === "-h" || sub === "--help" || sub === "help") {
468
+ process.stdout.write(USAGE);
469
+ return sub ? 0 : 1;
470
+ }
471
+ const { flags, command } = splitOnDoubleDash(rawArgv.slice(1));
472
+ const { values } = util.parseArgs({
473
+ args: flags,
474
+ options: {
475
+ tool: { type: "string" },
476
+ args: { type: "string" },
477
+ "no-validate": { type: "boolean", default: false },
478
+ json: { type: "boolean", default: false },
479
+ timeout: { type: "string" },
480
+ help: { type: "boolean", short: "h", default: false }
481
+ },
482
+ allowPositionals: true
483
+ });
484
+ if (values.help) {
485
+ process.stdout.write(USAGE);
486
+ return 0;
487
+ }
488
+ if (command.length === 0) {
489
+ process.stderr.write(c.red("error: no server command given. Put it after `--`, e.g. `-- node dist/server.js`\n"));
490
+ return 2;
491
+ }
492
+ const timeoutMs = values.timeout ? Number(values.timeout) : 15e3;
493
+ const [cmd, ...cmdArgs] = command;
494
+ const client = await createTestClient(
495
+ { command: cmd, args: cmdArgs },
496
+ { timeoutMs }
497
+ );
498
+ try {
499
+ switch (sub) {
500
+ case "inspect":
501
+ return await runInspect(client, !!values.json);
502
+ case "check":
503
+ return await runCheck(client, !!values.json);
504
+ case "call":
505
+ return await runCall(client, {
506
+ tool: values.tool,
507
+ args: values.args,
508
+ validate: !values["no-validate"]
509
+ });
510
+ default:
511
+ process.stderr.write(c.red(`error: unknown command "${sub}"
512
+
513
+ `));
514
+ process.stdout.write(USAGE);
515
+ return 2;
516
+ }
517
+ } finally {
518
+ await client.close().catch(() => {
519
+ });
520
+ }
521
+ }
522
+ async function runInspect(client, json) {
523
+ const [tools, resources, prompts] = await Promise.all([
524
+ client.tools(),
525
+ client.resources(),
526
+ client.prompts()
527
+ ]);
528
+ const info = client.serverInfo();
529
+ if (json) {
530
+ process.stdout.write(JSON.stringify({ server: info, tools, resources, prompts }, null, 2) + "\n");
531
+ return 0;
532
+ }
533
+ process.stdout.write(
534
+ `${c.bold(info?.name ?? "(unnamed server)")} ${c.dim("v" + (info?.version ?? "?"))}
535
+
536
+ `
537
+ );
538
+ process.stdout.write(c.bold(`Tools (${tools.length})
539
+ `));
540
+ for (const t of tools) {
541
+ process.stdout.write(` ${c.cyan(t.name)}${t.description ? " " + c.dim(t.description) : ""}
542
+ `);
543
+ const props = t.inputSchema?.properties;
544
+ if (props && Object.keys(props).length > 0) {
545
+ const req = new Set(t.inputSchema?.required ?? []);
546
+ for (const [name, sub] of Object.entries(props)) {
547
+ const type = Array.isArray(sub.type) ? sub.type.join("|") : sub.type ?? "any";
548
+ process.stdout.write(` ${name}${req.has(name) ? "*" : ""}: ${c.dim(String(type))}
549
+ `);
550
+ }
551
+ }
552
+ }
553
+ if (resources.length > 0) {
554
+ process.stdout.write("\n" + c.bold(`Resources (${resources.length})
555
+ `));
556
+ for (const r of resources) {
557
+ process.stdout.write(` ${c.cyan(r.uri)}${r.name ? " " + c.dim(r.name) : ""}
558
+ `);
559
+ }
560
+ }
561
+ if (prompts.length > 0) {
562
+ process.stdout.write("\n" + c.bold(`Prompts (${prompts.length})
563
+ `));
564
+ for (const p of prompts) {
565
+ process.stdout.write(` ${c.cyan(p.name)}${p.description ? " " + c.dim(p.description) : ""}
566
+ `);
567
+ }
568
+ }
569
+ return 0;
570
+ }
571
+ async function runCheck(client, json) {
572
+ const report = await conformanceCheck(client);
573
+ if (json) {
574
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
575
+ return report.ok ? 0 : 1;
576
+ }
577
+ const mark = (r) => r.status === "pass" ? c.green("\u2713") : r.status === "skip" ? c.dim("\u2013") : r.severity === "error" ? c.red("\u2717") : c.yellow("!");
578
+ for (const r of report.checks) {
579
+ process.stdout.write(`${mark(r)} ${r.title}${r.detail ? c.dim(" \u2014 " + r.detail) : ""}
580
+ `);
581
+ for (const f of r.findings ?? []) {
582
+ process.stdout.write(` ${c.dim("\u2022")} ${f}
583
+ `);
584
+ }
585
+ }
586
+ const { passed, failed, skipped } = report.summary;
587
+ process.stdout.write(
588
+ `
589
+ ${report.ok ? c.green("PASS") : c.red("FAIL")} ${passed} passed, ${failed} failed, ${skipped} skipped
590
+ `
591
+ );
592
+ return report.ok ? 0 : 1;
593
+ }
594
+ async function runCall(client, opts) {
595
+ if (!opts.tool) {
596
+ process.stderr.write(c.red("error: --tool <name> is required for `call`\n"));
597
+ return 2;
598
+ }
599
+ let args = {};
600
+ if (opts.args) {
601
+ try {
602
+ args = JSON.parse(opts.args);
603
+ } catch (e) {
604
+ process.stderr.write(c.red(`error: --args is not valid JSON: ${e.message}
605
+ `));
606
+ return 2;
607
+ }
608
+ }
609
+ const res = await client.call(opts.tool, args, { validate: opts.validate });
610
+ process.stdout.write(JSON.stringify(res, null, 2) + "\n");
611
+ return res.isError ? 1 : 0;
612
+ }
613
+ main().then((code) => process.exit(code)).catch((err) => {
614
+ process.stderr.write(`${c.red("error:")} ${err?.message ?? err}
615
+ `);
616
+ process.exit(1);
617
+ });
618
+ //# sourceMappingURL=cli.cjs.map
619
+ //# sourceMappingURL=cli.cjs.map