@stealthmole/mcp 0.1.12 → 0.1.14
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/README.md +10 -0
- package/bin/claude-desktop-config.mjs +38 -0
- package/bin/cli.mjs +48 -20
- package/bin/install.mjs +94 -0
- package/bin/node-version-guard.mjs +56 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,6 +4,16 @@ Connect [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs
|
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
|
+
### All MCP clients at once
|
|
8
|
+
|
|
9
|
+
Register StealthMole in every MCP client installed on this machine (Claude Desktop, Claude Code, Cursor, Codex, VS Code, Windsurf, and more):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx -y @stealthmole/mcp install
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Pick clients with `-a` (repeatable), e.g. `npx -y @stealthmole/mcp install -a cursor -a codex`. Run `npx -y @stealthmole/mcp install --help` for the full client list. A client that already runs StealthMole is left unchanged, so running it again is safe. Restart your clients afterwards.
|
|
16
|
+
|
|
7
17
|
### Claude Desktop
|
|
8
18
|
|
|
9
19
|
Simply run the following command — it will automatically register the MCP server in your Claude Desktop configuration:
|
|
@@ -27,6 +27,29 @@ const CONFIG_FILENAME = "claude_desktop_config.json";
|
|
|
27
27
|
/** npx specifier for the retired `stealthmole.mcp` package, with or without a version range. */
|
|
28
28
|
const RETIRED_PACKAGE_ARG = /^stealthmole\.mcp(@.+)?$/;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* npx specifier for either package that launches this server.
|
|
32
|
+
*
|
|
33
|
+
* Deliberately wider than {@link RETIRED_PACKAGE_ARG}: that one drives the
|
|
34
|
+
* rewrite and must not touch an entry already on the scoped package, while this
|
|
35
|
+
* one only answers "is this entry ours".
|
|
36
|
+
*/
|
|
37
|
+
const SERVER_PACKAGE_ARG = /^(?:@stealthmole\/mcp|stealthmole\.mcp)(@.+)?$/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether an `mcpServers` entry launches this server.
|
|
41
|
+
*
|
|
42
|
+
* Matched on `args`, not `command`: the command is `npx`, `npx.cmd`, or an
|
|
43
|
+
* absolute path depending on platform and install, while the package specifier
|
|
44
|
+
* is identical everywhere.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} entry an `mcpServers` entry
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
export function launchesThisServer(entry) {
|
|
50
|
+
return Array.isArray(entry?.args) && entry.args.some((arg) => SERVER_PACKAGE_ARG.test(arg));
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/**
|
|
31
54
|
* Repoint an entry's npx args at `@stealthmole/mcp`.
|
|
32
55
|
*
|
|
@@ -166,6 +189,21 @@ export function ensureClaudeDesktopConfig({
|
|
|
166
189
|
return "migrated";
|
|
167
190
|
}
|
|
168
191
|
|
|
192
|
+
// The customer may have registered this server by hand under a key of
|
|
193
|
+
// their own choosing. Identity is the package the entry launches, not the
|
|
194
|
+
// key it sits under — judging by key alone adds a SECOND entry, so the
|
|
195
|
+
// server runs twice, every tool shows up twice, and the entry we add
|
|
196
|
+
// carries none of their `env` (notably STEALTHMOLE_CLIENT_SECRET).
|
|
197
|
+
const foreignKey = Object.keys(servers).find((key) => launchesThisServer(servers[key]));
|
|
198
|
+
if (foreignKey) {
|
|
199
|
+
if (debug) {
|
|
200
|
+
console.error(
|
|
201
|
+
`${LOG_PREFIX} Already registered under "${foreignKey}", leaving it as is.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return "unchanged";
|
|
205
|
+
}
|
|
206
|
+
|
|
169
207
|
servers[SERVER_KEY] = {
|
|
170
208
|
command: "npx",
|
|
171
209
|
args: ["-y", `@stealthmole/mcp@^${pkgVersion}`],
|
package/bin/cli.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { createServer } from "net";
|
|
|
7
7
|
import { readFileSync } from "fs";
|
|
8
8
|
|
|
9
9
|
import { ensureClaudeDesktopConfig } from "./claude-desktop-config.mjs";
|
|
10
|
+
import { unsupportedNodeMessage } from "./node-version-guard.mjs";
|
|
10
11
|
import { migrateTokenCache } from "./token-cache-shim.mjs";
|
|
11
12
|
|
|
12
13
|
// Read current package version for pinned auto-registration
|
|
@@ -14,6 +15,53 @@ const PKG_VERSION = JSON.parse(
|
|
|
14
15
|
readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
15
16
|
).version;
|
|
16
17
|
|
|
18
|
+
// `install` owns everything after it on the command line (-a <agent>, its own
|
|
19
|
+
// --help), so it is routed before the server's flags are read. Loaded lazily:
|
|
20
|
+
// the MCP server launch path never imports add-mcp.
|
|
21
|
+
if (process.argv[2] === "install") {
|
|
22
|
+
const installNodeProblem = unsupportedNodeMessage();
|
|
23
|
+
if (installNodeProblem) {
|
|
24
|
+
// Registering an entry this Node cannot run is worse than not registering.
|
|
25
|
+
console.error(installNodeProblem);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
const { install } = await import("./install.mjs");
|
|
29
|
+
process.exit(await install({ pkgVersion: PKG_VERSION, args: process.argv.slice(3) }));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --help and --version come first. Both are diagnostics a customer runs to
|
|
33
|
+
// report what they have installed, so nothing below may run ahead of them:
|
|
34
|
+
// a fresh registration exits the process, and argv filtering rewrites
|
|
35
|
+
// process.argv out from under them.
|
|
36
|
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
37
|
+
console.error(
|
|
38
|
+
`@stealthmole/mcp v${PKG_VERSION}\n` +
|
|
39
|
+
"\nUsage: npx -y @stealthmole/mcp [options]\n" +
|
|
40
|
+
" npx -y @stealthmole/mcp install [-a <agent>]... Register in MCP clients\n" +
|
|
41
|
+
"\nOptions:\n" +
|
|
42
|
+
" --port <number> Callback port for OAuth redirect\n" +
|
|
43
|
+
" --callback-port <number> Alias for --port\n" +
|
|
44
|
+
" --debug Enable debug logging\n" +
|
|
45
|
+
" --help, -h Show this help message\n" +
|
|
46
|
+
" --version, -v Show version number\n"
|
|
47
|
+
);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
52
|
+
console.error(PKG_VERSION);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Refuse an unsupported Node before touching anything. Registering a config
|
|
57
|
+
// entry that cannot run is worse than not registering: Claude Desktop would
|
|
58
|
+
// keep launching a server that crashes inside node_modules.
|
|
59
|
+
const nodeProblem = unsupportedNodeMessage();
|
|
60
|
+
if (nodeProblem) {
|
|
61
|
+
console.error(nodeProblem);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
17
65
|
// Register (or migrate) the Claude Desktop entry before starting the proxy.
|
|
18
66
|
// A rename keeps running: Claude Desktop launched this process under the old
|
|
19
67
|
// key, so exiting would break the session the user is in right now.
|
|
@@ -37,26 +85,6 @@ const AUTH_SUCCESS_URL = "https://www.stealthmole.com/";
|
|
|
37
85
|
const CALLBACK_HOST = "localhost";
|
|
38
86
|
const DEBUG = process.argv.includes("--debug");
|
|
39
87
|
|
|
40
|
-
// Handle --help and --version before argv filtering
|
|
41
|
-
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
42
|
-
console.error(
|
|
43
|
-
`@stealthmole/mcp v${PKG_VERSION}\n` +
|
|
44
|
-
"\nUsage: npx -y @stealthmole/mcp [options]\n" +
|
|
45
|
-
"\nOptions:\n" +
|
|
46
|
-
" --port <number> Callback port for OAuth redirect\n" +
|
|
47
|
-
" --callback-port <number> Alias for --port\n" +
|
|
48
|
-
" --debug Enable debug logging\n" +
|
|
49
|
-
" --help, -h Show this help message\n" +
|
|
50
|
-
" --version, -v Show version number\n"
|
|
51
|
-
);
|
|
52
|
-
process.exit(0);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
56
|
-
console.error(PKG_VERSION);
|
|
57
|
-
process.exit(0);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
88
|
/**
|
|
61
89
|
* Find an available port not occupied by another process.
|
|
62
90
|
*
|
package/bin/install.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// `npx -y @stealthmole/mcp install [-a <agent>]...` — register this server in
|
|
2
|
+
// every MCP client on the machine (Claude Desktop, Claude Code, Cursor, Codex, …)
|
|
3
|
+
// through add-mcp's library, which knows each client's config path and format.
|
|
4
|
+
//
|
|
5
|
+
// chosen agents ──▶ listInstalledServers ──┬─ already launches us ─▶ skip
|
|
6
|
+
// (-a, or all detected) └─ not yet ─────────────▶ upsertServer
|
|
7
|
+
//
|
|
8
|
+
// Skipping is the point: add-mcp's own CLI overwrites a same-named entry (the
|
|
9
|
+
// customer's `env` is lost) and adds a second one under a new name (every tool
|
|
10
|
+
// shows up twice). Identity is the package an entry launches, as in
|
|
11
|
+
// claude-desktop-config.mjs.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
detectGlobalAgents,
|
|
15
|
+
getAgentTypes,
|
|
16
|
+
listInstalledServers,
|
|
17
|
+
upsertServer,
|
|
18
|
+
} from "add-mcp";
|
|
19
|
+
|
|
20
|
+
import { SERVER_KEY, launchesThisServer } from "./claude-desktop-config.mjs";
|
|
21
|
+
|
|
22
|
+
const LOG_PREFIX = "[stealthmole-mcp]";
|
|
23
|
+
const AGENT_FLAGS = new Set(["-a", "--agent"]);
|
|
24
|
+
|
|
25
|
+
const USAGE =
|
|
26
|
+
"Usage: npx -y @stealthmole/mcp install [-a <agent>]...\n" +
|
|
27
|
+
"\nRegisters StealthMole MCP in every detected MCP client, or only in the\n" +
|
|
28
|
+
"clients named with -a. Clients that already run it are left unchanged.\n" +
|
|
29
|
+
`\nAgents: ${getAgentTypes().join(", ")}\n`;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Collect `-a <agent>` / `--agent <agent>` values.
|
|
33
|
+
*
|
|
34
|
+
* @param {string[]} args argv after `install`
|
|
35
|
+
* @returns {string[]}
|
|
36
|
+
*/
|
|
37
|
+
export function parseAgents(args) {
|
|
38
|
+
return args.flatMap((arg, i) => (AGENT_FLAGS.has(arg) && args[i + 1] ? [args[i + 1]] : []));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register this server in each chosen agent's global config.
|
|
43
|
+
*
|
|
44
|
+
* @param {{pkgVersion: string, args: string[]}} options
|
|
45
|
+
* @returns {Promise<number>} process exit code
|
|
46
|
+
*/
|
|
47
|
+
export async function install({ pkgVersion, args }) {
|
|
48
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
49
|
+
console.error(USAGE);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const known = new Set(getAgentTypes());
|
|
54
|
+
const requested = parseAgents(args);
|
|
55
|
+
const unknown = requested.filter((agent) => !known.has(agent));
|
|
56
|
+
if (unknown.length) {
|
|
57
|
+
console.error(`${LOG_PREFIX} Unknown agent: ${unknown.join(", ")}\n\n${USAGE}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const targets = requested.length ? requested : await detectGlobalAgents();
|
|
62
|
+
if (!targets.length) {
|
|
63
|
+
console.error(`${LOG_PREFIX} No MCP clients found. Name one with -a <agent>.\n\n${USAGE}`);
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const entry = { command: "npx", args: ["-y", `@stealthmole/mcp@^${pkgVersion}`] };
|
|
68
|
+
let failures = 0;
|
|
69
|
+
let registered = 0;
|
|
70
|
+
for (const agent of await listInstalledServers({ global: true, agents: targets })) {
|
|
71
|
+
const label = `${agent.displayName} (${agent.configPath})`;
|
|
72
|
+
if (agent.error) {
|
|
73
|
+
failures += 1;
|
|
74
|
+
console.error(`${LOG_PREFIX} ✘ ${label}: ${agent.error}`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const existing = agent.servers.find((server) => launchesThisServer(server.config));
|
|
78
|
+
if (existing) {
|
|
79
|
+
console.error(`${LOG_PREFIX} • ${label}: already registered as "${existing.serverName}"`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const result = upsertServer(agent.agentType, SERVER_KEY, entry, { local: false });
|
|
83
|
+
if (result.success) {
|
|
84
|
+
registered += 1;
|
|
85
|
+
console.error(`${LOG_PREFIX} ✔ ${label}: registered "${SERVER_KEY}"`);
|
|
86
|
+
} else {
|
|
87
|
+
failures += 1;
|
|
88
|
+
console.error(`${LOG_PREFIX} ✘ ${label}: ${result.error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (registered) console.error(`\n${LOG_PREFIX} Restart your MCP clients to load StealthMole.`);
|
|
93
|
+
return failures ? 1 : 0;
|
|
94
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Node.js version guard for the StealthMole MCP client.
|
|
2
|
+
//
|
|
3
|
+
// mcp-remote depends on undici, which dereferences the global `File` at require
|
|
4
|
+
// time (undici/lib/web/webidl/index.js). `File` became global in Node 20, so on
|
|
5
|
+
// Node 18 the proxy dies with `ReferenceError: File is not defined` and a stack
|
|
6
|
+
// trace through node_modules — the customer sees a crash, not a cause.
|
|
7
|
+
//
|
|
8
|
+
// `engines.node` does not prevent this: npm and npx print a warning and run
|
|
9
|
+
// anyway. The check has to happen at runtime.
|
|
10
|
+
|
|
11
|
+
import { readFileSync } from "fs";
|
|
12
|
+
|
|
13
|
+
const LOG_PREFIX = "[stealthmole-mcp]";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Node.js version declared in `engines.node`, for the install advice.
|
|
17
|
+
*
|
|
18
|
+
* Read from package.json so the number is written once. It mirrors undici's own
|
|
19
|
+
* `engines` and is only a recommendation here — the guard below refuses strictly
|
|
20
|
+
* less, because versions between 20.0.0 and this one still run fine (measured:
|
|
21
|
+
* 20.9.0 loads undici without error).
|
|
22
|
+
*/
|
|
23
|
+
export const RECOMMENDED_NODE_VERSION = JSON.parse(
|
|
24
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
25
|
+
).engines.node.replace(/^[^\d]*/, "");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Explain why this Node cannot run the client, or `null` when it can.
|
|
29
|
+
*
|
|
30
|
+
* Tests the missing global directly instead of comparing version numbers.
|
|
31
|
+
* A version range has to be guessed and then maintained as undici moves, and
|
|
32
|
+
* guessing it too high is the worse error: it would refuse customers who are
|
|
33
|
+
* working today. The global either exists or it does not.
|
|
34
|
+
*
|
|
35
|
+
* Returns the message rather than printing and exiting, so the decision to
|
|
36
|
+
* terminate stays with the caller and the rule stays unit-testable.
|
|
37
|
+
*
|
|
38
|
+
* @param {boolean} [hasFileGlobal] whether `File` is a global in this runtime
|
|
39
|
+
* @param {string} [current] version to name in the message
|
|
40
|
+
* @returns {string|null}
|
|
41
|
+
*/
|
|
42
|
+
export function unsupportedNodeMessage(
|
|
43
|
+
hasFileGlobal = typeof File !== "undefined",
|
|
44
|
+
current = process.versions.node
|
|
45
|
+
) {
|
|
46
|
+
if (hasFileGlobal) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
`${LOG_PREFIX} Node.js ${current} is too old: mcp-remote needs the global ` +
|
|
52
|
+
`"File", which Node.js added in version 20.\n` +
|
|
53
|
+
`${LOG_PREFIX} Install Node.js ${RECOMMENDED_NODE_VERSION} or newer from ` +
|
|
54
|
+
`https://nodejs.org/ and restart Claude Desktop.`
|
|
55
|
+
);
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stealthmole/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "StealthMole Threat Intelligence MCP client - connect Claude Desktop, Cursor, and other MCP clients to StealthMole API",
|
|
5
5
|
"bin": {
|
|
6
6
|
"stealthmole-mcp": "bin/cli.mjs"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"bin/"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"add-mcp": "2.6.1",
|
|
15
16
|
"mcp-remote": "0.15.1"
|
|
16
17
|
},
|
|
17
18
|
"engines": {
|