@evantahler/mcpx 0.18.3 → 0.18.6

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 -63
  2. package/src/cli.ts +46 -54
  3. package/src/client/browser.ts +36 -15
  4. package/src/client/debug-fetch.ts +64 -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 +103 -103
  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 +794 -815
  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 +53 -58
@@ -1,241 +1,237 @@
1
1
  import type { Command } from "commander";
2
+ import type { ServerManager } from "../client/manager.ts";
3
+ import { DEFAULTS } from "../constants.ts";
2
4
  import { getContext } from "../context.ts";
5
+ import { parseJsonArgs, readStdin } from "../lib/input.ts";
3
6
  import {
4
- formatCallResult,
5
- formatError,
6
- formatServerTools,
7
- formatTaskCreated,
8
- formatValidationErrors,
7
+ formatCallResult,
8
+ formatError,
9
+ formatServerTools,
10
+ formatTaskCreated,
11
+ formatValidationErrors,
9
12
  } from "../output/formatter.ts";
10
13
  import { logger } from "../output/logger.ts";
11
14
  import { validateToolInput } from "../validation/schema.ts";
12
- import { parseJsonArgs, readStdin } from "../lib/input.ts";
13
- import { DEFAULTS } from "../constants.ts";
14
- import type { ServerManager } from "../client/manager.ts";
15
15
 
16
16
  type ResolvedArgs =
17
- | { mode: "list-tools"; server: string }
18
- | { mode: "call-tool"; server: string; tool: string; argsStr: string | undefined };
17
+ | { mode: "list-tools"; server: string }
18
+ | { mode: "call-tool"; server: string; tool: string; argsStr: string | undefined };
19
19
 
20
20
  /**
21
21
  * Resolve the positional args into either list-tools or call-tool mode.
22
22
  * Supports both `exec <server> <tool> [args]` and `exec <tool> [args]`.
23
23
  */
24
24
  async function resolveExecArgs(
25
- manager: ServerManager,
26
- first: string,
27
- second: string | undefined,
28
- third: string | undefined,
25
+ manager: ServerManager,
26
+ first: string,
27
+ second: string | undefined,
28
+ third: string | undefined,
29
29
  ): Promise<ResolvedArgs> {
30
- const serverNames = manager.getServerNames();
31
- const isServer = serverNames.includes(first);
32
-
33
- if (isServer) {
34
- // Traditional form: exec <server> [tool] [args]
35
- if (!second) {
36
- return { mode: "list-tools", server: first };
37
- }
38
-
39
- // Validate the tool exists on the specified server
40
- const serverTools = await manager.listTools(first);
41
- const toolExists = serverTools.some((t) => t.name === second);
42
-
43
- if (!toolExists) {
44
- const { tools } = await manager.getAllTools();
45
- const matches = tools.filter((t) => t.tool.name === second);
46
-
47
- if (matches.length === 1) {
48
- throw new Error(
49
- `Tool "${second}" not found on server "${first}". Did you mean:\n mcpx exec ${matches[0]!.server} ${second}`,
50
- );
51
- } else if (matches.length > 1) {
52
- const servers = matches.map((m) => m.server).join(", ");
53
- throw new Error(
54
- `Tool "${second}" not found on server "${first}". Found on: ${servers}\nUsage: mcpx exec <server> ${second} [args]`,
55
- );
56
- } else {
57
- throw new Error(
58
- `Tool "${second}" not found on server "${first}". Run "mcpx search ${second}" to find similar tools.`,
59
- );
60
- }
61
- }
62
-
63
- return { mode: "call-tool", server: first, tool: second, argsStr: third };
64
- }
65
-
66
- // Not a server name — treat first as a tool name
67
- const toolName = first;
68
- const { tools } = await manager.getAllTools();
69
- const matches = tools.filter((t) => t.tool.name === toolName);
70
-
71
- if (matches.length === 0) {
72
- throw new Error(
73
- `Unknown server or tool "${first}". Run "mcpx search ${first}" to find similar tools.`,
74
- );
75
- }
76
-
77
- if (matches.length > 1) {
78
- const servers = matches.map((m) => m.server).join(", ");
79
- throw new Error(
80
- `Ambiguous tool "${toolName}" — found on multiple servers: ${servers}\nSpecify the server: mcpx exec <server> ${toolName} [args]`,
81
- );
82
- }
83
-
84
- return { mode: "call-tool", server: matches[0]!.server, tool: toolName, argsStr: second };
30
+ const serverNames = manager.getServerNames();
31
+ const isServer = serverNames.includes(first);
32
+
33
+ if (isServer) {
34
+ // Traditional form: exec <server> [tool] [args]
35
+ if (!second) {
36
+ return { mode: "list-tools", server: first };
37
+ }
38
+
39
+ // Validate the tool exists on the specified server
40
+ const serverTools = await manager.listTools(first);
41
+ const toolExists = serverTools.some((t) => t.name === second);
42
+
43
+ if (!toolExists) {
44
+ const { tools } = await manager.getAllTools();
45
+ const matches = tools.filter((t) => t.tool.name === second);
46
+
47
+ if (matches.length === 1) {
48
+ throw new Error(
49
+ `Tool "${second}" not found on server "${first}". Did you mean:\n mcpx exec ${matches[0]?.server} ${second}`,
50
+ );
51
+ } else if (matches.length > 1) {
52
+ const servers = matches.map((m) => m.server).join(", ");
53
+ throw new Error(
54
+ `Tool "${second}" not found on server "${first}". Found on: ${servers}\nUsage: mcpx exec <server> ${second} [args]`,
55
+ );
56
+ } else {
57
+ throw new Error(
58
+ `Tool "${second}" not found on server "${first}". Run "mcpx search ${second}" to find similar tools.`,
59
+ );
60
+ }
61
+ }
62
+
63
+ return { mode: "call-tool", server: first, tool: second, argsStr: third };
64
+ }
65
+
66
+ // Not a server name — treat first as a tool name
67
+ const toolName = first;
68
+ const { tools } = await manager.getAllTools();
69
+ const matches = tools.filter((t) => t.tool.name === toolName);
70
+
71
+ if (matches.length === 0) {
72
+ throw new Error(`Unknown server or tool "${first}". Run "mcpx search ${first}" to find similar tools.`);
73
+ }
74
+
75
+ if (matches.length > 1) {
76
+ const servers = matches.map((m) => m.server).join(", ");
77
+ throw new Error(
78
+ `Ambiguous tool "${toolName}" — found on multiple servers: ${servers}\nSpecify the server: mcpx exec <server> ${toolName} [args]`,
79
+ );
80
+ }
81
+
82
+ return { mode: "call-tool", server: matches[0]!.server, tool: toolName, argsStr: second };
85
83
  }
