@stealthmole/mcp 0.1.7 → 0.1.9

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.
Files changed (3) hide show
  1. package/README.md +164 -10
  2. package/bin/cli.mjs +332 -3
  3. package/package.json +5 -6
package/README.md CHANGED
@@ -1,22 +1,176 @@
1
- # @stealthmole/mcp
1
+ # StealthMole MCP
2
2
 
3
- > Alias package for [`stealthmole.mcp`](https://www.npmjs.com/package/stealthmole.mcp)
4
-
5
- This package provides the **same functionality** as `stealthmole.mcp` under an alternative name.
6
- Use whichever name you prefer — both are maintained in sync.
3
+ Connect [Claude Desktop](https://claude.ai/download), [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.sh), [Windsurf](https://codeium.com/windsurf), and other MCP clients to the [StealthMole](https://www.stealthmole.com) Threat Intelligence API.
7
4
 
8
5
  ## Quick Start
9
6
 
7
+ ### Claude Desktop
8
+
9
+ Simply run the following command — it will automatically register the MCP server in your Claude Desktop configuration:
10
+
11
+ ```bash
12
+ npx -y @stealthmole/mcp
13
+ ```
14
+
15
+ Or manually add to your config file:
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "stealthmole": {
21
+ "command": "npx",
22
+ "args": ["-y", "@stealthmole/mcp"],
23
+ "env": {
24
+ "STEALTHMOLE_CLIENT_SECRET": "<your-client-secret>"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ ### Claude Code
32
+
33
+ Add to your project's `.mcp.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "stealthmole": {
39
+ "command": "npx",
40
+ "args": ["-y", "@stealthmole/mcp"],
41
+ "env": {
42
+ "STEALTHMOLE_CLIENT_SECRET": "<your-client-secret>"
43
+ }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ ### Other MCP Clients
50
+
51
+ Add the configuration above to your MCP client's config file:
52
+
53
+ | Client | Config File |
54
+ |--------|-------------|
55
+ | Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
56
+ | Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
57
+ | Claude Desktop (Linux) | `~/.config/Claude/claude_desktop_config.json` |
58
+ | Cursor | `.cursor/mcp.json` in your project root |
59
+ | Windsurf | `~/.codeium/windsurf/mcp_config.json` |
60
+
61
+ ## How It Works
62
+
63
+ 1. Run `npx -y @stealthmole/mcp` (or your MCP client starts it automatically)
64
+ 2. On first run, it auto-registers itself in Claude Desktop's config (if not already registered)
65
+ 3. A browser window opens for StealthMole authentication via OAuth 2.1
66
+ 4. After login the browser is redirected to [stealthmole.com](https://www.stealthmole.com/), and the OAuth token is cached in `~/.mcp-auth/`
67
+ 5. The stdio-to-HTTP bridge connects your MCP client to the remote server
68
+
69
+ ## Requirements
70
+
71
+ - **Node.js 20.18.1+**
72
+ - A StealthMole account with API access
73
+ - `STEALTHMOLE_CLIENT_SECRET` environment variable (optional, for confidential client flow)
74
+
75
+ ## Available Tools
76
+
77
+ Once connected, your MCP client will have access to the following tools:
78
+
79
+ ### Dark Web Tracker
80
+
81
+ | Tool | Description |
82
+ |------|-------------|
83
+ | `dark_web_search` | Search dark web content by indicator (domain, email, keyword, etc.) |
84
+ | `dark_web_targets` | Get available search targets for a given indicator |
85
+ | `download_dark_web_file` | Download a file by SHA256 hash |
86
+
87
+ ### Telegram Tracker
88
+
89
+ | Tool | Description |
90
+ |------|-------------|
91
+ | `telegram_search` | Search Telegram content by indicator |
92
+ | `telegram_targets` | Get available search targets for a given indicator |
93
+ | `download_telegram_file` | Download a file by SHA256 hash |
94
+
95
+ ### Credential Lookout
96
+
97
+ | Tool | Description |
98
+ |------|-------------|
99
+ | `credential_search` | Search for leaked credentials |
100
+ | `credential_export` | Export leaked credentials to CSV/JSON |
101
+
102
+ ### Compromised Dataset
103
+
104
+ | Tool | Description |
105
+ |------|-------------|
106
+ | `compromised_dataset_search` | Search compromised data with host/URL info |
107
+ | `compromised_dataset_node` | Get detailed node information |
108
+ | `compromised_dataset_export` | Export compromised data to CSV/JSON |
109
+
110
+ ### Combo Binder
111
+
112
+ | Tool | Description |
113
+ |------|-------------|
114
+ | `combo_binder_search` | Search leaked ID/Password combinations |
115
+ | `combo_binder_export` | Export combo data to CSV/JSON |
116
+
117
+ ### ULP Binder
118
+
119
+ | Tool | Description |
120
+ |------|-------------|
121
+ | `ulp_binder_search` | Search URL-Login-Password combinations |
122
+ | `ulp_binder_export` | Export ULP data to CSV/JSON |
123
+
124
+ ### Monitoring
125
+
126
+ | Tool | Description |
127
+ |------|-------------|
128
+ | `ransomware_monitoring_search` | Track ransomware activities and attack groups |
129
+ | `government_monitoring_search` | Monitor government sector threats |
130
+ | `leaked_monitoring_search` | Monitor enterprise data leaks |
131
+
132
+ ### Utilities
133
+
134
+ | Tool | Description |
135
+ |------|-------------|
136
+ | `node_details` | Get detailed information about a specific node from any service |
137
+ | `search_pagination` | Paginate through large result sets (DT/TT) |
138
+ | `search_targets` | Get available targets for any service and indicator |
139
+ | `api_usage` | Check API usage quotas by service |
140
+
141
+ ## CLI Options
142
+
10
143
  ```bash
11
- npx @stealthmole/mcp
144
+ npx -y @stealthmole/mcp --debug # Enable debug logging
145
+ npx -y @stealthmole/mcp --port 3335 # Use custom callback port
146
+ npx -y @stealthmole/mcp --callback-port 3335 # Alias for --port
147
+ npx -y @stealthmole/mcp --help # Show help message
148
+ npx -y @stealthmole/mcp --version # Show version number
12
149
  ```
13
150
 
14
- Or install globally:
151
+ > A free callback port is automatically selected if `--port` is not specified.
152
+ > Only the flags listed above are supported; other flags are silently ignored.
153
+
154
+ ## Troubleshooting
155
+
156
+ ### Reset authentication
157
+
158
+ Delete the cached token directory `~/.mcp-auth/`, then restart your MCP client to re-authenticate.
159
+
160
+ ### Connection issues
161
+
162
+ 1. Ensure Node.js 20.18.1+ is installed: `node --version`
163
+ 2. Try with debug logging: `npx -y @stealthmole/mcp --debug`
164
+ 3. Check that your StealthMole account has API access
165
+
166
+ ### Port conflicts
167
+
168
+ If you see "Port is already in use" errors, the client will automatically retry with a different port. You can also specify a port manually:
15
169
 
16
170
  ```bash
17
- npm install -g @stealthmole/mcp
171
+ npx -y @stealthmole/mcp --port 3335
18
172
  ```
19
173
 
20
- ## Documentation
174
+ ## License
21
175
 
22
- See the main package for full documentation: [stealthmole.mcp](https://www.npmjs.com/package/stealthmole.mcp)
176
+ MIT
package/bin/cli.mjs CHANGED
@@ -1,8 +1,337 @@
1
1
  #!/usr/bin/env node
2
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 { createServer } from "net";
7
+ import {
8
+ readFileSync,
9
+ writeFileSync,
10
+ mkdirSync,
11
+ renameSync,
12
+ unlinkSync,
13
+ } from "fs";
14
+ import { join } from "path";
15
+ import { platform, homedir } from "os";
16
+
17
+ // Read current package version for pinned auto-registration
18
+ const PKG_VERSION = JSON.parse(
19
+ readFileSync(new URL("../package.json", import.meta.url), "utf-8")
20
+ ).version;
21
+
22
+ /**
23
+ * Auto-register the StealthMole MCP server in Claude Desktop's claude_desktop_config.json
24
+ * if not already present. Returns true if newly registered, false otherwise.
25
+ */
26
+ function ensureClaudeDesktopConfig() {
27
+ try {
28
+ const os = platform();
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();
121
+
122
+ // First run after registration: exit without starting the proxy
123
+ if (newlyRegistered) {
124
+ process.exit(0);
125
+ }
126
+
127
+ const SERVER_URL = "https://mcp.stealthmole.com/mcp";
128
+ // OAuth 인증 완료 후 브라우저가 이동할 주소.
129
+ // mcp-remote 가 303 으로 리다이렉트하므로 "이 창을 닫으세요" 안내가 노출되지 않음.
130
+ const AUTH_SUCCESS_URL = "https://www.stealthmole.com/";
131
+ // mcp-remote 0.15 부터 win32 기본 콜백 호스트가 127.0.0.1 로 바뀌었다.
132
+ // Keycloak stealthmole_front 의 Valid Redirect URIs 가 localhost 기준이므로
133
+ // 플랫폼 무관하게 localhost 로 고정해 redirect_uri 를 기존과 동일하게 유지한다.
134
+ const CALLBACK_HOST = "localhost";
135
+ const DEBUG = process.argv.includes("--debug");
136
+
137
+ // Handle --help and --version before argv filtering
138
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
139
+ console.error(
140
+ `@stealthmole/mcp v${PKG_VERSION}\n` +
141
+ "\nUsage: npx -y @stealthmole/mcp [options]\n" +
142
+ "\nOptions:\n" +
143
+ " --port <number> Callback port for OAuth redirect\n" +
144
+ " --callback-port <number> Alias for --port\n" +
145
+ " --debug Enable debug logging\n" +
146
+ " --help, -h Show this help message\n" +
147
+ " --version, -v Show version number\n"
148
+ );
149
+ process.exit(0);
150
+ }
151
+
152
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
153
+ console.error(PKG_VERSION);
154
+ process.exit(0);
155
+ }
156
+
3
157
  /**
4
- * Thin wrapper that delegates to the main stealthmole.mcp package.
5
- * This allows `npx stealthmole-mcp` to work as an alias for `npx stealthmole.mcp`.
158
+ * Find an available port not occupied by another process.
159
+ *
160
+ * Step 1: Bind to port 0 — OS assigns a candidate port
161
+ * Step 2: Close candidate, rebind explicitly to verify availability
162
+ * (SO_REUSEADDR can cause listen(0) to return an in-use port)
163
+ * Step 3: On verification failure, retry with a new candidate (up to maxAttempts)
164
+ *
165
+ * The verified guard server is returned open to hold the port.
6
166
  */
167
+ function findFreePort(maxAttempts = 20) {
168
+ let attempts = 0;
169
+
170
+ function tryOnce() {
171
+ return new Promise((resolve, reject) => {
172
+ // Step 1: Get a candidate port from the OS
173
+ const probe = createServer();
174
+ probe.listen(0, "127.0.0.1", () => {
175
+ const { port } = probe.address();
176
+ probe.close(() => {
177
+ // Step 2: Verify by explicitly rebinding to the same port
178
+ const guard = createServer();
179
+ guard.once("error", (err) => {
180
+ // Port occupied by another process — retry
181
+ attempts++;
182
+ if (DEBUG) {
183
+ console.error(
184
+ `Port ${port} is occupied by another process, retrying... (${attempts}/${maxAttempts})`
185
+ );
186
+ }
187
+ if (attempts >= maxAttempts) {
188
+ reject(
189
+ new Error(
190
+ `Could not find a free port after ${maxAttempts} attempts`
191
+ )
192
+ );
193
+ } else {
194
+ resolve(tryOnce());
195
+ }
196
+ });
197
+ guard.listen(port, "127.0.0.1", () => {
198
+ // Port confirmed free — return with guard server still open
199
+ resolve({ port, server: guard });
200
+ });
201
+ });
202
+ });
203
+ probe.on("error", (err) => {
204
+ attempts++;
205
+ if (attempts >= maxAttempts) {
206
+ reject(err);
207
+ } else {
208
+ resolve(tryOnce());
209
+ }
210
+ });
211
+ });
212
+ }
213
+
214
+ return tryOnce();
215
+ }
216
+
217
+ // Show user-friendly message for EADDRINUSE instead of a stack trace
218
+ process.on("uncaughtException", (err) => {
219
+ if (err.code === "EADDRINUSE") {
220
+ console.error(
221
+ `[stealthmole-mcp] Port ${err.port ?? ""} is already in use. ` +
222
+ "Retrying should resolve this. " +
223
+ "To use a specific port: npx -y @stealthmole/mcp --port <number>"
224
+ );
225
+ process.exit(1);
226
+ }
227
+ // Re-throwing inside uncaughtException causes undefined behavior in Node.js.
228
+ // Log and exit instead.
229
+ console.error("[stealthmole-mcp] Unexpected error:", err);
230
+ process.exit(1);
231
+ });
232
+
233
+ const clientInfo = {
234
+ client_id: "stealthmole_front",
235
+ redirect_uris: ["http://127.0.0.1"],
236
+ };
237
+ if (process.env.STEALTHMOLE_CLIENT_SECRET) {
238
+ clientInfo.client_secret = process.env.STEALTHMOLE_CLIENT_SECRET;
239
+ }
240
+ const CLIENT_INFO = JSON.stringify(clientInfo);
241
+
242
+ // Inline Keycloak OIDC metadata — includes offline_access for token refresh support
243
+ // scope (string) controls actual scope sent to Keycloak via mcp-remote's getEffectiveScope()
244
+ // scopes_supported (array) is declarative only — not used by mcp-remote for scope resolution
245
+ const CLIENT_METADATA = JSON.stringify({
246
+ issuer: "https://xoxo.stealthmole.com/auth/realms/stealthmole",
247
+ authorization_endpoint:
248
+ "https://xoxo.stealthmole.com/auth/realms/stealthmole/protocol/openid-connect/auth",
249
+ token_endpoint:
250
+ "https://xoxo.stealthmole.com/auth/realms/stealthmole/protocol/openid-connect/token",
251
+ registration_endpoint:
252
+ "https://xoxo.stealthmole.com/auth/realms/stealthmole/clients-registrations/openid-connect",
253
+ jwks_uri:
254
+ "https://xoxo.stealthmole.com/auth/realms/stealthmole/protocol/openid-connect/certs",
255
+ scopes_supported: ["openid", "offline_access"],
256
+ response_types_supported: ["code"],
257
+ code_challenge_methods_supported: ["S256"],
258
+ token_endpoint_auth_methods_supported: [
259
+ "client_secret_basic",
260
+ "client_secret_post",
261
+ "none",
262
+ ],
263
+ scope: "openid offline_access",
264
+ });
265
+
266
+ // Extract only allowed flags and discard the rest
267
+ // Prevents injection of unverified flags like --allow-http or --header
268
+ const ALLOWED_FLAGS = new Set(["--port", "--callback-port", "--debug"]);
269
+ const VALUE_FLAGS = new Set(["--port", "--callback-port"]);
270
+ const filteredArgs = [];
271
+ // mcp-remote accepts the callback port ONLY as a positional argument
272
+ // (`<server-url> [callback-port]`), so the flag value is held here and injected
273
+ // as a positional below. Forwarding `--port` itself would be silently ignored.
274
+ let requestedPort = null;
275
+ {
276
+ const rawArgs = process.argv.slice(2);
277
+ for (let i = 0; i < rawArgs.length; i++) {
278
+ if (rawArgs[i].startsWith("--")) {
279
+ if (ALLOWED_FLAGS.has(rawArgs[i])) {
280
+ if (VALUE_FLAGS.has(rawArgs[i]) && i + 1 < rawArgs.length) {
281
+ const val = rawArgs[i + 1];
282
+ const port = Number(val);
283
+ // Reject flag-like values and invalid port ranges
284
+ if (val.startsWith("--") || !Number.isInteger(port) || port < 1 || port > 65535) {
285
+ i++;
286
+ continue;
287
+ }
288
+ requestedPort = port;
289
+ i++;
290
+ } else {
291
+ filteredArgs.push(rawArgs[i]);
292
+ }
293
+ }
294
+ // Unknown flags are silently ignored
295
+ }
296
+ }
297
+ }
298
+ process.argv.length = 2;
299
+ process.argv.push(...filteredArgs);
300
+
301
+ // Inject server URL as the first positional argument for mcp-remote
302
+ process.argv.splice(2, 0, SERVER_URL);
303
+
304
+ // If no port was specified, auto-discover a free port.
305
+ // The guard server holds the port until mcp-remote is ready to bind.
306
+ let guardServer = null;
307
+ if (requestedPort === null) {
308
+ const { port, server } = await findFreePort();
309
+ guardServer = server;
310
+ requestedPort = port;
311
+ if (DEBUG) {
312
+ console.error(`Using automatically selected callback port: ${requestedPort}`);
313
+ }
314
+ }
315
+
316
+ // Second positional argument: the callback port
317
+ process.argv.splice(3, 0, String(requestedPort));
318
+
319
+ process.argv.push("--auth-success-url", AUTH_SUCCESS_URL);
320
+ process.argv.push("--host", CALLBACK_HOST);
321
+ process.argv.push("--static-oauth-client-info", CLIENT_INFO);
322
+ process.argv.push("--static-oauth-client-metadata", CLIENT_METADATA);
323
+
324
+ // Suppress mcp-remote startup logs unless --debug is set.
325
+ // Uses mcp-remote's built-in --silent flag (safer than overriding console.error).
326
+ if (!DEBUG) {
327
+ process.argv.push("--silent");
328
+ }
329
+
330
+ // Release the guard server and immediately start mcp-remote to minimize the TOCTOU window.
331
+ // NOTE: mcp-remote binds the port itself, so the guard must be released first.
332
+ // This brief TOCTOU window is an architectural constraint of mcp-remote.
333
+ if (guardServer) {
334
+ await new Promise((resolve) => guardServer.close(resolve));
335
+ }
7
336
 
8
- import "stealthmole.mcp/bin/cli.mjs";
337
+ await import("mcp-remote/dist/proxy.js");
package/package.json CHANGED
@@ -1,10 +1,9 @@
1
1
  {
2
2
  "name": "@stealthmole/mcp",
3
- "version": "0.1.7",
4
- "description": "StealthMole Threat Intelligence MCP client — alias package for stealthmole.mcp",
3
+ "version": "0.1.9",
4
+ "description": "StealthMole Threat Intelligence MCP client - connect Claude Desktop, Cursor, and other MCP clients to StealthMole API",
5
5
  "bin": {
6
- "stealthmole-mcp": "./bin/cli.mjs",
7
- "stealthmole.mcp": "./bin/cli.mjs"
6
+ "stealthmole-mcp": "bin/cli.mjs"
8
7
  },
9
8
  "type": "module",
10
9
  "license": "MIT",
@@ -13,10 +12,10 @@
13
12
  "bin/"
14
13
  ],
15
14
  "dependencies": {
16
- "stealthmole.mcp": "0.1.7"
15
+ "mcp-remote": "0.15.1"
17
16
  },
18
17
  "engines": {
19
- "node": ">=18.0.0"
18
+ "node": ">=20.18.1"
20
19
  },
21
20
  "keywords": [
22
21
  "mcp",