@evantahler/mcpx 0.18.2 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/package.json +63 -62
  2. package/src/cli.ts +46 -54
  3. package/src/client/browser.ts +15 -15
  4. package/src/client/debug-fetch.ts +53 -56
  5. package/src/client/elicitation.ts +279 -291
  6. package/src/client/http.ts +1 -1
  7. package/src/client/manager.ts +481 -514
  8. package/src/client/oauth.ts +272 -282
  9. package/src/client/sse.ts +1 -1
  10. package/src/client/stdio.ts +7 -7
  11. package/src/client/trace.ts +146 -152
  12. package/src/client/transport-options.ts +20 -20
  13. package/src/commands/add.ts +160 -165
  14. package/src/commands/allow.ts +141 -142
  15. package/src/commands/auth.ts +86 -90
  16. package/src/commands/check-update.ts +49 -53
  17. package/src/commands/deny.ts +114 -117
  18. package/src/commands/exec.ts +218 -222
  19. package/src/commands/index.ts +41 -41
  20. package/src/commands/info.ts +48 -50
  21. package/src/commands/list.ts +49 -49
  22. package/src/commands/ping.ts +47 -50
  23. package/src/commands/prompt.ts +40 -50
  24. package/src/commands/remove.ts +54 -56
  25. package/src/commands/resource.ts +31 -36
  26. package/src/commands/search.ts +35 -39
  27. package/src/commands/servers.ts +44 -48
  28. package/src/commands/skill.ts +89 -95
  29. package/src/commands/task.ts +50 -60
  30. package/src/commands/upgrade.ts +191 -208
  31. package/src/commands/with-command.ts +27 -29
  32. package/src/config/env.ts +26 -28
  33. package/src/config/loader.ts +99 -102
  34. package/src/config/schemas.ts +78 -87
  35. package/src/constants.ts +17 -17
  36. package/src/context.ts +51 -51
  37. package/src/lib/client-settings.ts +127 -140
  38. package/src/lib/input.ts +23 -26
  39. package/src/output/format-output.ts +12 -16
  40. package/src/output/format-table.ts +39 -42
  41. package/src/output/formatter.ts +790 -814
  42. package/src/output/logger.ts +140 -152
  43. package/src/sdk.ts +283 -291
  44. package/src/search/index.ts +50 -54
  45. package/src/search/indexer.ts +65 -65
  46. package/src/search/keyword.ts +54 -54
  47. package/src/search/semantic.ts +39 -39
  48. package/src/search/staleness.ts +3 -3
  49. package/src/search/types.ts +4 -4
  50. package/src/update/background.ts +51 -51
  51. package/src/update/cache.ts +21 -21
  52. package/src/update/checker.ts +81 -86
  53. package/src/validation/schema.ts +52 -58
@@ -1,210 +1,197 @@
1
- import { join } from "path";
2
- import { homedir } from "os";
3
- import { readFile, mkdir, writeFile } from "fs/promises";
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
4
 
5
5
  export type Client = "claude" | "cursor";
6
6
  export type Scope = "local" | "project" | "global";
7
7
 
8
8
  export interface ClientSettings {
9
- permissions?: {
10
- allow?: string[];
11
- deny?: string[];
12
- };
13
- [key: string]: unknown;
9
+ permissions?: {
10
+ allow?: string[];
11
+ deny?: string[];
12
+ };
13
+ [key: string]: unknown;
14
14
  }
15
15
 
16
16
  function prefix(client: Client): string {
17
- return client === "claude" ? "Bash" : "Shell";
17
+ return client === "claude" ? "Bash" : "Shell";
18
18
  }
19
19
 
20
20
  /** Resolve the settings file path for a given scope and client */
21
21
  export function resolveSettingsPath(scope: Scope, client: Client = "claude"): string {
22
- if (client === "cursor") {
23
- switch (scope) {
24
- case "local":
25
- case "project":
26
- return join(process.cwd(), ".cursor", "cli.json");
27
- case "global":
28
- return join(homedir(), ".cursor", "cli-config.json");
29
- }
30
- }
31
-
32
- switch (scope) {
33
- case "local":
34
- return join(process.cwd(), ".claude", "settings.local.json");
35
- case "project":
36
- return join(process.cwd(), ".claude", "settings.json");
37
- case "global":
38
- return join(homedir(), ".claude", "settings.json");
39
- }
22
+ if (client === "cursor") {
23
+ switch (scope) {
24
+ case "local":
25
+ case "project":
26
+ return join(process.cwd(), ".cursor", "cli.json");
27
+ case "global":
28
+ return join(homedir(), ".cursor", "cli-config.json");
29
+ }
30
+ }
31
+
32
+ switch (scope) {
33
+ case "local":
34
+ return join(process.cwd(), ".claude", "settings.local.json");
35
+ case "project":
36
+ return join(process.cwd(), ".claude", "settings.json");
37
+ case "global":
38
+ return join(homedir(), ".claude", "settings.json");
39
+ }
40
40
  }
