@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,114 +1,110 @@
1
1
  import type { Command } from "commander";
2
- import { getContext } from "../context.ts";
3
- import { isHttpServer } from "../config/schemas.ts";
4
- import { saveAuth } from "../config/loader.ts";
5
2
  import { McpOAuthProvider, runOAuthFlow } from "../client/oauth.ts";
3
+ import { saveAuth } from "../config/loader.ts";
4
+ import { isHttpServer } from "../config/schemas.ts";
5
+ import { getContext } from "../context.ts";
6
6
  import { logger } from "../output/logger.ts";
7
7
  import { runIndex } from "./index.ts";
8
8
 
9
9
  export function registerAuthCommand(program: Command) {
10
- program
11
- .command("auth <server>")
12
- .description("authenticate with an HTTP MCP server")
13
- .option("-s, --status", "check auth status and token TTL")
14
- .option("-r, --refresh", "force token refresh")
15
- .option("--no-index", "skip rebuilding the search index after auth")
16
- .action(
17
- async (server: string, options: { status?: boolean; refresh?: boolean; index?: boolean }) => {
18
- const { config, formatOptions } = await getContext(program);
10
+ program
11
+ .command("auth <server>")
12
+ .description("authenticate with an HTTP MCP server")
13
+ .option("-s, --status", "check auth status and token TTL")
14
+ .option("-r, --refresh", "force token refresh")
15
+ .option("--no-index", "skip rebuilding the search index after auth")
16
+ .action(async (server: string, options: { status?: boolean; refresh?: boolean; index?: boolean }) => {
17
+ const { config, formatOptions } = await getContext(program);
19
18
 
20
- const serverConfig = config.servers.mcpServers[server];
21
- if (!serverConfig) {
22
- console.error(`Unknown server: "${server}"`);
23
- process.exit(1);
24
- }
25
- if (!isHttpServer(serverConfig)) {
26
- console.error(
27
- `Server "${server}" is not an HTTP server — OAuth only applies to HTTP servers`,
28
- );
29
- process.exit(1);
30
- }
19
+ const serverConfig = config.servers.mcpServers[server];
20
+ if (!serverConfig) {
21
+ console.error(`Unknown server: "${server}"`);
22
+ process.exit(1);
23
+ }
24
+ if (!isHttpServer(serverConfig)) {
25
+ console.error(`Server "${server}" is not an HTTP server — OAuth only applies to HTTP servers`);
26
+ process.exit(1);
27
+ }
31
28
 
32
- const provider = new McpOAuthProvider({
33
- serverName: server,
34
- configDir: config.configDir,
35
- auth: config.auth,
36
- });
29
+ const provider = new McpOAuthProvider({
30
+ serverName: server,
31
+ configDir: config.configDir,
32
+ auth: config.auth,
33
+ });
37
34
 
38
- if (options.status) {
39
- showStatus(server, provider);
40
- return;
41
- }
35
+ if (options.status) {
36
+ showStatus(server, provider);
37
+ return;
38
+ }
42
39
 
43
- if (options.refresh) {
44
- const spinner = logger.startSpinner(`Refreshing token for "${server}"…`, formatOptions);
45
- try {
46
- await provider.refreshIfNeeded(serverConfig.url);
47
- spinner.success(`Token refreshed for "${server}"`);
48
- } catch (err) {
49
- spinner.error(`Refresh failed: ${err instanceof Error ? err.message : err}`);
50
- process.exit(1);
51
- }
52
- return;
53
- }
40
+ if (options.refresh) {
41
+ const spinner = logger.startSpinner(`Refreshing token for "${server}"…`, formatOptions);
42
+ try {
43
+ await provider.refreshIfNeeded(serverConfig.url);
44
+ spinner.success(`Token refreshed for "${server}"`);
45
+ } catch (err) {
46
+ spinner.error(`Refresh failed: ${err instanceof Error ? err.message : err}`);
47
+ process.exit(1);
48
+ }
49
+ return;
50
+ }
54
51
 
55
- // Default: full OAuth flow
56
- const spinner = logger.startSpinner(`Authenticating with "${server}"…`, formatOptions);
57
- try {
58
- await runOAuthFlow(serverConfig.url, provider);
59
- spinner.success(`Authenticated with "${server}"`);
60
- } catch (err) {
61
- spinner.error(`Authentication failed: ${err instanceof Error ? err.message : err}`);
62
- process.exit(1);
63
- }
52
+ // Default: full OAuth flow
53
+ const spinner = logger.startSpinner(`Authenticating with "${server}"…`, formatOptions);
54
+ try {
55
+ await runOAuthFlow(serverConfig.url, provider);
56
+ spinner.success(`Authenticated with "${server}"`);
57
+ } catch (err) {
58
+ spinner.error(`Authentication failed: ${err instanceof Error ? err.message : err}`);
59
+ process.exit(1);
60
+ }
64
61
 
65
- if (options.index !== false) {
66
- await runIndex(program);
67
- }
68
- },
69
- );
62
+ if (options.index !== false) {
63
+ await runIndex(program);
64
+ }
65
+ });
70
66
  }