86
84
 
87
85
  export function registerExecCommand(program: Command) {
88
- program
89
- .command("exec <first> [second] [third]")
90
- .description("execute a tool (server is optional if tool name is unambiguous)")
91
- .option("-f, --file <path>", "read JSON args from a file")
92
- .option("--no-wait", "return task handle immediately without waiting for completion")
93
- .option("--ttl <ms>", "task TTL in milliseconds", String(DEFAULTS.TASK_TTL_MS))
94
- .action(
95
- async (
96
- first: string,
97
- second: string | undefined,
98
- third: string | undefined,
99
- options: { file?: string; wait: boolean; ttl: string },
100
- ) => {
101
- const { manager, formatOptions } = await getContext(program);
102
-
103
- let resolved: ResolvedArgs;
104
- try {
105
- resolved = await resolveExecArgs(manager, first, second, third);
106
- } catch (err) {
107
- console.error(formatError(String(err), formatOptions));
108
- await manager.close();
109
- process.exit(1);
110
- }
111
-
112
- if (resolved.mode === "list-tools") {
113
- try {
114
- const tools = await manager.listTools(resolved.server);
115
- console.log(formatServerTools(resolved.server, tools, formatOptions));
116
- } catch (err) {
117
- console.error(formatError(String(err), formatOptions));
118
- process.exit(1);
119
- } finally {
120
- await manager.close();
121
- }
122
- return;
123
- }
124
-
125
- const { server, tool, argsStr } = resolved;
126
-
127
- try {
128
- // Error if both --file and positional arg provided
129
- if (options.file && argsStr) {
130
- throw new Error("Cannot specify both --file and inline JSON args");
131
- }
132
-
133
- // Parse args from: --file > positional arg > stdin > empty
134
- let args: Record<string, unknown> = {};
135
-
136
- if (options.file) {
137
- const file = Bun.file(options.file);
138
- if (!(await file.exists())) {
139
- throw new Error(`File not found: ${options.file}`);
140
- }
141
- const content = await file.text();
142
- args = parseJsonArgs(content);
143
- } else if (argsStr) {
144
- args = parseJsonArgs(argsStr);
145
- } else if (!process.stdin.isTTY) {
146
- // Read from stdin
147
- const stdin = await readStdin();
148
- if (stdin.trim()) {
149
- args = parseJsonArgs(stdin);
150
- }
151
- }
152
-
153
- // Validate args against tool inputSchema before calling
154
- const toolSchema = await manager.getToolSchema(server, tool);
155
- if (toolSchema) {
156
- const validation = validateToolInput(server, toolSchema, args);
157
- if (!validation.valid) {
158
- console.error(formatValidationErrors(server, tool, validation.errors, formatOptions));
159
- process.exit(1);
160
- }
161
- }
162
-
163
- // Check if tool supports task-augmented execution
164
- const taskSupport = (toolSchema as Record<string, unknown> | undefined)?.execution as
165
- | { taskSupport?: string }
166
- | undefined;
167
- const supportsTask = await manager.serverSupportsTask(server);
168
- const useTask =
169
- supportsTask &&
170
- taskSupport?.taskSupport !== undefined &&
171
- taskSupport.taskSupport !== "forbidden";
172
-
173
- if (useTask) {
174
- const abortController = new AbortController();
175
- let currentTaskId: string | undefined;
176
-
177
- // Graceful Ctrl+C: cancel the task before exiting
178
- const sigintHandler = async () => {
179
- abortController.abort();
180
- if (currentTaskId) {
181
- try {
182
- await manager.cancelTask(server, currentTaskId);
183
- } catch {
184
- // best effort
185
- }
186
- }
187
- await manager.close();
188
- process.exit(130);
189
- };
190
- process.on("SIGINT", sigintHandler);
191
-
192
- const spinner = logger.startSpinner(`Executing ${server}/${tool}...`, formatOptions);
193
- try {
194
- const stream = manager.callToolStream(server, tool, args, {
195
- ttl: parseInt(options.ttl, 10),
196
- signal: abortController.signal,
197
- });
198
-
199
- for await (const message of stream) {
200
- switch (message.type) {
201
- case "taskCreated":
202
- currentTaskId = message.task.taskId;
203
- if (!options.wait) {
204
- // --no-wait: output the task handle and exit
205
- spinner.stop();
206
- console.log(formatTaskCreated(message.task, formatOptions));
207
- return;
208
- }
209
- spinner.update(`Task ${message.task.taskId} (${message.task.status})...`);
210
- break;
211
- case "taskStatus":
212
- spinner.update(`Task ${message.task.taskId} (${message.task.status})...`);
213
- break;
214
- case "result":
215
- spinner.stop();
216
- console.log(formatCallResult(message.result, formatOptions));
217
- return;
218
- case "error":
219
- spinner.error("Task failed");
220
- throw message.error;
221
- }
222
- }
223
- } finally {
224
- process.removeListener("SIGINT", sigintHandler);
225
- }
226
- } else {
227
- // Standard synchronous tool call
228
- const spinner = logger.startSpinner(`Executing ${server}/${tool}...`, formatOptions);
229
- const result = await manager.callTool(server, tool, args);
230
- spinner.stop();
231
- console.log(formatCallResult(result, formatOptions));
232
- }
233
- } catch (err) {
234
- console.error(formatError(String(err), formatOptions));
235
- process.exit(1);
236
- } finally {
237
- await manager.close();
238
- }
239
- },
240
- );
86
+ program
87
+ .command("exec <first> [second] [third]")
88
+ .description("execute a tool (server is optional if tool name is unambiguous)")
89
+ .option("-f, --file <path>", "read JSON args from a file")
90
+ .option("--no-wait", "return task handle immediately without waiting for completion")
91
+ .option("--ttl <ms>", "task TTL in milliseconds", String(DEFAULTS.TASK_TTL_MS))
92
+ .action(
93
+ async (
94
+ first: string,
95
+ second: string | undefined,
96
+ third: string | undefined,
97
+ options: { file?: string; wait: boolean; ttl: string },
98
+ ) => {
99
+ const { manager, formatOptions } = await getContext(program);
100
+
101
+ let resolved: ResolvedArgs;
102
+ try {
103
+ resolved = await resolveExecArgs(manager, first, second, third);
104
+ } catch (err) {
105
+ console.error(formatError(String(err), formatOptions));
106
+ await manager.close();
107
+ process.exit(1);
108
+ }
109
+
110
+ if (resolved.mode === "list-tools") {
111
+ try {
112
+ const tools = await manager.listTools(resolved.server);
113
+ console.log(formatServerTools(resolved.server, tools, formatOptions));
114
+ } catch (err) {
115
+ console.error(formatError(String(err), formatOptions));
116
+ process.exit(1);
117
+ } finally {
118
+ await manager.close();
119
+ }
120
+ return;
121
+ }
122
+
123
+ const { server, tool, argsStr } = resolved;
124
+
125
+ try {
126
+ // Error if both --file and positional arg provided
127
+ if (options.file && argsStr) {
128
+ throw new Error("Cannot specify both --file and inline JSON args");
129
+ }
130
+
131
+ // Parse args from: --file > positional arg > stdin > empty
132
+ let args: Record<string, unknown> = {};
133
+
134
+ if (options.file) {
135
+ const file = Bun.file(options.file);
136
+ if (!(await file.exists())) {
137
+ throw new Error(`File not found: ${options.file}`);
138
+ }
139
+ const content = await file.text();
140
+ args = parseJsonArgs(content);
141
+ } else if (argsStr) {
142
+ args = parseJsonArgs(argsStr);
143
+ } else if (!process.stdin.isTTY) {
144
+ // Read from stdin
145
+ const stdin = await readStdin();
146
+ if (stdin.trim()) {
147
+ args = parseJsonArgs(stdin);
148
+ }
149
+ }
150
+
151
+ // Validate args against tool inputSchema before calling
152
+ const toolSchema = await manager.getToolSchema(server, tool);
153
+ if (toolSchema) {
154
+ const validation = validateToolInput(server, toolSchema, args);
155
+ if (!validation.valid) {
156
+ console.error(formatValidationErrors(server, tool, validation.errors, formatOptions));
157
+ process.exit(1);
158
+ }
159
+ }
160
+
161
+ // Check if tool supports task-augmented execution
162
+ const taskSupport = (toolSchema as Record<string, unknown> | undefined)?.execution as
163
+ | { taskSupport?: string }
164
+ | undefined;
165
+ const supportsTask = await manager.serverSupportsTask(server);
166
+ const useTask =
167
+ supportsTask && taskSupport?.taskSupport !== undefined && taskSupport.taskSupport !== "forbidden";
168
+
169
+ if (useTask) {
170
+ const abortController = new AbortController();
171
+ let currentTaskId: string | undefined;
172
+
173
+ // Graceful Ctrl+C: cancel the task before exiting
174
+ const sigintHandler = async () => {
175
+ abortController.abort();
176
+ if (currentTaskId) {
177
+ try {
178
+ await manager.cancelTask(server, currentTaskId);
179
+ } catch {
180
+ // best effort
181
+ }
182
+ }
183
+ await manager.close();
184
+ process.exit(130);
185
+ };
186
+ process.on("SIGINT", sigintHandler);
187
+
188
+ const spinner = logger.startSpinner(`Executing ${server}/${tool}...`, formatOptions);
189
+ try {
190
+ const stream = manager.callToolStream(server, tool, args, {
191
+ ttl: parseInt(options.ttl, 10),
192
+ signal: abortController.signal,
193
+ });
194
+
195
+ for await (const message of stream) {
196
+ switch (message.type) {
197
+ case "taskCreated":
198
+ currentTaskId = message.task.taskId;
199
+ if (!options.wait) {
200
+ // --no-wait: output the task handle and exit
201
+ spinner.stop();
202
+ console.log(formatTaskCreated(message.task, formatOptions));
203
+ return;
204
+ }
205
+ spinner.update(`Task ${message.task.taskId} (${message.task.status})...`);
206
+ break;
207
+ case "taskStatus":
208
+ spinner.update(`Task ${message.task.taskId} (${message.task.status})...`);
209
+ break;
210
+ case "result":
211
+ spinner.stop();
212
+ console.log(formatCallResult(message.result, formatOptions));
213
+ return;
214
+ case "error":
215
+ spinner.error("Task failed");
216
+ throw message.error;
217
+ }
218
+ }
219
+ } finally {
220
+ process.removeListener("SIGINT", sigintHandler);
221
+ }
222
+ } else {
223
+ // Standard synchronous tool call
224
+ const spinner = logger.startSpinner(`Executing ${server}/${tool}...`, formatOptions);
225
+ const result = await manager.callTool(server, tool, args);
226
+ spinner.stop();
227
+ console.log(formatCallResult(result, formatOptions));
228
+ }
229
+ } catch (err) {
230
+ console.error(formatError(String(err), formatOptions));
231
+ process.exit(1);
232
+ } finally {
233
+ await manager.close();
234
+ }
235
+ },
236
+ );
241
237
  }
