@evantahler/mcpx 0.15.4 → 0.15.9

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.
@@ -3,6 +3,8 @@ import type { Tool, Resource, Prompt } from "../config/schemas.ts";
3
3
  import type { ToolWithServer, ResourceWithServer, PromptWithServer } from "../client/manager.ts";
4
4
  import type { ValidationError } from "../validation/schema.ts";
5
5
  import type { SearchResult } from "../search/index.ts";
6
+ import { formatOutput } from "./format-output.ts";
7
+ import { formatTable } from "./format-table.ts";
6
8
 
7
9
  export interface FormatOptions {
8
10
  json?: boolean;
@@ -109,133 +111,104 @@ const KNOWN_CAPABILITIES = ["tools", "resources", "prompts", "logging", "complet
109
111
 
110
112
  /** Format a full server overview (version, capabilities, tools, counts) */
111
113
  export function formatServerOverview(overview: ServerOverview, options: FormatOptions): string {
112
- if (!isInteractive(options)) {
113
- return JSON.stringify(
114
- {
115
- server: overview.serverName,
116
- version: overview.version ?? null,
117
- capabilities: overview.capabilities ?? null,
118
- instructions: overview.instructions ?? null,
119
- tools: overview.tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
120
- resourceCount: overview.resourceCount,
121
- promptCount: overview.promptCount,
122
- },
123
- null,
124
- 2,
125
- );
126
- }
127
-
128
- const lines: string[] = [];
129
-
130
- // Header: server name + version
131
- const header = cyan.bold(overview.serverName);
132
- if (overview.version) {
133
- lines.push(
134
- `${header} ${dim(`v${overview.version.version}`)} ${dim(`(${overview.version.name})`)}`,
135
- );
136
- } else {
137
- lines.push(header);
138
- }
114
+ return formatOutput(
115
+ {
116
+ server: overview.serverName,
117
+ version: overview.version ?? null,
118
+ capabilities: overview.capabilities ?? null,
119
+ instructions: overview.instructions ?? null,
120
+ tools: overview.tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
121
+ resourceCount: overview.resourceCount,
122
+ promptCount: overview.promptCount,
123
+ },
124
+ () => {
125
+ const lines: string[] = [];
126
+
127
+ // Header: server name + version
128
+ const header = cyan.bold(overview.serverName);
129
+ if (overview.version) {
130
+ lines.push(
131
+ `${header} ${dim(`v${overview.version.version}`)} ${dim(`(${overview.version.name})`)}`,
132
+ );
133
+ } else {
134
+ lines.push(header);
135
+ }
139
136
 
140
- // Capabilities
141
- if (overview.capabilities) {
142
- lines.push("");
143
- lines.push(bold("Capabilities:"));
144
- const caps = overview.capabilities;
145
- const present = KNOWN_CAPABILITIES.filter((k) => k in caps);
146
- const absent = KNOWN_CAPABILITIES.filter((k) => !(k in caps));
147
- const capLines: string[] = [];
148
- for (const k of present) capLines.push(` ${green("")} ${k}`);
149
- for (const k of absent) capLines.push(` ${dim("✗")} ${dim(k)}`);
150
- lines.push(...capLines);
151
- }
137
+ // Capabilities
138
+ if (overview.capabilities) {
139
+ lines.push("");
140
+ lines.push(bold("Capabilities:"));
141
+ const caps = overview.capabilities;
142
+ const present = KNOWN_CAPABILITIES.filter((k) => k in caps);
143
+ const absent = KNOWN_CAPABILITIES.filter((k) => !(k in caps));
144
+ for (const k of present) lines.push(` ${green("✓")} ${k}`);
145
+ for (const k of absent) lines.push(` ${dim("")} ${dim(k)}`);
146
+ }
152
147
 
153
- // Instructions
154
- if (overview.instructions) {
155
- lines.push("");
156
- lines.push(bold("Instructions:"));
157
- lines.push(` ${dim(overview.instructions)}`);
158
- }
148
+ // Instructions
149
+ if (overview.instructions) {
150
+ lines.push("");
151
+ lines.push(bold("Instructions:"));
152
+ lines.push(` ${dim(overview.instructions)}`);
153
+ }
159
154
 
160
- // Tools
161
- lines.push("");
162
- if (overview.tools.length === 0) {
163
- lines.push(bold("Tools:") + " " + dim("none"));
164
- } else {
165
- lines.push(bold(`Tools (${overview.tools.length}):`));
166
- const maxName = Math.max(...overview.tools.map((t) => t.name.length));
167
- const termWidth = getTerminalWidth();
168
- for (let i = 0; i < overview.tools.length; i++) {
169
- const t = overview.tools[i];
170
- if (i > 0) lines.push("");
171
- const name = ` ${bold(t.name.padEnd(maxName))}`;
172
- if (t.description) {
173
- const pw = visibleLength(name) + 2;
174
- const desc =
175
- termWidth != null ? wrapDescription(t.description, pw, termWidth) : dim(t.description);
176
- lines.push(`${name} ${desc}`);
155
+ // Tools
156
+ lines.push("");
157
+ if (overview.tools.length === 0) {
158
+ lines.push(bold("Tools:") + " " + dim("none"));
177
159
  } else {
178
- lines.push(name);
160
+ lines.push(bold(`Tools (${overview.tools.length}):`));
161
+ const maxName = Math.max(...overview.tools.map((t) => t.name.length));
162
+ const termWidth = getTerminalWidth();
163
+ for (let i = 0; i < overview.tools.length; i++) {
164
+ const t = overview.tools[i];
165
+ if (i > 0) lines.push("");
166
+ const name = ` ${bold(t.name.padEnd(maxName))}`;
167
+ if (t.description) {
168
+ const pw = visibleLength(name) + 2;
169
+ const desc =
170
+ termWidth != null
171
+ ? wrapDescription(t.description, pw, termWidth)
172
+ : dim(t.description);
173
+ lines.push(`${name} ${desc}`);
174
+ } else {
175
+ lines.push(name);
176
+ }
177
+ }
179
178
  }
180
- }
181
- }
182
-
183
- // Resource/prompt counts
184
- const counts: string[] = [];
185
- counts.push(`Resources: ${overview.resourceCount}`);
186
- counts.push(`Prompts: ${overview.promptCount}`);
187
- lines.push("");
188
- lines.push(dim(counts.join(" | ")));
189
179
 
190
- return lines.join("\n");
180
+ // Resource/prompt counts
181
+ const counts: string[] = [];
182
+ counts.push(`Resources: ${overview.resourceCount}`);
183
+ counts.push(`Prompts: ${overview.promptCount}`);
184
+ lines.push("");
185
+ lines.push(dim(counts.join(" | ")));
186
+
187
+ return lines.join("\n");
188
+ },
189
+ options,
190
+ );
191
191
  }
192
192
 
193
193
  /** Format a list of tools with server names */
194
194
  export function formatToolList(tools: ToolWithServer[], options: FormatOptions): string {
195
- if (!isInteractive(options)) {
196
- if (options.withDescriptions) {
197
- return JSON.stringify(
198
- tools.map((t) => ({
199
- server: t.server,
200
- tool: t.tool.name,
201
- description: t.tool.description ?? "",
202
- })),
203
- null,
204
- 2,
205
- );
206
- }
207
- return JSON.stringify(
208
- tools.map((t) => ({ server: t.server, tool: t.tool.name })),
209
- null,
210
- 2,
211
- );
212
- }
213
-
214
- if (tools.length === 0) {
215
- return dim("No tools found");
216
- }
217
-
218
- // Calculate column widths
219
- const maxServer = Math.max(...tools.map((t) => t.server.length));
220
- const maxTool = Math.max(...tools.map((t) => t.tool.name.length));
221
- const termWidth = getTerminalWidth();
222
-
223
- return tools
224
- .map((t) => {
225
- const server = cyan(t.server.padEnd(maxServer));
226
- const tool = bold(t.tool.name.padEnd(maxTool));
227
- if (options.withDescriptions && t.tool.description) {
228
- const prefix = `${server} ${tool}`;
229
- const pw = visibleLength(prefix) + 2;
230
- const desc =
231
- termWidth != null
232
- ? wrapDescription(t.tool.description, pw, termWidth)
233
- : dim(t.tool.description);
234
- return `${prefix} ${desc}`;
235
- }
236
- return `${server} ${tool}`;
237
- })
238
- .join("\n");
195
+ return formatOutput(
196
+ tools.map((t) => ({
197
+ server: t.server,
198
+ tool: t.tool.name,
199
+ ...(options.withDescriptions ? { description: t.tool.description ?? "" } : {}),
200
+ })),
201
+ () =>
202
+ formatTable(tools, {
203
+ columns: [
204
+ { value: (t) => t.server, style: cyan },
205
+ { value: (t) => t.tool.name, style: bold },
206
+ ],
207
+ description: options.withDescriptions ? (t) => t.tool.description : undefined,
208
+ emptyMessage: "No tools found",
209
+ }),
210
+ options,
211
+ );
239
212
  }
240
213
 
241
214
  /** Format tools for a single server */
@@ -244,66 +217,46 @@ export function formatServerTools(
244
217
  tools: Tool[],
245
218
  options: FormatOptions,
246
219
  ): string {
247
- if (!isInteractive(options)) {
248
- return JSON.stringify(
249
- {
250
- server: serverName,
251
- tools: tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
252
- },
253
- null,
254
- 2,
255
- );
256
- }
257
-
258
- if (tools.length === 0) {
259
- return dim(`No tools found for ${serverName}`);
260
- }
261
-
262
- const header = cyan.bold(serverName);
263
- const maxName = Math.max(...tools.map((t) => t.name.length));
264
- const termWidth = getTerminalWidth();
265
-
266
- const lines = tools.map((t) => {
267
- const name = ` ${bold(t.name.padEnd(maxName))}`;
268
- if (t.description) {
269
- const pw = visibleLength(name) + 2;
270
- const desc =
271
- termWidth != null ? wrapDescription(t.description, pw, termWidth) : dim(t.description);
272
- return `${name} ${desc}`;
273
- }
274
- return name;
275
- });
276
-
277
- return [header, ...lines].join("\n");
220
+ return formatOutput(
221
+ {
222
+ server: serverName,
223
+ tools: tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
224
+ },
225
+ () => {
226
+ if (tools.length === 0) {
227
+ return dim(`No tools found for ${serverName}`);
228
+ }
229
+ const header = cyan.bold(serverName);
230
+ const body = formatTable(tools, {
231
+ columns: [{ value: (t) => ` ${t.name}`, style: bold }],
232
+ description: (t) => t.description,
233
+ });
234
+ return `${header}\n${body}`;
235
+ },
236
+ options,
237
+ );
278
238
  }
279
239
 
280
240
  /** Format a tool schema */
281
241
  export function formatToolSchema(serverName: string, tool: Tool, options: FormatOptions): string {
282
- if (!isInteractive(options)) {
283
- return JSON.stringify(
284
- {
285
- server: serverName,
286
- tool: tool.name,
287
- description: tool.description ?? "",
288
- inputSchema: tool.inputSchema,
289
- },
290
- null,
291
- 2,
292
- );
293
- }
294
-
295
- const lines: string[] = [];
296
- lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
297
-
298
- if (tool.description) {
299
- lines.push(dim(tool.description));
300
- }
301
-
302
- lines.push("");
303
- lines.push(bold("Input Schema:"));
304
- lines.push(formatSchema(tool.inputSchema, 2));
305
-
306
- return lines.join("\n");
242
+ return formatOutput(
243
+ {
244
+ server: serverName,
245
+ tool: tool.name,
246
+ description: tool.description ?? "",
247
+ inputSchema: tool.inputSchema,
248
+ },
249
+ () => {
250
+ const lines: string[] = [];
251
+ lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
252
+ if (tool.description) lines.push(dim(tool.description));
253
+ lines.push("");
254
+ lines.push(bold("Input Schema:"));
255
+ lines.push(formatSchema(tool.inputSchema, 2));
256
+ return lines.join("\n");
257
+ },
258
+ options,
259
+ );
307
260
  }
308
261
 
309
262
  /** Format a JSON schema as a readable parameter list */
@@ -329,37 +282,29 @@ function formatSchema(schema: Tool["inputSchema"], indent: number): string {
329
282
 
330
283
  /** Format detailed tool help with example payload */
331
284
  export function formatToolHelp(serverName: string, tool: Tool, options: FormatOptions): string {
332
- if (!isInteractive(options)) {
333
- return JSON.stringify(
334
- {
335
- server: serverName,
336
- tool: tool.name,
337
- description: tool.description ?? "",
338
- inputSchema: tool.inputSchema,
339
- example: generateExample(tool.inputSchema),
340
- },
341
- null,
342
- 2,
343
- );
344
- }
345
-
346
- const lines: string[] = [];
347
- lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
348
-
349
- if (tool.description) {
350
- lines.push(dim(tool.description));
351
- }
352
-
353
- lines.push("");
354
- lines.push(bold("Parameters:"));
355
- lines.push(formatSchema(tool.inputSchema, 2));
356
-
357
- const example = generateExample(tool.inputSchema);
358
- lines.push("");
359
- lines.push(bold("Example:"));
360
- lines.push(dim(` mcpx call ${serverName} ${tool.name} '${JSON.stringify(example)}'`));
361
-
362
- return lines.join("\n");
285
+ return formatOutput(
286
+ {
287
+ server: serverName,
288
+ tool: tool.name,
289
+ description: tool.description ?? "",
290
+ inputSchema: tool.inputSchema,
291
+ example: generateExample(tool.inputSchema),
292
+ },
293
+ () => {
294
+ const lines: string[] = [];
295
+ lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
296
+ if (tool.description) lines.push(dim(tool.description));
297
+ lines.push("");
298
+ lines.push(bold("Parameters:"));
299
+ lines.push(formatSchema(tool.inputSchema, 2));
300
+ const example = generateExample(tool.inputSchema);
301
+ lines.push("");
302
+ lines.push(bold("Example:"));
303
+ lines.push(dim(` mcpx call ${serverName} ${tool.name} '${JSON.stringify(example)}'`));
304
+ return lines.join("\n");
305
+ },
306
+ options,
307
+ );
363
308
  }
364
309
 
365
310
  /** Generate an example payload from a JSON schema */
@@ -370,7 +315,6 @@ function generateExample(schema: Tool["inputSchema"]): Record<string, unknown> {
370
315
 
371
316
  for (const [name, prop] of Object.entries(properties)) {
372
317
  const p = prop as Record<string, unknown>;
373
- // Include required fields and first few optional fields
374
318
  if (required.has(name) || Object.keys(example).length < 3) {
375
319
  example[name] = exampleValue(name, p);
376
320
  }
@@ -380,15 +324,8 @@ function generateExample(schema: Tool["inputSchema"]): Record<string, unknown> {
380
324
  }
381
325
 
382
326
  function exampleValue(name: string, prop: Record<string, unknown>): unknown {
383
- // Use enum first choice if available
384
- if (Array.isArray(prop.enum) && prop.enum.length > 0) {
385
- return prop.enum[0];
386
- }
387
-
388
- // Use default if provided
389
- if (prop.default !== undefined) {
390
- return prop.default;
391
- }
327
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) return prop.enum[0];
328
+ if (prop.default !== undefined) return prop.default;
392
329
 
393
330
  const type = prop.type as string | undefined;
394
331
  switch (type) {
@@ -438,51 +375,46 @@ export function formatValidationErrors(
438
375
  errors: ValidationError[],
439
376
  options: FormatOptions,
440
377
  ): string {
441
- if (!isInteractive(options)) {
442
- return JSON.stringify({
443
- error: "validation",
444
- server: serverName,
445
- tool: toolName,
446
- details: errors,
447
- });
448
- }
449
-
450
- const header = `${red("error:")} invalid arguments for ${cyan(serverName)}/${bold(toolName)}`;
451
- const details = errors.map((e) => ` ${yellow(e.path)}: ${e.message}`).join("\n");
452
- return `${header}\n${details}`;
378
+ return formatOutput(
379
+ { error: "validation", server: serverName, tool: toolName, details: errors },
380
+ () => {
381
+ const header = `${red("error:")} invalid arguments for ${cyan(serverName)}/${bold(toolName)}`;
382
+ const details = errors.map((e) => ` ${yellow(e.path)}: ${e.message}`).join("\n");
383
+ return `${header}\n${details}`;
384
+ },
385
+ options,
386
+ );
453
387
  }
454
388
 
455
389
  /** Format search results */
456
390
  export function formatSearchResults(results: SearchResult[], options: FormatOptions): string {
457
- if (!isInteractive(options)) {
458
- return JSON.stringify(results, null, 2);
459
- }
460
-
461
- if (results.length === 0) {
462
- return dim("No matching tools found");
463
- }
464
-
465
- const termWidth = getTerminalWidth();
466
- const descIndent = 2;
467
-
468
- return results
469
- .map((r) => {
470
- const header = `${cyan(r.server)} ${bold(r.tool)} ${yellow(r.score.toFixed(2))}`;
471
-
472
- // Join all description lines into a single string for wrapping
473
- const fullDesc = r.description
474
- .split("\n")
475
- .map((l) => l.trim())
476
- .filter((l) => l.length > 0)
477
- .join(" ");
478
-
479
- const indent = " ".repeat(descIndent);
480
- const desc =
481
- termWidth != null ? wrapDescription(fullDesc, descIndent, termWidth) : dim(fullDesc);
391
+ return formatOutput(
392
+ results,
393
+ () => {
394
+ if (results.length === 0) {
395
+ return dim("No matching tools found");
396
+ }
482
397
 
483
- return `${header}\n${indent}${desc}`;
484
- })
485
- .join("\n\n");
398
+ const termWidth = getTerminalWidth();
399
+ const descIndent = 2;
400
+
401
+ return results
402
+ .map((r) => {
403
+ const header = `${cyan(r.server)} ${bold(r.tool)} ${yellow(r.score.toFixed(2))}`;
404
+ const fullDesc = r.description
405
+ .split("\n")
406
+ .map((l) => l.trim())
407
+ .filter((l) => l.length > 0)
408
+ .join(" ");
409
+ const indent = " ".repeat(descIndent);
410
+ const desc =
411
+ termWidth != null ? wrapDescription(fullDesc, descIndent, termWidth) : dim(fullDesc);
412
+ return `${header}\n${indent}${desc}`;
413
+ })
414
+ .join("\n\n");
415
+ },
416
+ options,
417
+ );
486
418
  }
487
419
 
488
420
  /** Format a list of resources with server names */
@@ -490,43 +422,24 @@ export function formatResourceList(
490
422
  resources: ResourceWithServer[],
491
423
  options: FormatOptions,
492
424
  ): string {
493
- if (!isInteractive(options)) {
494
- return JSON.stringify(
495
- resources.map((r) => ({
496
- server: r.server,
497
- uri: r.resource.uri,
498
- name: r.resource.name,
499
- ...(options.withDescriptions ? { description: r.resource.description ?? "" } : {}),
500
- })),
501
- null,
502
- 2,
503
- );
504
- }
505
-
506
- if (resources.length === 0) {
507
- return dim("No resources found");
508
- }
509
-
510
- const maxServer = Math.max(...resources.map((r) => r.server.length));
511
- const maxUri = Math.max(...resources.map((r) => r.resource.uri.length));
512
- const termWidth = getTerminalWidth();
513
-
514
- return resources
515
- .map((r) => {
516
- const server = cyan(r.server.padEnd(maxServer));
517
- const uri = bold(r.resource.uri.padEnd(maxUri));
518
- if (options.withDescriptions && r.resource.description) {
519
- const prefix = `${server} ${uri}`;
520
- const pw = visibleLength(prefix) + 2;
521
- const desc =
522
- termWidth != null
523
- ? wrapDescription(r.resource.description, pw, termWidth)
524
- : dim(r.resource.description);
525
- return `${prefix} ${desc}`;
526
- }
527
- return `${server} ${uri}`;
528
- })
529
- .join("\n");
425
+ return formatOutput(
426
+ resources.map((r) => ({
427
+ server: r.server,
428
+ uri: r.resource.uri,
429
+ name: r.resource.name,
430
+ ...(options.withDescriptions ? { description: r.resource.description ?? "" } : {}),
431
+ })),
432
+ () =>
433
+ formatTable(resources, {
434
+ columns: [
435
+ { value: (r) => r.server, style: cyan },
436
+ { value: (r) => r.resource.uri, style: bold },
437
+ ],
438
+ description: options.withDescriptions ? (r) => r.resource.description : undefined,
439
+ emptyMessage: "No resources found",
440
+ }),
441
+ options,
442
+ );
530
443
  }
531
444
 
532
445
  /** Format resources for a single server */
@@ -535,42 +448,29 @@ export function formatServerResources(
535
448
  resources: Resource[],
536
449
  options: FormatOptions,
537
450
  ): string {
538
- if (!isInteractive(options)) {
539
- return JSON.stringify(
540
- {
541
- server: serverName,
542
- resources: resources.map((r) => ({
543
- uri: r.uri,
544
- name: r.name,
545
- description: r.description ?? "",
546
- mimeType: r.mimeType ?? "",
547
- })),
548
- },
549
- null,
550
- 2,
551
- );
552
- }
553
-
554
- if (resources.length === 0) {
555
- return dim(`No resources found for ${serverName}`);
556
- }
557
-
558
- const header = cyan.bold(serverName);
559
- const maxUri = Math.max(...resources.map((r) => r.uri.length));
560
- const termWidth = getTerminalWidth();
561
-
562
- const lines = resources.map((r) => {
563
- const uri = ` ${bold(r.uri.padEnd(maxUri))}`;
564
- if (r.description) {
565
- const pw = visibleLength(uri) + 2;
566
- const desc =
567
- termWidth != null ? wrapDescription(r.description, pw, termWidth) : dim(r.description);
568
- return `${uri} ${desc}`;
569
- }
570
- return uri;
571
- });
572
-
573
- return [header, ...lines].join("\n");
451
+ return formatOutput(
452
+ {
453
+ server: serverName,
454
+ resources: resources.map((r) => ({
455
+ uri: r.uri,
456
+ name: r.name,
457
+ description: r.description ?? "",
458
+ mimeType: r.mimeType ?? "",
459
+ })),
460
+ },
461
+ () => {
462
+ if (resources.length === 0) {
463
+ return dim(`No resources found for ${serverName}`);
464
+ }
465
+ const header = cyan.bold(serverName);
466
+ const body = formatTable(resources, {
467
+ columns: [{ value: (r) => ` ${r.uri}`, style: bold }],
468
+ description: (r) => r.description,
469
+ });
470
+ return `${header}\n${body}`;
471
+ },
472
+ options,
473
+ );
574
474
  }
575
475
 
576
476
  /** Format resource contents */
@@ -580,74 +480,53 @@ export function formatResourceContents(
580
480
  result: unknown,
581
481
  options: FormatOptions,
582
482
  ): string {
583
- if (!isInteractive(options)) {
584
- return JSON.stringify(
585
- { server: serverName, uri, contents: (result as { contents: unknown })?.contents ?? result },
586
- null,
587
- 2,
588
- );
589
- }
590
-
591
- const contents =
592
- (result as { contents?: Array<{ text?: string; blob?: string; mimeType?: string }> })
593
- ?.contents ?? [];
594
- const lines: string[] = [];
595
- lines.push(`${cyan(serverName)}/${bold(uri)}`);
596
- lines.push("");
597
-
598
- if (contents.length === 0) {
599
- lines.push(dim("(empty)"));
600
- } else {
601
- for (const item of contents) {
602
- if (item.text !== undefined) {
603
- lines.push(item.text);
604
- } else if (item.blob !== undefined) {
605
- lines.push(dim(`<binary blob, ${item.blob.length} bytes base64>`));
483
+ return formatOutput(
484
+ { server: serverName, uri, contents: (result as { contents: unknown })?.contents ?? result },
485
+ () => {
486
+ const contents =
487
+ (result as { contents?: Array<{ text?: string; blob?: string; mimeType?: string }> })
488
+ ?.contents ?? [];
489
+ const lines: string[] = [];
490
+ lines.push(`${cyan(serverName)}/${bold(uri)}`);
491
+ lines.push("");
492
+
493
+ if (contents.length === 0) {
494
+ lines.push(dim("(empty)"));
495
+ } else {
496
+ for (const item of contents) {
497
+ if (item.text !== undefined) {
498
+ lines.push(item.text);
499
+ } else if (item.blob !== undefined) {
500
+ lines.push(dim(`<binary blob, ${item.blob.length} bytes base64>`));
501
+ }
502
+ }
606
503
  }
607
- }
608
- }
609
504
 
610
- return lines.join("\n");
505
+ return lines.join("\n");
506
+ },
507
+ options,
508
+ );
611
509
  }
612
510
 
613
511
  /** Format a list of prompts with server names */
614
512
  export function formatPromptList(prompts: PromptWithServer[], options: FormatOptions): string {
615
- if (!isInteractive(options)) {
616
- return JSON.stringify(
617
- prompts.map((p) => ({
618
- server: p.server,
619
- name: p.prompt.name,
620
- ...(options.withDescriptions ? { description: p.prompt.description ?? "" } : {}),
621
- })),
622
- null,
623
- 2,
624
- );
625
- }
626
-
627
- if (prompts.length === 0) {
628
- return dim("No prompts found");
629
- }
630
-
631
- const maxServer = Math.max(...prompts.map((p) => p.server.length));
632
- const maxName = Math.max(...prompts.map((p) => p.prompt.name.length));
633
- const termWidth = getTerminalWidth();
634
-
635
- return prompts
636
- .map((p) => {
637
- const server = cyan(p.server.padEnd(maxServer));
638
- const name = bold(p.prompt.name.padEnd(maxName));
639
- if (options.withDescriptions && p.prompt.description) {
640
- const prefix = `${server} ${name}`;
641
- const pw = visibleLength(prefix) + 2;
642
- const desc =
643
- termWidth != null
644
- ? wrapDescription(p.prompt.description, pw, termWidth)
645
- : dim(p.prompt.description);
646
- return `${prefix} ${desc}`;
647
- }
648
- return `${server} ${name}`;
649
- })
650
- .join("\n");
513
+ return formatOutput(
514
+ prompts.map((p) => ({
515
+ server: p.server,
516
+ name: p.prompt.name,
517
+ ...(options.withDescriptions ? { description: p.prompt.description ?? "" } : {}),
518
+ })),
519
+ () =>
520
+ formatTable(prompts, {
521
+ columns: [
522
+ { value: (p) => p.server, style: cyan },
523
+ { value: (p) => p.prompt.name, style: bold },
524
+ ],
525
+ description: options.withDescriptions ? (p) => p.prompt.description : undefined,
526
+ emptyMessage: "No prompts found",
527
+ }),
528
+ options,
529
+ );
651
530
  }
652
531
 
653
532
  /** Format prompts for a single server */
@@ -656,47 +535,44 @@ export function formatServerPrompts(
656
535
  prompts: Prompt[],
657
536
  options: FormatOptions,
658
537
  ): string {
659
- if (!isInteractive(options)) {
660
- return JSON.stringify(
661
- {
662
- server: serverName,
663
- prompts: prompts.map((p) => ({
664
- name: p.name,
665
- description: p.description ?? "",
666
- arguments: p.arguments ?? [],
667
- })),
668
- },
669
- null,
670
- 2,
671
- );
672
- }
673
-
674
- if (prompts.length === 0) {
675
- return dim(`No prompts found for ${serverName}`);
676
- }
677
-
678
- const header = cyan.bold(serverName);
679
- const maxName = Math.max(...prompts.map((p) => p.name.length));
680
-
681
- const termWidth = getTerminalWidth();
682
-
683
- const lines = prompts.map((p) => {
684
- const name = ` ${bold(p.name.padEnd(maxName))}`;
685
- const args =
686
- p.arguments && p.arguments.length > 0
687
- ? ` ${dim(`(${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(", ")})`)}`
688
- : "";
689
- if (p.description) {
690
- const prefix = `${name}${args}`;
691
- const pw = visibleLength(prefix) + 2;
692
- const desc =
693
- termWidth != null ? wrapDescription(p.description, pw, termWidth) : dim(p.description);
694
- return `${prefix} ${desc}`;
695
- }
696
- return `${name}${args}`;
697
- });
538
+ return formatOutput(
539
+ {
540
+ server: serverName,
541
+ prompts: prompts.map((p) => ({
542
+ name: p.name,
543
+ description: p.description ?? "",
544
+ arguments: p.arguments ?? [],
545
+ })),
546
+ },
547
+ () => {
548
+ if (prompts.length === 0) {
549
+ return dim(`No prompts found for ${serverName}`);
550
+ }
698
551
 
699
- return [header, ...lines].join("\n");
552
+ const header = cyan.bold(serverName);
553
+ const maxName = Math.max(...prompts.map((p) => p.name.length));
554
+ const termWidth = getTerminalWidth();
555
+
556
+ const lines = prompts.map((p) => {
557
+ const name = ` ${bold(p.name.padEnd(maxName))}`;
558
+ const args =
559
+ p.arguments && p.arguments.length > 0
560
+ ? ` ${dim(`(${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(", ")})`)}`
561
+ : "";
562
+ if (p.description) {
563
+ const prefix = `${name}${args}`;
564
+ const pw = visibleLength(prefix) + 2;
565
+ const desc =
566
+ termWidth != null ? wrapDescription(p.description, pw, termWidth) : dim(p.description);
567
+ return `${prefix} ${desc}`;
568
+ }
569
+ return `${name}${args}`;
570
+ });
571
+
572
+ return [header, ...lines].join("\n");
573
+ },
574
+ options,
575
+ );
700
576
  }
701
577
 
702
578
  /** Format prompt messages */
@@ -706,80 +582,56 @@ export function formatPromptMessages(
706
582
  result: unknown,
707
583
  options: FormatOptions,
708
584
  ): string {
709
- if (!isInteractive(options)) {
710
- return JSON.stringify({ server: serverName, prompt: name, ...(result as object) }, null, 2);
711
- }
712
-
713
- const r = result as {
714
- description?: string;
715
- messages?: Array<{ role: string; content: { type: string; text?: string } }>;
716
- };
717
- const lines: string[] = [];
718
- lines.push(`${cyan(serverName)}/${bold(name)}`);
719
-
720
- if (r.description) {
721
- lines.push(dim(r.description));
722
- }
723
-
724
- lines.push("");
725
-
726
- for (const msg of r.messages ?? []) {
727
- lines.push(`${bold(msg.role)}:`);
728
- if (msg.content.text !== undefined) {
729
- lines.push(` ${msg.content.text}`);
730
- }
731
- }
732
-
733
- return lines.join("\n");
585
+ return formatOutput(
586
+ { server: serverName, prompt: name, ...(result as object) },
587
+ () => {
588
+ const r = result as {
589
+ description?: string;
590
+ messages?: Array<{ role: string; content: { type: string; text?: string } }>;
591
+ };
592
+ const lines: string[] = [];
593
+ lines.push(`${cyan(serverName)}/${bold(name)}`);
594
+ if (r.description) lines.push(dim(r.description));
595
+ lines.push("");
596
+ for (const msg of r.messages ?? []) {
597
+ lines.push(`${bold(msg.role)}:`);
598
+ if (msg.content.text !== undefined) {
599
+ lines.push(` ${msg.content.text}`);
600
+ }
601
+ }
602
+ return lines.join("\n");
603
+ },
604
+ options,
605
+ );
734
606
  }
735
607
 
736
608
  /** Format a unified list of tools, resources, and prompts across servers */
737
609
  export function formatUnifiedList(items: UnifiedItem[], options: FormatOptions): string {
738
- if (!isInteractive(options)) {
739
- return JSON.stringify(
740
- items.map((i) => ({
741
- server: i.server,
742
- type: i.type,
743
- name: i.name,
744
- ...(options.withDescriptions ? { description: i.description ?? "" } : {}),
745
- })),
746
- null,
747
- 2,
748
- );
749
- }
750
-
751
- if (items.length === 0) {
752
- return dim("No tools, resources, or prompts found");
753
- }
754
-
755
- const maxServer = Math.max(...items.map((i) => i.server.length));
756
- const maxType = 8; // "resource" is the longest at 8 chars
757
- const maxName = Math.max(...items.map((i) => i.name.length));
758
-
759
- const typeLabel = (t: UnifiedItem["type"]) => {
760
- const padded = t.padEnd(maxType);
761
- if (t === "tool") return green(padded);
762
- if (t === "resource") return cyan(padded);
763
- return yellow(padded);
610
+ const typeLabel = (t: string) => {
611
+ if (t === "tool") return green(t);
612
+ if (t === "resource") return cyan(t);
613
+ return yellow(t);
764
614
  };
765
615
 
766
- const termWidth = getTerminalWidth();
767
-
768
- return items
769
- .map((i) => {
770
- const server = cyan(i.server.padEnd(maxServer));
771
- const type = typeLabel(i.type);
772
- const name = bold(i.name.padEnd(maxName));
773
- if (options.withDescriptions && i.description) {
774
- const prefix = `${server} ${type} ${name}`;
775
- const pw = visibleLength(prefix) + 2;
776
- const desc =
777
- termWidth != null ? wrapDescription(i.description, pw, termWidth) : dim(i.description);
778
- return `${prefix} ${desc}`;
779
- }
780
- return `${server} ${type} ${name}`;
781
- })
782
- .join("\n");
616
+ return formatOutput(
617
+ items.map((i) => ({
618
+ server: i.server,
619
+ type: i.type,
620
+ name: i.name,
621
+ ...(options.withDescriptions ? { description: i.description ?? "" } : {}),
622
+ })),
623
+ () =>
624
+ formatTable(items, {
625
+ columns: [
626
+ { value: (i) => i.server, style: cyan },
627
+ { value: (i) => i.type, style: typeLabel },
628
+ { value: (i) => i.name, style: bold },
629
+ ],
630
+ description: options.withDescriptions ? (i) => i.description : undefined,
631
+ emptyMessage: "No tools, resources, or prompts found",
632
+ }),
633
+ options,
634
+ );
783
635
  }
784
636
 
785
637
  /** Format a single task status */
@@ -787,36 +639,38 @@ export function formatTaskStatus(
787
639
  task: { taskId: string; status: string; [key: string]: unknown },
788
640
  options: FormatOptions,
789
641
  ): string {
790
- if (!isInteractive(options)) {
791
- return JSON.stringify(task, null, 2);
792
- }
793
-
794
- const statusColor = (s: string) => {
795
- switch (s) {
796
- case "completed":
797
- return green(s);
798
- case "working":
799
- return yellow(s);
800
- case "failed":
801
- case "cancelled":
802
- return red(s);
803
- case "input_required":
804
- return yellow(s);
805
- default:
806
- return s;
807
- }
808
- };
809
-
810
- const lines: string[] = [];
811
- lines.push(`${bold("Task:")} ${cyan(task.taskId)}`);
812
- lines.push(`${bold("Status:")} ${statusColor(task.status)}`);
813
- if (task.statusMessage) lines.push(`${bold("Message:")} ${dim(String(task.statusMessage))}`);
814
- if (task.createdAt) lines.push(`${bold("Created:")} ${dim(String(task.createdAt))}`);
815
- if (task.lastUpdatedAt) lines.push(`${bold("Updated:")} ${dim(String(task.lastUpdatedAt))}`);
816
- if (task.ttl != null) lines.push(`${bold("TTL:")} ${dim(String(task.ttl) + "ms")}`);
817
- if (task.pollInterval != null)
818
- lines.push(`${bold("Poll interval:")} ${dim(String(task.pollInterval) + "ms")}`);
819
- return lines.join("\n");
642
+ return formatOutput(
643
+ task,
644
+ () => {
645
+ const statusColor = (s: string) => {
646
+ switch (s) {
647
+ case "completed":
648
+ return green(s);
649
+ case "working":
650
+ return yellow(s);
651
+ case "failed":
652
+ case "cancelled":
653
+ return red(s);
654
+ case "input_required":
655
+ return yellow(s);
656
+ default:
657
+ return s;
658
+ }
659
+ };
660
+
661
+ const lines: string[] = [];
662
+ lines.push(`${bold("Task:")} ${cyan(task.taskId)}`);
663
+ lines.push(`${bold("Status:")} ${statusColor(task.status)}`);
664
+ if (task.statusMessage) lines.push(`${bold("Message:")} ${dim(String(task.statusMessage))}`);
665
+ if (task.createdAt) lines.push(`${bold("Created:")} ${dim(String(task.createdAt))}`);
666
+ if (task.lastUpdatedAt) lines.push(`${bold("Updated:")} ${dim(String(task.lastUpdatedAt))}`);
667
+ if (task.ttl != null) lines.push(`${bold("TTL:")} ${dim(String(task.ttl) + "ms")}`);
668
+ if (task.pollInterval != null)
669
+ lines.push(`${bold("Poll interval:")} ${dim(String(task.pollInterval) + "ms")}`);
670
+ return lines.join("\n");
671
+ },
672
+ options,
673
+ );
820
674
  }
821
675
 
822
676
  /** Format a list of tasks */
@@ -825,43 +679,45 @@ export function formatTasksList(
825
679
  nextCursor: string | undefined,
826
680
  options: FormatOptions,
827
681
  ): string {
828
- if (!isInteractive(options)) {
829
- return JSON.stringify({ tasks, ...(nextCursor ? { nextCursor } : {}) }, null, 2);
830
- }
831
-
832
- if (tasks.length === 0) {
833
- return dim("No tasks found");
834
- }
835
-
836
- const statusColor = (s: string) => {
837
- switch (s) {
838
- case "completed":
839
- return green(s.padEnd(14));
840
- case "working":
841
- return yellow(s.padEnd(14));
842
- case "failed":
843
- case "cancelled":
844
- return red(s.padEnd(14));
845
- default:
846
- return s.padEnd(14);
847
- }
848
- };
849
-
850
- const maxId = Math.max(...tasks.map((t) => t.taskId.length));
851
-
852
- const lines = tasks.map((t) => {
853
- const id = cyan(t.taskId.padEnd(maxId));
854
- const status = statusColor(t.status);
855
- const updated = t.lastUpdatedAt ? dim(String(t.lastUpdatedAt)) : "";
856
- return `${id} ${status} ${updated}`;
857
- });
682
+ return formatOutput(
683
+ { tasks, ...(nextCursor ? { nextCursor } : {}) },
684
+ () => {
685
+ if (tasks.length === 0) {
686
+ return dim("No tasks found");
687
+ }
858
688
 
859
- if (nextCursor) {
860
- lines.push("");
861
- lines.push(dim(`Next cursor: ${nextCursor}`));
862
- }
689
+ const statusColor = (s: string) => {
690
+ switch (s) {
691
+ case "completed":
692
+ return green(s.padEnd(14));
693
+ case "working":
694
+ return yellow(s.padEnd(14));
695
+ case "failed":
696
+ case "cancelled":
697
+ return red(s.padEnd(14));
698
+ default:
699
+ return s.padEnd(14);
700
+ }
701
+ };
702
+
703
+ const maxId = Math.max(...tasks.map((t) => t.taskId.length));
704
+
705
+ const lines = tasks.map((t) => {
706
+ const id = cyan(t.taskId.padEnd(maxId));
707
+ const status = statusColor(t.status);
708
+ const updated = t.lastUpdatedAt ? dim(String(t.lastUpdatedAt)) : "";
709
+ return `${id} ${status} ${updated}`;
710
+ });
711
+
712
+ if (nextCursor) {
713
+ lines.push("");
714
+ lines.push(dim(`Next cursor: ${nextCursor}`));
715
+ }
863
716
 
864
- return lines.join("\n");
717
+ return lines.join("\n");
718
+ },
719
+ options,
720
+ );
865
721
  }
866
722
 
867
723
  /** Format task creation output (for --no-wait) */
@@ -869,16 +725,14 @@ export function formatTaskCreated(
869
725
  task: { taskId: string; status: string; [key: string]: unknown },
870
726
  options: FormatOptions,
871
727
  ): string {
872
- if (!isInteractive(options)) {
873
- return JSON.stringify({ task }, null, 2);
874
- }
875
- return `${green("Task created:")} ${cyan(task.taskId)} ${dim(`(status: ${task.status})`)}`;
728
+ return formatOutput(
729
+ { task },
730
+ () => `${green("Task created:")} ${cyan(task.taskId)} ${dim(`(status: ${task.status})`)}`,
731
+ options,
732
+ );
876
733
  }
877
734
 
878
735
  /** Format an error message */
879
736
  export function formatError(message: string, options: FormatOptions): string {
880
- if (!isInteractive(options)) {
881
- return JSON.stringify({ error: message });
882
- }
883
- return `${red("error:")} ${message}`;
737
+ return formatOutput({ error: message }, () => `${red("error:")} ${message}`, options);
884
738
  }