71
67
 
72
68
  export function registerDeauthCommand(program: Command) {
73
- program
74
- .command("deauth <server>")
75
- .description("remove stored authentication for a server")
76
- .action(async (server: string) => {
77
- const { config } = await getContext(program);
69
+ program
70
+ .command("deauth <server>")
71
+ .description("remove stored authentication for a server")
72
+ .action(async (server: string) => {
73
+ const { config } = await getContext(program);
78
74
 
79
- if (!config.auth[server]) {
80
- console.log(`No auth stored for "${server}"`);
81
- return;
82
- }
75
+ if (!config.auth[server]) {
76
+ console.log(`No auth stored for "${server}"`);
77
+ return;
78
+ }
83
79
 
84
- delete config.auth[server];
85
- await saveAuth(config.configDir, config.auth);
86
- console.log(`Deauthenticated "${server}"`);
87
- });
80
+ delete config.auth[server];
81
+ await saveAuth(config.configDir, config.auth);
82
+ console.log(`Deauthenticated "${server}"`);
83
+ });
88
84
  }
89
85
 
90
86
  function showStatus(server: string, provider: McpOAuthProvider) {
91
- if (!provider.isComplete()) {
92
- console.log(`${server}: not authenticated`);
93
- return;
94
- }
87
+ if (!provider.isComplete()) {
88
+ console.log(`${server}: not authenticated`);
89
+ return;
90
+ }
95
91
 
96
- const expired = provider.isExpired();
97
- const hasRefresh = provider.hasRefreshToken();
98
- const status = expired ? "expired" : "authenticated";
92
+ const expired = provider.isExpired();
93
+ const hasRefresh = provider.hasRefreshToken();
94
+ const status = expired ? "expired" : "authenticated";
99
95
 
100
- console.log(`${server}: ${status}`);
101
- if (hasRefresh) {
102
- console.log(" refresh token: present");
103
- }
96
+ console.log(`${server}: ${status}`);
97
+ if (hasRefresh) {
98
+ console.log(" refresh token: present");
99
+ }
104
100
 
105
- if (!expired) {
106
- // Show TTL if we have expires_at from the auth entry
107
- const entry = provider["auth"][server];
108
- if (entry?.expires_at) {
109
- const remaining = new Date(entry.expires_at).getTime() - Date.now();
110
- const minutes = Math.round(remaining / 60000);
111
- console.log(` expires in: ${minutes} minutes`);
112
- }
113
- }
101
+ if (!expired) {
102
+ // Show TTL if we have expires_at from the auth entry
103
+ const entry = provider.auth[server];
104
+ if (entry?.expires_at) {
105
+ const remaining = new Date(entry.expires_at).getTime() - Date.now();
106
+ const minutes = Math.round(remaining / 60000);
107
+ console.log(` expires in: ${minutes} minutes`);
108
+ }
109
+ }
114
110
  }
@@ -1,70 +1,66 @@
1
- import { green, yellow, cyan, dim } from "ansis";
1
+ import { cyan, dim, green, yellow } from "ansis";
2
2
  import type { Command } from "commander";
3
3
  import { createSpinner } from "nanospinner";
4
4
  import pkg from "../../package.json";
5
- import { checkForUpdate } from "../update/checker.ts";
6
5
  import { saveUpdateCache } from "../update/cache.ts";
7
6
  import type { UpdateCache } from "../update/checker.ts";
7
+ import { checkForUpdate } from "../update/checker.ts";
8
8
 
