@evantahler/mcpcli 0.5.1 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/mcpcli.md +8 -0
- package/README.md +28 -20
- package/package.json +1 -1
- package/src/cli.ts +6 -0
- package/src/client/manager.ts +173 -18
- package/src/client/oauth.ts +17 -0
- package/src/commands/add.ts +16 -2
- package/src/commands/list.ts +39 -5
- package/src/commands/ping.ts +69 -0
- package/src/commands/prompt.ts +85 -0
- package/src/commands/resource.ts +46 -0
- package/src/config/schemas.ts +9 -2
- package/src/output/formatter.ts +276 -2
package/.claude/skills/mcpcli.md
CHANGED
|
@@ -87,7 +87,15 @@ mcpcli deauth <server> # remove stored auth
|
|
|
87
87
|
| `mcpcli auth <server> -s` | Check token status and TTL |
|
|
88
88
|
| `mcpcli auth <server> -r` | Force token refresh |
|
|
89
89
|
| `mcpcli deauth <server>` | Remove stored authentication |
|
|
90
|
+
| `mcpcli ping` | Check connectivity to all servers |
|
|
91
|
+
| `mcpcli ping <server> [server2...]` | Check specific server(s) |
|
|
90
92
|
| `mcpcli add <name> --command <cmd>` | Add a stdio MCP server |
|
|
91
93
|
| `mcpcli add <name> --url <url>` | Add an HTTP MCP server |
|
|
92
94
|
| `mcpcli remove <name>` | Remove an MCP server |
|
|
93
95
|
| `mcpcli skill install --claude` | Install mcpcli skill for Claude |
|
|
96
|
+
| `mcpcli resource` | List all resources across servers |
|
|
97
|
+
| `mcpcli resource <server>` | List resources for a server |
|
|
98
|
+
| `mcpcli resource <server> <uri>` | Read a specific resource |
|
|
99
|
+
| `mcpcli prompt` | List all prompts across servers |
|
|
100
|
+
| `mcpcli prompt <server>` | List prompts for a server |
|
|
101
|
+
| `mcpcli prompt <server> <name> '<json>'` | Get a specific prompt |
|
package/README.md
CHANGED
|
@@ -49,26 +49,34 @@ mcpcli search -q "manage pull requests"
|
|
|
49
49
|
|
|
50
50
|
## Commands
|
|
51
51
|
|
|
52
|
-
| Command
|
|
53
|
-
|
|
|
54
|
-
| `mcpcli`
|
|
55
|
-
| `mcpcli info <server>`
|
|
56
|
-
| `mcpcli info <server> <tool>`
|
|
57
|
-
| `mcpcli search <query>`
|
|
58
|
-
| `mcpcli search -k <pattern>`
|
|
59
|
-
| `mcpcli search -q <query>`
|
|
60
|
-
| `mcpcli index`
|
|
61
|
-
| `mcpcli index -i`
|
|
62
|
-
| `mcpcli exec <server> <tool> [json]`
|
|
63
|
-
| `mcpcli exec <server>`
|
|
64
|
-
| `mcpcli auth <server>`
|
|
65
|
-
| `mcpcli auth <server> -s`
|
|
66
|
-
| `mcpcli auth <server> -r`
|
|
67
|
-
| `mcpcli deauth <server>`
|
|
68
|
-
| `mcpcli add <name> --command <cmd>`
|
|
69
|
-
| `mcpcli add <name> --url <url>`
|
|
70
|
-
| `mcpcli remove <name>`
|
|
71
|
-
| `mcpcli
|
|
52
|
+
| Command | Description |
|
|
53
|
+
| -------------------------------------- | -------------------------------------------- |
|
|
54
|
+
| `mcpcli` | List all configured servers and tools |
|
|
55
|
+
| `mcpcli info <server>` | Show tools for a server |
|
|
56
|
+
| `mcpcli info <server> <tool>` | Show tool schema |
|
|
57
|
+
| `mcpcli search <query>` | Search tools (keyword + semantic) |
|
|
58
|
+
| `mcpcli search -k <pattern>` | Keyword/glob search only |
|
|
59
|
+
| `mcpcli search -q <query>` | Semantic search only |
|
|
60
|
+
| `mcpcli index` | Build/rebuild the search index |
|
|
61
|
+
| `mcpcli index -i` | Show index status |
|
|
62
|
+
| `mcpcli exec <server> <tool> [json]` | Validate inputs locally, then execute tool |
|
|
63
|
+
| `mcpcli exec <server>` | List available tools for a server |
|
|
64
|
+
| `mcpcli auth <server>` | Authenticate with an HTTP MCP server (OAuth) |
|
|
65
|
+
| `mcpcli auth <server> -s` | Check auth status and token TTL |
|
|
66
|
+
| `mcpcli auth <server> -r` | Force token refresh |
|
|
67
|
+
| `mcpcli deauth <server>` | Remove stored authentication for a server |
|
|
68
|
+
| `mcpcli add <name> --command <cmd>` | Add a stdio MCP server to your config |
|
|
69
|
+
| `mcpcli add <name> --url <url>` | Add an HTTP MCP server to your config |
|
|
70
|
+
| `mcpcli remove <name>` | Remove an MCP server from your config |
|
|
71
|
+
| `mcpcli ping` | Check connectivity to all configured servers |
|
|
72
|
+
| `mcpcli ping <server> [server2...]` | Check connectivity to specific server(s) |
|
|
73
|
+
| `mcpcli skill install --claude` | Install the mcpcli skill for Claude Code |
|
|
74
|
+
| `mcpcli resource` | List all resources across all servers |
|
|
75
|
+
| `mcpcli resource <server>` | List resources for a server |
|
|
76
|
+
| `mcpcli resource <server> <uri>` | Read a specific resource |
|
|
77
|
+
| `mcpcli prompt` | List all prompts across all servers |
|
|
78
|
+
| `mcpcli prompt <server>` | List prompts for a server |
|
|
79
|
+
| `mcpcli prompt <server> <name> [json]` | Get a specific prompt |
|
|
72
80
|
|
|
73
81
|
## Options
|
|
74
82
|
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -10,6 +10,9 @@ import { registerIndexCommand } from "./commands/index.ts";
|
|
|
10
10
|
import { registerAddCommand } from "./commands/add.ts";
|
|
11
11
|
import { registerRemoveCommand } from "./commands/remove.ts";
|
|
12
12
|
import { registerSkillCommand } from "./commands/skill.ts";
|
|
13
|
+
import { registerPingCommand } from "./commands/ping.ts";
|
|
14
|
+
import { registerResourceCommand } from "./commands/resource.ts";
|
|
15
|
+
import { registerPromptCommand } from "./commands/prompt.ts";
|
|
13
16
|
|
|
14
17
|
declare const BUILD_VERSION: string | undefined;
|
|
15
18
|
|
|
@@ -35,5 +38,8 @@ registerIndexCommand(program);
|
|
|
35
38
|
registerAddCommand(program);
|
|
36
39
|
registerRemoveCommand(program);
|
|
37
40
|
registerSkillCommand(program);
|
|
41
|
+
registerPingCommand(program);
|
|
42
|
+
registerResourceCommand(program);
|
|
43
|
+
registerPromptCommand(program);
|
|
38
44
|
|
|
39
45
|
program.parse();
|
package/src/client/manager.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
2
|
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
3
3
|
import picomatch from "picomatch";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
Tool,
|
|
6
|
+
Resource,
|
|
7
|
+
Prompt,
|
|
8
|
+
ServerConfig,
|
|
9
|
+
ServersFile,
|
|
10
|
+
AuthFile,
|
|
11
|
+
} from "../config/schemas.ts";
|
|
5
12
|
import { isStdioServer, isHttpServer } from "../config/schemas.ts";
|
|
6
13
|
import { createStdioTransport } from "./stdio.ts";
|
|
7
14
|
import { createHttpTransport } from "./http.ts";
|
|
@@ -12,6 +19,16 @@ export interface ToolWithServer {
|
|
|
12
19
|
tool: Tool;
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
export interface ResourceWithServer {
|
|
23
|
+
server: string;
|
|
24
|
+
resource: Resource;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PromptWithServer {
|
|
28
|
+
server: string;
|
|
29
|
+
prompt: Prompt;
|
|
30
|
+
}
|
|
31
|
+
|
|
15
32
|
export interface ServerError {
|
|
16
33
|
server: string;
|
|
17
34
|
message: string;
|
|
@@ -30,6 +47,7 @@ export interface ServerManagerOptions {
|
|
|
30
47
|
|
|
31
48
|
export class ServerManager {
|
|
32
49
|
private clients = new Map<string, Client>();
|
|
50
|
+
private connecting = new Map<string, Promise<Client>>();
|
|
33
51
|
private transports = new Map<string, Transport>();
|
|
34
52
|
private oauthProviders = new Map<string, McpOAuthProvider>();
|
|
35
53
|
private servers: ServersFile;
|
|
@@ -57,32 +75,45 @@ export class ServerManager {
|
|
|
57
75
|
const existing = this.clients.get(serverName);
|
|
58
76
|
if (existing) return existing;
|
|
59
77
|
|
|
78
|
+
// If a connection is already in flight, wait for it instead of opening a second one
|
|
79
|
+
const inflight = this.connecting.get(serverName);
|
|
80
|
+
if (inflight) return inflight;
|
|
81
|
+
|
|
60
82
|
const config = this.servers.mcpServers[serverName];
|
|
61
83
|
if (!config) {
|
|
62
84
|
throw new Error(`Unknown server: "${serverName}"`);
|
|
63
85
|
}
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
const connectPromise = (async () => {
|
|
88
|
+
// Auto-refresh expired OAuth tokens before connecting to HTTP servers
|
|
89
|
+
if (isHttpServer(config)) {
|
|
90
|
+
const provider = this.getOrCreateOAuthProvider(serverName);
|
|
91
|
+
if (!provider.isComplete()) {
|
|
92
|
+
throw new Error(`Not authenticated with "${serverName}". Run: mcpcli auth ${serverName}`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await provider.refreshIfNeeded(config.url);
|
|
96
|
+
} catch {
|
|
97
|
+
// If refresh fails, continue — the transport will send the existing token
|
|
98
|
+
}
|
|
75
99
|
}
|
|
76
|
-
}
|
|
77
100
|
|
|
78
|
-
|
|
79
|
-
|
|
101
|
+
const transport = this.createTransport(serverName, config);
|
|
102
|
+
this.transports.set(serverName, transport);
|
|
80
103
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
104
|
+
const client = new Client({ name: "mcpcli", version: "0.1.0" });
|
|
105
|
+
await this.withTimeout(client.connect(transport), `connect(${serverName})`);
|
|
106
|
+
this.clients.set(serverName, client);
|
|
107
|
+
this.connecting.delete(serverName);
|
|
108
|
+
|
|
109
|
+
return client;
|
|
110
|
+
})().catch((err) => {
|
|
111
|
+
this.connecting.delete(serverName);
|
|
112
|
+
throw err;
|
|
113
|
+
});
|
|
84
114
|
|
|
85
|
-
|
|
115
|
+
this.connecting.set(serverName, connectPromise);
|
|
116
|
+
return connectPromise;
|
|
86
117
|
}
|
|
87
118
|
|
|
88
119
|
private getOrCreateOAuthProvider(serverName: string): McpOAuthProvider {
|
|
@@ -150,6 +181,7 @@ export class ServerManager {
|
|
|
150
181
|
// ignore close errors
|
|
151
182
|
}
|
|
152
183
|
this.clients.delete(serverName);
|
|
184
|
+
this.connecting.delete(serverName);
|
|
153
185
|
this.transports.delete(serverName);
|
|
154
186
|
}
|
|
155
187
|
}
|
|
@@ -228,6 +260,128 @@ export class ServerManager {
|
|
|
228
260
|
return tools.find((t) => t.name === toolName);
|
|
229
261
|
}
|
|
230
262
|
|
|
263
|
+
/** List resources for a single server */
|
|
264
|
+
async listResources(serverName: string): Promise<Resource[]> {
|
|
265
|
+
return this.withRetry(
|
|
266
|
+
async () => {
|
|
267
|
+
const client = await this.getClient(serverName);
|
|
268
|
+
const result = await this.withTimeout(
|
|
269
|
+
client.listResources(),
|
|
270
|
+
`listResources(${serverName})`,
|
|
271
|
+
);
|
|
272
|
+
return result.resources;
|
|
273
|
+
},
|
|
274
|
+
`listResources(${serverName})`,
|
|
275
|
+
serverName,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** List resources across all configured servers */
|
|
280
|
+
async getAllResources(): Promise<{ resources: ResourceWithServer[]; errors: ServerError[] }> {
|
|
281
|
+
const serverNames = Object.keys(this.servers.mcpServers);
|
|
282
|
+
const resources: ResourceWithServer[] = [];
|
|
283
|
+
const errors: ServerError[] = [];
|
|
284
|
+
|
|
285
|
+
for (let i = 0; i < serverNames.length; i += this.concurrency) {
|
|
286
|
+
const batch = serverNames.slice(i, i + this.concurrency);
|
|
287
|
+
const batchResults = await Promise.allSettled(
|
|
288
|
+
batch.map(async (name) => {
|
|
289
|
+
const serverResources = await this.listResources(name);
|
|
290
|
+
return serverResources.map((resource) => ({ server: name, resource }));
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
295
|
+
const result = batchResults[j]!;
|
|
296
|
+
if (result.status === "fulfilled") {
|
|
297
|
+
resources.push(...result.value);
|
|
298
|
+
} else {
|
|
299
|
+
const name = batch[j]!;
|
|
300
|
+
const message =
|
|
301
|
+
result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
302
|
+
errors.push({ server: name, message });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return { resources, errors };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Read a specific resource by URI */
|
|
311
|
+
async readResource(serverName: string, uri: string): Promise<unknown> {
|
|
312
|
+
return this.withRetry(
|
|
313
|
+
async () => {
|
|
314
|
+
const client = await this.getClient(serverName);
|
|
315
|
+
return this.withTimeout(client.readResource({ uri }), `readResource(${serverName}/${uri})`);
|
|
316
|
+
},
|
|
317
|
+
`readResource(${serverName}/${uri})`,
|
|
318
|
+
serverName,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** List prompts for a single server */
|
|
323
|
+
async listPrompts(serverName: string): Promise<Prompt[]> {
|
|
324
|
+
return this.withRetry(
|
|
325
|
+
async () => {
|
|
326
|
+
const client = await this.getClient(serverName);
|
|
327
|
+
const result = await this.withTimeout(client.listPrompts(), `listPrompts(${serverName})`);
|
|
328
|
+
return result.prompts;
|
|
329
|
+
},
|
|
330
|
+
`listPrompts(${serverName})`,
|
|
331
|
+
serverName,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** List prompts across all configured servers */
|
|
336
|
+
async getAllPrompts(): Promise<{ prompts: PromptWithServer[]; errors: ServerError[] }> {
|
|
337
|
+
const serverNames = Object.keys(this.servers.mcpServers);
|
|
338
|
+
const prompts: PromptWithServer[] = [];
|
|
339
|
+
const errors: ServerError[] = [];
|
|
340
|
+
|
|
341
|
+
for (let i = 0; i < serverNames.length; i += this.concurrency) {
|
|
342
|
+
const batch = serverNames.slice(i, i + this.concurrency);
|
|
343
|
+
const batchResults = await Promise.allSettled(
|
|
344
|
+
batch.map(async (name) => {
|
|
345
|
+
const serverPrompts = await this.listPrompts(name);
|
|
346
|
+
return serverPrompts.map((prompt) => ({ server: name, prompt }));
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
351
|
+
const result = batchResults[j]!;
|
|
352
|
+
if (result.status === "fulfilled") {
|
|
353
|
+
prompts.push(...result.value);
|
|
354
|
+
} else {
|
|
355
|
+
const name = batch[j]!;
|
|
356
|
+
const message =
|
|
357
|
+
result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
358
|
+
errors.push({ server: name, message });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { prompts, errors };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Get a specific prompt by name, optionally with arguments */
|
|
367
|
+
async getPrompt(
|
|
368
|
+
serverName: string,
|
|
369
|
+
name: string,
|
|
370
|
+
args?: Record<string, string>,
|
|
371
|
+
): Promise<unknown> {
|
|
372
|
+
return this.withRetry(
|
|
373
|
+
async () => {
|
|
374
|
+
const client = await this.getClient(serverName);
|
|
375
|
+
return this.withTimeout(
|
|
376
|
+
client.getPrompt({ name, arguments: args }),
|
|
377
|
+
`getPrompt(${serverName}/${name})`,
|
|
378
|
+
);
|
|
379
|
+
},
|
|
380
|
+
`getPrompt(${serverName}/${name})`,
|
|
381
|
+
serverName,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
231
385
|
/** Get all server names */
|
|
232
386
|
getServerNames(): string[] {
|
|
233
387
|
return Object.keys(this.servers.mcpServers);
|
|
@@ -245,6 +399,7 @@ export class ServerManager {
|
|
|
245
399
|
this.transports.delete(name);
|
|
246
400
|
});
|
|
247
401
|
await Promise.allSettled(closePromises);
|
|
402
|
+
this.connecting.clear();
|
|
248
403
|
}
|
|
249
404
|
}
|
|
250
405
|
|
package/src/client/oauth.ts
CHANGED
|
@@ -248,6 +248,23 @@ export function startCallbackServer(): {
|
|
|
248
248
|
return { server, authCodePromise };
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/** Resolve the canonical resource URL for an HTTP MCP server.
|
|
252
|
+
* Some servers advertise a canonical URL in their OAuth protected resource metadata
|
|
253
|
+
* that may differ from the URL provided by the user (e.g. hf.co → huggingface.co).
|
|
254
|
+
* Returns the canonical URL if found, or the original URL otherwise. */
|
|
255
|
+
export async function resolveResourceUrl(serverUrl: string): Promise<string> {
|
|
256
|
+
try {
|
|
257
|
+
const info = await discoverOAuthServerInfo(serverUrl);
|
|
258
|
+
const canonical = info.resourceMetadata?.resource;
|
|
259
|
+
if (canonical && canonical !== serverUrl) {
|
|
260
|
+
return canonical;
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// OAuth discovery not available — use original URL
|
|
264
|
+
}
|
|
265
|
+
return serverUrl;
|
|
266
|
+
}
|
|
267
|
+
|
|
251
268
|
/** Probe for OAuth support and run the auth flow if the server supports it.
|
|
252
269
|
* Returns true if auth ran, false if server doesn't support OAuth (silent skip). */
|
|
253
270
|
export async function tryOAuthIfSupported(
|
package/src/commands/add.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import type { ServerConfig } from "../config/schemas.ts";
|
|
3
3
|
import { loadRawAuth, loadRawServers, saveServers } from "../config/loader.ts";
|
|
4
|
-
import { tryOAuthIfSupported } from "../client/oauth.ts";
|
|
4
|
+
import { tryOAuthIfSupported, resolveResourceUrl } from "../client/oauth.ts";
|
|
5
5
|
import { runIndex } from "./index.ts";
|
|
6
6
|
|
|
7
7
|
export function registerAddCommand(program: Command) {
|
|
@@ -72,6 +72,20 @@ export function registerAddCommand(program: Command) {
|
|
|
72
72
|
config.disabledTools = options.disabledTools.split(",").map((t) => t.trim());
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// For HTTP servers, resolve the canonical resource URL before saving.
|
|
76
|
+
// Some servers (e.g. hf.co → huggingface.co) advertise a different canonical
|
|
77
|
+
// URL in their OAuth protected resource metadata, and the SDK enforces that the
|
|
78
|
+
// stored URL matches this canonical URL during the OAuth token flow.
|
|
79
|
+
let effectiveUrl = options.url!;
|
|
80
|
+
if (hasUrl && options.auth !== false) {
|
|
81
|
+
const canonical = await resolveResourceUrl(effectiveUrl);
|
|
82
|
+
if (canonical !== effectiveUrl) {
|
|
83
|
+
(config as { url: string }).url = canonical;
|
|
84
|
+
effectiveUrl = canonical;
|
|
85
|
+
console.log(`Resolved canonical URL: ${canonical}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
servers.mcpServers[name] = config;
|
|
76
90
|
await saveServers(configDir, servers);
|
|
77
91
|
console.log(`Added server "${name}" to ${configDir}/servers.json`);
|
|
@@ -85,7 +99,7 @@ export function registerAddCommand(program: Command) {
|
|
|
85
99
|
showSecrets: false,
|
|
86
100
|
};
|
|
87
101
|
try {
|
|
88
|
-
await tryOAuthIfSupported(name,
|
|
102
|
+
await tryOAuthIfSupported(name, effectiveUrl, configDir, auth, formatOptions);
|
|
89
103
|
} catch {
|
|
90
104
|
console.error(`Warning: OAuth authentication failed. Run: mcpcli auth ${name}`);
|
|
91
105
|
}
|
package/src/commands/list.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
import { getContext } from "../context.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { formatUnifiedList, formatError } from "../output/formatter.ts";
|
|
4
|
+
import type { UnifiedItem } from "../output/formatter.ts";
|
|
4
5
|
import { logger } from "../output/logger.ts";
|
|
5
6
|
|
|
6
7
|
export function registerListCommand(program: Command) {
|
|
@@ -8,19 +9,52 @@ export function registerListCommand(program: Command) {
|
|
|
8
9
|
const { manager, formatOptions } = await getContext(program);
|
|
9
10
|
const spinner = logger.startSpinner("Connecting to servers...", formatOptions);
|
|
10
11
|
try {
|
|
11
|
-
const
|
|
12
|
+
const [toolsResult, resourcesResult, promptsResult] = await Promise.all([
|
|
13
|
+
manager.getAllTools(),
|
|
14
|
+
manager.getAllResources(),
|
|
15
|
+
manager.getAllPrompts(),
|
|
16
|
+
]);
|
|
12
17
|
spinner.stop();
|
|
13
18
|
|
|
19
|
+
const items: UnifiedItem[] = [
|
|
20
|
+
...toolsResult.tools.map((t) => ({
|
|
21
|
+
server: t.server,
|
|
22
|
+
type: "tool" as const,
|
|
23
|
+
name: t.tool.name,
|
|
24
|
+
description: t.tool.description,
|
|
25
|
+
})),
|
|
26
|
+
...resourcesResult.resources.map((r) => ({
|
|
27
|
+
server: r.server,
|
|
28
|
+
type: "resource" as const,
|
|
29
|
+
name: r.resource.uri,
|
|
30
|
+
description: r.resource.description,
|
|
31
|
+
})),
|
|
32
|
+
...promptsResult.prompts.map((p) => ({
|
|
33
|
+
server: p.server,
|
|
34
|
+
type: "prompt" as const,
|
|
35
|
+
name: p.prompt.name,
|
|
36
|
+
description: p.prompt.description,
|
|
37
|
+
})),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const typeOrder = { tool: 0, resource: 1, prompt: 2 };
|
|
41
|
+
items.sort((a, b) => {
|
|
42
|
+
if (a.server !== b.server) return a.server.localeCompare(b.server);
|
|
43
|
+
if (a.type !== b.type) return typeOrder[a.type] - typeOrder[b.type];
|
|
44
|
+
return a.name.localeCompare(b.name);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const errors = [...toolsResult.errors, ...resourcesResult.errors, ...promptsResult.errors];
|
|
14
48
|
if (errors.length > 0) {
|
|
15
49
|
for (const err of errors) {
|
|
16
50
|
console.error(`"${err.server}": ${err.message}`);
|
|
17
51
|
}
|
|
18
|
-
if (
|
|
52
|
+
if (items.length > 0) console.log("");
|
|
19
53
|
}
|
|
20
54
|
|
|
21
|
-
console.log(
|
|
55
|
+
console.log(formatUnifiedList(items, formatOptions));
|
|
22
56
|
} catch (err) {
|
|
23
|
-
spinner.error("Failed to list
|
|
57
|
+
spinner.error("Failed to list servers");
|
|
24
58
|
console.error(formatError(String(err), formatOptions));
|
|
25
59
|
process.exit(1);
|
|
26
60
|
} finally {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { green, red } from "ansis";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { getContext } from "../context.ts";
|
|
4
|
+
import { formatError } from "../output/formatter.ts";
|
|
5
|
+
import { logger } from "../output/logger.ts";
|
|
6
|
+
|
|
7
|
+
interface PingResult {
|
|
8
|
+
server: string;
|
|
9
|
+
success: boolean;
|
|
10
|
+
latencyMs?: number;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerPingCommand(program: Command) {
|
|
15
|
+
program
|
|
16
|
+
.command("ping [servers...]")
|
|
17
|
+
.description("Check connectivity to MCP servers")
|
|
18
|
+
.action(async (servers: string[]) => {
|
|
19
|
+
const { manager, formatOptions } = await getContext(program);
|
|
20
|
+
|
|
21
|
+
const targetServers = servers.length > 0 ? servers : manager.getServerNames();
|
|
22
|
+
|
|
23
|
+
if (targetServers.length === 0) {
|
|
24
|
+
console.error(formatError("No servers configured", formatOptions));
|
|
25
|
+
await manager.close();
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spinner = logger.startSpinner(
|
|
30
|
+
`Pinging ${targetServers.length} server(s)...`,
|
|
31
|
+
formatOptions,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const results: PingResult[] = [];
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await Promise.all(
|
|
38
|
+
targetServers.map(async (serverName) => {
|
|
39
|
+
const start = Date.now();
|
|
40
|
+
try {
|
|
41
|
+
await manager.getClient(serverName);
|
|
42
|
+
results.push({ server: serverName, success: true, latencyMs: Date.now() - start });
|
|
43
|
+
} catch (err) {
|
|
44
|
+
results.push({ server: serverName, success: false, error: String(err) });
|
|
45
|
+
}
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
spinner.stop();
|
|
50
|
+
|
|
51
|
+
if (formatOptions.json) {
|
|
52
|
+
console.log(JSON.stringify(results, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
for (const r of results) {
|
|
55
|
+
if (r.success) {
|
|
56
|
+
console.log(`${green("✔")} ${r.server} connected (${r.latencyMs}ms)`);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(`${red("✖")} ${r.server} failed: ${r.error}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
await manager.close();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const anyFailed = results.some((r) => !r.success);
|
|
67
|
+
if (anyFailed) process.exit(1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { getContext } from "../context.ts";
|
|
3
|
+
import {
|
|
4
|
+
formatPromptList,
|
|
5
|
+
formatServerPrompts,
|
|
6
|
+
formatPromptMessages,
|
|
7
|
+
formatError,
|
|
8
|
+
} from "../output/formatter.ts";
|
|
9
|
+
import { logger } from "../output/logger.ts";
|
|
10
|
+
|
|
11
|
+
export function registerPromptCommand(program: Command) {
|
|
12
|
+
program
|
|
13
|
+
.command("prompt [server] [name] [args]")
|
|
14
|
+
.description("list prompts for a server, or get a specific prompt")
|
|
15
|
+
.action(
|
|
16
|
+
async (server: string | undefined, name: string | undefined, argsStr: string | undefined) => {
|
|
17
|
+
const { manager, formatOptions } = await getContext(program);
|
|
18
|
+
const spinner = logger.startSpinner(
|
|
19
|
+
server ? `Connecting to ${server}...` : "Connecting to servers...",
|
|
20
|
+
formatOptions,
|
|
21
|
+
);
|
|
22
|
+
try {
|
|
23
|
+
if (server && name) {
|
|
24
|
+
let args: Record<string, string> | undefined;
|
|
25
|
+
|
|
26
|
+
if (argsStr) {
|
|
27
|
+
args = parseJsonArgs(argsStr);
|
|
28
|
+
} else if (!process.stdin.isTTY) {
|
|
29
|
+
const stdin = await readStdin();
|
|
30
|
+
if (stdin.trim()) {
|
|
31
|
+
args = parseJsonArgs(stdin);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const result = await manager.getPrompt(server, name, args);
|
|
36
|
+
spinner.stop();
|
|
37
|
+
console.log(formatPromptMessages(server, name, result, formatOptions));
|
|
38
|
+
} else if (server) {
|
|
39
|
+
const prompts = await manager.listPrompts(server);
|
|
40
|
+
spinner.stop();
|
|
41
|
+
console.log(formatServerPrompts(server, prompts, formatOptions));
|
|
42
|
+
} else {
|
|
43
|
+
const { prompts, errors } = await manager.getAllPrompts();
|
|
44
|
+
spinner.stop();
|
|
45
|
+
console.log(formatPromptList(prompts, formatOptions));
|
|
46
|
+
for (const err of errors) {
|
|
47
|
+
console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner.error("Failed");
|
|
52
|
+
console.error(formatError(String(err), formatOptions));
|
|
53
|
+
process.exit(1);
|
|
54
|
+
} finally {
|
|
55
|
+
await manager.close();
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseJsonArgs(str: string): Record<string, string> {
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(str);
|
|
64
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
65
|
+
throw new Error("Prompt arguments must be a JSON object");
|
|
66
|
+
}
|
|
67
|
+
// Coerce all values to strings (MCP prompt args are Record<string, string>)
|
|
68
|
+
return Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err instanceof SyntaxError) {
|
|
71
|
+
throw new Error(`Invalid JSON: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function readStdin(): Promise<string> {
|
|
78
|
+
const chunks: string[] = [];
|
|
79
|
+
const reader = process.stdin;
|
|
80
|
+
reader.setEncoding("utf-8");
|
|
81
|
+
for await (const chunk of reader) {
|
|
82
|
+
chunks.push(chunk as string);
|
|
83
|
+
}
|
|
84
|
+
return chunks.join("");
|
|
85
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { getContext } from "../context.ts";
|
|
3
|
+
import {
|
|
4
|
+
formatResourceList,
|
|
5
|
+
formatServerResources,
|
|
6
|
+
formatResourceContents,
|
|
7
|
+
formatError,
|
|
8
|
+
} from "../output/formatter.ts";
|
|
9
|
+
import { logger } from "../output/logger.ts";
|
|
10
|
+
|
|
11
|
+
export function registerResourceCommand(program: Command) {
|
|
12
|
+
program
|
|
13
|
+
.command("resource [server] [uri]")
|
|
14
|
+
.description("list resources for a server, or read a specific resource")
|
|
15
|
+
.action(async (server: string | undefined, uri: string | undefined) => {
|
|
16
|
+
const { manager, formatOptions } = await getContext(program);
|
|
17
|
+
const spinner = logger.startSpinner(
|
|
18
|
+
server ? `Connecting to ${server}...` : "Connecting to servers...",
|
|
19
|
+
formatOptions,
|
|
20
|
+
);
|
|
21
|
+
try {
|
|
22
|
+
if (server && uri) {
|
|
23
|
+
const result = await manager.readResource(server, uri);
|
|
24
|
+
spinner.stop();
|
|
25
|
+
console.log(formatResourceContents(server, uri, result, formatOptions));
|
|
26
|
+
} else if (server) {
|
|
27
|
+
const resources = await manager.listResources(server);
|
|
28
|
+
spinner.stop();
|
|
29
|
+
console.log(formatServerResources(server, resources, formatOptions));
|
|
30
|
+
} else {
|
|
31
|
+
const { resources, errors } = await manager.getAllResources();
|
|
32
|
+
spinner.stop();
|
|
33
|
+
console.log(formatResourceList(resources, formatOptions));
|
|
34
|
+
for (const err of errors) {
|
|
35
|
+
console.error(formatError(`${err.server}: ${err.message}`, formatOptions));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
spinner.error("Failed");
|
|
40
|
+
console.error(formatError(String(err), formatOptions));
|
|
41
|
+
process.exit(1);
|
|
42
|
+
} finally {
|
|
43
|
+
await manager.close();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
package/src/config/schemas.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import type { Tool, Resource, Prompt } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import type {
|
|
3
3
|
OAuthTokens,
|
|
4
4
|
OAuthClientInformation,
|
|
@@ -6,7 +6,14 @@ import type {
|
|
|
6
6
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
7
7
|
|
|
8
8
|
// Re-export SDK types we use throughout the codebase
|
|
9
|
-
export type {
|
|
9
|
+
export type {
|
|
10
|
+
Tool,
|
|
11
|
+
Resource,
|
|
12
|
+
Prompt,
|
|
13
|
+
OAuthTokens,
|
|
14
|
+
OAuthClientInformation,
|
|
15
|
+
OAuthClientInformationMixed,
|
|
16
|
+
};
|
|
10
17
|
|
|
11
18
|
// --- Server config (our format, not MCP spec) ---
|
|
12
19
|
|
package/src/output/formatter.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { bold, cyan, dim, green, red, yellow } from "ansis";
|
|
2
|
-
import type { Tool } from "../config/schemas.ts";
|
|
3
|
-
import type { ToolWithServer } from "../client/manager.ts";
|
|
2
|
+
import type { Tool, Resource, Prompt } from "../config/schemas.ts";
|
|
3
|
+
import type { ToolWithServer, ResourceWithServer, PromptWithServer } from "../client/manager.ts";
|
|
4
4
|
import type { ValidationError } from "../validation/schema.ts";
|
|
5
5
|
import type { SearchResult } from "../search/index.ts";
|
|
6
6
|
|
|
@@ -11,6 +11,13 @@ export interface FormatOptions {
|
|
|
11
11
|
showSecrets?: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface UnifiedItem {
|
|
15
|
+
server: string;
|
|
16
|
+
type: "tool" | "resource" | "prompt";
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
/** Check if stdout is a TTY (interactive terminal) */
|
|
15
22
|
export function isInteractive(options: FormatOptions): boolean {
|
|
16
23
|
if (options.json) return false;
|
|
@@ -307,6 +314,273 @@ export function formatSearchResults(results: SearchResult[], options: FormatOpti
|
|
|
307
314
|
.join("\n");
|
|
308
315
|
}
|
|
309
316
|
|
|
317
|
+
/** Format a list of resources with server names */
|
|
318
|
+
export function formatResourceList(
|
|
319
|
+
resources: ResourceWithServer[],
|
|
320
|
+
options: FormatOptions,
|
|
321
|
+
): string {
|
|
322
|
+
if (!isInteractive(options)) {
|
|
323
|
+
return JSON.stringify(
|
|
324
|
+
resources.map((r) => ({
|
|
325
|
+
server: r.server,
|
|
326
|
+
uri: r.resource.uri,
|
|
327
|
+
name: r.resource.name,
|
|
328
|
+
...(options.withDescriptions ? { description: r.resource.description ?? "" } : {}),
|
|
329
|
+
})),
|
|
330
|
+
null,
|
|
331
|
+
2,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (resources.length === 0) {
|
|
336
|
+
return dim("No resources found");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const maxServer = Math.max(...resources.map((r) => r.server.length));
|
|
340
|
+
const maxUri = Math.max(...resources.map((r) => r.resource.uri.length));
|
|
341
|
+
|
|
342
|
+
return resources
|
|
343
|
+
.map((r) => {
|
|
344
|
+
const server = cyan(r.server.padEnd(maxServer));
|
|
345
|
+
const uri = bold(r.resource.uri.padEnd(maxUri));
|
|
346
|
+
if (options.withDescriptions && r.resource.description) {
|
|
347
|
+
return `${server} ${uri} ${dim(r.resource.description)}`;
|
|
348
|
+
}
|
|
349
|
+
return `${server} ${uri}`;
|
|
350
|
+
})
|
|
351
|
+
.join("\n");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Format resources for a single server */
|
|
355
|
+
export function formatServerResources(
|
|
356
|
+
serverName: string,
|
|
357
|
+
resources: Resource[],
|
|
358
|
+
options: FormatOptions,
|
|
359
|
+
): string {
|
|
360
|
+
if (!isInteractive(options)) {
|
|
361
|
+
return JSON.stringify(
|
|
362
|
+
{
|
|
363
|
+
server: serverName,
|
|
364
|
+
resources: resources.map((r) => ({
|
|
365
|
+
uri: r.uri,
|
|
366
|
+
name: r.name,
|
|
367
|
+
description: r.description ?? "",
|
|
368
|
+
mimeType: r.mimeType ?? "",
|
|
369
|
+
})),
|
|
370
|
+
},
|
|
371
|
+
null,
|
|
372
|
+
2,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (resources.length === 0) {
|
|
377
|
+
return dim(`No resources found for ${serverName}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const header = cyan.bold(serverName);
|
|
381
|
+
const maxUri = Math.max(...resources.map((r) => r.uri.length));
|
|
382
|
+
|
|
383
|
+
const lines = resources.map((r) => {
|
|
384
|
+
const uri = ` ${bold(r.uri.padEnd(maxUri))}`;
|
|
385
|
+
if (r.description) {
|
|
386
|
+
return `${uri} ${dim(r.description)}`;
|
|
387
|
+
}
|
|
388
|
+
return uri;
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
return [header, ...lines].join("\n");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** Format resource contents */
|
|
395
|
+
export function formatResourceContents(
|
|
396
|
+
serverName: string,
|
|
397
|
+
uri: string,
|
|
398
|
+
result: unknown,
|
|
399
|
+
options: FormatOptions,
|
|
400
|
+
): string {
|
|
401
|
+
if (!isInteractive(options)) {
|
|
402
|
+
return JSON.stringify(
|
|
403
|
+
{ server: serverName, uri, contents: (result as { contents: unknown })?.contents ?? result },
|
|
404
|
+
null,
|
|
405
|
+
2,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const contents =
|
|
410
|
+
(result as { contents?: Array<{ text?: string; blob?: string; mimeType?: string }> })
|
|
411
|
+
?.contents ?? [];
|
|
412
|
+
const lines: string[] = [];
|
|
413
|
+
lines.push(`${cyan(serverName)}/${bold(uri)}`);
|
|
414
|
+
lines.push("");
|
|
415
|
+
|
|
416
|
+
if (contents.length === 0) {
|
|
417
|
+
lines.push(dim("(empty)"));
|
|
418
|
+
} else {
|
|
419
|
+
for (const item of contents) {
|
|
420
|
+
if (item.text !== undefined) {
|
|
421
|
+
lines.push(item.text);
|
|
422
|
+
} else if (item.blob !== undefined) {
|
|
423
|
+
lines.push(dim(`<binary blob, ${item.blob.length} bytes base64>`));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return lines.join("\n");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** Format a list of prompts with server names */
|
|
432
|
+
export function formatPromptList(prompts: PromptWithServer[], options: FormatOptions): string {
|
|
433
|
+
if (!isInteractive(options)) {
|
|
434
|
+
return JSON.stringify(
|
|
435
|
+
prompts.map((p) => ({
|
|
436
|
+
server: p.server,
|
|
437
|
+
name: p.prompt.name,
|
|
438
|
+
...(options.withDescriptions ? { description: p.prompt.description ?? "" } : {}),
|
|
439
|
+
})),
|
|
440
|
+
null,
|
|
441
|
+
2,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (prompts.length === 0) {
|
|
446
|
+
return dim("No prompts found");
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const maxServer = Math.max(...prompts.map((p) => p.server.length));
|
|
450
|
+
const maxName = Math.max(...prompts.map((p) => p.prompt.name.length));
|
|
451
|
+
|
|
452
|
+
return prompts
|
|
453
|
+
.map((p) => {
|
|
454
|
+
const server = cyan(p.server.padEnd(maxServer));
|
|
455
|
+
const name = bold(p.prompt.name.padEnd(maxName));
|
|
456
|
+
if (options.withDescriptions && p.prompt.description) {
|
|
457
|
+
return `${server} ${name} ${dim(p.prompt.description)}`;
|
|
458
|
+
}
|
|
459
|
+
return `${server} ${name}`;
|
|
460
|
+
})
|
|
461
|
+
.join("\n");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** Format prompts for a single server */
|
|
465
|
+
export function formatServerPrompts(
|
|
466
|
+
serverName: string,
|
|
467
|
+
prompts: Prompt[],
|
|
468
|
+
options: FormatOptions,
|
|
469
|
+
): string {
|
|
470
|
+
if (!isInteractive(options)) {
|
|
471
|
+
return JSON.stringify(
|
|
472
|
+
{
|
|
473
|
+
server: serverName,
|
|
474
|
+
prompts: prompts.map((p) => ({
|
|
475
|
+
name: p.name,
|
|
476
|
+
description: p.description ?? "",
|
|
477
|
+
arguments: p.arguments ?? [],
|
|
478
|
+
})),
|
|
479
|
+
},
|
|
480
|
+
null,
|
|
481
|
+
2,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (prompts.length === 0) {
|
|
486
|
+
return dim(`No prompts found for ${serverName}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const header = cyan.bold(serverName);
|
|
490
|
+
const maxName = Math.max(...prompts.map((p) => p.name.length));
|
|
491
|
+
|
|
492
|
+
const lines = prompts.map((p) => {
|
|
493
|
+
const name = ` ${bold(p.name.padEnd(maxName))}`;
|
|
494
|
+
const args =
|
|
495
|
+
p.arguments && p.arguments.length > 0
|
|
496
|
+
? ` ${dim(`(${p.arguments.map((a) => (a.required ? a.name : `[${a.name}]`)).join(", ")})`)}`
|
|
497
|
+
: "";
|
|
498
|
+
if (p.description) {
|
|
499
|
+
return `${name}${args} ${dim(p.description)}`;
|
|
500
|
+
}
|
|
501
|
+
return `${name}${args}`;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
return [header, ...lines].join("\n");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Format prompt messages */
|
|
508
|
+
export function formatPromptMessages(
|
|
509
|
+
serverName: string,
|
|
510
|
+
name: string,
|
|
511
|
+
result: unknown,
|
|
512
|
+
options: FormatOptions,
|
|
513
|
+
): string {
|
|
514
|
+
if (!isInteractive(options)) {
|
|
515
|
+
return JSON.stringify({ server: serverName, prompt: name, ...(result as object) }, null, 2);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const r = result as {
|
|
519
|
+
description?: string;
|
|
520
|
+
messages?: Array<{ role: string; content: { type: string; text?: string } }>;
|
|
521
|
+
};
|
|
522
|
+
const lines: string[] = [];
|
|
523
|
+
lines.push(`${cyan(serverName)}/${bold(name)}`);
|
|
524
|
+
|
|
525
|
+
if (r.description) {
|
|
526
|
+
lines.push(dim(r.description));
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
lines.push("");
|
|
530
|
+
|
|
531
|
+
for (const msg of r.messages ?? []) {
|
|
532
|
+
lines.push(`${bold(msg.role)}:`);
|
|
533
|
+
if (msg.content.text !== undefined) {
|
|
534
|
+
lines.push(` ${msg.content.text}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return lines.join("\n");
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/** Format a unified list of tools, resources, and prompts across servers */
|
|
542
|
+
export function formatUnifiedList(items: UnifiedItem[], options: FormatOptions): string {
|
|
543
|
+
if (!isInteractive(options)) {
|
|
544
|
+
return JSON.stringify(
|
|
545
|
+
items.map((i) => ({
|
|
546
|
+
server: i.server,
|
|
547
|
+
type: i.type,
|
|
548
|
+
name: i.name,
|
|
549
|
+
...(options.withDescriptions ? { description: i.description ?? "" } : {}),
|
|
550
|
+
})),
|
|
551
|
+
null,
|
|
552
|
+
2,
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (items.length === 0) {
|
|
557
|
+
return dim("No tools, resources, or prompts found");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const maxServer = Math.max(...items.map((i) => i.server.length));
|
|
561
|
+
const maxType = 8; // "resource" is the longest at 8 chars
|
|
562
|
+
const maxName = Math.max(...items.map((i) => i.name.length));
|
|
563
|
+
|
|
564
|
+
const typeLabel = (t: UnifiedItem["type"]) => {
|
|
565
|
+
const padded = t.padEnd(maxType);
|
|
566
|
+
if (t === "tool") return green(padded);
|
|
567
|
+
if (t === "resource") return cyan(padded);
|
|
568
|
+
return yellow(padded);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
return items
|
|
572
|
+
.map((i) => {
|
|
573
|
+
const server = cyan(i.server.padEnd(maxServer));
|
|
574
|
+
const type = typeLabel(i.type);
|
|
575
|
+
const name = bold(i.name.padEnd(maxName));
|
|
576
|
+
if (options.withDescriptions && i.description) {
|
|
577
|
+
return `${server} ${type} ${name} ${dim(i.description)}`;
|
|
578
|
+
}
|
|
579
|
+
return `${server} ${type} ${name}`;
|
|
580
|
+
})
|
|
581
|
+
.join("\n");
|
|
582
|
+
}
|
|
583
|
+
|
|
310
584
|
/** Format an error message */
|
|
311
585
|
export function formatError(message: string, options: FormatOptions): string {
|
|
312
586
|
if (!isInteractive(options)) {
|