@evantahler/mcpx 0.18.2 → 0.18.5

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.
Files changed (53) hide show
  1. package/package.json +63 -62
  2. package/src/cli.ts +46 -54
  3. package/src/client/browser.ts +15 -15
  4. package/src/client/debug-fetch.ts +53 -56
  5. package/src/client/elicitation.ts +279 -291
  6. package/src/client/http.ts +1 -1
  7. package/src/client/manager.ts +481 -514
  8. package/src/client/oauth.ts +272 -282
  9. package/src/client/sse.ts +1 -1
  10. package/src/client/stdio.ts +7 -7
  11. package/src/client/trace.ts +146 -152
  12. package/src/client/transport-options.ts +20 -20
  13. package/src/commands/add.ts +160 -165
  14. package/src/commands/allow.ts +141 -142
  15. package/src/commands/auth.ts +86 -90
  16. package/src/commands/check-update.ts +49 -53
  17. package/src/commands/deny.ts +114 -117
  18. package/src/commands/exec.ts +218 -222
  19. package/src/commands/index.ts +41 -41
  20. package/src/commands/info.ts +48 -50
  21. package/src/commands/list.ts +49 -49
  22. package/src/commands/ping.ts +47 -50
  23. package/src/commands/prompt.ts +40 -50
  24. package/src/commands/remove.ts +54 -56
  25. package/src/commands/resource.ts +31 -36
  26. package/src/commands/search.ts +35 -39
  27. package/src/commands/servers.ts +44 -48
  28. package/src/commands/skill.ts +89 -95
  29. package/src/commands/task.ts +50 -60
  30. package/src/commands/upgrade.ts +191 -208
  31. package/src/commands/with-command.ts +27 -29
  32. package/src/config/env.ts +26 -28
  33. package/src/config/loader.ts +99 -102
  34. package/src/config/schemas.ts +78 -87
  35. package/src/constants.ts +17 -17
  36. package/src/context.ts +51 -51
  37. package/src/lib/client-settings.ts +127 -140
  38. package/src/lib/input.ts +23 -26
  39. package/src/output/format-output.ts +12 -16
  40. package/src/output/format-table.ts +39 -42
  41. package/src/output/formatter.ts +790 -814
  42. package/src/output/logger.ts +140 -152
  43. package/src/sdk.ts +283 -291
  44. package/src/search/index.ts +50 -54
  45. package/src/search/indexer.ts +65 -65
  46. package/src/search/keyword.ts +54 -54
  47. package/src/search/semantic.ts +39 -39
  48. package/src/search/staleness.ts +3 -3
  49. package/src/search/types.ts +4 -4
  50. package/src/update/background.ts +51 -51
  51. package/src/update/cache.ts +21 -21
  52. package/src/update/checker.ts +81 -86
  53. package/src/validation/schema.ts +52 -58
@@ -1,8 +1,8 @@
1
1
  import ansis, { bold, cyan, dim, green, red, yellow } from "ansis";
2
- import type { Tool, Resource, Prompt } from "../config/schemas.ts";
3
- import type { ToolWithServer, ResourceWithServer, PromptWithServer } from "../client/manager.ts";
4
- import type { ValidationError } from "../validation/schema.ts";
2
+ import type { PromptWithServer, ResourceWithServer, ToolWithServer } from "../client/manager.ts";
3
+ import type { Prompt, Resource, Tool } from "../config/schemas.ts";
5
4
  import type { SearchResult } from "../search/index.ts";
5
+ import type { ValidationError } from "../validation/schema.ts";
6
6
  import { formatOutput } from "./format-output.ts";
7
7
  import { formatTable } from "./format-table.ts";
8
8
 
@@ -11,68 +11,68 @@ export const VALID_FORMATS = ["json", "markdown"] as const;
11
11
  export type OutputFormat = (typeof VALID_FORMATS)[number];
12
12
 
13
13
  export interface FormatOptions {
14
- json?: boolean;
15
- withDescriptions?: boolean;
16
- verbose?: boolean;
17
- showSecrets?: boolean;
18
- logLevel?: string;
19
- format?: OutputFormat;
14
+ json?: boolean;
15
+ withDescriptions?: boolean;
16
+ verbose?: boolean;
17
+ showSecrets?: boolean;
18
+ logLevel?: string;
19
+ format?: OutputFormat;
20
20
  }
21
21
 
22
22
  export interface UnifiedItem {
23
- server: string;
24
- type: "tool" | "resource" | "prompt";
25
- name: string;
26
- description?: string;
23
+ server: string;
24
+ type: "tool" | "resource" | "prompt";
25
+ name: string;
26
+ description?: string;
27
27
  }
28
28
 
29
29
  /** Check if stdout is a TTY (interactive terminal) */
30
30
  export function isInteractive(options: FormatOptions): boolean {
31
- if (options.json) return false;
32
- return process.stdout.isTTY ?? false;
31
+ if (options.json) return false;
32
+ return process.stdout.isTTY ?? false;
33
33
  }
34
34
 
35
35
  /** Get terminal width, or undefined if not a TTY. Subtracts 1 for safety margin. */
36
36
  function getTerminalWidth(): number | undefined {
37
- if (process.stdout.isTTY) return Math.max(process.stdout.columns - 1, 40);
38
- return undefined;
37
+ if (process.stdout.isTTY) return Math.max(process.stdout.columns - 1, 40);
38
+ return undefined;
39
39
  }
40
40
 
41
41
  /** Measure visible length of a string (excluding ANSI escape codes) */
42
42
  function visibleLength(s: string): number {
43
- return ansis.strip(s).length;
43
+ return ansis.strip(s).length;
44
44
  }
45
45
 
46
46
  /** Word-wrap text to a max width, hard-breaking words that exceed it */
47
47
  function wrapLines(text: string, maxWidth: number): string[] {
48
- const words = text.split(/\s+/).filter((w) => w.length > 0);
49
- if (words.length === 0) return [""];
50
-
51
- const lines: string[] = [];
52
- let current = "";
53
-
54
- for (const word of words) {
55
- if (word.length > maxWidth) {
56
- if (current) {
57
- lines.push(current);
58
- current = "";
59
- }
60
- for (let j = 0; j < word.length; j += maxWidth) {
61
- lines.push(word.slice(j, j + maxWidth));
62
- }
63
- continue;
64
- }
65
- if (current.length === 0) {
66
- current = word;
67
- } else if (current.length + 1 + word.length <= maxWidth) {
68
- current += " " + word;
69
- } else {
70
- lines.push(current);
71
- current = word;
72
- }
73
- }
74
- if (current) lines.push(current);
75
- return lines;
48
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
49
+ if (words.length === 0) return [""];
50
+
51
+ const lines: string[] = [];
52
+ let current = "";
53
+
54
+ for (const word of words) {
55
+ if (word.length > maxWidth) {
56
+ if (current) {
57
+ lines.push(current);
58
+ current = "";
59
+ }
60
+ for (let j = 0; j < word.length; j += maxWidth) {
61
+ lines.push(word.slice(j, j + maxWidth));
62
+ }
63
+ continue;
64
+ }
65
+ if (current.length === 0) {
66
+ current = word;
67
+ } else if (current.length + 1 + word.length <= maxWidth) {
68
+ current += ` ${word}`;
69
+ } else {
70
+ lines.push(current);
71
+ current = word;
72
+ }
73
+ }
74
+ if (current) lines.push(current);
75
+ return lines;
76
76
  }
77
77
 
78
78
  /**
@@ -83,350 +83,340 @@ function wrapLines(text: string, maxWidth: number): string[] {
83
83
  * @param termWidth - terminal width in columns
84
84
  */