41
41
 
42
42
  /** Read client settings from a file, returning empty settings if the file doesn't exist */
43
43
  export async function readClientSettings(path: string): Promise<ClientSettings> {
44
- try {
45
- const content = await readFile(path, "utf-8");
46
- return JSON.parse(content) as ClientSettings;
47
- } catch {
48
- return {};
49
- }
44
+ try {
45
+ const content = await readFile(path, "utf-8");
46
+ return JSON.parse(content) as ClientSettings;
47
+ } catch {
48
+ return {};
49
+ }
50
50
  }
51
51
 
52
52
  /** Write client settings to a file, creating parent directories as needed */
53
53
  export async function writeClientSettings(path: string, settings: ClientSettings): Promise<void> {
54
- const dir = join(path, "..");
55
- await mkdir(dir, { recursive: true });
56
- await writeFile(path, JSON.stringify(settings, null, 2) + "\n", "utf-8");
54
+ const dir = join(path, "..");
55
+ await mkdir(dir, { recursive: true });
56
+ await writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
57
57
  }
58
58
 
59
59
  /** Generate a permission pattern for mcpx exec with a specific server and optional tool */
60
60
  export function execPattern(server: string, tool?: string, client: Client = "claude"): string {
61
- const p = prefix(client);
62
- if (tool) {
63
- return `${p}(mcpx exec:${server}:${tool}:*)`;
64
- }
65
- return `${p}(mcpx exec:${server}:*)`;
61
+ const p = prefix(client);
62
+ if (tool) {
63
+ return `${p}(mcpx exec:${server}:${tool}:*)`;
64
+ }
65
+ return `${p}(mcpx exec:${server}:*)`;
66
66
  }
67
67
 
68
68
  /** Read-only mcpx commands that are safe to allow broadly */
69
- const READ_ONLY_COMMANDS = [
70
- "search",
71
- "info",
72
- "servers",
73
- "ping",
74
- "resource",
75
- "prompt",
76
- "task",
77
- "index",
78
- ];
69
+ const READ_ONLY_COMMANDS = ["search", "info", "servers", "ping", "resource", "prompt", "task", "index"];
79
70
 
80
71
  /** Generate patterns for all read-only mcpx commands */
81
72
  export function readOnlyPatterns(client: Client = "claude"): string[] {
82
- const p = prefix(client);
83
- return READ_ONLY_COMMANDS.map((cmd) => `${p}(mcpx ${cmd}:*)`);
73
+ const p = prefix(client);
74
+ return READ_ONLY_COMMANDS.map((cmd) => `${p}(mcpx ${cmd}:*)`);
84
75
  }
85
76
 
86
77
  /** Generate the broad allow-all pattern for mcpx exec */
87
78
  export function allExecPattern(client: Client = "claude"): string {
88
- return `${prefix(client)}(mcpx exec:*)`;
79
+ return `${prefix(client)}(mcpx exec:*)`;
89
80
  }
90
81
 
91
82
  /** Generate the allow pattern for mcpx allow itself */
92
83
  export function allowCommandPattern(client: Client = "claude"): string {
93
- return `${prefix(client)}(mcpx allow:*)`;
84
+ return `${prefix(client)}(mcpx allow:*)`;
94
85
  }
95
86
 
96
87
  /** Generate the allow pattern for mcpx deny itself */
97
88
  export function denyCommandPattern(client: Client = "claude"): string {
98
- return `${prefix(client)}(mcpx deny:*)`;
89
+ return `${prefix(client)}(mcpx deny:*)`;
99
90
  }
100
91
 
101
92
  /** Check if a permission pattern is mcpx-related */
102
93
  export function isMcpxPattern(pattern: string, client: Client = "claude"): boolean {
103
- return pattern.startsWith(`${prefix(client)}(mcpx `);
94
+ return pattern.startsWith(`${prefix(client)}(mcpx `);
104
95
  }
