@evantahler/mcpx 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.claude/settings.local.json +18 -0
  2. package/.claude/skills/mcpx.md +165 -0
  3. package/.claude/worktrees/elastic-jennings/.claude/settings.local.json +18 -0
  4. package/.claude/worktrees/elastic-jennings/.claude/skills/mcpcli.md +93 -0
  5. package/.claude/worktrees/elastic-jennings/.github/workflows/auto-release.yml +117 -0
  6. package/.claude/worktrees/elastic-jennings/.github/workflows/ci.yml +18 -0
  7. package/.claude/worktrees/elastic-jennings/.prettierignore +4 -0
  8. package/.claude/worktrees/elastic-jennings/.prettierrc +7 -0
  9. package/.claude/worktrees/elastic-jennings/CLAUDE.md +19 -0
  10. package/.claude/worktrees/elastic-jennings/LICENSE +21 -0
  11. package/.claude/worktrees/elastic-jennings/README.md +487 -0
  12. package/.claude/worktrees/elastic-jennings/bun.lock +381 -0
  13. package/.claude/worktrees/elastic-jennings/install.sh +55 -0
  14. package/.claude/worktrees/elastic-jennings/package.json +56 -0
  15. package/.claude/worktrees/elastic-jennings/src/cli.ts +39 -0
  16. package/.claude/worktrees/elastic-jennings/src/client/http.ts +100 -0
  17. package/.claude/worktrees/elastic-jennings/src/client/manager.ts +266 -0
  18. package/.claude/worktrees/elastic-jennings/src/client/oauth.ts +299 -0
  19. package/.claude/worktrees/elastic-jennings/src/client/stdio.ts +12 -0
  20. package/.claude/worktrees/elastic-jennings/src/commands/add.ts +155 -0
  21. package/.claude/worktrees/elastic-jennings/src/commands/auth.ts +114 -0
  22. package/.claude/worktrees/elastic-jennings/src/commands/exec.ts +91 -0
  23. package/.claude/worktrees/elastic-jennings/src/commands/index.ts +62 -0
  24. package/.claude/worktrees/elastic-jennings/src/commands/info.ts +38 -0
  25. package/.claude/worktrees/elastic-jennings/src/commands/list.ts +30 -0
  26. package/.claude/worktrees/elastic-jennings/src/commands/remove.ts +67 -0
  27. package/.claude/worktrees/elastic-jennings/src/commands/search.ts +45 -0
  28. package/.claude/worktrees/elastic-jennings/src/commands/skill.ts +70 -0
  29. package/.claude/worktrees/elastic-jennings/src/config/env.ts +41 -0
  30. package/.claude/worktrees/elastic-jennings/src/config/loader.ts +156 -0
  31. package/.claude/worktrees/elastic-jennings/src/config/schemas.ts +137 -0
  32. package/.claude/worktrees/elastic-jennings/src/context.ts +53 -0
  33. package/.claude/worktrees/elastic-jennings/src/output/formatter.ts +316 -0
  34. package/.claude/worktrees/elastic-jennings/src/output/logger.ts +114 -0
  35. package/.claude/worktrees/elastic-jennings/src/search/index.ts +69 -0
  36. package/.claude/worktrees/elastic-jennings/src/search/indexer.ts +92 -0
  37. package/.claude/worktrees/elastic-jennings/src/search/keyword.ts +86 -0
  38. package/.claude/worktrees/elastic-jennings/src/search/semantic.ts +75 -0
  39. package/.claude/worktrees/elastic-jennings/src/search/staleness.ts +8 -0
  40. package/.claude/worktrees/elastic-jennings/src/validation/schema.ts +77 -0
  41. package/.claude/worktrees/elastic-jennings/test/cli.test.ts +51 -0
  42. package/.claude/worktrees/elastic-jennings/test/client/manager.test.ts +249 -0
  43. package/.claude/worktrees/elastic-jennings/test/client/oauth.test.ts +328 -0
  44. package/.claude/worktrees/elastic-jennings/test/commands/add-remove.test.ts +253 -0
  45. package/.claude/worktrees/elastic-jennings/test/commands/exec.test.ts +105 -0
  46. package/.claude/worktrees/elastic-jennings/test/commands/info.test.ts +48 -0
  47. package/.claude/worktrees/elastic-jennings/test/commands/list.test.ts +39 -0
  48. package/.claude/worktrees/elastic-jennings/test/commands/skill.test.ts +98 -0
  49. package/.claude/worktrees/elastic-jennings/test/config/env.test.ts +61 -0
  50. package/.claude/worktrees/elastic-jennings/test/config/loader.test.ts +139 -0
  51. package/.claude/worktrees/elastic-jennings/test/fixtures/.keep +0 -0
  52. package/.claude/worktrees/elastic-jennings/test/fixtures/auth.json +10 -0
  53. package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/.keep +0 -0
  54. package/.claude/worktrees/elastic-jennings/test/fixtures/mock-config/servers.json +8 -0
  55. package/.claude/worktrees/elastic-jennings/test/fixtures/mock-server.ts +113 -0
  56. package/.claude/worktrees/elastic-jennings/test/fixtures/search.json +15 -0
  57. package/.claude/worktrees/elastic-jennings/test/fixtures/servers.json +18 -0
  58. package/.claude/worktrees/elastic-jennings/test/integration/stdio-server.test.ts +149 -0
  59. package/.claude/worktrees/elastic-jennings/test/output/formatter.test.ts +54 -0
  60. package/.claude/worktrees/elastic-jennings/test/output/logger.test.ts +89 -0
  61. package/.claude/worktrees/elastic-jennings/test/search/indexer.test.ts +32 -0
  62. package/.claude/worktrees/elastic-jennings/test/search/keyword.test.ts +80 -0
  63. package/.claude/worktrees/elastic-jennings/test/search/semantic.test.ts +32 -0
  64. package/.claude/worktrees/elastic-jennings/test/validation/schema.test.ts +113 -0
  65. package/.claude/worktrees/elastic-jennings/tsconfig.json +29 -0
  66. package/.cursor/rules/mcpx.mdc +165 -0
  67. package/LICENSE +21 -0
  68. package/README.md +627 -0
  69. package/package.json +58 -0
  70. package/src/cli.ts +72 -0
  71. package/src/client/browser.ts +24 -0
  72. package/src/client/debug-fetch.ts +81 -0
  73. package/src/client/elicitation.ts +368 -0
  74. package/src/client/http.ts +25 -0
  75. package/src/client/manager.ts +566 -0
  76. package/src/client/oauth.ts +314 -0
  77. package/src/client/sse.ts +17 -0
  78. package/src/client/stdio.ts +12 -0
  79. package/src/client/trace.ts +184 -0
  80. package/src/commands/add.ts +179 -0
  81. package/src/commands/auth.ts +114 -0
  82. package/src/commands/exec.ts +156 -0
  83. package/src/commands/index.ts +62 -0
  84. package/src/commands/info.ts +63 -0
  85. package/src/commands/list.ts +64 -0
  86. package/src/commands/ping.ts +69 -0
  87. package/src/commands/prompt.ts +60 -0
  88. package/src/commands/remove.ts +67 -0
  89. package/src/commands/resource.ts +46 -0
  90. package/src/commands/search.ts +49 -0
  91. package/src/commands/servers.ts +66 -0
  92. package/src/commands/skill.ts +112 -0
  93. package/src/commands/task.ts +82 -0
  94. package/src/config/env.ts +41 -0
  95. package/src/config/loader.ts +156 -0
  96. package/src/config/schemas.ts +152 -0
  97. package/src/context.ts +62 -0
  98. package/src/lib/input.ts +36 -0
  99. package/src/output/formatter.ts +884 -0
  100. package/src/output/logger.ts +173 -0
  101. package/src/search/index.ts +69 -0
  102. package/src/search/indexer.ts +92 -0
  103. package/src/search/keyword.ts +86 -0
  104. package/src/search/semantic.ts +75 -0
  105. package/src/search/staleness.ts +8 -0
  106. package/src/validation/schema.ts +103 -0