85
85
  export function wrapDescription(text: string, prefixWidth: number, termWidth: number): string {
86
- const available = termWidth - prefixWidth;
86
+ const available = termWidth - prefixWidth;
87
87
 
88
- // If prefix is so wide there's barely room, wrap onto the next line with a small indent
89
- if (available < 20) {
90
- const fallbackIndent = Math.min(prefixWidth, 4);
91
- const fallbackAvail = termWidth - fallbackIndent;
92
- if (fallbackAvail < 20) {
93
- return dim(text.length > termWidth ? text.slice(0, termWidth - 3) + "..." : text);
94
- }
95
- const wrapped = wrapLines(text, fallbackAvail);
96
- const indent = " ".repeat(fallbackIndent);
97
- return wrapped.map((l) => `\n${indent}${dim(l)}`).join("");
98
- }
88
+ // If prefix is so wide there's barely room, wrap onto the next line with a small indent
89
+ if (available < 20) {
90
+ const fallbackIndent = Math.min(prefixWidth, 4);
91
+ const fallbackAvail = termWidth - fallbackIndent;
92
+ if (fallbackAvail < 20) {
93
+ return dim(text.length > termWidth ? `${text.slice(0, termWidth - 3)}...` : text);
94
+ }
95
+ const wrapped = wrapLines(text, fallbackAvail);
96
+ const indent = " ".repeat(fallbackIndent);
97
+ return wrapped.map((l) => `\n${indent}${dim(l)}`).join("");
98
+ }
99
99
 
100
- const wrapped = wrapLines(text, available);
101
- const indent = " ".repeat(prefixWidth);
102
- return wrapped.map((l, i) => (i === 0 ? dim(l) : `\n${indent}${dim(l)}`)).join("");
100
+ const wrapped = wrapLines(text, available);
101
+ const indent = " ".repeat(prefixWidth);
102
+ return wrapped.map((l, i) => (i === 0 ? dim(l) : `\n${indent}${dim(l)}`)).join("");
103
103
  }
104
104
 
105
105
  export interface ServerOverview {
106
- serverName: string;
107
- version?: { name: string; version: string };
108
- capabilities?: Record<string, unknown>;
109
- instructions?: string;
110
- tools: Tool[];
111
- resourceCount: number;
112
- promptCount: number;
106
+ serverName: string;
107
+ version?: { name: string; version: string };
108
+ capabilities?: Record<string, unknown>;
109
+ instructions?: string;
110
+ tools: Tool[];
111
+ resourceCount: number;
112
+ promptCount: number;
113
113
  }
114
114
 
115
115
  const KNOWN_CAPABILITIES = ["tools", "resources", "prompts", "logging", "completions", "tasks"];
116
116
 
117
117
  /** Format a full server overview (version, capabilities, tools, counts) */
118
118
  export function formatServerOverview(overview: ServerOverview, options: FormatOptions): string {
119
- return formatOutput(
120
- {
121
- server: overview.serverName,
122
- version: overview.version ?? null,
123
- capabilities: overview.capabilities ?? null,
124
- instructions: overview.instructions ?? null,
125
- tools: overview.tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
126
- resourceCount: overview.resourceCount,
127
- promptCount: overview.promptCount,
128
- },
129
- () => {
130
- const lines: string[] = [];
131
-
132
- // Header: server name + version
133
- const header = cyan.bold(overview.serverName);
134
- if (overview.version) {
135
- lines.push(
136
- `${header} ${dim(`v${overview.version.version}`)} ${dim(`(${overview.version.name})`)}`,
137
- );
138
- } else {
139
- lines.push(header);
140
- }
141
-
142
- // Capabilities
143
- if (overview.capabilities) {
144
- lines.push("");
145
- lines.push(bold("Capabilities:"));
146
- const caps = overview.capabilities;
147
- const present = KNOWN_CAPABILITIES.filter((k) => k in caps);
148
- const absent = KNOWN_CAPABILITIES.filter((k) => !(k in caps));
149
- for (const k of present) lines.push(` ${green("✓")} ${k}`);
150
- for (const k of absent) lines.push(` ${dim("✗")} ${dim(k)}`);
151
- }
152
-
153
- // Instructions
154
- if (overview.instructions) {
155
- lines.push("");
156
- lines.push(bold("Instructions:"));
157
- lines.push(` ${dim(overview.instructions)}`);
158
- }
159
-
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
176
- ? wrapDescription(t.description, pw, termWidth)
177
- : dim(t.description);
178
- lines.push(`${name} ${desc}`);
179
- } else {
180
- lines.push(name);
181
- }
182
- }
183
- }
184
-
185
- // Resource/prompt counts
186
- const counts: string[] = [];
187
- counts.push(`Resources: ${overview.resourceCount}`);
188
- counts.push(`Prompts: ${overview.promptCount}`);
189
- lines.push("");
190
- lines.push(dim(counts.join(" | ")));
191
-
192
- return lines.join("\n");
193
- },
194
- options,
195
- );
119
+ return formatOutput(
120
+ {
121
+ server: overview.serverName,
122
+ version: overview.version ?? null,
123
+ capabilities: overview.capabilities ?? null,
124
+ instructions: overview.instructions ?? null,
125
+ tools: overview.tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
126
+ resourceCount: overview.resourceCount,
127
+ promptCount: overview.promptCount,
128
+ },
129
+ () => {
130
+ const lines: string[] = [];
131
+
132
+ // Header: server name + version
133
+ const header = cyan.bold(overview.serverName);
134
+ if (overview.version) {
135
+ lines.push(`${header} ${dim(`v${overview.version.version}`)} ${dim(`(${overview.version.name})`)}`);
136
+ } else {
137
+ lines.push(header);
138
+ }
139
+
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
+ for (const k of present) lines.push(` ${green("✓")} ${k}`);
148
+ for (const k of absent) lines.push(` ${dim("✗")} ${dim(k)}`);
149
+ }
150
+
151
+ // Instructions
152
+ if (overview.instructions) {
153
+ lines.push("");
154
+ lines.push(bold("Instructions:"));
155
+ lines.push(` ${dim(overview.instructions)}`);
156
+ }
157
+
158
+ // Tools
159
+ lines.push("");
160
+ if (overview.tools.length === 0) {
161
+ lines.push(`${bold("Tools:")} ${dim("none")}`);
162
+ } else {
163
+ lines.push(bold(`Tools (${overview.tools.length}):`));
164
+ const maxName = Math.max(...overview.tools.map((t) => t.name.length));
165
+ const termWidth = getTerminalWidth();
166
+ for (let i = 0; i < overview.tools.length; i++) {
167
+ const t = overview.tools[i]!;
168
+ if (i > 0) lines.push("");
169
+ const name = ` ${bold(t.name.padEnd(maxName))}`;
170
+ if (t.description) {
171
+ const pw = visibleLength(name) + 2;
172
+ const desc = termWidth != null ? wrapDescription(t.description, pw, termWidth) : dim(t.description);
173
+ lines.push(`${name} ${desc}`);
174
+ } else {
175
+ lines.push(name);
176
+ }
177
+ }
178
+ }
179
+
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
+ );
196
191
  }
197
192
 
198
193
  /** Format a list of tools with server names */
199
194
  export function formatToolList(tools: ToolWithServer[], options: FormatOptions): string {
200
- return formatOutput(
201
- tools.map((t) => ({
202
- server: t.server,
203
- tool: t.tool.name,
204
- ...(options.withDescriptions ? { description: t.tool.description ?? "" } : {}),
205
- })),
206
- () =>
207
- formatTable(tools, {
208
- columns: [
209
- { value: (t) => t.server, style: cyan },
210
- { value: (t) => t.tool.name, style: bold },
211
- ],
212
- description: options.withDescriptions ? (t) => t.tool.description : undefined,
213
- emptyMessage: "No tools found",
214
- }),
215
- options,
216
- );
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
+ );
217
212
  }
218
213
 
219
214
  /** Format tools for a single server */