9
9
  export function registerCheckUpdateCommand(program: Command) {
10
- program
11
- .command("check-update")
12
- .description("Check for a newer version of mcpx")
13
- .action(async () => {
14
- const opts = program.opts();
15
- const json = !!(opts.json as boolean | undefined);
16
- const isTTY = process.stderr.isTTY ?? false;
10
+ program
11
+ .command("check-update")
12
+ .description("Check for a newer version of mcpx")
13
+ .action(async () => {
14
+ const opts = program.opts();
15
+ const json = !!(opts.json as boolean | undefined);
16
+ const isTTY = process.stderr.isTTY ?? false;
17
17
 
18
- const spinner =
19
- !json && isTTY
20
- ? createSpinner("Checking for updates...", { stream: process.stderr }).start()
21
- : null;
18
+ const spinner =
19
+ !json && isTTY ? createSpinner("Checking for updates...", { stream: process.stderr }).start() : null;
22
20
 
23
- try {
24
- const info = await checkForUpdate(pkg.version);
21
+ try {
22
+ const info = await checkForUpdate(pkg.version);
25
23
 
26
- // Save to cache
27
- const cache: UpdateCache = {
28
- lastCheckAt: new Date().toISOString(),
29
- latestVersion: info.latestVersion,
30
- hasUpdate: info.hasUpdate,
31
- changelog: info.changelog,
32
- };
33
- await saveUpdateCache(cache);
24
+ // Save to cache
25
+ const cache: UpdateCache = {
26
+ lastCheckAt: new Date().toISOString(),
27
+ latestVersion: info.latestVersion,
28
+ hasUpdate: info.hasUpdate,
29
+ changelog: info.changelog,
30
+ };
31
+ await saveUpdateCache(cache);
34
32
 
35
- spinner?.stop();
33
+ spinner?.stop();
36
34
 
37
- if (json) {
38
- console.log(JSON.stringify(info, null, 2));
39
- return;
40
- }
35
+ if (json) {
36
+ console.log(JSON.stringify(info, null, 2));
37
+ return;
38
+ }
41
39
 
42
- if (!info.hasUpdate) {
43
- if (info.aheadOfLatest) {
44
- console.log(
45
- yellow(
46
- `mcpx v${info.currentVersion} is ahead of latest published release (v${info.latestVersion})`,
47
- ),
48
- );
49
- } else {
50
- console.log(green(`mcpx is up to date (v${info.currentVersion})`));
51
- }
52
- return;
53
- }
40
+ if (!info.hasUpdate) {
41
+ if (info.aheadOfLatest) {
42
+ console.log(
43
+ yellow(`mcpx v${info.currentVersion} is ahead of latest published release (v${info.latestVersion})`),
44
+ );
45
+ } else {
46
+ console.log(green(`mcpx is up to date (v${info.currentVersion})`));
47
+ }
48
+ return;
49
+ }
54
50
 
55
- console.log(yellow(`Update available: ${info.currentVersion} → ${info.latestVersion}`));
51
+ console.log(yellow(`Update available: ${info.currentVersion} → ${info.latestVersion}`));
56
52
 
57
- if (info.changelog) {
58
- console.log("");
59
- console.log(dim(info.changelog));
60
- }
53
+ if (info.changelog) {
54
+ console.log("");
55
+ console.log(dim(info.changelog));
56
+ }
61
57
 
62
- console.log("");
63
- console.log(cyan(`Run \`mcpx upgrade\` to update`));
64
- } catch (err) {
65
- spinner?.error({ text: "Failed to check for updates" });
66
- console.error(String(err));
67
- process.exit(1);
68
- }
69
- });
58
+ console.log("");
59
+ console.log(cyan(`Run \`mcpx upgrade\` to update`));
60
+ } catch (err) {
61
+ spinner?.error({ text: "Failed to check for updates" });
62
+ console.error(String(err));
63
+ process.exit(1);
64
+ }
65
+ });
70
66
  }
@@ -1,134 +1,131 @@
1
+ import { bold, dim, red, yellow } from "ansis";
1
2
  import type { Command } from "commander";