105
96
 
106
97
  /** Add patterns to settings, deduplicating. Returns the updated settings and list of newly added patterns. */
107
98
  export function addPatterns(
108
- settings: ClientSettings,
109
- patterns: string[],
99
+ settings: ClientSettings,
100
+ patterns: string[],
110
101
  ): { settings: ClientSettings; added: string[] } {
111
- const existing = new Set(settings.permissions?.allow ?? []);
112
- const added: string[] = [];
113
-
114
- for (const p of patterns) {
115
- if (!existing.has(p)) {
116
- existing.add(p);
117
- added.push(p);
118
- }
119
- }
120
-
121
- return {
122
- settings: {
123
- ...settings,
124
- permissions: {
125
- ...settings.permissions,
126
- allow: [...existing],
127
- },
128
- },
129
- added,
130
- };
102
+ const existing = new Set(settings.permissions?.allow ?? []);
103
+ const added: string[] = [];
104
+
105
+ for (const p of patterns) {
106
+ if (!existing.has(p)) {
107
+ existing.add(p);
108
+ added.push(p);
109
+ }
110
+ }
111
+
112
+ return {
113
+ settings: {
114
+ ...settings,
115
+ permissions: {
116
+ ...settings.permissions,
117
+ allow: [...existing],
118
+ },
119
+ },
120
+ added,
121
+ };
131
122
  }
132
123
 
133
124
  /** Remove specific patterns from settings. Returns the updated settings and list of removed patterns. */
134
125
  export function removePatterns(
135
- settings: ClientSettings,
136
- patterns: string[],
126
+ settings: ClientSettings,
127
+ patterns: string[],
137
128
  ): { settings: ClientSettings; removed: string[] } {
138
- const existing = settings.permissions?.allow ?? [];
139
- const toRemove = new Set(patterns);
140
- const removed: string[] = [];
141
- const remaining: string[] = [];
142
-
143
- for (const p of existing) {
144
- if (toRemove.has(p)) {
145
- removed.push(p);
146
- } else {
147
- remaining.push(p);
148
- }
149
- }
150
-
151
- return {
152
- settings: {
153
- ...settings,
154
- permissions: {
155
- ...settings.permissions,
156
- allow: remaining,
157
- },
158
- },
159
- removed,
160
- };
129
+ const existing = settings.permissions?.allow ?? [];
130
+ const toRemove = new Set(patterns);
131
+ const removed: string[] = [];
132
+ const remaining: string[] = [];
133
+
134
+ for (const p of existing) {
135
+ if (toRemove.has(p)) {
136
+ removed.push(p);
137
+ } else {
138
+ remaining.push(p);
139
+ }
140
+ }
141
+
142
+ return {
143
+ settings: {
144
+ ...settings,
145
+ permissions: {
146
+ ...settings.permissions,
147
+ allow: remaining,
148
+ },
149
+ },
150
+ removed,
151
+ };
161
152
  }
162
153
 
163
154
  /** Remove all mcpx-related patterns from settings. Returns the updated settings and list of removed patterns. */
164
155
  export function removeAllMcpxPatterns(
165
- settings: ClientSettings,
166
- client: Client = "claude",
156
+ settings: ClientSettings,
157
+ client: Client = "claude",
167
158
  ): {
168
- settings: ClientSettings;
169
- removed: string[];
159
+ settings: ClientSettings;
160
+ removed: string[];
170
161
  } {
171
- const existing = settings.permissions?.allow ?? [];
172
- const removed: string[] = [];
173
- const remaining: string[] = [];
174
-
175
- for (const p of existing) {
176
- if (isMcpxPattern(p, client)) {
177
- removed.push(p);
178
- } else {
179
- remaining.push(p);
180
- }
181
- }
182
-
183
- return {
184
- settings: {
185
- ...settings,
186
- permissions: {
187
- ...settings.permissions,
188
- allow: remaining,
189
- },
190
- },
191
- removed,
192
- };
162
+ const existing = settings.permissions?.allow ?? [];
163
+ const removed: string[] = [];
164
+ const remaining: string[] = [];
165
+
166
+ for (const p of existing) {
167
+ if (isMcpxPattern(p, client)) {
168
+ removed.push(p);
169
+ } else {
170
+ remaining.push(p);
171
+ }
172
+ }
173
+
174
+ return {
175
+ settings: {
176
+ ...settings,
177
+ permissions: {
178
+ ...settings.permissions,
179
+ allow: remaining,
180
+ },
181
+ },
182
+ removed,
183
+ };
193
184
  }
