@stealthmole/mcp 0.1.10 → 0.1.11
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/bin/claude-desktop-config.mjs +201 -0
- package/bin/cli.mjs +12 -110
- package/package.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Claude Desktop config registration for the StealthMole MCP server.
|
|
2
|
+
//
|
|
3
|
+
// NOTE: In MCP (Model Context Protocol), stdout is reserved for JSON-RPC messages.
|
|
4
|
+
// All user-facing messages MUST use console.error (stderr), not console.log (stdout).
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { platform, homedir } from "os";
|
|
9
|
+
|
|
10
|
+
const LOG_PREFIX = "[stealthmole-mcp]";
|
|
11
|
+
|
|
12
|
+
/** Key this server is registered under in `mcpServers`. */
|
|
13
|
+
export const SERVER_KEY = "stealthmole-mcp";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Keys this server used to be registered under.
|
|
17
|
+
*
|
|
18
|
+
* `stealthmole.mcp` mirrored the npm package of the same name, which has since
|
|
19
|
+
* been deprecated in favour of `@stealthmole/mcp`. A legacy entry is renamed
|
|
20
|
+
* rather than left in place, otherwise the user ends up running the same server
|
|
21
|
+
* twice and sees every tool duplicated.
|
|
22
|
+
*/
|
|
23
|
+
export const LEGACY_SERVER_KEYS = ["stealthmole.mcp"];
|
|
24
|
+
|
|
25
|
+
const CONFIG_FILENAME = "claude_desktop_config.json";
|
|
26
|
+
|
|
27
|
+
/** npx specifier for the retired `stealthmole.mcp` package, with or without a version range. */
|
|
28
|
+
const RETIRED_PACKAGE_ARG = /^stealthmole\.mcp(@.+)?$/;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Repoint an entry's npx args at `@stealthmole/mcp`.
|
|
32
|
+
*
|
|
33
|
+
* A hand-written entry may still launch the retired `stealthmole.mcp` wrapper,
|
|
34
|
+
* which pins the user to whatever `@stealthmole/mcp` version that wrapper
|
|
35
|
+
* depends on. Only the package specifier is swapped — the user's own flags and
|
|
36
|
+
* `env` are left alone.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} entry an `mcpServers` entry
|
|
39
|
+
* @param {string} pkgVersion version to pin
|
|
40
|
+
* @returns {object} the entry, with args repointed when needed
|
|
41
|
+
*/
|
|
42
|
+
function repointRetiredPackage(entry, pkgVersion) {
|
|
43
|
+
if (!Array.isArray(entry?.args)) {
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const args = entry.args.map((arg) =>
|
|
48
|
+
RETIRED_PACKAGE_ARG.test(arg) ? `@stealthmole/mcp@^${pkgVersion}` : arg
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return args.some((arg, i) => arg !== entry.args[i]) ? { ...entry, args } : entry;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve Claude Desktop's config file path for the current platform.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} [os] platform identifier, as returned by `os.platform()`
|
|
58
|
+
* @param {Record<string, string|undefined>} [env] environment variables
|
|
59
|
+
* @param {string} [home] home directory; injectable so tests can stay hermetic
|
|
60
|
+
* @returns {string|null} absolute path, or null when it cannot be determined
|
|
61
|
+
*/
|
|
62
|
+
export function resolveConfigPath(os = platform(), env = process.env, home = homedir()) {
|
|
63
|
+
let configDir;
|
|
64
|
+
|
|
65
|
+
if (os === "darwin") {
|
|
66
|
+
configDir = join(home, "Library", "Application Support", "Claude");
|
|
67
|
+
} else if (os === "win32") {
|
|
68
|
+
if (!env.APPDATA) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
configDir = join(env.APPDATA, "Claude");
|
|
72
|
+
} else {
|
|
73
|
+
// Linux: use XDG_CONFIG_HOME or fall back to ~/.config
|
|
74
|
+
configDir = join(env.XDG_CONFIG_HOME || join(home, ".config"), "Claude");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return join(configDir, CONFIG_FILENAME);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Write `config` to `configPath` atomically.
|
|
82
|
+
*
|
|
83
|
+
* Writes to a sibling temp file then renames, so a concurrent run can never
|
|
84
|
+
* observe (or be truncated to) a half-written config.
|
|
85
|
+
*
|
|
86
|
+
* @param {string} configPath
|
|
87
|
+
* @param {object} config
|
|
88
|
+
*/
|
|
89
|
+
function writeConfigAtomically(configPath, config) {
|
|
90
|
+
// Owner-only permissions: the config may hold a client secret
|
|
91
|
+
mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
|
|
92
|
+
|
|
93
|
+
const tmpPath = configPath + ".tmp";
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", {
|
|
96
|
+
encoding: "utf-8",
|
|
97
|
+
mode: 0o600,
|
|
98
|
+
});
|
|
99
|
+
renameSync(tmpPath, configPath);
|
|
100
|
+
} catch (writeErr) {
|
|
101
|
+
try {
|
|
102
|
+
unlinkSync(tmpPath);
|
|
103
|
+
} catch {
|
|
104
|
+
/* ignore cleanup failure */
|
|
105
|
+
}
|
|
106
|
+
throw writeErr;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Ensure this server is registered in Claude Desktop under {@link SERVER_KEY}.
|
|
112
|
+
*
|
|
113
|
+
* Outcomes:
|
|
114
|
+
* - `"unchanged"` — already registered correctly, config untouched
|
|
115
|
+
* - `"migrated"` — a legacy key was renamed; the caller should keep running so
|
|
116
|
+
* the current session (launched under the old key) still works
|
|
117
|
+
* - `"registered"` — newly added; the caller should exit and ask for a restart
|
|
118
|
+
* - `"failed"` — config could not be read or written; non-fatal
|
|
119
|
+
*
|
|
120
|
+
* @param {object} options
|
|
121
|
+
* @param {string} options.pkgVersion version to pin in the npx args
|
|
122
|
+
* @param {boolean} [options.debug] include the error message on failure
|
|
123
|
+
* @param {string|null} [options.configPath] override the resolved config path
|
|
124
|
+
* @returns {"unchanged"|"migrated"|"registered"|"failed"}
|
|
125
|
+
*/
|
|
126
|
+
export function ensureClaudeDesktopConfig({
|
|
127
|
+
pkgVersion,
|
|
128
|
+
debug = false,
|
|
129
|
+
configPath = resolveConfigPath(),
|
|
130
|
+
}) {
|
|
131
|
+
if (!configPath) {
|
|
132
|
+
console.error(`${LOG_PREFIX} Could not determine the Claude Desktop config path, skipping registration.`);
|
|
133
|
+
return "failed";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
let config;
|
|
138
|
+
try {
|
|
139
|
+
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
140
|
+
} catch {
|
|
141
|
+
// File missing or invalid JSON — start fresh
|
|
142
|
+
config = {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!config.mcpServers) {
|
|
146
|
+
config.mcpServers = {};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const servers = config.mcpServers;
|
|
150
|
+
const staleKeys = LEGACY_SERVER_KEYS.filter((key) => key in servers);
|
|
151
|
+
|
|
152
|
+
if (staleKeys.length === 0) {
|
|
153
|
+
const current = servers[SERVER_KEY];
|
|
154
|
+
if (current) {
|
|
155
|
+
// The key may be current while the args still launch the retired wrapper
|
|
156
|
+
const repointed = repointRetiredPackage(current, pkgVersion);
|
|
157
|
+
if (repointed === current) {
|
|
158
|
+
return "unchanged";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
servers[SERVER_KEY] = repointed;
|
|
162
|
+
writeConfigAtomically(configPath, config);
|
|
163
|
+
|
|
164
|
+
console.error(`\n${LOG_PREFIX} ✔ Repointed "${SERVER_KEY}" at @stealthmole/mcp in Claude Desktop settings.`);
|
|
165
|
+
console.error(`${LOG_PREFIX} Restart Claude Desktop when convenient; this session keeps working.\n`);
|
|
166
|
+
return "migrated";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
servers[SERVER_KEY] = {
|
|
170
|
+
command: "npx",
|
|
171
|
+
args: ["-y", `@stealthmole/mcp@^${pkgVersion}`],
|
|
172
|
+
};
|
|
173
|
+
writeConfigAtomically(configPath, config);
|
|
174
|
+
|
|
175
|
+
console.error(`\n${LOG_PREFIX} ✔ Registered ${SERVER_KEY} in Claude Desktop settings.`);
|
|
176
|
+
console.error(`${LOG_PREFIX} Please restart Claude Desktop to apply the changes.\n`);
|
|
177
|
+
return "registered";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Carry the legacy entry over so user customisation (env, extra args)
|
|
181
|
+
// survives the rename. An entry already under SERVER_KEY wins.
|
|
182
|
+
for (const key of staleKeys) {
|
|
183
|
+
servers[SERVER_KEY] ??= repointRetiredPackage(servers[key], pkgVersion);
|
|
184
|
+
delete servers[key];
|
|
185
|
+
}
|
|
186
|
+
writeConfigAtomically(configPath, config);
|
|
187
|
+
|
|
188
|
+
console.error(`\n${LOG_PREFIX} ✔ Renamed "${staleKeys.join('", "')}" to "${SERVER_KEY}" in Claude Desktop settings.`);
|
|
189
|
+
console.error(`${LOG_PREFIX} Restart Claude Desktop when convenient; this session keeps working.\n`);
|
|
190
|
+
return "migrated";
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Config write failed — warn and let the caller continue normal execution
|
|
193
|
+
if (debug) {
|
|
194
|
+
console.error(`${LOG_PREFIX} ⚠ Failed to update Claude Desktop settings: ${err.message}`);
|
|
195
|
+
console.error(err.stack);
|
|
196
|
+
} else {
|
|
197
|
+
console.error(`${LOG_PREFIX} ⚠ Failed to update Claude Desktop settings.`);
|
|
198
|
+
}
|
|
199
|
+
return "failed";
|
|
200
|
+
}
|
|
201
|
+
}
|
package/bin/cli.mjs
CHANGED
|
@@ -4,123 +4,25 @@
|
|
|
4
4
|
// All user-facing messages MUST use console.error (stderr), not console.log (stdout).
|
|
5
5
|
|
|
6
6
|
import { createServer } from "net";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
mkdirSync,
|
|
11
|
-
renameSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
} from "fs";
|
|
14
|
-
import { join } from "path";
|
|
15
|
-
import { platform, homedir } from "os";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
|
|
9
|
+
import { ensureClaudeDesktopConfig } from "./claude-desktop-config.mjs";
|
|
16
10
|
|
|
17
11
|
// Read current package version for pinned auto-registration
|
|
18
12
|
const PKG_VERSION = JSON.parse(
|
|
19
13
|
readFileSync(new URL("../package.json", import.meta.url), "utf-8")
|
|
20
14
|
).version;
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
let configDir;
|
|
30
|
-
|
|
31
|
-
if (os === "darwin") {
|
|
32
|
-
configDir = join(
|
|
33
|
-
homedir(),
|
|
34
|
-
"Library",
|
|
35
|
-
"Application Support",
|
|
36
|
-
"Claude"
|
|
37
|
-
);
|
|
38
|
-
} else if (os === "win32") {
|
|
39
|
-
if (!process.env.APPDATA) {
|
|
40
|
-
console.error(
|
|
41
|
-
"[stealthmole-mcp] APPDATA environment variable not set, skipping config registration."
|
|
42
|
-
);
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
configDir = join(process.env.APPDATA, "Claude");
|
|
46
|
-
} else {
|
|
47
|
-
// Linux: use XDG_CONFIG_HOME or fall back to ~/.config
|
|
48
|
-
configDir = join(
|
|
49
|
-
process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
|
|
50
|
-
"Claude"
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const configPath = join(configDir, "claude_desktop_config.json");
|
|
55
|
-
|
|
56
|
-
let config;
|
|
57
|
-
try {
|
|
58
|
-
config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
59
|
-
} catch {
|
|
60
|
-
// File missing or invalid JSON — start fresh
|
|
61
|
-
config = {};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (!config.mcpServers) {
|
|
65
|
-
config.mcpServers = {};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Already registered — nothing to do
|
|
69
|
-
if (config.mcpServers["stealthmole.mcp"]) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
config.mcpServers["stealthmole.mcp"] = {
|
|
74
|
-
command: "npx",
|
|
75
|
-
args: ["-y", `@stealthmole/mcp@^${PKG_VERSION}`],
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// Create directory with owner-only permissions if it doesn't exist
|
|
79
|
-
mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
80
|
-
|
|
81
|
-
// Atomic write: write to temp file then rename to prevent data loss on concurrent runs
|
|
82
|
-
const tmpPath = configPath + ".tmp";
|
|
83
|
-
try {
|
|
84
|
-
writeFileSync(tmpPath, JSON.stringify(config, null, 2) + "\n", {
|
|
85
|
-
encoding: "utf-8",
|
|
86
|
-
mode: 0o600,
|
|
87
|
-
});
|
|
88
|
-
renameSync(tmpPath, configPath);
|
|
89
|
-
} catch (writeErr) {
|
|
90
|
-
try {
|
|
91
|
-
unlinkSync(tmpPath);
|
|
92
|
-
} catch {
|
|
93
|
-
/* ignore cleanup failure */
|
|
94
|
-
}
|
|
95
|
-
throw writeErr;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.error(
|
|
99
|
-
"\n[stealthmole-mcp] ✔ Registered stealthmole.mcp in Claude Desktop settings."
|
|
100
|
-
);
|
|
101
|
-
console.error(
|
|
102
|
-
"[stealthmole-mcp] Please restart Claude Desktop to apply the changes.\n"
|
|
103
|
-
);
|
|
104
|
-
return true;
|
|
105
|
-
} catch (err) {
|
|
106
|
-
// Config write failed — warn and continue normal execution
|
|
107
|
-
if (process.argv.includes("--debug")) {
|
|
108
|
-
console.error(
|
|
109
|
-
`[stealthmole-mcp] ⚠ Failed to auto-register in Claude Desktop settings: ${err.message}`
|
|
110
|
-
);
|
|
111
|
-
} else {
|
|
112
|
-
console.error(
|
|
113
|
-
"[stealthmole-mcp] ⚠ Failed to auto-register in Claude Desktop settings."
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const newlyRegistered = ensureClaudeDesktopConfig();
|
|
16
|
+
// Register (or migrate) the Claude Desktop entry before starting the proxy.
|
|
17
|
+
// A rename keeps running: Claude Desktop launched this process under the old
|
|
18
|
+
// key, so exiting would break the session the user is in right now.
|
|
19
|
+
const configStatus = ensureClaudeDesktopConfig({
|
|
20
|
+
pkgVersion: PKG_VERSION,
|
|
21
|
+
debug: process.argv.includes("--debug"),
|
|
22
|
+
});
|
|
121
23
|
|
|
122
|
-
// First run after registration: exit without starting the proxy
|
|
123
|
-
if (
|
|
24
|
+
// First run after a fresh registration: exit without starting the proxy
|
|
25
|
+
if (configStatus === "registered") {
|
|
124
26
|
process.exit(0);
|
|
125
27
|
}
|
|
126
28
|
|
package/package.json
CHANGED