2
- import { bold, dim, green, red, yellow } from "ansis";
3
3
  import {
4
- type Client,
5
- type Scope,
6
- resolveSettingsPath,
7
- readClientSettings,
8
- writeClientSettings,
9
- execPattern,
10
- readOnlyPatterns,
11
- allExecPattern,
12
- removePatterns,
13
- removeAllMcpxPatterns,
14
- getServerPatterns,
4
+ type Client,
5
+ execPattern,
6
+ getServerPatterns,
7
+ readClientSettings,
8
+ readOnlyPatterns,
9
+ removeAllMcpxPatterns,
10
+ removePatterns,
11
+ resolveSettingsPath,
12
+ type Scope,
13
+ writeClientSettings,
15
14
  } from "../lib/client-settings.ts";
16
15
  import { formatOutput } from "../output/format-output.ts";
17
16
  import type { FormatOptions } from "../output/formatter.ts";
18
17
 
19
18
  export function registerDenyCommand(program: Command) {
20
- program
21
- .command("deny")
22
- .description("remove permission rules for mcpx commands (Claude Code or Cursor)")
23
- .argument("[server]", "server name to deny")
24
- .argument("[tools...]", "specific tool names to deny")
25
- .option("--all", "remove all mcpx-related permissions")
26
- .option("--all-read", "remove read-only command permissions")
27
- .option("--cursor", "target Cursor settings instead of Claude Code")
28
- .option("--local", "write to local settings (default)")
29
- .option("--project", "write to project settings (shared)")
30
- .option("--global", "write to global settings")
31
- .option("--dry-run", "show what would be removed")
32
- .action(
33
- async (
34
- server: string | undefined,
35
- tools: string[],
36
- options: {
37
- all?: boolean;
38
- allRead?: boolean;
39
- cursor?: boolean;
40
- local?: boolean;
41
- project?: boolean;
42
- global?: boolean;
43
- dryRun?: boolean;
44
- },
45
- ) => {
46
- const formatOptions: FormatOptions = { json: program.opts().json };
47
- const client: Client = options.cursor ? "cursor" : "claude";
48
- const scope: Scope = options.global ? "global" : options.project ? "project" : "local";
49
- const path = resolveSettingsPath(scope, client);
50
- const settings = await readClientSettings(path);
19
+ program
20
+ .command("deny")
21
+ .description("remove permission rules for mcpx commands (Claude Code or Cursor)")
22
+ .argument("[server]", "server name to deny")
23
+ .argument("[tools...]", "specific tool names to deny")
24
+ .option("--all", "remove all mcpx-related permissions")
25
+ .option("--all-read", "remove read-only command permissions")
26
+ .option("--cursor", "target Cursor settings instead of Claude Code")
27
+ .option("--local", "write to local settings (default)")
28
+ .option("--project", "write to project settings (shared)")
29
+ .option("--global", "write to global settings")
30
+ .option("--dry-run", "show what would be removed")
31
+ .action(
32
+ async (
33
+ server: string | undefined,
34
+ tools: string[],
35
+ options: {
36
+ all?: boolean;
37
+ allRead?: boolean;
38
+ cursor?: boolean;
39
+ local?: boolean;
40
+ project?: boolean;
41
+ global?: boolean;
42
+ dryRun?: boolean;
43
+ },
44
+ ) => {
45
+ const formatOptions: FormatOptions = { json: program.opts().json };
46
+ const client: Client = options.cursor ? "cursor" : "claude";
47
+ const scope: Scope = options.global ? "global" : options.project ? "project" : "local";
48
+ const path = resolveSettingsPath(scope, client);
49
+ const settings = await readClientSettings(path);
51
50
 
52
- let result: { settings: typeof settings; removed: string[] };
51
+ let result: { settings: typeof settings; removed: string[] };
53
52
 
54
- if (options.all) {
55
- // Remove all mcpx-related patterns
56
- result = removeAllMcpxPatterns(settings, client);
57
- } else {
58
- // Build the list of patterns to remove
59
- const patterns: string[] = [];
53
+ if (options.all) {
54
+ // Remove all mcpx-related patterns
55
+ result = removeAllMcpxPatterns(settings, client);
56
+ } else {
57
+ // Build the list of patterns to remove
58
+ const patterns: string[] = [];
60
59
 
61
- if (options.allRead) {
62
- patterns.push(...readOnlyPatterns(client));
63
- }
60
+ if (options.allRead) {
61
+ patterns.push(...readOnlyPatterns(client));
62
+ }
64
63
 
65
- if (server && tools.length > 0) {
66
- for (const tool of tools) {
67
- patterns.push(execPattern(server, tool, client));
68
- }
69
- } else if (server) {
70
- // Remove the server-level pattern AND all tool-specific patterns for this server
71
- patterns.push(execPattern(server, undefined, client));
72
- patterns.push(...getServerPatterns(settings, server, client));
73
- }
64
+ if (server && tools.length > 0) {
65
+ for (const tool of tools) {
66
+ patterns.push(execPattern(server, tool, client));
67
+ }
68
+ } else if (server) {
69
+ // Remove the server-level pattern AND all tool-specific patterns for this server
70
+ patterns.push(execPattern(server, undefined, client));
71
+ patterns.push(...getServerPatterns(settings, server, client));
72
+ }
74
73
 
75
- if (patterns.length === 0) {
76
- console.error("error: specify a server, --all, or --all-read. See 'mcpx deny --help'.");
77
- process.exit(1);
78
- }
74
+ if (patterns.length === 0) {
75
+ console.error("error: specify a server, --all, or --all-read. See 'mcpx deny --help'.");
76
+ process.exit(1);
77
+ }
79
78
 
80
- result = removePatterns(settings, patterns);
81
- }
79
+ result = removePatterns(settings, patterns);
80
+ }
82
81
 
83
- if (options.dryRun) {
84
- console.log(
85
- formatOutput(
86
- { scope, path, wouldRemove: result.removed },
87
- () => {
88
- const lines: string[] = [];
89
- lines.push(bold("Dry run") + dim(` — would remove from ${path}:`));
90
- if (result.removed.length === 0) {
91
- lines.push(` ${dim("(no matching patterns found)")}`);
92
- } else {
93
- for (const p of result.removed) {
94
- lines.push(` ${yellow("-")} ${p}`);
95
- }
96
- }
97
- return lines.join("\n");
98
- },
99
- formatOptions,
100
- ),
101
- );
102
- return;
103
- }
82
+ if (options.dryRun) {
83
+ console.log(
84
+ formatOutput(
85
+ { scope, path, wouldRemove: result.removed },
86
+ () => {
87
+ const lines: string[] = [];
88
+ lines.push(bold("Dry run") + dim(` — would remove from ${path}:`));
89
+ if (result.removed.length === 0) {
90
+ lines.push(` ${dim("(no matching patterns found)")}`);
91
+ } else {
92
+ for (const p of result.removed) {
93
+ lines.push(` ${yellow("-")} ${p}`);
94
+ }
95
+ }
96
+ return lines.join("\n");
97
+ },
98
+ formatOptions,
99
+ ),
100
+ );
101
+ return;
102
+ }
104
103
 
105
- await writeClientSettings(path, result.settings);
104
+ await writeClientSettings(path, result.settings);
106
105
 
107
- console.log(
108
- formatOutput(
109
- {
110
- scope,
111
- path,
112
- removed: result.removed,
113
- total: (result.settings.permissions?.allow ?? []).length,
114
- },
115
- () => {
116
- const lines: string[] = [];
117
- if (result.removed.length === 0) {
118
- lines.push(dim("No matching patterns found — no changes."));
119
- } else {
120
- lines.push(
121
- bold(`Removed ${result.removed.length} permission(s)`) + dim(` → ${path}`),
122
- );
123
- for (const p of result.removed) {
124
- lines.push(` ${red("-")} ${p}`);
125
- }
126
- }
127
- return lines.join("\n");
128
- },
129
- formatOptions,
130
- ),
131
- );
132
- },
133
- );
106
+ console.log(
107
+ formatOutput(
108
+ {
109
+ scope,
110
+ path,
111
+ removed: result.removed,
112
+ total: (result.settings.permissions?.allow ?? []).length,
113
+ },
114
+ () => {
115
+ const lines: string[] = [];
116
+ if (result.removed.length === 0) {
117
+ lines.push(dim("No matching patterns found — no changes."));
118
+ } else {
119
+ lines.push(bold(`Removed ${result.removed.length} permission(s)`) + dim(` → ${path}`));
120
+ for (const p of result.removed) {
121
+ lines.push(` ${red("-")} ${p}`);
122
+ }
123
+ }
124
+ return lines.join("\n");
125
+ },
126
+ formatOptions,
127
+ ),
128
+ );
129
+ },
130
+ );
134
131
  }