@@ -1,57 +1,57 @@
1
- import type { Command } from "commander";
2
1
  import { yellow } from "ansis";
2
+ import type { Command } from "commander";
3
+ import { saveSearchIndex } from "../config/loader.ts";
3
4
  import { getContext } from "../context.ts";
5
+ import { logger } from "../output/logger.ts";
4
6
  import { buildSearchIndex } from "../search/indexer.ts";
5
7
  import { getStaleServers } from "../search/staleness.ts";
6
- import { saveSearchIndex } from "../config/loader.ts";
7
- import { logger } from "../output/logger.ts";
8
8
  import { withCommand } from "./with-command.ts";
9
9
 
10
10
  /** Run the search index build. Reusable from other commands (e.g. add). */
11
11
  export async function runIndex(program: Command): Promise<void> {
12
- await withCommand(
13
- program,
14
- { spinnerText: "Connecting to servers...", errorLabel: "Indexing failed" },
15
- async ({ config, manager, spinner }) => {
16
- const start = performance.now();
17
- const index = await buildSearchIndex(manager, (progress) => {
18
- spinner.update(`Indexing ${progress.current}/${progress.total}: ${progress.tool}`);
19
- });
20
- const elapsed = ((performance.now() - start) / 1000).toFixed(1);
12
+ await withCommand(
13
+ program,
14
+ { spinnerText: "Connecting to servers...", errorLabel: "Indexing failed" },
15
+ async ({ config, manager, spinner }) => {
16
+ const start = performance.now();
17
+ const index = await buildSearchIndex(manager, (progress) => {
18
+ spinner.update(`Indexing ${progress.current}/${progress.total}: ${progress.tool}`);
19
+ });
20
+ const elapsed = ((performance.now() - start) / 1000).toFixed(1);
21
21
 
22
- await saveSearchIndex(config.configDir, index);
23
- spinner.success(`Indexed ${index.tools.length} tools in ${elapsed}s`);
22
+ await saveSearchIndex(config.configDir, index);
23
+ spinner.success(`Indexed ${index.tools.length} tools in ${elapsed}s`);
24
24
 
25
- logger.info(`Saved to ${config.configDir}/search.json`);
26
- },
27
- )();
25
+ logger.info(`Saved to ${config.configDir}/search.json`);
26
+ },
27
+ )();
28
28
  }
