@evantahler/mcpx 0.18.5 → 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
CHANGED
package/src/client/browser.ts
CHANGED
|
@@ -1,20 +1,41 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Open a URL in the default browser (macOS/Windows/Linux).
|
|
5
5
|
* Falls back to printing the URL to stderr if no browser is available
|
|
6
6
|
* (e.g., headless servers, Docker containers).
|
|
7
|
+
*
|
|
8
|
+
* Uses execFile (not exec) to avoid shell injection via malicious URLs.
|
|
7
9
|
*/
|
|
8
10
|
export function openBrowser(url: string): Promise<void> {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
// Validate URL scheme to prevent non-HTTP protocols
|
|
12
|
+
try {
|
|
13
|
+
const parsed = new URL(url);
|
|
14
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
15
|
+
process.stderr.write(`Refusing to open non-HTTP URL: ${url}\n`);
|
|
16
|
+
return Promise.resolve();
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
process.stderr.write(`Invalid URL: ${url}\n`);
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let cmd: string;
|
|
24
|
+
let args: string[];
|
|
25
|
+
|
|
26
|
+
if (process.platform === "darwin") {
|
|
27
|
+
cmd = "open";
|
|
28
|
+
args = [url];
|
|
29
|
+
} else if (process.platform === "win32") {
|
|
30
|
+
cmd = "cmd";
|
|
31
|
+
args = ["/c", "start", "", url];
|
|
32
|
+
} else {
|
|
33
|
+
cmd = "xdg-open";
|
|
34
|
+
args = [url];
|
|
35
|
+
}
|
|
15
36
|
|
|
16
37
|
return new Promise((resolve) => {
|
|
17
|
-
|
|
38
|
+
execFile(cmd, args, (err) => {
|
|
18
39
|
if (err) {
|
|
19
40
|
process.stderr.write(`Could not open browser. Please visit:\n ${url}\n`);
|
|
20
41
|
}
|
|
@@ -68,11 +68,22 @@ function logBody(body: string, fmt: (s: string) => string) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
const SENSITIVE_HEADERS = new Set([
|
|
72
|
+
"authorization",
|
|
73
|
+
"cookie",
|
|
74
|
+
"set-cookie",
|
|
75
|
+
"proxy-authorization",
|
|
76
|
+
"x-api-key",
|
|
77
|
+
"api-key",
|
|
78
|
+
"x-auth-token",
|
|
79
|
+
"x-token",
|
|
80
|
+
"token",
|
|
81
|
+
]);
|
|
82
|
+
|
|
71
83
|
export function maskSensitive(key: string, value: string): string {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return `${value.slice(0, 12)}...`;
|
|
84
|
+
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
|
|
85
|
+
if (value.length <= 6) return "***";
|
|
86
|
+
return `${value.slice(0, 4)}...`;
|
|
76
87
|
}
|
|
77
88
|
return value;
|
|
78
89
|
}
|
package/src/config/loader.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chmod } from "node:fs/promises";
|
|
1
2
|
import { join, resolve } from "node:path";
|
|
2
3
|
import { DEFAULT_CONFIG_DIR, ENV } from "../constants.ts";
|
|
3
4
|
import { interpolateEnv } from "./env.ts";
|
|
@@ -63,6 +64,7 @@ export async function loadConfig(options: LoadConfigOptions = {}): Promise<Confi
|
|
|
63
64
|
const cwd = process.cwd();
|
|
64
65
|
if (await hasServersFile(cwd)) {
|
|
65
66
|
configDir = cwd;
|
|
67
|
+
process.stderr.write(`Note: using servers.json from current directory (${cwd})\n`);
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -109,9 +111,10 @@ async function saveJsonFile(configDir: string, filename: string, data: unknown):
|
|
|
109
111
|
await Bun.write(join(configDir, filename), `${JSON.stringify(data, null, 2)}\n`);
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/** Save auth.json to the config directory */
|
|
114
|
+
/** Save auth.json to the config directory with restrictive permissions */
|
|
113
115
|
export async function saveAuth(configDir: string, auth: AuthFile): Promise<void> {
|
|
114
|
-
|
|
116
|
+
await saveJsonFile(configDir, "auth.json", auth);
|
|
117
|
+
await chmod(join(configDir, "auth.json"), 0o600).catch(() => {});
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
/** Load search.json from the config directory */
|
package/src/output/formatter.ts
CHANGED
|
@@ -619,20 +619,23 @@ export function renderMarkdownToAnsi(input: string): string {
|
|
|
619
619
|
return restored;
|
|
620
620
|
}
|
|
621
621
|
|
|
622
|
+
const MAX_NESTED_JSON_DEPTH = 10;
|
|
623
|
+
|
|
622
624
|
/** Recursively parse JSON strings inside MCP content blocks */
|
|
623
|
-
function parseNestedJson(value: unknown): unknown {
|
|
625
|
+
function parseNestedJson(value: unknown, depth = 0): unknown {
|
|
626
|
+
if (depth > MAX_NESTED_JSON_DEPTH) return value;
|
|
624
627
|
if (typeof value === "string") {
|
|
625
628
|
try {
|
|
626
|
-
return parseNestedJson(JSON.parse(value));
|
|
629
|
+
return parseNestedJson(JSON.parse(value), depth + 1);
|
|
627
630
|
} catch {
|
|
628
631
|
return value;
|
|
629
632
|
}
|
|
630
633
|
}
|
|
631
634
|
if (Array.isArray(value)) {
|
|
632
|
-
return value.map(parseNestedJson);
|
|
635
|
+
return value.map((v) => parseNestedJson(v, depth + 1));
|
|
633
636
|
}
|
|
634
637
|
if (typeof value === "object" && value !== null) {
|
|
635
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v)]));
|
|
638
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, parseNestedJson(v, depth + 1)]));
|
|
636
639
|
}
|
|
637
640
|
return value;
|
|
638
641
|
}
|
package/src/validation/schema.ts
CHANGED
|
@@ -28,8 +28,9 @@ function validateWithSchema(
|
|
|
28
28
|
try {
|
|
29
29
|
validate = ajv.compile(schema);
|
|
30
30
|
validatorCache.set(cacheKey, validate);
|
|
31
|
-
} catch {
|
|
32
|
-
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const msg = err instanceof Error ? err.message : "unknown error";
|
|
33
|
+
return { valid: false, errors: [{ path: "(schema)", message: `schema compilation failed: ${msg}` }] };
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|