194
185
 
195
186
  /** Extract all mcpx-related patterns from settings */
196
187
  export function getMcpxPatterns(settings: ClientSettings, client: Client = "claude"): string[] {
197
- return (settings.permissions?.allow ?? []).filter((p) => isMcpxPattern(p, client));
188
+ return (settings.permissions?.allow ?? []).filter((p) => isMcpxPattern(p, client));
198
189
  }
199
190
 
200
191
  /** Get all mcpx-related patterns for a specific server */
201
- export function getServerPatterns(
202
- settings: ClientSettings,
203
- server: string,
204
- client: Client = "claude",
205
- ): string[] {
206
- const p = prefix(client);
207
- return getMcpxPatterns(settings, client).filter(
208
- (pat) => pat.startsWith(`${p}(mcpx exec:${server}:`) || pat === `${p}(mcpx exec:${server}:*)`,
209
- );
192
+ export function getServerPatterns(settings: ClientSettings, server: string, client: Client = "claude"): string[] {
193
+ const p = prefix(client);
194
+ return getMcpxPatterns(settings, client).filter(
195
+ (pat) => pat.startsWith(`${p}(mcpx exec:${server}:`) || pat === `${p}(mcpx exec:${server}:*)`,
196
+ );
210
197
  }
package/src/lib/input.ts CHANGED
@@ -3,34 +3,31 @@
3
3
  */
4
4
 
5
5
  /** Parse a JSON string as a key-value object, optionally coercing all values to strings. */
6
- export function parseJsonArgs(
7
- str: string,
8
- opts?: { coerceToString?: boolean },
9
- ): Record<string, unknown> {
10
- try {
11
- const parsed = JSON.parse(str);
12
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
13
- throw new Error("Arguments must be a JSON object");
14
- }
15
- if (opts?.coerceToString) {
16
- return Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
17
- }
18
- return parsed as Record<string, unknown>;
19
- } catch (err) {
20
- if (err instanceof SyntaxError) {
21
- throw new Error(`Invalid JSON: ${err.message}`);
22
- }
23
- throw err;
24
- }
6
+ export function parseJsonArgs(str: string, opts?: { coerceToString?: boolean }): Record<string, unknown> {
7
+ try {
8
+ const parsed = JSON.parse(str);
9
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10
+ throw new Error("Arguments must be a JSON object");
11
+ }
12
+ if (opts?.coerceToString) {
13
+ return Object.fromEntries(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
14
+ }
15
+ return parsed as Record<string, unknown>;
16
+ } catch (err) {
17
+ if (err instanceof SyntaxError) {
18
+ throw new Error(`Invalid JSON: ${err.message}`);
19
+ }
20
+ throw err;
21
+ }
25
22
  }
26
23
 
27
24
  /** Read all data from stdin until EOF. */
28
25
  export async function readStdin(): Promise<string> {
29
- const chunks: string[] = [];
30
- const reader = process.stdin;
31
- reader.setEncoding("utf-8");
32
- for await (const chunk of reader) {
33
- chunks.push(chunk as string);
34
- }
35
- return chunks.join("");
26
+ const chunks: string[] = [];
27
+ const reader = process.stdin;
28
+ reader.setEncoding("utf-8");
29
+ for await (const chunk of reader) {
30
+ chunks.push(chunk as string);
31
+ }
32
+ return chunks.join("");
36
33
  }
@@ -9,20 +9,16 @@ import { isInteractive } from "./formatter.ts";
9
9
  * Otherwise falls back to the existing auto-detection:
10
10
  * non-interactive → JSON, interactive → formatted text.
11
11
  */
12
- export function formatOutput(
13
- jsonData: unknown,
14
- interactiveFn: () => string,
15
- options: FormatOptions,
16
- ): string {
17
- if (options.format) {
18
- if (options.format === "json") {
19
- return JSON.stringify(jsonData, null, 2);
20
- }
21
- // markdown uses the interactive formatter for non-exec commands
22
- return interactiveFn();
23
- }
24
- if (!isInteractive(options)) {
25
- return JSON.stringify(jsonData, null, 2);
26
- }
27
- return interactiveFn();
12
+ export function formatOutput(jsonData: unknown, interactiveFn: () => string, options: FormatOptions): string {
13
+ if (options.format) {
14
+ if (options.format === "json") {
15
+ return JSON.stringify(jsonData, null, 2);
16
+ }
17
+ // markdown uses the interactive formatter for non-exec commands
18
+ return interactiveFn();
19
+ }
20
+ if (!isInteractive(options)) {
21
+ return JSON.stringify(jsonData, null, 2);
22
+ }
23
+ return interactiveFn();
28
24
  }
@@ -1,63 +1,60 @@
1
- import ansis from "ansis";
2
- import { dim } from "ansis";
1
+ import ansis, { dim } from "ansis";
3
2
  import { wrapDescription } from "./formatter.ts";
4
3
 
5
4
  export interface Column<T> {
6
- value: (item: T) => string;
7
- style: (text: string) => string;
5
+ value: (item: T) => string;
6
+ style: (text: string) => string;
8
7
  }
9
8
 
10
9
  export interface TableOptions<T> {
11
- columns: Column<T>[];
12
- description?: (item: T) => string | undefined;
13
- separator?: string;
14
- emptyMessage?: string;
10
+ columns: Column<T>[];
11
+ description?: (item: T) => string | undefined;
12
+ separator?: string;
13
+ emptyMessage?: string;
15
14
  }
16
15
 
17
16
  /** Measure visible length of a string (excluding ANSI escape codes) */
18
17
  function visibleLength(s: string): number {
19
- return ansis.strip(s).length;
18
+ return ansis.strip(s).length;
20
19
  }
21
20
 
22
21
  /** Get terminal width, or undefined if not a TTY */
23
22
  function getTerminalWidth(): number | undefined {
24
- if (process.stdout.isTTY) return Math.max(process.stdout.columns - 1, 40);
25
- return undefined;
23
+ if (process.stdout.isTTY) return Math.max(process.stdout.columns - 1, 40);
24
+ return undefined;
26
25
  }
27
26
 
28
27
  /**
29
28
  * Format a list of items as an aligned table with optional description wrapping.
30
29
  */
31
30
  export function formatTable<T>(items: T[], options: TableOptions<T>): string {
32
- if (items.length === 0) {
33
- return dim(options.emptyMessage ?? "No items found");
34
- }
35
-
36
- const sep = options.separator ?? " ";
37
- const termWidth = getTerminalWidth();
38
-
39
- // Calculate max width for each column
40
- const maxWidths = options.columns.map((col) =>
41
- Math.max(...items.map((item) => col.value(item).length)),
42
- );
43
-
44
- return items
45
- .map((item) => {
46
- const parts = options.columns.map((col, i) => {
47
- const raw = col.value(item);
48
- const pad = maxWidths[i]! - raw.length;
49
- return col.style(raw) + " ".repeat(Math.max(0, pad));
50
- });
51
- const prefix = parts.join(sep);
52
-
53
- const desc = options.description?.(item);
54
- if (desc) {
55
- const pw = visibleLength(prefix) + sep.length;
56
- const formatted = termWidth != null ? wrapDescription(desc, pw, termWidth) : dim(desc);
57
- return `${prefix}${sep}${formatted}`;
58
- }
59
-
60
- return prefix;
61
- })
62
- .join("\n");
31
+ if (items.length === 0) {
32
+ return dim(options.emptyMessage ?? "No items found");
33
+ }
34
+
35
+ const sep = options.separator ?? " ";
36
+ const termWidth = getTerminalWidth();
37
+
38
+ // Calculate max width for each column
39
+ const maxWidths = options.columns.map((col) => Math.max(...items.map((item) => col.value(item).length)));
40
+
41
+ return items
42
+ .map((item) => {
43
+ const parts = options.columns.map((col, i) => {
44
+ const raw = col.value(item);
45
+ const pad = maxWidths[i]! - raw.length;
46
+ return col.style(raw) + " ".repeat(Math.max(0, pad));
47
+ });
48
+ const prefix = parts.join(sep);
49
+
50
+ const desc = options.description?.(item);
51
+ if (desc) {
52
+ const pw = visibleLength(prefix) + sep.length;
53
+ const formatted = termWidth != null ? wrapDescription(desc, pw, termWidth) : dim(desc);
54
+ return `${prefix}${sep}${formatted}`;
55
+ }
56
+
57
+ return prefix;
58
+ })
59
+ .join("\n");
63
60
  }