@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.
- package/package.json +63 -63
- package/src/cli.ts +46 -54
- package/src/client/browser.ts +36 -15
- package/src/client/debug-fetch.ts +64 -56
- package/src/client/elicitation.ts +279 -291
- package/src/client/http.ts +1 -1
- package/src/client/manager.ts +481 -514
- package/src/client/oauth.ts +272 -282
- package/src/client/sse.ts +1 -1
- package/src/client/stdio.ts +7 -7
- package/src/client/trace.ts +146 -152
- package/src/client/transport-options.ts +20 -20
- package/src/commands/add.ts +160 -165
- package/src/commands/allow.ts +141 -142
- package/src/commands/auth.ts +86 -90
- package/src/commands/check-update.ts +49 -53
- package/src/commands/deny.ts +114 -117
- package/src/commands/exec.ts +218 -222
- package/src/commands/index.ts +41 -41
- package/src/commands/info.ts +48 -50
- package/src/commands/list.ts +49 -49
- package/src/commands/ping.ts +47 -50
- package/src/commands/prompt.ts +40 -50
- package/src/commands/remove.ts +54 -56
- package/src/commands/resource.ts +31 -36
- package/src/commands/search.ts +35 -39
- package/src/commands/servers.ts +44 -48
- package/src/commands/skill.ts +89 -95
- package/src/commands/task.ts +50 -60
- package/src/commands/upgrade.ts +191 -208
- package/src/commands/with-command.ts +27 -29
- package/src/config/env.ts +26 -28
- package/src/config/loader.ts +103 -103
- package/src/config/schemas.ts +78 -87
- package/src/constants.ts +17 -17
- package/src/context.ts +51 -51
- package/src/lib/client-settings.ts +127 -140
- package/src/lib/input.ts +23 -26
- package/src/output/format-output.ts +12 -16
- package/src/output/format-table.ts +39 -42
- package/src/output/formatter.ts +794 -815
- package/src/output/logger.ts +140 -152
- package/src/sdk.ts +283 -291
- package/src/search/index.ts +50 -54
- package/src/search/indexer.ts +65 -65
- package/src/search/keyword.ts +54 -54
- package/src/search/semantic.ts +39 -39
- package/src/search/staleness.ts +3 -3
- package/src/search/types.ts +4 -4
- package/src/update/background.ts +51 -51
- package/src/update/cache.ts +21 -21
- package/src/update/checker.ts +81 -86
- package/src/validation/schema.ts +53 -58
|
@@ -1,210 +1,197 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from "os";
|
|
3
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
99
|
+
settings: ClientSettings,
|
|
100
|
+
patterns: string[],
|
|
110
101
|
): { settings: ClientSettings; added: string[] } {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
136
|
-
|
|
126
|
+
settings: ClientSettings,
|
|
127
|
+
patterns: string[],
|
|
137
128
|
): { settings: ClientSettings; removed: string[] } {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
166
|
-
|
|
156
|
+
settings: ClientSettings,
|
|
157
|
+
client: Client = "claude",
|
|
167
158
|
): {
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
settings: ClientSettings;
|
|
160
|
+
removed: string[];
|
|
170
161
|
} {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
7
|
-
|
|
5
|
+
value: (item: T) => string;
|
|
6
|
+
style: (text: string) => string;
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export interface TableOptions<T> {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
}
|