220
- export function formatServerTools(
221
- serverName: string,
222
- tools: Tool[],
223
- options: FormatOptions,
224
- ): string {
225
- return formatOutput(
226
- {
227
- server: serverName,
228
- tools: tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
229
- },
230
- () => {
231
- if (tools.length === 0) {
232
- return dim(`No tools found for ${serverName}`);
233
- }
234
- const header = cyan.bold(serverName);
235
- const body = formatTable(tools, {
236
- columns: [{ value: (t) => ` ${t.name}`, style: bold }],
237
- description: (t) => t.description,
238
- });
239
- return `${header}\n${body}`;
240
- },
241
- options,
242
- );
215
+ export function formatServerTools(serverName: string, tools: Tool[], options: FormatOptions): string {
216
+ return formatOutput(
217
+ {
218
+ server: serverName,
219
+ tools: tools.map((t) => ({ name: t.name, description: t.description ?? "" })),
220
+ },
221
+ () => {
222
+ if (tools.length === 0) {
223
+ return dim(`No tools found for ${serverName}`);
224
+ }
225
+ const header = cyan.bold(serverName);
226
+ const body = formatTable(tools, {
227
+ columns: [{ value: (t) => ` ${t.name}`, style: bold }],
228
+ description: (t) => t.description,
229
+ });
230
+ return `${header}\n${body}`;
231
+ },
232
+ options,
233
+ );
243
234
  }
244
235
 
245
236
  /** Format a tool schema */
246
237
  export function formatToolSchema(serverName: string, tool: Tool, options: FormatOptions): string {
247
- return formatOutput(
248
- {
249
- server: serverName,
250
- tool: tool.name,
251
- description: tool.description ?? "",
252
- inputSchema: tool.inputSchema,
253
- },
254
- () => {
255
- const lines: string[] = [];
256
- lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
257
- if (tool.description) lines.push(dim(tool.description));
258
- lines.push("");
259
- lines.push(bold("Input Schema:"));
260
- lines.push(formatSchema(tool.inputSchema, 2));
261
- return lines.join("\n");
262
- },
263
- options,
264
- );
238
+ return formatOutput(
239
+ {
240
+ server: serverName,
241
+ tool: tool.name,
242
+ description: tool.description ?? "",
243
+ inputSchema: tool.inputSchema,
244
+ },
245
+ () => {
246
+ const lines: string[] = [];
247
+ lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
248
+ if (tool.description) lines.push(dim(tool.description));
249
+ lines.push("");
250
+ lines.push(bold("Input Schema:"));
251
+ lines.push(formatSchema(tool.inputSchema, 2));
252
+ return lines.join("\n");
253
+ },
254
+ options,
255
+ );
265
256
  }
266
257
 
267
258
  /** Format a JSON schema as a readable parameter list */
268
259
  function formatSchema(schema: Tool["inputSchema"], indent: number): string {
269
- const pad = " ".repeat(indent);
270
- const properties = schema.properties ?? {};
271
- const required = new Set(schema.required ?? []);
260
+ const pad = " ".repeat(indent);
261
+ const properties = schema.properties ?? {};
262
+ const required = new Set(schema.required ?? []);
272
263
 
273
- if (Object.keys(properties).length === 0) {
274
- return `${pad}${dim("(no parameters)")}`;
275
- }
264
+ if (Object.keys(properties).length === 0) {
265
+ return `${pad}${dim("(no parameters)")}`;
266
+ }
276
267
 
277
- return Object.entries(properties)
278
- .map(([name, prop]) => {
279
- const p = prop as Record<string, unknown>;
280
- const type = (p.type as string) ?? "any";
281
- const req = required.has(name) ? red("*") : "";
282
- const desc = p.description ? ` ${dim(String(p.description))}` : "";
283
- return `${pad}${green(name)}${req} ${dim(`(${type})`)}${desc}`;
284
- })
285
- .join("\n");
268
+ return Object.entries(properties)
269
+ .map(([name, prop]) => {
270
+ const p = prop as Record<string, unknown>;
271
+ const type = (p.type as string) ?? "any";
272
+ const req = required.has(name) ? red("*") : "";
273
+ const desc = p.description ? ` ${dim(String(p.description))}` : "";
274
+ return `${pad}${green(name)}${req} ${dim(`(${type})`)}${desc}`;
275
+ })
276
+ .join("\n");
286
277
  }
287
278
 
288
279
  /** Format detailed tool help with example payload */
289
280
  export function formatToolHelp(serverName: string, tool: Tool, options: FormatOptions): string {
290
- return formatOutput(
291
- {
292
- server: serverName,
293
- tool: tool.name,
294
- description: tool.description ?? "",
295
- inputSchema: tool.inputSchema,
296
- example: generateExample(tool.inputSchema),
297
- },
298
- () => {
299
- const lines: string[] = [];
300
- lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
301
- if (tool.description) lines.push(dim(tool.description));
302
- lines.push("");
303
- lines.push(bold("Parameters:"));
304
- lines.push(formatSchema(tool.inputSchema, 2));
305
- const example = generateExample(tool.inputSchema);
306
- lines.push("");
307
- lines.push(bold("Example:"));
308
- lines.push(dim(` mcpx call ${serverName} ${tool.name} '${JSON.stringify(example)}'`));
309
- return lines.join("\n");
310
- },
311
- options,
312
- );
281
+ return formatOutput(
282
+ {
283
+ server: serverName,
284
+ tool: tool.name,
285
+ description: tool.description ?? "",
286
+ inputSchema: tool.inputSchema,
287
+ example: generateExample(tool.inputSchema),
288
+ },
289
+ () => {
290
+ const lines: string[] = [];
291
+ lines.push(`${cyan(serverName)}/${bold(tool.name)}`);
292
+ if (tool.description) lines.push(dim(tool.description));
293
+ lines.push("");
294
+ lines.push(bold("Parameters:"));
295
+ lines.push(formatSchema(tool.inputSchema, 2));
296
+ const example = generateExample(tool.inputSchema);
297
+ lines.push("");
298
+ lines.push(bold("Example:"));
299
+ lines.push(dim(` mcpx call ${serverName} ${tool.name} '${JSON.stringify(example)}'`));
300
+ return lines.join("\n");
301
+ },
302
+ options,
303
+ );
313
304
  }
314
305
 
315
306
  /** Generate an example payload from a JSON schema */
316
307
  function generateExample(schema: Tool["inputSchema"]): Record<string, unknown> {
317
- const properties = schema.properties ?? {};
318
- const required = new Set(schema.required ?? []);
319
- const example: Record<string, unknown> = {};
308
+ const properties = schema.properties ?? {};
309
+ const required = new Set(schema.required ?? []);
310
+ const example: Record<string, unknown> = {};
320
311
 
321
- for (const [name, prop] of Object.entries(properties)) {
322
- const p = prop as Record<string, unknown>;
323
- if (required.has(name) || Object.keys(example).length < 3) {
324
- example[name] = exampleValue(name, p);
325
- }
326
- }
312
+ for (const [name, prop] of Object.entries(properties)) {
313
+ const p = prop as Record<string, unknown>;
314
+ if (required.has(name) || Object.keys(example).length < 3) {
315
+ example[name] = exampleValue(name, p);
316
+ }
317
+ }
327
318
 
328
- return example;
319
+ return example;
329
320
  }
330
321
 
331
322
  function exampleValue(name: string, prop: Record<string, unknown>): unknown {
332
- if (Array.isArray(prop.enum) && prop.enum.length > 0) return prop.enum[0];
333
- if (prop.default !== undefined) return prop.default;
334
-
335
- const type = prop.type as string | undefined;
336
- switch (type) {
337
- case "string":
338
- return `<${name}>`;
339
- case "number":
340
- case "integer":
341
- return 0;
342
- case "boolean":
343
- return true;
344
- case "array":
345
- return [];
346
- case "object":
347
- return {};
348
- default:
349
- return `<${name}>`;
350
- }
323
+ if (Array.isArray(prop.enum) && prop.enum.length > 0) return prop.enum[0];
324
+ if (prop.default !== undefined) return prop.default;
325
+
326
+ const type = prop.type as string | undefined;
327
+ switch (type) {
328
+ case "string":
329
+ return `<${name}>`;
330
+ case "number":
331
+ case "integer":
332
+ return 0;
333
+ case "boolean":
334
+ return true;
335
+ case "array":
336
+ return [];
337
+ case "object":
338
+ return {};
339
+ default:
340
+ return `<${name}>`;
341
+ }
351
342
  }
352
343
 
353
344
  /** Format a tool call result, dispatching on the --format option */
354
345
  export function formatCallResult(result: unknown, options: FormatOptions): string {
355
- const format = options.format ?? "json";
346
+ const format = options.format ?? "json";
356
347
 
357
- switch (format) {
358
- case "markdown":
359
- return formatCallResultAsMarkdown(result);
360
- case "json":
361
- default:
362
- return JSON.stringify(parseNestedJson(result), null, 2);
363
- }
348
+ switch (format) {
349
+ case "markdown":
350
+ return formatCallResultAsMarkdown(result);
351
+ default:
352
+ return JSON.stringify(parseNestedJson(result), null, 2);
353
+ }
364
354
  }
365
355
 
366
356
  /** Render an MCP tool call result as styled markdown for terminal output */
367
357
  function formatCallResultAsMarkdown(result: unknown): string {
368
- const r = result as {
369
- content?: Array<{
370
- type: string;
371
- text?: string;
372
- data?: string;
373
- mimeType?: string;
374
- uri?: string;
375
- }>;
376
- isError?: boolean;
377
- };
378
-
379
- if (!r.content || !Array.isArray(r.content) || r.content.length === 0) {
380
- return renderMarkdownToAnsi(jsonToMarkdown(result));
381
- }
382
-
383
- const parts: string[] = [];
384
-
385
- for (const block of r.content) {
386
- switch (block.type) {
387
- case "text":
388
- if (block.text !== undefined) {
389
- try {
390
- const parsed = JSON.parse(block.text);
391
- parts.push(jsonToMarkdown(parsed));
392
- } catch {
393
- // Plain text / already markdown — pass through as-is
394
- parts.push(block.text);
395
- }
396
- }
397
- break;
398
- case "image":
399
- parts.push(
400
- `[image: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
401
- );
402
- break;
403
- case "resource":
404
- parts.push(`[resource: ${block.uri ?? "unknown"}]`);
405
- break;
406
- default:
407
- parts.push(`[${block.type}]`);
408
- break;
409
- }
410
- }
411
-
412
- let output = parts.join("\n\n");
413
- if (r.isError) {
414
- output = `**error:** ${output}`;
415
- }
416
- return renderMarkdownToAnsi(output);
358
+ const r = result as {
359
+ content?: Array<{
360
+ type: string;
361
+ text?: string;
362
+ data?: string;
363
+ mimeType?: string;
364
+ uri?: string;
365
+ }>;
366
+ isError?: boolean;
367
+ };
368
+
369
+ if (!r.content || !Array.isArray(r.content) || r.content.length === 0) {
370
+ return renderMarkdownToAnsi(jsonToMarkdown(result));
371
+ }
372
+
373
+ const parts: string[] = [];
374
+
375
+ for (const block of r.content) {
376
+ switch (block.type) {
377
+ case "text":
378
+ if (block.text !== undefined) {
379
+ try {
380
+ const parsed = JSON.parse(block.text);
381
+ parts.push(jsonToMarkdown(parsed));
382
+ } catch {
383
+ // Plain text / already markdown — pass through as-is
384
+ parts.push(block.text);
385
+ }
386
+ }
387
+ break;
388
+ case "image":
389
+ parts.push(
390
+ `[image: ${block.mimeType ?? "unknown type"}, ${block.data ? Math.ceil((block.data.length * 3) / 4) : 0} bytes]`,
391
+ );
392
+ break;
393
+ case "resource":
394
+ parts.push(`[resource: ${block.uri ?? "unknown"}]`);
395
+ break;
396
+ default:
397
+ parts.push(`[${block.type}]`);
398
+ break;
399
+ }
400
+ }
401
+
402
+ let output = parts.join("\n\n");
403
+ if (r.isError) {
404
+ output = `**error:** ${output}`;
405
+ }
406
+ return renderMarkdownToAnsi(output);
417
407
  }
418
408
 
419
409
  /** Convert a key name like "display_name" to "Display Name" */
420
410
  function humanizeKey(key: string): string {
421
- return key
422
- .replace(/[_-]/g, " ")
423
- .replace(/([a-z])([A-Z])/g, "$1 $2")
424
- .replace(/\b\w/g, (c) => c.toUpperCase());
411
+ return key
412
+ .replace(/[_-]/g, " ")
413
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
414
+ .replace(/\b\w/g, (c) => c.toUpperCase());
425
415
  }
426
416
 
427
417
  /** Check if a value is a plain primitive (string, number, boolean, null) */
428
418
  function isPrimitive(value: unknown): value is string | number | boolean | null {
429
- return value === null || typeof value !== "object";
419
+ return value === null || typeof value !== "object";
430
420
  }
431
421
 
432
422
  /**
@@ -437,107 +427,107 @@ let urlCounter = 0;
437
427
  let urlMap = new Map<string, string>();
438
428
 
439
429
  function resetUrlPlaceholders(): void {
440
- urlCounter = 0;
441
- urlMap = new Map();
430
+ urlCounter = 0;
431
+ urlMap = new Map();
442
432
  }
443
433
 
444
434
  function restoreUrlPlaceholders(ansiOutput: string): string {
445
- for (const [token, url] of urlMap) {
446
- ansiOutput = ansiOutput.replace(token, `\x1b[34m\x1b[4m${url}\x1b[24m\x1b[39m`);
447
- }
448
- return ansiOutput;
435
+ for (const [token, url] of urlMap) {
436
+ ansiOutput = ansiOutput.replace(token, `\x1b[34m\x1b[4m${url}\x1b[24m\x1b[39m`);
437
+ }
438
+ return ansiOutput;
449
439
  }
450
440
 
451
441
  /** Format a primitive value, replacing URLs with placeholders to avoid mangling */
452
442
  function formatPrimitive(value: string | number | boolean | null): string {
453
- const str = String(value ?? "null");
454
- if (typeof value === "string" && /^https?:\/\/\S+$/.test(value)) {
455
- const token = `URLPLACEHOLDER${urlCounter++}`;
456
- urlMap.set(token, str);
457
- return token;
458
- }
459
- return str;
443
+ const str = String(value ?? "null");
444
+ if (typeof value === "string" && /^https?:\/\/\S+$/.test(value)) {
445
+ const token = `URLPLACEHOLDER${urlCounter++}`;
446
+ urlMap.set(token, str);
447
+ return token;
448
+ }
449
+ return str;
460
450
  }
461
451
 
462
452
  /** Normalize a key for label matching: lowercase, strip underscores/hyphens */
463
453
  function normalizeKey(key: string): string {
464
- return key.replace(/[_-]/g, "").toLowerCase();
454
+ return key.replace(/[_-]/g, "").toLowerCase();
465
455
  }
466
456
 
467
457
  /** Priority-ordered label keys (checked after normalization) */
468
458
  const LABEL_KEYS = [
469
- "name",
470
- "displayname",
471
- "fullname",
472
- "username",
473
- "screenname",
474
- "title",
475
- "subject",
476
- "headline",
477
- "heading",
478
- "label",
479
- "description",
480
- "summary",
481
- "email",
482
- "url",
483
- "slug",
484
- "key",
485
- "identifier",
459
+ "name",
460
+ "displayname",
461
+ "fullname",
462
+ "username",
463
+ "screenname",
464
+ "title",
465
+ "subject",
466
+ "headline",
467
+ "heading",
468
+ "label",
469
+ "description",
470
+ "summary",
471
+ "email",
472
+ "url",
473
+ "slug",
474
+ "key",
475
+ "identifier",
486
476
  ];
487
477
 
488
478
  /** Find the best label field in an object, returning { originalKey, value } or null */
489
479
  function findLabel(obj: Record<string, unknown>): { originalKey: string; value: string } | null {
490
- const entries = Object.entries(obj);
491
- for (const candidate of LABEL_KEYS) {
492
- for (const [key, val] of entries) {
493
- if (normalizeKey(key) === candidate && typeof val === "string" && val.length > 0) {
494
- return { originalKey: key, value: val };
495
- }
496
- }
497
- }
498
- return null;
480
+ const entries = Object.entries(obj);
481
+ for (const candidate of LABEL_KEYS) {
482
+ for (const [key, val] of entries) {
483
+ if (normalizeKey(key) === candidate && typeof val === "string" && val.length > 0) {
484
+ return { originalKey: key, value: val };
485
+ }
486
+ }
487
+ }
488
+ return null;
499
489
  }
500
490
 
501
491
  /** Render object entries as an indented bullet list */
502
492
  function objectToBullets(entries: [string, unknown][], indent: number, skipKey?: string): string {
503
- const prefix = " ".repeat(indent);
504
- const lines: string[] = [];
505
-
506
- for (const [key, val] of entries) {
507
- if (key === skipKey) continue;
508
- const heading = humanizeKey(key);
509
-
510
- if (isPrimitive(val)) {
511
- lines.push(`${prefix}- **${heading}:** ${formatPrimitive(val)}`);
512
- } else if (Array.isArray(val) && val.every(isPrimitive)) {
513
- lines.push(`${prefix}- **${heading}:**`);
514
- for (const v of val) {
515
- lines.push(`${prefix} - ${formatPrimitive(v)}`);
516
- }
517
- } else if (Array.isArray(val)) {
518
- lines.push(`${prefix}- **${heading}:**`);
519
- for (const item of val) {
520
- if (isPrimitive(item)) {
521
- lines.push(`${prefix} - ${formatPrimitive(item)}`);
522
- } else {
523
- const itemObj = item as Record<string, unknown>;
524
- const label = findLabel(itemObj);
525
- if (label) {
526
- lines.push(`${prefix} - ${label.value}`);
527
- lines.push(objectToBullets(Object.entries(itemObj), indent + 4, label.originalKey));
528
- } else {
529
- lines.push(`${prefix} -`);
530
- lines.push(objectToBullets(Object.entries(itemObj), indent + 4));
531
- }
532
- }
533
- }
534
- } else {
535
- lines.push(`${prefix}- **${heading}:**`);
536
- lines.push(objectToBullets(Object.entries(val as Record<string, unknown>), indent + 2));
537
- }
538
- }
539
-
540
- return lines.join("\n");
493
+ const prefix = " ".repeat(indent);
494
+ const lines: string[] = [];
495
+
496
+ for (const [key, val] of entries) {
497
+ if (key === skipKey) continue;
498
+ const heading = humanizeKey(key);
499
+
500
+ if (isPrimitive(val)) {
501
+ lines.push(`${prefix}- **${heading}:** ${formatPrimitive(val)}`);
502
+ } else if (Array.isArray(val) && val.every(isPrimitive)) {
503
+ lines.push(`${prefix}- **${heading}:**`);
504
+ for (const v of val) {
505
+ lines.push(`${prefix} - ${formatPrimitive(v)}`);
506
+ }
507
+ } else if (Array.isArray(val)) {
508
+ lines.push(`${prefix}- **${heading}:**`);
509
+ for (const item of val) {
510
+ if (isPrimitive(item)) {
511
+ lines.push(`${prefix} - ${formatPrimitive(item)}`);
512
+ } else {
513
+ const itemObj = item as Record<string, unknown>;
514
+ const label = findLabel(itemObj);
515
+ if (label) {
516
+ lines.push(`${prefix} - ${label.value}`);
517
+ lines.push(objectToBullets(Object.entries(itemObj), indent + 4, label.originalKey));
518
+ } else {
519
+ lines.push(`${prefix} -`);
520
+ lines.push(objectToBullets(Object.entries(itemObj), indent + 4));
521
+ }
522
+ }
523
+ }
524
+ } else {
525
+ lines.push(`${prefix}- **${heading}:**`);
526
+ lines.push(objectToBullets(Object.entries(val as Record<string, unknown>), indent + 2));
527
+ }
528
+ }
529
+
530
+ return lines.join("\n");
541
531
  }
542
532
 
543
533
  /**
@@ -546,471 +536,457 @@ function objectToBullets(entries: [string, unknown][], indent: number, skipKey?:
546
536
  * Arrays of objects use a label field (name, title, etc.) in the heading when available.
547
537
  */
548
538
  export function jsonToMarkdown(value: unknown, depth: number = 1, skipKey?: string): string {
549
- if (isPrimitive(value)) {
550
- return formatPrimitive(value);
551
- }
552
-
553
- // At depth >= 3, switch to bullet-list rendering
554
- if (depth >= 3) {
555
- if (Array.isArray(value)) {
556
- if (value.every(isPrimitive)) {
557
- return value.map((v) => `- ${formatPrimitive(v)}`).join("\n");
558
- }
559
- return value
560
- .map((item) => {
561
- if (isPrimitive(item)) return `- ${formatPrimitive(item)}`;
562
- const obj = item as Record<string, unknown>;
563
- const label = findLabel(obj);
564
- const header = label ? `- ${label.value}` : `-`;
565
- return `${header}\n${objectToBullets(Object.entries(obj), 2, label?.originalKey)}`;
566
- })
567
- .join("\n");
568
- }
569
- return objectToBullets(Object.entries(value as Record<string, unknown>), 0, skipKey);
570
- }
571
-
572
- if (Array.isArray(value)) {
573
- // Array of all primitives → bullet list
574
- if (value.every(isPrimitive)) {
575
- return value.map((v) => `- ${formatPrimitive(v)}`).join("\n");
576
- }
577
- // Array of objects → numbered sub-sections with label
578
- return value
579
- .map((item, i) => {
580
- if (isPrimitive(item)) {
581
- return `- ${formatPrimitive(item)}`;
582
- }
583
- const obj = item as Record<string, unknown>;
584
- const labelInfo = findLabel(obj);
585
- const numberLabel = labelInfo ? `${i + 1}. ${labelInfo.value}` : `${i + 1}`;
586
- const heading = depth <= 6 ? `${"#".repeat(depth)} ${numberLabel}` : `**${numberLabel}**`;
587
- return `${heading}\n\n${jsonToMarkdown(item, depth + 1, labelInfo?.originalKey)}`;
588
- })
589
- .join("\n\n");
590
- }
591
-
592
- // Object → each key becomes a heading
593
- const entries = Object.entries(value as Record<string, unknown>);
594
- const lines: string[] = [];
595
-
596
- for (const [key, val] of entries) {
597
- const heading = humanizeKey(key);
598
-
599
- if (isPrimitive(val)) {
600
- if (depth <= 6) {
601
- lines.push(`${"#".repeat(depth)} ${heading}\n\n${formatPrimitive(val)}`);
602
- } else {
603
- lines.push(`**${heading}:** ${formatPrimitive(val)}`);
604
- }
605
- } else if (Array.isArray(val) && val.every(isPrimitive)) {
606
- // Array of primitives: heading then bullet list
607
- const list = val.map((v) => `- ${formatPrimitive(v)}`).join("\n");
608
- if (depth <= 6) {
609
- lines.push(`${"#".repeat(depth)} ${heading}\n\n${list}`);
610
- } else {
611
- lines.push(`**${heading}:**\n${list}`);
612
- }
613
- } else {
614
- // Nested object or array of objects
615
- const label = depth <= 6 ? `${"#".repeat(depth)} ${heading}` : `**${heading}**`;
616
- lines.push(`${label}\n\n${jsonToMarkdown(val, depth + 1)}`);
617
- }
618
- }
619
-
620
- return lines.join("\n\n");
539
+ if (isPrimitive(value)) {
540
+ return formatPrimitive(value);
541
+ }
542
+
543
+ // At depth >= 3, switch to bullet-list rendering
544
+ if (depth >= 3) {
545
+ if (Array.isArray(value)) {
546
+ if (value.every(isPrimitive)) {
547
+ return value.map((v) => `- ${formatPrimitive(v)}`).join("\n");
548
+ }
549
+ return value
550
+ .map((item) => {
551
+ if (isPrimitive(item)) return `- ${formatPrimitive(item)}`;
552
+ const obj = item as Record<string, unknown>;
553
+ const label = findLabel(obj);
554
+ const header = label ? `- ${label.value}` : `-`;
555
+ return `${header}\n${objectToBullets(Object.entries(obj), 2, label?.originalKey)}`;
556
+ })
557
+ .join("\n");
558
+ }
559
+ return objectToBullets(Object.entries(value as Record<string, unknown>), 0, skipKey);
560
+ }
561
+
562
+ if (Array.isArray(value)) {
563
+ // Array of all primitives → bullet list
564
+ if (value.every(isPrimitive)) {
565
+ return value.map((v) => `- ${formatPrimitive(v)}`).join("\n");
566
+ }
567
+ // Array of objects → numbered sub-sections with label
568
+ return value
569
+ .map((item, i) => {
570
+ if (isPrimitive(item)) {
571
+ return `- ${formatPrimitive(item)}`;
572
+ }
573
+ const obj = item as Record<string, unknown>;
574
+ const labelInfo = findLabel(obj);
575
+ const numberLabel = labelInfo ? `${i + 1}. ${labelInfo.value}` : `${i + 1}`;
576
+ const heading = depth <= 6 ? `${"#".repeat(depth)} ${numberLabel}` : `**${numberLabel}**`;
577
+ return `${heading}\n\n${jsonToMarkdown(item, depth + 1, labelInfo?.originalKey)}`;
578
+ })
579
+ .join("\n\n");
580
+ }
581
+
582
+ // Object → each key becomes a heading
583
+ const entries = Object.entries(value as Record<string, unknown>);
584
+ const lines: string[] = [];
585
+
586
+ for (const [key, val] of entries) {
587
+ const heading = humanizeKey(key);
588
+
589
+ if (isPrimitive(val)) {
590
+ if (depth <= 6) {
591
+ lines.push(`${"#".repeat(depth)} ${heading}\n\n${formatPrimitive(val)}`);
592
+ } else {
593
+ lines.push(`**${heading}:** ${formatPrimitive(val)}`);
594
+ }
595
+ } else if (Array.isArray(val) && val.every(isPrimitive)) {
596
+ // Array of primitives: heading then bullet list
597
+ const list = val.map((v) => `- ${formatPrimitive(v)}`).join("\n");
598
+ if (depth <= 6) {
599
+ lines.push(`${"#".repeat(depth)} ${heading}\n\n${list}`);
600
+ } else {
601
+ lines.push(`**${heading}:**\n${list}`);
602
+ }
603
+ } else {
604
+ // Nested object or array of objects
605
+ const label = depth <= 6 ? `${"#".repeat(depth)} ${heading}` : `**${heading}**`;
606
+ lines.push(`${label}\n\n${jsonToMarkdown(val, depth + 1)}`);
607
+ }
608
+ }
609
+
610
+ return lines.join("\n\n");
621
611
  }
622
612
 
623
613
  /** Render a markdown string to ANSI-styled terminal output using Bun's built-in renderer */
624
614
  export function renderMarkdownToAnsi(input: string): string {
625
- const result = Bun.markdown.ansi(input);
626
- const restored = restoreUrlPlaceholders(result);
627
- resetUrlPlaceholders();
628
- return restored;
615
+ // biome-ignore lint/suspicious/noExplicitAny: Bun.markdown.ansi is not yet in @types/bun
616
+ const result = (Bun as any).markdown.ansi(input) as string;
617
+ const restored = restoreUrlPlaceholders(result);
618
+ resetUrlPlaceholders();
619
+ return restored;
629
620
  }
630
621
 
631
622
  /** Recursively parse JSON strings inside MCP content blocks */
632
623
  function parseNestedJson(value: unknown): unknown {
633
- if (typeof value === "string") {
634
- try {
635
- return parseNestedJson(JSON.parse(value));
636
- } catch {
637
- return value;
638
- }
639
- }
640
- if (Array.isArray(value)) {
641
- return value.map(parseNestedJson);
642
- }
643
- if (typeof value === "object" && value !== null) {
644
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v)]));
645
- }
646
- return value;
624
+ if (typeof value === "string") {
625
+ try {
626
+ return parseNestedJson(JSON.parse(value));
627
+ } catch {
628
+ return value;
629
+ }
630
+ }
631
+ if (Array.isArray(value)) {
632
+ return value.map(parseNestedJson);
633
+ }
634
+ if (typeof value === "object" && value !== null) {
635
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v)]));
636
+ }
637
+ return value;
647
638
  }
648
639
 
649
640
  /** Format validation errors for tool input */
650
641
  export function formatValidationErrors(
651
- serverName: string,
652
- toolName: string,
653
- errors: ValidationError[],
654
- options: FormatOptions,
642
+ serverName: string,
643
+ toolName: string,
644
+ errors: ValidationError[],
645
+ options: FormatOptions,
655
646
  ): string {
656
- return formatOutput(
657
- { error: "validation", server: serverName, tool: toolName, details: errors },
658
- () => {
659
- const header = `${red("error:")} invalid arguments for ${cyan(serverName)}/${bold(toolName)}`;
660
- const details = errors.map((e) => ` ${yellow(e.path)}: ${e.message}`).join("\n");
661
- return `${header}\n${details}`;
662
- },
663
- options,
664
- );
647
+ return formatOutput(
648
+ { error: "validation", server: serverName, tool: toolName, details: errors },
649
+ () => {
650
+ const header = `${red("error:")} invalid arguments for ${cyan(serverName)}/${bold(toolName)}`;
651
+ const details = errors.map((e) => ` ${yellow(e.path)}: ${e.message}`).join("\n");
652
+ return `${header}\n${details}`;
653
+ },
654
+ options,
655
+ );
665
656
  }
666
657
 
667
658
  /** Format search results */
668
659
  export function formatSearchResults(results: SearchResult[], options: FormatOptions): string {
669
- return formatOutput(
670
- results,
671
- () => {
672
- if (results.length === 0) {
673
- return dim("No matching tools found");
674
- }
675
-
676
- const termWidth = getTerminalWidth();
677
- const descIndent = 2;
678
-
679
- return results
680
- .map((r) => {
681
- const header = `${cyan(r.server)} ${bold(r.tool)} ${yellow(r.score.toFixed(2))}`;
682
- const fullDesc = r.description
683
- .split("\n")
684
- .map((l) => l.trim())
685
- .filter((l) => l.length > 0)
686
- .join(" ");
687
- const indent = " ".repeat(descIndent);
688
- const desc =
689
- termWidth != null ? wrapDescription(fullDesc, descIndent, termWidth) : dim(fullDesc);
690
- return `${header}\n${indent}${desc}`;
691
- })
692
- .join("\n\n");
693
- },
694
- options,
695
- );
660
+ return formatOutput(
661
+ results,
662
+ () => {
663
+ if (results.length === 0) {
664
+ return dim("No matching tools found");
665
+ }
666
+
667
+ const termWidth = getTerminalWidth();
668
+ const descIndent = 2;
669
+
670
+ return results
671
+ .map((r) => {
672
+ const header = `${cyan(r.server)} ${bold(r.tool)} ${yellow(r.score.toFixed(2))}`;
673
+ const fullDesc = r.description
674
+ .split("\n")
675
+ .map((l) => l.trim())
676
+ .filter((l) => l.length > 0)
677
+ .join(" ");
678
+ const indent = " ".repeat(descIndent);
679
+ const desc = termWidth != null ? wrapDescription(fullDesc, descIndent, termWidth) : dim(fullDesc);
680
+ return `${header}\n${indent}${desc}`;
681
+ })
682
+ .join("\n\n");
683
+ },
684
+ options,
685
+ );
696
686
  }
697
687
 
698
688
  /** Format a list of resources with server names */
699
- export function formatResourceList(
700
- resources: ResourceWithServer[],
701
- options: FormatOptions,
702
- ): string {
703
- return formatOutput(
704
- resources.map((r) => ({
705
- server: r.server,
706
- uri: r.resource.uri,
707
- name: r.resource.name,
708
- ...(options.withDescriptions ? { description: r.resource.description ?? "" } : {}),
709
- })),
710
- () =>
711
- formatTable(resources, {
712
- columns: [
713
- { value: (r) => r.server, style: cyan },
714
- { value: (r) => r.resource.uri, style: bold },
715
- ],
716
- description: options.withDescriptions ? (r) => r.resource.description : undefined,
717
- emptyMessage: "No resources found",
718
- }),
719
- options,
720
- );
689
+ export function formatResourceList(resources: ResourceWithServer[], options: FormatOptions): string {
690
+ return formatOutput(
691
+ resources.map((r) => ({
692
+ server: r.server,
693
+ uri: r.resource.uri,
694
+ name: r.resource.name,
695
+ ...(options.withDescriptions ? { description: r.resource.description ?? "" } : {}),
696
+ })),
697
+ () =>
698
+ formatTable(resources, {
699
+ columns: [
700
+ { value: (r) => r.server, style: cyan },
701
+ { value: (r) => r.resource.uri, style: bold },
702
+ ],
703
+ description: options.withDescriptions ? (r) => r.resource.description : undefined,
704
+ emptyMessage: "No resources found",
705
+ }),
706
+ options,
707
+ );
721
708
  }
722
709
 
723
710
  /** Format resources for a single server */
724
- export function formatServerResources(
725
- serverName: string,
726
- resources: Resource[],
727
- options: FormatOptions,
728
- ): string {
729
- return formatOutput(
730
- {
731
- server: serverName,
732
- resources: resources.map((r) => ({
733
- uri: r.uri,
734
- name: r.name,
735
- description: r.description ?? "",
736
- mimeType: r.mimeType ?? "",
737
- })),
738
- },
739
- () => {
740
- if (resources.length === 0) {
741
- return dim(`No resources found for ${serverName}`);
742
- }
743
- const header = cyan.bold(serverName);
744
- const body = formatTable(resources, {
745
- columns: [{ value: (r) => ` ${r.uri}`, style: bold }],
746
- description: (r) => r.description,
747
- });
748
- return `${header}\n${body}`;
749
- },
750
- options,
751
- );
711
+ export function formatServerResources(serverName: string, resources: Resource[], options: FormatOptions): string {
712
+ return formatOutput(
713
+ {
714
+ server: serverName,
715
+ resources: resources.map((r) => ({
716
+ uri: r.uri,
717
+ name: r.name,
718
+ description: r.description ?? "",
719
+ mimeType: r.mimeType ?? "",
720
+ })),
721
+ },
722
+ () => {
723
+ if (resources.length === 0) {
724
+ return dim(`No resources found for ${serverName}`);
725
+ }
726
+ const header = cyan.bold(serverName);
727
+ const body = formatTable(resources, {
728
+ columns: [{ value: (r) => ` ${r.uri}`, style: bold }],
729
+ description: (r) => r.description,
730
+ });
731
+ return `${header}\n${body}`;
732
+ },
733
+ options,
734
+ );
752
735
  }
753
736
 
754
737
  /** Format resource contents */
755
738
  export function formatResourceContents(
756
- serverName: string,
757
- uri: string,
758
- result: unknown,
759
- options: FormatOptions,
739
+ serverName: string,
740
+ uri: string,
741
+ result: unknown,
742
+ options: FormatOptions,
760
743
  ): string {
761
- return formatOutput(
762
- { server: serverName, uri, contents: (result as { contents: unknown })?.contents ?? result },
763
- () => {
764
- const contents =
765
- (result as { contents?: Array<{ text?: string; blob?: string; mimeType?: string }> })
766
- ?.contents ?? [];
767
- const lines: string[] = [];
768
- lines.push(`${cyan(serverName)}/${bold(uri)}`);
769
- lines.push("");
770
-
771
- if (contents.length === 0) {
772
- lines.push(dim("(empty)"));
773
- } else {
774
- for (const item of contents) {
775
- if (item.text !== undefined) {
776
- lines.push(item.text);
777
- } else if (item.blob !== undefined) {
778
- lines.push(dim(`<binary blob, ${item.blob.length} bytes base64>`));
779
- }
780
- }
781
- }
782
-
783
- return lines.join("\n");
784
- },
785
- options,
786
- );
744
+ return formatOutput(
745
+ { server: serverName, uri, contents: (result as { contents: unknown })?.contents ?? result },
746
+ () => {
747
+ const contents =
748
+ (result as { contents?: Array<{ text?: string; blob?: string; mimeType?: string }> })?.contents ?? [];
749
+ const lines: string[] = [];
750
+ lines.push(`${cyan(serverName)}/${bold(uri)}`);
751
+ lines.push("");
752
+
753
+ if (contents.length === 0) {
754
+ lines.push(dim("(empty)"));
755
+ } else {
756
+ for (const item of contents) {
757
+ if (item.text !== undefined) {
758
+ lines.push(item.text);
759
+ } else if (item.blob !== undefined) {
760
+ lines.push(dim(`<binary blob, ${item.blob.length} bytes base64>`));
761
+ }
762
+ }
763
+ }
764
+
765
+ return lines.join("\n");
766
+ },
767
+ options,
768
+ );
787
769
  }
788
770
 
789
771
  /** Format a list of prompts with server names */
790
772
  export function formatPromptList(prompts: PromptWithServer[], options: FormatOptions): string {
791
- return formatOutput(
792
- prompts.map((p) => ({
793
- server: p.server,
794
- name: p.prompt.name,
795
- ...(options.withDescriptions ? { description: p.prompt.description ?? "" } : {}),
796
- })),
797
- () =>
798
- formatTable(prompts, {
799
- columns: [
800
- { value: (p) => p.server, style: cyan },
801
- { value: (p) => p.prompt.name, style: bold },
802
- ],
803
- description: options.withDescriptions ? (p) => p.prompt.description : undefined,
804
- emptyMessage: "No prompts found",
805
- }),
806
- options,
807
- );
773
+ return formatOutput(
774
+ prompts.map((p) => ({
775
+ server: p.server,
776
+ name: p.prompt.name,
777
+ ...(options.withDescriptions ? { description: p.prompt.description ?? "" } : {}),
778
+ })),
779
+ () =>
780
+ formatTable(prompts, {
781
+ columns: [
782
+ { value: (p) => p.server, style: cyan },
783
+ { value: (p) => p.prompt.name, style: bold },
784
+ ],
785
+ description: options.withDescriptions ? (p) => p.prompt.description : undefined,
786
+ emptyMessage: "No prompts found",
787
+ }),
788
+ options,
789
+ );
808
790
  }
809
791
 
810
792
  /** Format prompts for a single server */
811
- export function formatServerPrompts(
812
- serverName: string,
813
- prompts: Prompt[],
814
- options: FormatOptions,
815
- ): string {
816
- return formatOutput(
817
- {
818
- server: serverName,
819
- prompts: prompts.map((p) => ({
820
- name: p.name,
821
- description: p.description ?? "",
822
- arguments: p.arguments ?? [],
823
- })),
824
- },
825
- () => {
826
- if (prompts.length === 0) {
827
- return dim(`No prompts found for ${serverName}`);
828
- }
829
-
830
- const header = cyan.bold(serverName);
831
- const maxName = Math.max(...prompts.map((p) => p.name.length));
832
- const termWidth = getTerminalWidth();
833
-
834
- const lines = prompts.map((p) => {
835
- const name = ` ${bold(p.name.padEnd(maxName))}`;
836
- const args =
837
- p.arguments && p.arguments.length > 0
838
- ? ` ${dim(`(${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(", ")})`)}`
839
- : "";
840
- if (p.description) {
841
- const prefix = `${name}${args}`;
842
- const pw = visibleLength(prefix) + 2;
843
- const desc =
844
- termWidth != null ? wrapDescription(p.description, pw, termWidth) : dim(p.description);
845
- return `${prefix} ${desc}`;
846
- }
847
- return `${name}${args}`;
848
- });
849
-
850
- return [header, ...lines].join("\n");
851
- },
852
- options,
853
- );
793
+ export function formatServerPrompts(serverName: string, prompts: Prompt[], options: FormatOptions): string {
794
+ return formatOutput(
795
+ {
796
+ server: serverName,
797
+ prompts: prompts.map((p) => ({
798
+ name: p.name,
799
+ description: p.description ?? "",
800
+ arguments: p.arguments ?? [],
801
+ })),
802
+ },
803
+ () => {
804
+ if (prompts.length === 0) {
805
+ return dim(`No prompts found for ${serverName}`);
806
+ }
807
+
808
+ const header = cyan.bold(serverName);
809
+ const maxName = Math.max(...prompts.map((p) => p.name.length));
810
+ const termWidth = getTerminalWidth();
811
+
812
+ const lines = prompts.map((p) => {
813
+ const name = ` ${bold(p.name.padEnd(maxName))}`;
814
+ const args =
815
+ p.arguments && p.arguments.length > 0
816
+ ? ` ${dim(`(${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(", ")})`)}`
817
+ : "";
818
+ if (p.description) {
819
+ const prefix = `${name}${args}`;
820
+ const pw = visibleLength(prefix) + 2;
821
+ const desc = termWidth != null ? wrapDescription(p.description, pw, termWidth) : dim(p.description);
822
+ return `${prefix} ${desc}`;
823
+ }
824
+ return `${name}${args}`;
825
+ });
826
+
827
+ return [header, ...lines].join("\n");
828
+ },
829
+ options,
830
+ );
854
831
  }
855
832
 
856
833
  /** Format prompt messages */
857
834
  export function formatPromptMessages(
858
- serverName: string,
859
- name: string,
860
- result: unknown,
861
- options: FormatOptions,
835
+ serverName: string,
836
+ name: string,
837
+ result: unknown,
838
+ options: FormatOptions,
862
839
  ): string {
863
- return formatOutput(
864
- { server: serverName, prompt: name, ...(result as object) },
865
- () => {
866
- const r = result as {
867
- description?: string;
868
- messages?: Array<{ role: string; content: { type: string; text?: string } }>;
869
- };
870
- const lines: string[] = [];
871
- lines.push(`${cyan(serverName)}/${bold(name)}`);
872
- if (r.description) lines.push(dim(r.description));
873
- lines.push("");
874
- for (const msg of r.messages ?? []) {
875
- lines.push(`${bold(msg.role)}:`);
876
- if (msg.content.text !== undefined) {
877
- lines.push(` ${msg.content.text}`);
878
- }
879
- }
880
- return lines.join("\n");
881
- },
882
- options,
883
- );
840
+ return formatOutput(
841
+ { server: serverName, prompt: name, ...(result as object) },
842
+ () => {
843
+ const r = result as {
844
+ description?: string;
845
+ messages?: Array<{ role: string; content: { type: string; text?: string } }>;
846
+ };
847
+ const lines: string[] = [];
848
+ lines.push(`${cyan(serverName)}/${bold(name)}`);
849
+ if (r.description) lines.push(dim(r.description));
850
+ lines.push("");
851
+ for (const msg of r.messages ?? []) {
852
+ lines.push(`${bold(msg.role)}:`);
853
+ if (msg.content.text !== undefined) {
854
+ lines.push(` ${msg.content.text}`);
855
+ }
856
+ }
857
+ return lines.join("\n");
858
+ },
859
+ options,
860
+ );
884
861
  }
885
862
 
886
863
  /** Format a unified list of tools, resources, and prompts across servers */
887
864
  export function formatUnifiedList(items: UnifiedItem[], options: FormatOptions): string {
888
- const typeLabel = (t: string) => {
889
- if (t === "tool") return green(t);
890
- if (t === "resource") return cyan(t);
891
- return yellow(t);
892
- };
893
-
894
- return formatOutput(
895
- items.map((i) => ({
896
- server: i.server,
897
- type: i.type,
898
- name: i.name,
899
- ...(options.withDescriptions ? { description: i.description ?? "" } : {}),
900
- })),
901
- () =>
902
- formatTable(items, {
903
- columns: [
904
- { value: (i) => i.server, style: cyan },
905
- { value: (i) => i.type, style: typeLabel },
906
- { value: (i) => i.name, style: bold },
907
- ],
908
- description: options.withDescriptions ? (i) => i.description : undefined,
909
- emptyMessage: "No tools, resources, or prompts found",
910
- }),
911
- options,
912
- );
865
+ const typeLabel = (t: string) => {
866
+ if (t === "tool") return green(t);
867
+ if (t === "resource") return cyan(t);
868
+ return yellow(t);
869
+ };
870
+
871
+ return formatOutput(
872
+ items.map((i) => ({
873
+ server: i.server,
874
+ type: i.type,
875
+ name: i.name,
876
+ ...(options.withDescriptions ? { description: i.description ?? "" } : {}),
877
+ })),
878
+ () =>
879
+ formatTable(items, {
880
+ columns: [
881
+ { value: (i) => i.server, style: cyan },
882
+ { value: (i) => i.type, style: typeLabel },
883
+ { value: (i) => i.name, style: bold },
884
+ ],
885
+ description: options.withDescriptions ? (i) => i.description : undefined,
886
+ emptyMessage: "No tools, resources, or prompts found",
887
+ }),
888
+ options,
889
+ );
913
890
  }
914
891
 
915
892
  /** Format a single task status */
916
893
  export function formatTaskStatus(
917
- task: { taskId: string; status: string; [key: string]: unknown },
918
- options: FormatOptions,
894
+ task: { taskId: string; status: string; [key: string]: unknown },
895
+ options: FormatOptions,
919
896
  ): string {
920
- return formatOutput(
921
- task,
922
- () => {
923
- const statusColor = (s: string) => {
924
- switch (s) {
925
- case "completed":
926
- return green(s);
927
- case "working":
928
- return yellow(s);
929
- case "failed":
930
- case "cancelled":
931
- return red(s);
932
- case "input_required":
933
- return yellow(s);
934
- default:
935
- return s;
936
- }
937
- };
938
-
939
- const lines: string[] = [];
940
- lines.push(`${bold("Task:")} ${cyan(task.taskId)}`);
941
- lines.push(`${bold("Status:")} ${statusColor(task.status)}`);
942
- if (task.statusMessage) lines.push(`${bold("Message:")} ${dim(String(task.statusMessage))}`);
943
- if (task.createdAt) lines.push(`${bold("Created:")} ${dim(String(task.createdAt))}`);
944
- if (task.lastUpdatedAt) lines.push(`${bold("Updated:")} ${dim(String(task.lastUpdatedAt))}`);
945
- if (task.ttl != null) lines.push(`${bold("TTL:")} ${dim(String(task.ttl) + "ms")}`);
946
- if (task.pollInterval != null)
947
- lines.push(`${bold("Poll interval:")} ${dim(String(task.pollInterval) + "ms")}`);
948
- return lines.join("\n");
949
- },
950
- options,
951
- );
897
+ return formatOutput(
898
+ task,
899
+ () => {
900
+ const statusColor = (s: string) => {
901
+ switch (s) {
902
+ case "completed":
903
+ return green(s);
904
+ case "working":
905
+ return yellow(s);
906
+ case "failed":
907
+ case "cancelled":
908
+ return red(s);
909
+ case "input_required":
910
+ return yellow(s);
911
+ default:
912
+ return s;
913
+ }
914
+ };
915
+
916
+ const lines: string[] = [];
917
+ lines.push(`${bold("Task:")} ${cyan(task.taskId)}`);
918
+ lines.push(`${bold("Status:")} ${statusColor(task.status)}`);
919
+ if (task.statusMessage) lines.push(`${bold("Message:")} ${dim(String(task.statusMessage))}`);
920
+ if (task.createdAt) lines.push(`${bold("Created:")} ${dim(String(task.createdAt))}`);
921
+ if (task.lastUpdatedAt) lines.push(`${bold("Updated:")} ${dim(String(task.lastUpdatedAt))}`);
922
+ if (task.ttl != null) lines.push(`${bold("TTL:")} ${dim(`${String(task.ttl)}ms`)}`);
923
+ if (task.pollInterval != null) lines.push(`${bold("Poll interval:")} ${dim(`${String(task.pollInterval)}ms`)}`);
924
+ return lines.join("\n");
925
+ },
926
+ options,
927
+ );
952
928
  }
953
929
 
954
930
  /** Format a list of tasks */
955
931
  export function formatTasksList(
956
- tasks: Array<{ taskId: string; status: string; [key: string]: unknown }>,
957
- nextCursor: string | undefined,
958
- options: FormatOptions,
932
+ tasks: Array<{ taskId: string; status: string; [key: string]: unknown }>,
933
+ nextCursor: string | undefined,
934
+ options: FormatOptions,
959
935
  ): string {
960
- return formatOutput(
961
- { tasks, ...(nextCursor ? { nextCursor } : {}) },
962
- () => {
963
- if (tasks.length === 0) {
964
- return dim("No tasks found");
965
- }
966
-
967
- const statusColor = (s: string) => {
968
- switch (s) {
969
- case "completed":
970
- return green(s.padEnd(14));
971
- case "working":
972
- return yellow(s.padEnd(14));
973
- case "failed":
974
- case "cancelled":
975
- return red(s.padEnd(14));
976
- default:
977
- return s.padEnd(14);
978
- }
979
- };
980
-
981
- const maxId = Math.max(...tasks.map((t) => t.taskId.length));
982
-
983
- const lines = tasks.map((t) => {
984
- const id = cyan(t.taskId.padEnd(maxId));
985
- const status = statusColor(t.status);
986
- const updated = t.lastUpdatedAt ? dim(String(t.lastUpdatedAt)) : "";
987
- return `${id} ${status} ${updated}`;
988
- });
989
-
990
- if (nextCursor) {
991
- lines.push("");
992
- lines.push(dim(`Next cursor: ${nextCursor}`));
993
- }
994
-
995
- return lines.join("\n");
996
- },
997
- options,
998
- );
936
+ return formatOutput(
937
+ { tasks, ...(nextCursor ? { nextCursor } : {}) },
938
+ () => {
939
+ if (tasks.length === 0) {
940
+ return dim("No tasks found");
941
+ }
942
+
943
+ const statusColor = (s: string) => {
944
+ switch (s) {
945
+ case "completed":
946
+ return green(s.padEnd(14));
947
+ case "working":
948
+ return yellow(s.padEnd(14));
949
+ case "failed":
950
+ case "cancelled":
951
+ return red(s.padEnd(14));
952
+ default:
953
+ return s.padEnd(14);
954
+ }
955
+ };
956
+
957
+ const maxId = Math.max(...tasks.map((t) => t.taskId.length));
958
+
959
+ const lines = tasks.map((t) => {
960
+ const id = cyan(t.taskId.padEnd(maxId));
961
+ const status = statusColor(t.status);
962
+ const updated = t.lastUpdatedAt ? dim(String(t.lastUpdatedAt)) : "";
963
+ return `${id} ${status} ${updated}`;
964
+ });
965
+
966
+ if (nextCursor) {
967
+ lines.push("");
968
+ lines.push(dim(`Next cursor: ${nextCursor}`));
969
+ }
970
+
971
+ return lines.join("\n");
972
+ },
973
+ options,
974
+ );
999
975
  }
1000
976
 
1001
977
  /** Format task creation output (for --no-wait) */
1002
978
  export function formatTaskCreated(
1003
- task: { taskId: string; status: string; [key: string]: unknown },
1004
- options: FormatOptions,
979
+ task: { taskId: string; status: string; [key: string]: unknown },
980
+ options: FormatOptions,
1005
981
  ): string {
1006
- return formatOutput(
1007
- { task },
1008
- () => `${green("Task created:")} ${cyan(task.taskId)} ${dim(`(status: ${task.status})`)}`,
1009
- options,
1010
- );
982
+ return formatOutput(
983
+ { task },
984
+ () => `${green("Task created:")} ${cyan(task.taskId)} ${dim(`(status: ${task.status})`)}`,
985
+ options,
986
+ );
1011
987
  }
1012
988
 
1013
989
  /** Format an error message */
1014
990
  export function formatError(message: string, options: FormatOptions): string {
1015
- return formatOutput({ error: message }, () => `${red("error:")} ${message}`, options);
991
+ return formatOutput({ error: message }, () => `${red("error:")} ${message}`, options);
1016
992
  }