@@ -0,0 +1,884 @@
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";
5
+ import type { SearchResult } from "../search/index.ts";
6
+
7
+ export interface FormatOptions {
8
+ json?: boolean;
9
+ withDescriptions?: boolean;
10
+ verbose?: boolean;
11
+ showSecrets?: boolean;
12
+ logLevel?: string;
13
+ }
14
+
15
+ export interface UnifiedItem {
16
+ server: string;
17
+ type: "tool" | "resource" | "prompt";
18
+ name: string;
19
+ description?: string;
20
+ }
21
+
22
+ /** Check if stdout is a TTY (interactive terminal) */
23
+ export function isInteractive(options: FormatOptions): boolean {
24
+ if (options.json) return false;
25
+ return process.stdout.isTTY ?? false;
26
+ }
27
+
28
+ /** Get terminal width, or undefined if not a TTY. Subtracts 1 for safety margin. */
29
+ function getTerminalWidth(): number | undefined {
30
+ if (process.stdout.isTTY) return Math.max(process.stdout.columns - 1, 40);
31
+ return undefined;
32
+ }
33
+
34
+ /** Measure visible length of a string (excluding ANSI escape codes) */
35
+ function visibleLength(s: string): number {
36
+ return ansis.strip(s).length;
37
+ }
38
+
39
+ /** Word-wrap text to a max width, hard-breaking words that exceed it */
40
+ function wrapLines(text: string, maxWidth: number): string[] {
41
+ const words = text.split(/\s+/).filter((w) => w.length > 0);
42
+ if (words.length === 0) return [""];
43
+
44
+ const lines: string[] = [];
45
+ let current = "";
46
+
47
+ for (const word of words) {
48
+ if (word.length > maxWidth) {
49
+ if (current) {
50
+ lines.push(current);
51
+ current = "";
52
+ }
53
+ for (let j = 0; j < word.length; j += maxWidth) {
54
+ lines.push(word.slice(j, j + maxWidth));
55
+ }
56
+ continue;
57
+ }
58
+ if (current.length === 0) {
59
+ current = word;
60
+ } else if (current.length + 1 + word.length <= maxWidth) {
61
+ current += " " + word;
62
+ } else {
63
+ lines.push(current);
64
+ current = word;
65
+ }
66
+ }
67
+ if (current) lines.push(current);
68
+ return lines;
69
+ }
70
+
71
+ /**
72
+ * Word-wrap a description string to fit within the available terminal width.
73
+ * Returns dim()-wrapped text with continuation lines indented to prefixWidth.
74
+ * @param text - raw description text (before dim())
75
+ * @param prefixWidth - visible character width of everything before the description
76
+ * @param termWidth - terminal width in columns
77
+ */
78
+ export function wrapDescription(text: string, prefixWidth: number, termWidth: number): string {
79
+ const available = termWidth - prefixWidth;
80
+
81
+ // If prefix is so wide there's barely room, wrap onto the next line with a small indent
82
+ if (available < 20) {
83
+ const fallbackIndent = Math.min(prefixWidth, 4);
84
+ const fallbackAvail = termWidth - fallbackIndent;
85
+ if (fallbackAvail < 20) {
86
+ return dim(text.length > termWidth ? text.slice(0, termWidth - 3) + "..." : text);
87
+ }
88
+ const wrapped = wrapLines(text, fallbackAvail);
89
+ const indent = " ".repeat(fallbackIndent);
90
+ return wrapped.map((l) => `\n${indent}${dim(l)}`).join("");
91
+ }
92
+
93
+ const wrapped = wrapLines(text, available);
94
+ const indent = " ".repeat(prefixWidth);
95
+ return wrapped.map((l, i) => (i === 0 ? dim(l) : `\n${indent}${dim(l)}`)).join("");
96
+ }
97
+
98
+ export interface ServerOverview {
99
+ serverName: string;
100
+ version?: { name: string; version: string };
101
+ capabilities?: Record<string, unknown>;
102
+ instructions?: string;
103
+ tools: Tool[];
104
+ resourceCount: number;
105
+ promptCount: number;
106
+ }
107
+
108
+ const KNOWN_CAPABILITIES = ["tools", "resources", "prompts", "logging", "completions", "tasks"];
109
+
110
+ /** Format a full server overview (version, capabilities, tools, counts) */
111
+ 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
+ }
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
+ 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
+ }
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 ? wrapDescription(t.description, pw, termWidth) : dim(t.description);
176
+ lines.push(`${name} ${desc}`);
177
+ } else {
178
+ lines.push(name);
179
+ }
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
+
190
+ return lines.join("\n");
191
+ }
192
+
193
+ /** Format a list of tools with server names */
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");
239
+ }
240
+
241
+ /** Format tools for a single server */
242
+ export function formatServerTools(
243
+ serverName: string,
244
+ tools: Tool[],
245
+ options: FormatOptions,
246
+ ): 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");
278
+ }
279
+
280
+ /** Format a tool schema */
281
+ 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");
307
+ }
308
+
309
+ /** Format a JSON schema as a readable parameter list */
310
+ function formatSchema(schema: Tool["inputSchema"], indent: number): string {
311
+ const pad = " ".repeat(indent);
312
+ const properties = schema.properties ?? {};
313
+ const required = new Set(schema.required ?? []);
314
+
315
+ if (Object.keys(properties).length === 0) {
316
+ return `${pad}${dim("(no parameters)")}`;
317
+ }
318
+
319
+ return Object.entries(properties)
320
+ .map(([name, prop]) => {
321
+ const p = prop as Record<string, unknown>;
322
+ const type = (p.type as string) ?? "any";
323
+ const req = required.has(name) ? red("*") : "";
324
+ const desc = p.description ? ` ${dim(String(p.description))}` : "";
325
+ return `${pad}${green(name)}${req} ${dim(`(${type})`)}${desc}`;
326
+ })
327
+ .join("\n");
328
+ }
329
+
330
+ /** Format detailed tool help with example payload */
331
+ 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");
363
+ }
364
+
365
+ /** Generate an example payload from a JSON schema */
366
+ function generateExample(schema: Tool["inputSchema"]): Record<string, unknown> {
367
+ const properties = schema.properties ?? {};
368
+ const required = new Set(schema.required ?? []);
369
+ const example: Record<string, unknown> = {};
370
+
371
+ for (const [name, prop] of Object.entries(properties)) {
372
+ const p = prop as Record<string, unknown>;
373
+ // Include required fields and first few optional fields
374
+ if (required.has(name) || Object.keys(example).length < 3) {
375
+ example[name] = exampleValue(name, p);
376
+ }
377
+ }
378
+
379
+ return example;
380
+ }
381
+
382
+ 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
+ }
392
+
393
+ const type = prop.type as string | undefined;
394
+ switch (type) {
395
+ case "string":
396
+ return `<${name}>`;
397
+ case "number":
398
+ case "integer":
399
+ return 0;
400
+ case "boolean":
401
+ return true;
402
+ case "array":
403
+ return [];
404
+ case "object":
405
+ return {};
406
+ default:
407
+ return `<${name}>`;
408
+ }
409
+ }
410
+
411
+ /** Format a tool call result */
412
+ export function formatCallResult(result: unknown, _options: FormatOptions): string {
413
+ return JSON.stringify(parseNestedJson(result), null, 2);
414
+ }
415
+
416
+ /** Recursively parse JSON strings inside MCP content blocks */
417
+ function parseNestedJson(value: unknown): unknown {
418
+ if (typeof value === "string") {
419
+ try {
420
+ return parseNestedJson(JSON.parse(value));
421
+ } catch {
422
+ return value;
423
+ }
424
+ }
425
+ if (Array.isArray(value)) {
426
+ return value.map(parseNestedJson);
427
+ }
428
+ if (typeof value === "object" && value !== null) {
429
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v)]));
430
+ }
431
+ return value;
432
+ }
433
+
434
+ /** Format validation errors for tool input */
435
+ export function formatValidationErrors(
436
+ serverName: string,
437
+ toolName: string,
438
+ errors: ValidationError[],
439
+ options: FormatOptions,
440
+ ): 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}`;
453
+ }
454
+
455
+ /** Format search results */
456
+ 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);
482
+
483
+ return `${header}\n${indent}${desc}`;
484
+ })
485
+ .join("\n\n");
486
+ }
487
+
488
+ /** Format a list of resources with server names */
489
+ export function formatResourceList(
490
+ resources: ResourceWithServer[],
491
+ options: FormatOptions,
492
+ ): 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");
530
+ }
531
+
532
+ /** Format resources for a single server */
533
+ export function formatServerResources(
534
+ serverName: string,
535
+ resources: Resource[],
536
+ options: FormatOptions,
537
+ ): 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");
574
+ }
575
+
576
+ /** Format resource contents */
577
+ export function formatResourceContents(
578
+ serverName: string,
579
+ uri: string,
580
+ result: unknown,
581
+ options: FormatOptions,
582
+ ): 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>`));
606
+ }
607
+ }
608
+ }
609
+
610
+ return lines.join("\n");
611
+ }
612
+
613
+ /** Format a list of prompts with server names */
614
+ 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");
651
+ }
652
+
653
+ /** Format prompts for a single server */
654
+ export function formatServerPrompts(
655
+ serverName: string,
656
+ prompts: Prompt[],
657
+ options: FormatOptions,
658
+ ): 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
+ });
698
+
699
+ return [header, ...lines].join("\n");
700
+ }
701
+
702
+ /** Format prompt messages */
703
+ export function formatPromptMessages(
704
+ serverName: string,
705
+ name: string,
706
+ result: unknown,
707
+ options: FormatOptions,
708
+ ): 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");
734
+ }
735
+
736
+ /** Format a unified list of tools, resources, and prompts across servers */
737
+ 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);
764
+ };
765
+
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");
783
+ }
784
+
785
+ /** Format a single task status */
786
+ export function formatTaskStatus(
787
+ task: { taskId: string; status: string; [key: string]: unknown },
788
+ options: FormatOptions,
789
+ ): 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");
820
+ }
821
+
822
+ /** Format a list of tasks */
823
+ export function formatTasksList(
824
+ tasks: Array<{ taskId: string; status: string; [key: string]: unknown }>,
825
+ nextCursor: string | undefined,
826
+ options: FormatOptions,
827
+ ): 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
+ });
858
+
859
+ if (nextCursor) {
860
+ lines.push("");
861
+ lines.push(dim(`Next cursor: ${nextCursor}`));
862
+ }
863
+
864
+ return lines.join("\n");
865
+ }
866
+
867
+ /** Format task creation output (for --no-wait) */
868
+ export function formatTaskCreated(
869
+ task: { taskId: string; status: string; [key: string]: unknown },
870
+ options: FormatOptions,
871
+ ): 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})`)}`;
876
+ }
877
+
878
+ /** Format an error message */
879
+ export function formatError(message: string, options: FormatOptions): string {
880
+ if (!isInteractive(options)) {
881
+ return JSON.stringify({ error: message });
882
+ }
883
+ return `${red("error:")} ${message}`;
884
+ }