@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
package/package.json
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
2
|
+
"name": "@evantahler/mcpx",
|
|
3
|
+
"version": "0.18.6",
|
|
4
|
+
"description": "A command-line interface for MCP servers. curl for MCP.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/sdk.ts",
|
|
8
|
+
"./cli": "./src/cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"main": "./src/sdk.ts",
|
|
11
|
+
"types": "./src/sdk.ts",
|
|
12
|
+
"bin": {
|
|
13
|
+
"mcpx": "./src/cli.ts"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
".claude",
|
|
18
|
+
".cursor",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "bun run src/cli.ts",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"test:e2e": "bun test test/integration/remote-server.test.ts",
|
|
26
|
+
"lint": "biome ci . && tsc --noEmit",
|
|
27
|
+
"format": "biome check --write .",
|
|
28
|
+
"build": "bun build --compile --minify --sourcemap ./src/cli.ts --outfile dist/mcpx"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/evantahler/mcpx.git"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"mcp",
|
|
39
|
+
"cli",
|
|
40
|
+
"model-context-protocol",
|
|
41
|
+
"ai",
|
|
42
|
+
"llm",
|
|
43
|
+
"tools"
|
|
44
|
+
],
|
|
45
|
+
"author": "Evan Tahler",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@huggingface/transformers": "^3.8.1",
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
50
|
+
"ajv": "^8.18.0",
|
|
51
|
+
"ansis": "^4.2.0",
|
|
52
|
+
"commander": "^14.0.3",
|
|
53
|
+
"nanospinner": "^1.2.2",
|
|
54
|
+
"picomatch": "^4.0.3",
|
|
55
|
+
"@types/picomatch": "^4.0.2"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@biomejs/biome": "^2.4.11",
|
|
59
|
+
"@types/bun": "latest",
|
|
60
|
+
"typescript": "^5"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"typescript": "^5"
|
|
64
|
+
}
|
|
65
65
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,53 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { bold, cyan, dim, green, yellow } from "ansis";
|
|
3
4
|
import { program } from "commander";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { registerSearchCommand } from "./commands/search.ts";
|
|
8
|
-
import { registerExecCommand } from "./commands/exec.ts";
|
|
5
|
+
import pkg from "../package.json";
|
|
6
|
+
import { registerAddCommand } from "./commands/add.ts";
|
|
7
|
+
import { registerAllowCommand } from "./commands/allow.ts";
|
|
9
8
|
import { registerAuthCommand, registerDeauthCommand } from "./commands/auth.ts";
|
|
9
|
+
import { registerCheckUpdateCommand } from "./commands/check-update.ts";
|
|
10
|
+
import { registerDenyCommand } from "./commands/deny.ts";
|
|
11
|
+
import { registerExecCommand } from "./commands/exec.ts";
|
|
10
12
|
import { registerIndexCommand } from "./commands/index.ts";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { registerSkillCommand } from "./commands/skill.ts";
|
|
13
|
+
import { registerInfoCommand } from "./commands/info.ts";
|
|
14
|
+
import { registerListCommand } from "./commands/list.ts";
|
|
14
15
|
import { registerPingCommand } from "./commands/ping.ts";
|
|
15
|
-
import { registerResourceCommand } from "./commands/resource.ts";
|
|
16
16
|
import { registerPromptCommand } from "./commands/prompt.ts";
|
|
17
|
+
import { registerRemoveCommand } from "./commands/remove.ts";
|
|
18
|
+
import { registerResourceCommand } from "./commands/resource.ts";
|
|
19
|
+
import { registerSearchCommand } from "./commands/search.ts";
|
|
17
20
|
import { registerServersCommand } from "./commands/servers.ts";
|
|
21
|
+
import { registerSkillCommand } from "./commands/skill.ts";
|
|
18
22
|
import { registerTaskCommand } from "./commands/task.ts";
|
|
19
|
-
import { registerAllowCommand } from "./commands/allow.ts";
|
|
20
|
-
import { registerDenyCommand } from "./commands/deny.ts";
|
|
21
|
-
import { registerCheckUpdateCommand } from "./commands/check-update.ts";
|
|
22
23
|
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
23
24
|
import { maybeCheckForUpdate } from "./update/background.ts";
|
|
24
25
|
|
|
25
|
-
import pkg from "../package.json";
|
|
26
|
-
|
|
27
26
|
program
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
.name("mcpx")
|
|
28
|
+
.description("A command-line interface for MCP servers. curl for MCP.")
|
|
29
|
+
.version(pkg.version)
|
|
30
|
+
.option("-c, --config <path>", "config directory path")
|
|
31
|
+
.option("-d, --with-descriptions", "include tool descriptions in output")
|
|
32
|
+
.option("-j, --json", "force JSON output")
|
|
33
|
+
.option("-F, --format <format>", "output format (json, markdown)")
|
|
34
|
+
.option("-v, --verbose", "show HTTP details and JSON-RPC protocol messages")
|
|
35
|
+
.option("-S, --show-secrets", "show full auth tokens in verbose output")
|
|
36
|
+
.option("-N, --no-interactive", "decline server elicitation requests")
|
|
37
|
+
.option(
|
|
38
|
+
"-l, --log-level <level>",
|
|
39
|
+
"minimum server log level (debug|info|notice|warning|error|critical|alert|emergency)",
|
|
40
|
+
"warning",
|
|
41
|
+
);
|
|
43
42
|
|
|
44
43
|
program.configureHelp({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
44
|
+
styleTitle: (str) => bold(str),
|
|
45
|
+
styleCommandText: (str) => cyan(str),
|
|
46
|
+
styleSubcommandText: (str) => cyan(str),
|
|
47
|
+
styleOptionText: (str) => yellow(str),
|
|
48
|
+
styleArgumentText: (str) => green(str),
|
|
49
|
+
styleDescriptionText: (str) => dim(str),
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
registerListCommand(program);
|
|
@@ -75,25 +74,18 @@ const knownCommands = new Set(program.commands.map((c) => c.name()));
|
|
|
75
74
|
const cliArgs = process.argv.slice(2);
|
|
76
75
|
let firstCommand: string | undefined;
|
|
77
76
|
for (let i = 0; i < cliArgs.length; i++) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
) {
|
|
87
|
-
i++; // skip the option's value argument
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (a.startsWith("-")) continue;
|
|
91
|
-
firstCommand = a;
|
|
92
|
-
break;
|
|
77
|
+
const a = cliArgs[i]!;
|
|
78
|
+
if (a === "-c" || a === "--config" || a === "-l" || a === "--log-level" || a === "-F" || a === "--format") {
|
|
79
|
+
i++; // skip the option's value argument
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (a.startsWith("-")) continue;
|
|
83
|
+
firstCommand = a;
|
|
84
|
+
break;
|
|
93
85
|
}
|
|
94
86
|
if (firstCommand && !knownCommands.has(firstCommand)) {
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
console.error(`error: unknown command '${firstCommand}'. See 'mcpx --help'.`);
|
|
88
|
+
process.exit(1);
|
|
97
89
|
}
|
|
98
90
|
|
|
99
91
|
// Fire-and-forget background update check
|
|
@@ -103,6 +95,6 @@ program.parse();
|
|
|
103
95
|
|
|
104
96
|
// Print update notice after command output completes
|
|
105
97
|
process.on("beforeExit", async () => {
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
const notice = await updateNotice;
|
|
99
|
+
if (notice) process.stderr.write(notice);
|
|
108
100
|
});
|
package/src/client/browser.ts
CHANGED
|
@@ -1,24 +1,45 @@
|
|
|
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
|
+
}
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
|
36
|
+
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
execFile(cmd, args, (err) => {
|
|
39
|
+
if (err) {
|
|
40
|
+
process.stderr.write(`Could not open browser. Please visit:\n ${url}\n`);
|
|
41
|
+
}
|
|
42
|
+
resolve();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
24
45
|
}
|
|
@@ -4,78 +4,86 @@ import { logger } from "../output/logger.ts";
|
|
|
4
4
|
export type FetchLike = (url: string | URL, init?: RequestInit) => Promise<Response>;
|
|
5
5
|
|
|
6
6
|
export function createDebugFetch(showSecrets: boolean): FetchLike {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const isTTY = process.stderr.isTTY ?? false;
|
|
8
|
+
const fmt = (s: string) => (isTTY ? dim(s) : s);
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
return async (url, init) => {
|
|
11
|
+
const start = performance.now();
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
// Request
|
|
14
|
+
log("");
|
|
15
|
+
log(fmt(`> ${init?.method ?? "GET"} ${url}`));
|
|
16
|
+
logHeaders(">", init?.headers, fmt, showSecrets);
|
|
17
|
+
log(fmt(">"));
|
|
18
|
+
if (init?.body) {
|
|
19
|
+
logBody(String(init.body), fmt);
|
|
20
|
+
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const response = await fetch(url, init);
|
|
23
|
+
const elapsed = Math.round(performance.now() - start);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
// Response
|
|
26
|
+
log(fmt(`< ${response.status} ${response.statusText} (${elapsed}ms)`));
|
|
27
|
+
logHeaders("<", response.headers, fmt, showSecrets);
|
|
28
|
+
log(fmt("<"));
|
|
29
|
+
log("");
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
return response;
|
|
32
|
+
};
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function log(line: string) {
|
|
36
|
-
|
|
36
|
+
logger.writeRaw(`${line}\n`);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function logHeaders(
|
|
40
|
-
|
|
41
|
-
headers: RequestInit["headers"],
|
|
42
|
-
fmt: (s: string) => string,
|
|
43
|
-
showSecrets: boolean,
|
|
44
|
-
) {
|
|
45
|
-
if (!headers) return;
|
|
39
|
+
function logHeaders(prefix: string, headers: RequestInit["headers"], fmt: (s: string) => string, showSecrets: boolean) {
|
|
40
|
+
if (!headers) return;
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
const format = (key: string, value: string) =>
|
|
43
|
+
fmt(`${prefix} ${key}: ${showSecrets ? value : maskSensitive(key, value)}`);
|
|
49
44
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
if (headers instanceof Headers) {
|
|
46
|
+
headers.forEach((value, key) => {
|
|
47
|
+
log(format(key, value));
|
|
48
|
+
});
|
|
49
|
+
} else if (Array.isArray(headers)) {
|
|
50
|
+
for (const pair of headers) {
|
|
51
|
+
log(format(String(pair[0]), String(pair[1])));
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
for (const [key, value] of Object.entries(headers as Record<string, string>)) {
|
|
55
|
+
log(format(key, value));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
function logBody(body: string, fmt: (s: string) => string) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
try {
|
|
62
|
+
const formatted = JSON.stringify(JSON.parse(body), null, 2);
|
|
63
|
+
for (const line of formatted.split("\n")) {
|
|
64
|
+
log(fmt(line));
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
log(fmt(body));
|
|
68
|
+
}
|
|
72
69
|
}
|
|
73
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
|
+
|
|
74
83
|
export function maskSensitive(key: string, value: string): string {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return value;
|
|
84
|
+
if (SENSITIVE_HEADERS.has(key.toLowerCase())) {
|
|
85
|
+
if (value.length <= 6) return "***";
|
|
86
|
+
return `${value.slice(0, 4)}...`;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
81
89
|
}
|