@tronsfey/ucli 0.4.1 → 0.4.3

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/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-NNFC34A5.js";
2
+ import { createRequire as __createRequire } from "module";
3
+ const require = __createRequire(import.meta.url);
4
+ import "./chunk-FJU3QOHW.js";
3
5
 
4
6
  // src/index.ts
5
7
  import { Command } from "commander";
@@ -8,7 +10,61 @@ import { createRequire as createRequire2 } from "module";
8
10
  // src/config.ts
9
11
  import Conf from "conf";
10
12
  import { homedir } from "os";
11
- import { join } from "path";
13
+ import { join, dirname } from "path";
14
+ import { chmodSync, mkdirSync } from "fs";
15
+
16
+ // src/lib/config-encryption.ts
17
+ import {
18
+ createCipheriv,
19
+ createDecipheriv,
20
+ randomBytes,
21
+ pbkdf2Sync
22
+ } from "crypto";
23
+ import { hostname, userInfo } from "os";
24
+ var ENC_PREFIX = "enc:v1:";
25
+ var ALGORITHM = "aes-256-gcm";
26
+ var SALT_LEN = 32;
27
+ var IV_LEN = 12;
28
+ var TAG_LEN = 16;
29
+ var PBKDF2_ITERATIONS = 1e5;
30
+ function deriveKey(salt) {
31
+ let user = "default";
32
+ try {
33
+ user = userInfo().username;
34
+ } catch {
35
+ }
36
+ const material = `ucli:${user}@${hostname()}`;
37
+ return pbkdf2Sync(material, salt, PBKDF2_ITERATIONS, 32, "sha512");
38
+ }
39
+ function encryptValue(plaintext) {
40
+ const salt = randomBytes(SALT_LEN);
41
+ const key = deriveKey(salt);
42
+ const iv = randomBytes(IV_LEN);
43
+ const cipher = createCipheriv(ALGORITHM, key, iv);
44
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
45
+ const tag = cipher.getAuthTag();
46
+ const packed = Buffer.concat([salt, iv, tag, encrypted]);
47
+ return ENC_PREFIX + packed.toString("base64");
48
+ }
49
+ function decryptValue(stored) {
50
+ if (!stored.startsWith(ENC_PREFIX)) {
51
+ return stored;
52
+ }
53
+ const packed = Buffer.from(stored.slice(ENC_PREFIX.length), "base64");
54
+ const salt = packed.subarray(0, SALT_LEN);
55
+ const iv = packed.subarray(SALT_LEN, SALT_LEN + IV_LEN);
56
+ const tag = packed.subarray(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN);
57
+ const encrypted = packed.subarray(SALT_LEN + IV_LEN + TAG_LEN);
58
+ const key = deriveKey(salt);
59
+ const decipher = createDecipheriv(ALGORITHM, key, iv);
60
+ decipher.setAuthTag(tag);
61
+ return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
62
+ }
63
+ function isEncrypted(value) {
64
+ return value.startsWith(ENC_PREFIX);
65
+ }
66
+
67
+ // src/config.ts
12
68
  var conf = new Conf({
13
69
  projectName: "ucli",
14
70
  schema: {
@@ -17,18 +73,35 @@ var conf = new Conf({
17
73
  }
18
74
  });
19
75
  var cacheDir = join(homedir(), ".cache", "ucli");
76
+ function hardenConfigPermissions() {
77
+ try {
78
+ const configPath = conf.path;
79
+ const configDir = dirname(configPath);
80
+ mkdirSync(configDir, { recursive: true, mode: 448 });
81
+ chmodSync(configDir, 448);
82
+ chmodSync(configPath, 384);
83
+ } catch {
84
+ console.warn("Warning: Could not enforce restrictive file permissions on config. Token is encrypted but file permissions may be permissive.");
85
+ }
86
+ }
20
87
  function getConfig() {
21
88
  const serverUrl = conf.get("serverUrl");
22
- const token = conf.get("token");
23
- if (!serverUrl || !token) {
89
+ const rawToken = conf.get("token");
90
+ if (!serverUrl || !rawToken) {
24
91
  console.error("ucli is not configured. Run: ucli configure --server <url> --token <jwt>");
25
92
  process.exit(1);
26
93
  }
94
+ const token = decryptValue(rawToken);
95
+ if (!isEncrypted(rawToken)) {
96
+ conf.set("token", encryptValue(token));
97
+ hardenConfigPermissions();
98
+ }
27
99
  return { serverUrl, token };
28
100
  }
29
101
  function saveConfig(cfg) {
30
102
  conf.set("serverUrl", cfg.serverUrl);
31
- conf.set("token", cfg.token);
103
+ conf.set("token", encryptValue(cfg.token));
104
+ hardenConfigPermissions();
32
105
  }
33
106
  function isConfigured() {
34
107
  return Boolean(conf.get("serverUrl") && conf.get("token"));
@@ -103,11 +176,17 @@ function registerConfigure(program2) {
103
176
  }
104
177
 
105
178
  // src/lib/cache.ts
106
- import { readFile, writeFile, mkdir } from "fs/promises";
179
+ import { readFile, writeFile, mkdir, unlink, chmod } from "fs/promises";
107
180
  import { join as join2 } from "path";
108
181
  var LIST_CACHE_FILE = join2(cacheDir, "oas-list.json");
109
182
  async function ensureCacheDir() {
110
- await mkdir(cacheDir, { recursive: true });
183
+ await mkdir(cacheDir, { recursive: true, mode: 448 });
184
+ }
185
+ function redactEntries(entries) {
186
+ return entries.map((e) => ({
187
+ ...e,
188
+ authConfig: { type: e.authConfig["type"] ?? e.authType }
189
+ }));
111
190
  }
112
191
  async function readOASListCache() {
113
192
  try {
@@ -122,25 +201,45 @@ async function readOASListCache() {
122
201
  }
123
202
  async function writeOASListCache(entries, ttlSec) {
124
203
  await ensureCacheDir();
125
- const cached = { entries, fetchedAt: Date.now(), ttlSec };
126
- await writeFile(LIST_CACHE_FILE, JSON.stringify(cached, null, 2), "utf8");
204
+ const cached = { entries: redactEntries(entries), fetchedAt: Date.now(), ttlSec };
205
+ await writeFile(LIST_CACHE_FILE, JSON.stringify(cached, null, 2), { encoding: "utf8", mode: 384 });
206
+ await chmod(LIST_CACHE_FILE, 384);
127
207
  }
128
208
  async function clearOASListCache() {
129
209
  try {
130
- await writeFile(LIST_CACHE_FILE, "{}", "utf8");
210
+ await unlink(LIST_CACHE_FILE);
131
211
  } catch {
132
212
  }
133
213
  }
214
+ async function clearOASCache(name) {
215
+ try {
216
+ const raw = await readFile(LIST_CACHE_FILE, "utf8");
217
+ const cached = JSON.parse(raw);
218
+ const entries = Array.isArray(cached.entries) ? cached.entries.filter((e) => e.name !== name) : [];
219
+ if (entries.length === 0) {
220
+ await clearOASListCache();
221
+ return;
222
+ }
223
+ const next = {
224
+ entries,
225
+ fetchedAt: cached.fetchedAt ?? Date.now(),
226
+ ttlSec: cached.ttlSec ?? 0
227
+ };
228
+ await writeFile(LIST_CACHE_FILE, JSON.stringify(next, null, 2), { encoding: "utf8", mode: 384 });
229
+ } catch {
230
+ await clearOASListCache();
231
+ }
232
+ }
134
233
 
135
234
  // src/lib/oas-runner.ts
136
235
  import { spawn } from "child_process";
137
236
  import { createRequire } from "module";
138
- import { join as join3, dirname } from "path";
237
+ import { join as join3, dirname as dirname2 } from "path";
139
238
  var require2 = createRequire(import.meta.url);
140
239
  function resolveOpenapi2CliBin() {
141
240
  try {
142
241
  const pkgPath = require2.resolve("@tronsfey/openapi2cli/package.json");
143
- const pkgDir = dirname(pkgPath);
242
+ const pkgDir = dirname2(pkgPath);
144
243
  const pkg2 = require2("@tronsfey/openapi2cli/package.json");
145
244
  const binEntry = pkg2.bin?.["openapi2cli"] ?? "bin/openapi2cli.js";
146
245
  return join3(pkgDir, binEntry);
@@ -170,6 +269,58 @@ function buildAuthEnv(entry) {
170
269
  return {};
171
270
  }
172
271
  }
272
+ var SAFE_ENV_KEYS = [
273
+ // System essentials
274
+ "PATH",
275
+ "HOME",
276
+ "USER",
277
+ "LOGNAME",
278
+ "SHELL",
279
+ "TMPDIR",
280
+ "TMP",
281
+ "TEMP",
282
+ // Terminal / display
283
+ "TERM",
284
+ "COLORTERM",
285
+ "NO_COLOR",
286
+ "FORCE_COLOR",
287
+ "LANG",
288
+ "LC_ALL",
289
+ "LC_CTYPE",
290
+ "LC_MESSAGES",
291
+ "LC_COLLATE",
292
+ // Node.js
293
+ "NODE_ENV",
294
+ "NODE_PATH",
295
+ "NODE_OPTIONS",
296
+ "NODE_EXTRA_CA_CERTS",
297
+ // Network proxy (required for tools behind corporate proxies)
298
+ "HTTP_PROXY",
299
+ "HTTPS_PROXY",
300
+ "NO_PROXY",
301
+ "http_proxy",
302
+ "https_proxy",
303
+ "no_proxy",
304
+ // OS-specific
305
+ "SYSTEMROOT",
306
+ "COMSPEC",
307
+ "APPDATA",
308
+ "LOCALAPPDATA",
309
+ "PROGRAMFILES",
310
+ "XDG_RUNTIME_DIR",
311
+ "XDG_CONFIG_HOME",
312
+ "XDG_CACHE_HOME",
313
+ "XDG_DATA_HOME"
314
+ ];
315
+ function buildSafeEnv(authEnv) {
316
+ const safe = {};
317
+ for (const key of SAFE_ENV_KEYS) {
318
+ if (process.env[key] !== void 0) {
319
+ safe[key] = process.env[key];
320
+ }
321
+ }
322
+ return { ...safe, ...authEnv };
323
+ }
173
324
  async function runOperation(opts) {
174
325
  const bin = resolveOpenapi2CliBin();
175
326
  const { entry, operationArgs, format, query } = opts;
@@ -185,17 +336,13 @@ async function runOperation(opts) {
185
336
  ...operationArgs
186
337
  ];
187
338
  const authEnv = buildAuthEnv(entry);
188
- await new Promise((resolve, reject) => {
339
+ await new Promise((resolve2, reject) => {
189
340
  const child = spawn(process.execPath, [bin, ...args], {
190
341
  stdio: "inherit",
191
- env: {
192
- ...process.env,
193
- ...authEnv
194
- // inject auth — not visible to parent shell
195
- }
342
+ env: buildSafeEnv(authEnv)
196
343
  });
197
344
  child.on("close", (code) => {
198
- if (code === 0) resolve();
345
+ if (code === 0) resolve2();
199
346
  else reject(new Error(`openapi2cli exited with code ${code}`));
200
347
  });
201
348
  child.on("error", reject);
@@ -211,7 +358,7 @@ async function getServiceHelp(entry) {
211
358
  String(entry.cacheTtl),
212
359
  "--help"
213
360
  ];
214
- return new Promise((resolve, reject) => {
361
+ return new Promise((resolve2, reject) => {
215
362
  let output = "";
216
363
  const child = spawn(process.execPath, [bin, ...args], {
217
364
  stdio: ["ignore", "pipe", "pipe"]
@@ -222,7 +369,7 @@ async function getServiceHelp(entry) {
222
369
  child.stderr?.on("data", (d) => {
223
370
  output += d.toString();
224
371
  });
225
- child.on("close", () => resolve(output));
372
+ child.on("close", () => resolve2(output));
226
373
  child.on("error", reject);
227
374
  });
228
375
  }
@@ -230,10 +377,14 @@ async function getServiceHelp(entry) {
230
377
  // src/commands/services.ts
231
378
  function registerServices(program2) {
232
379
  const services = program2.command("services").description("Manage and inspect available OAS services");
233
- services.command("list").description("List all OAS services available in the current group").option("--no-cache", "Bypass local cache and fetch fresh from server").action(async (opts) => {
380
+ services.command("list").description("List all OAS services available in the current group").option("--refresh", "Bypass local cache and fetch fresh from server").option("--format <fmt>", "Output format: table | json | yaml", "table").option("--no-cache", "Bypass local cache and fetch fresh from server").action(async (opts) => {
234
381
  const cfg = getConfig();
235
382
  const client = new ServerClient(cfg);
236
- let entries = opts.cache ? await readOASListCache() : null;
383
+ if (!opts.cache) {
384
+ process.emitWarning("The --no-cache flag is deprecated. Please use --refresh instead.");
385
+ }
386
+ const useCache = opts.cache && !opts.refresh;
387
+ let entries = useCache ? await readOASListCache() : null;
237
388
  if (!entries) {
238
389
  entries = await client.listOAS();
239
390
  if (entries.length > 0) {
@@ -241,10 +392,19 @@ function registerServices(program2) {
241
392
  await writeOASListCache(entries, maxTtl);
242
393
  }
243
394
  }
395
+ const format = (opts.format ?? "table").toLowerCase();
244
396
  if (entries.length === 0) {
245
397
  console.log("No services registered in this group.");
246
398
  return;
247
399
  }
400
+ if (format === "json") {
401
+ const safe = entries.map(({ authConfig, ...rest }) => ({
402
+ ...rest,
403
+ authConfig: { type: authConfig["type"] ?? rest.authType }
404
+ }));
405
+ console.log(JSON.stringify(safe, null, 2));
406
+ return;
407
+ }
248
408
  const nameWidth = Math.max(10, ...entries.map((e) => e.name.length));
249
409
  console.log(`
250
410
  ${"SERVICE".padEnd(nameWidth)} AUTH DESCRIPTION`);
@@ -256,7 +416,7 @@ ${"SERVICE".padEnd(nameWidth)} AUTH DESCRIPTION`);
256
416
  }
257
417
  console.log();
258
418
  });
259
- services.command("info <name>").description("Show detailed information and available operations for a service").action(async (name) => {
419
+ services.command("info <name>").description("Show detailed information and available operations for a service").option("--format <fmt>", "Output format: table | json | yaml", "table").action(async (name, opts) => {
260
420
  const cfg = getConfig();
261
421
  const client = new ServerClient(cfg);
262
422
  let entry;
@@ -267,6 +427,18 @@ ${"SERVICE".padEnd(nameWidth)} AUTH DESCRIPTION`);
267
427
  console.error("Run `ucli services list` to see available services.");
268
428
  process.exit(1);
269
429
  }
430
+ const help = await getServiceHelp(entry);
431
+ const format = (opts.format ?? "table").toLowerCase();
432
+ if (format === "json") {
433
+ const { authConfig, ...rest } = entry;
434
+ const safe = {
435
+ ...rest,
436
+ authConfig: { type: authConfig["type"] ?? rest.authType },
437
+ operationsHelp: help
438
+ };
439
+ console.log(JSON.stringify(safe, null, 2));
440
+ return;
441
+ }
270
442
  console.log(`
271
443
  Service: ${entry.name}`);
272
444
  console.log(`Description: ${entry.description || "(none)"}`);
@@ -276,14 +448,22 @@ Service: ${entry.name}`);
276
448
  console.log(`Cache TTL: ${entry.cacheTtl}s`);
277
449
  console.log("\nAvailable operations:");
278
450
  console.log("\u2500".repeat(60));
279
- const help = await getServiceHelp(entry);
280
451
  console.log(help);
281
452
  });
282
453
  }
283
454
 
284
455
  // src/commands/run.ts
285
456
  function registerRun(program2) {
286
- program2.command("run <service> [args...]").description("Execute an operation on a service").option("--format <fmt>", "Output format: json | table | yaml", "json").option("--query <jmespath>", "Filter response with JMESPath expression").option("--data <json>", "Request body (JSON string or @filename)").allowUnknownOption(true).action(async (service, args, opts) => {
457
+ program2.command("run [service] [args...]").description("Execute an operation on a service").option("--service <name>", "Service name (from `services list`)").option("--operation <id>", "OperationId to execute").option("--params <json>", "JSON string of operation parameters").option("--format <fmt>", "Output format: json | table | yaml", "json").option("--query <jmespath>", "Filter response with JMESPath expression").option("--data <json>", "Request body (JSON string or @filename)").allowUnknownOption(true).action(async (serviceArg, args, opts) => {
458
+ if (serviceArg && opts.service && serviceArg !== opts.service) {
459
+ console.error(`Conflicting service values: positional "${serviceArg}" and --service "${opts.service}". Use either the positional argument or --service flag, not both.`);
460
+ process.exit(1);
461
+ }
462
+ const service = opts.service ?? serviceArg;
463
+ if (!service) {
464
+ console.error("Missing service name. Use positional <service> or --service <name>.");
465
+ process.exit(1);
466
+ }
287
467
  const cfg = getConfig();
288
468
  const client = new ServerClient(cfg);
289
469
  let entry;
@@ -295,11 +475,29 @@ function registerRun(program2) {
295
475
  process.exit(1);
296
476
  }
297
477
  const extraArgs = opts.args ?? [];
298
- const operationArgs = [
299
- ...args,
300
- ...extraArgs,
301
- ...opts.data ? ["--data", opts.data] : []
302
- ];
478
+ const operationArgs = [...args, ...extraArgs];
479
+ if (opts.operation) {
480
+ operationArgs.unshift(opts.operation);
481
+ }
482
+ if (opts.params) {
483
+ let parsed;
484
+ try {
485
+ parsed = JSON.parse(opts.params);
486
+ } catch {
487
+ console.error(`Invalid --params JSON. Example: --params '{"petId": 1}'`);
488
+ process.exit(1);
489
+ }
490
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
491
+ for (const [k, v] of Object.entries(parsed)) {
492
+ if (v === void 0 || v === null) continue;
493
+ const strVal = typeof v === "object" ? JSON.stringify(v) : String(v);
494
+ operationArgs.push(`--${k}`, strVal);
495
+ }
496
+ }
497
+ }
498
+ if (opts.data) {
499
+ operationArgs.push("--data", opts.data);
500
+ }
303
501
  const format = opts.format;
304
502
  const query = opts.query;
305
503
  try {
@@ -318,11 +516,15 @@ function registerRun(program2) {
318
516
 
319
517
  // src/commands/refresh.ts
320
518
  function registerRefresh(program2) {
321
- program2.command("refresh").description("Force-refresh the local OAS cache from the server").action(async () => {
519
+ program2.command("refresh").description("Force-refresh the local OAS cache from the server").option("--service <name>", "Refresh only a specific service").action(async (opts) => {
322
520
  const cfg = getConfig();
323
521
  const client = new ServerClient(cfg);
324
522
  console.log("Refreshing OAS list from server...");
325
- await clearOASListCache();
523
+ if (opts.service) {
524
+ await clearOASCache(opts.service);
525
+ } else {
526
+ await clearOASListCache();
527
+ }
326
528
  const entries = await client.listOAS();
327
529
  if (entries.length > 0) {
328
530
  const maxTtl = Math.min(...entries.map((e) => e.cacheTtl));
@@ -427,10 +629,24 @@ ERRORS
427
629
  }
428
630
 
429
631
  // src/lib/mcp-runner.ts
632
+ function resolve(mod, name) {
633
+ if (typeof mod[name] === "function") return mod[name];
634
+ if (mod.default && typeof mod.default[name] === "function") return mod.default[name];
635
+ throw new Error(`Cannot resolve export "${name}" from module`);
636
+ }
430
637
  async function getMcp2cli() {
431
- const clientMod = await import("./client-O6QINOP2.js");
432
- const runnerMod = await import("./runner-VTUGJUH7.js");
433
- return { createMcpClient: clientMod.createMcpClient, getTools: runnerMod.getTools, runTool: runnerMod.runTool };
638
+ const clientMod = await import("./client-3I7XBDJU.js");
639
+ const runnerMod = await import("./runner-GVYIJNHN.js");
640
+ return {
641
+ createMcpClient: resolve(clientMod, "createMcpClient"),
642
+ getTools: resolve(runnerMod, "getTools"),
643
+ runTool: resolve(runnerMod, "runTool")
644
+ };
645
+ }
646
+ async function closeClient(client) {
647
+ if (typeof client.close === "function") {
648
+ await client.close();
649
+ }
434
650
  }
435
651
  function buildMcpConfig(entry) {
436
652
  const base = { type: entry.transport };
@@ -451,23 +667,42 @@ async function listMcpTools(entry) {
451
667
  const { createMcpClient, getTools } = await getMcp2cli();
452
668
  const config = buildMcpConfig(entry);
453
669
  const client = await createMcpClient(config);
454
- const tools = await getTools(client, config, { noCache: true });
455
- return tools;
670
+ try {
671
+ const tools = await getTools(client, config, { noCache: true });
672
+ return tools;
673
+ } finally {
674
+ await closeClient(client);
675
+ }
456
676
  }
457
677
  async function runMcpTool(entry, toolName, rawArgs) {
458
678
  const { createMcpClient, getTools, runTool } = await getMcp2cli();
459
679
  const config = buildMcpConfig(entry);
460
680
  const client = await createMcpClient(config);
461
- const tools = await getTools(client, config, { noCache: false, cacheTtl: 3600 });
462
- const tool = tools.find((t) => t.name === toolName);
463
- if (!tool) throw new Error(`Tool "${toolName}" not found on MCP server "${entry.name}"`);
464
- await runTool(client, tool, rawArgs, {});
681
+ try {
682
+ const tools = await getTools(client, config, { noCache: false, cacheTtl: 3600 });
683
+ const tool = tools.find((t) => t.name === toolName);
684
+ if (!tool) throw new Error(`Tool "${toolName}" not found on MCP server "${entry.name}"`);
685
+ const normalizedArgs = [];
686
+ for (const arg of rawArgs) {
687
+ if (arg.includes("=") && !arg.startsWith("--")) {
688
+ const idx = arg.indexOf("=");
689
+ const key = arg.slice(0, idx);
690
+ const value = arg.slice(idx + 1);
691
+ normalizedArgs.push(`--${key}`, value);
692
+ } else {
693
+ normalizedArgs.push(arg);
694
+ }
695
+ }
696
+ await runTool(client, tool, normalizedArgs, {});
697
+ } finally {
698
+ await closeClient(client);
699
+ }
465
700
  }
466
701
 
467
702
  // src/commands/mcp.ts
468
703
  function registerMcp(program2) {
469
704
  const mcp = program2.command("mcp").description("Interact with MCP servers registered in your group");
470
- mcp.command("list").description("List all MCP servers available in the current group").action(async () => {
705
+ mcp.command("list").description("List all MCP servers available in the current group").option("--format <fmt>", "Output format: table | json | yaml", "table").action(async (opts) => {
471
706
  const cfg = getConfig();
472
707
  const client = new ServerClient(cfg);
473
708
  const entries = await client.listMCP();
@@ -475,6 +710,15 @@ function registerMcp(program2) {
475
710
  console.log("No MCP servers registered in this group.");
476
711
  return;
477
712
  }
713
+ const format = (opts.format ?? "table").toLowerCase();
714
+ if (format === "json") {
715
+ const safe = entries.map(({ authConfig, ...rest }) => ({
716
+ ...rest,
717
+ authConfig: { type: authConfig.type }
718
+ }));
719
+ console.log(JSON.stringify(safe, null, 2));
720
+ return;
721
+ }
478
722
  const nameWidth = Math.max(10, ...entries.map((e) => e.name.length));
479
723
  console.log(`
480
724
  ${"SERVER".padEnd(nameWidth)} TRANSPORT DESCRIPTION`);
@@ -485,7 +729,7 @@ ${"SERVER".padEnd(nameWidth)} TRANSPORT DESCRIPTION`);
485
729
  }
486
730
  console.log();
487
731
  });
488
- mcp.command("tools <server>").description("List tools available on a MCP server").action(async (serverName) => {
732
+ mcp.command("tools <server>").description("List tools available on a MCP server").option("--format <fmt>", "Output format: table | json", "table").action(async (serverName, opts) => {
489
733
  const cfg = getConfig();
490
734
  const client = new ServerClient(cfg);
491
735
  let entry;
@@ -507,6 +751,11 @@ ${"SERVER".padEnd(nameWidth)} TRANSPORT DESCRIPTION`);
507
751
  console.log(`No tools found on MCP server "${serverName}".`);
508
752
  return;
509
753
  }
754
+ const format = (opts.format ?? "table").toLowerCase();
755
+ if (format === "json") {
756
+ console.log(JSON.stringify(tools, null, 2));
757
+ return;
758
+ }
510
759
  console.log(`
511
760
  Tools on "${serverName}":`);
512
761
  console.log("\u2500".repeat(60));