29
29
 
30
30
  export function registerIndexCommand(program: Command) {
31
- program
32
- .command("index")
33
- .description("build the search index from all configured servers")
34
- .option("-i, --status", "show index status")
35
- .action(async (options: { status?: boolean }) => {
36
- if (options.status) {
37
- const { config, manager } = await getContext(program);
38
- const idx = config.searchIndex;
39
- if (idx.tools.length === 0) {
40
- console.log("No search index. Run: mcpx index");
41
- } else {
42
- console.log(`Tools: ${idx.tools.length}`);
43
- console.log(`Model: ${idx.embedding_model}`);
44
- console.log(`Indexed: ${idx.indexed_at}`);
31
+ program
32
+ .command("index")
33
+ .description("build the search index from all configured servers")
34
+ .option("-i, --status", "show index status")
35
+ .action(async (options: { status?: boolean }) => {
36
+ if (options.status) {
37
+ const { config, manager } = await getContext(program);
38
+ const idx = config.searchIndex;
39
+ if (idx.tools.length === 0) {
40
+ console.log("No search index. Run: mcpx index");
41
+ } else {
42
+ console.log(`Tools: ${idx.tools.length}`);
43
+ console.log(`Model: ${idx.embedding_model}`);
44
+ console.log(`Indexed: ${idx.indexed_at}`);
45
45
 
46
- const stale = getStaleServers(idx, config.servers);
47
- if (stale.length > 0) {
48
- console.log(yellow(`Stale: ${stale.join(", ")} (run mcpx index to refresh)`));
49
- }
50
- }
51
- await manager.close();
52
- return;
53
- }
46
+ const stale = getStaleServers(idx, config.servers);
47
+ if (stale.length > 0) {
48
+ console.log(yellow(`Stale: ${stale.join(", ")} (run mcpx index to refresh)`));
49
+ }
50
+ }
51
+ await manager.close();
52
+ return;
53
+ }
54
54
 
55
- await runIndex(program);
56
- });
55
+ await runIndex(program);
56
+ });
57
57
  }