@stealthmole/mcp 0.1.8 → 0.1.10
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 +164 -10
- package/bin/cli.mjs +33 -23
- package/package.json +5 -10
- package/scripts/patch-oauth-callback.mjs +0 -108
package/README.md
CHANGED
|
@@ -1,22 +1,176 @@
|
|
|
1
|
-
#
|
|
1
|
+
# StealthMole MCP
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
+
npx -y @stealthmole/mcp --port 3335
|
|
18
172
|
```
|
|
19
173
|
|
|
20
|
-
##
|
|
174
|
+
## License
|
|
21
175
|
|
|
22
|
-
|
|
176
|
+
MIT
|
package/bin/cli.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const PKG_VERSION = JSON.parse(
|
|
|
20
20
|
).version;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Auto-register
|
|
23
|
+
* Auto-register the StealthMole MCP server in Claude Desktop's claude_desktop_config.json
|
|
24
24
|
* if not already present. Returns true if newly registered, false otherwise.
|
|
25
25
|
*/
|
|
26
26
|
function ensureClaudeDesktopConfig() {
|
|
@@ -38,7 +38,7 @@ function ensureClaudeDesktopConfig() {
|
|
|
38
38
|
} else if (os === "win32") {
|
|
39
39
|
if (!process.env.APPDATA) {
|
|
40
40
|
console.error(
|
|
41
|
-
"[stealthmole
|
|
41
|
+
"[stealthmole-mcp] APPDATA environment variable not set, skipping config registration."
|
|
42
42
|
);
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
@@ -96,21 +96,21 @@ function ensureClaudeDesktopConfig() {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
console.error(
|
|
99
|
-
"\n[stealthmole
|
|
99
|
+
"\n[stealthmole-mcp] ✔ Registered stealthmole.mcp in Claude Desktop settings."
|
|
100
100
|
);
|
|
101
101
|
console.error(
|
|
102
|
-
"[stealthmole
|
|
102
|
+
"[stealthmole-mcp] Please restart Claude Desktop to apply the changes.\n"
|
|
103
103
|
);
|
|
104
104
|
return true;
|
|
105
105
|
} catch (err) {
|
|
106
106
|
// Config write failed — warn and continue normal execution
|
|
107
107
|
if (process.argv.includes("--debug")) {
|
|
108
108
|
console.error(
|
|
109
|
-
`[stealthmole
|
|
109
|
+
`[stealthmole-mcp] ⚠ Failed to auto-register in Claude Desktop settings: ${err.message}`
|
|
110
110
|
);
|
|
111
111
|
} else {
|
|
112
112
|
console.error(
|
|
113
|
-
"[stealthmole
|
|
113
|
+
"[stealthmole-mcp] ⚠ Failed to auto-register in Claude Desktop settings."
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
return false;
|
|
@@ -125,13 +125,20 @@ if (newlyRegistered) {
|
|
|
125
125
|
}
|
|
126
126
|
|
|
127
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";
|
|
128
135
|
const DEBUG = process.argv.includes("--debug");
|
|
129
136
|
|
|
130
137
|
// Handle --help and --version before argv filtering
|
|
131
138
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
132
139
|
console.error(
|
|
133
|
-
|
|
134
|
-
"\nUsage: npx stealthmole
|
|
140
|
+
`@stealthmole/mcp v${PKG_VERSION}\n` +
|
|
141
|
+
"\nUsage: npx -y @stealthmole/mcp [options]\n" +
|
|
135
142
|
"\nOptions:\n" +
|
|
136
143
|
" --port <number> Callback port for OAuth redirect\n" +
|
|
137
144
|
" --callback-port <number> Alias for --port\n" +
|
|
@@ -211,15 +218,15 @@ function findFreePort(maxAttempts = 20) {
|
|
|
211
218
|
process.on("uncaughtException", (err) => {
|
|
212
219
|
if (err.code === "EADDRINUSE") {
|
|
213
220
|
console.error(
|
|
214
|
-
`[stealthmole
|
|
221
|
+
`[stealthmole-mcp] Port ${err.port ?? ""} is already in use. ` +
|
|
215
222
|
"Retrying should resolve this. " +
|
|
216
|
-
"To use a specific port: npx stealthmole
|
|
223
|
+
"To use a specific port: npx -y @stealthmole/mcp --port <number>"
|
|
217
224
|
);
|
|
218
225
|
process.exit(1);
|
|
219
226
|
}
|
|
220
227
|
// Re-throwing inside uncaughtException causes undefined behavior in Node.js.
|
|
221
228
|
// Log and exit instead.
|
|
222
|
-
console.error("[stealthmole
|
|
229
|
+
console.error("[stealthmole-mcp] Unexpected error:", err);
|
|
223
230
|
process.exit(1);
|
|
224
231
|
});
|
|
225
232
|
|
|
@@ -261,6 +268,10 @@ const CLIENT_METADATA = JSON.stringify({
|
|
|
261
268
|
const ALLOWED_FLAGS = new Set(["--port", "--callback-port", "--debug"]);
|
|
262
269
|
const VALUE_FLAGS = new Set(["--port", "--callback-port"]);
|
|
263
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;
|
|
264
275
|
{
|
|
265
276
|
const rawArgs = process.argv.slice(2);
|
|
266
277
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
@@ -274,7 +285,7 @@ const filteredArgs = [];
|
|
|
274
285
|
i++;
|
|
275
286
|
continue;
|
|
276
287
|
}
|
|
277
|
-
|
|
288
|
+
requestedPort = port;
|
|
278
289
|
i++;
|
|
279
290
|
} else {
|
|
280
291
|
filteredArgs.push(rawArgs[i]);
|
|
@@ -287,27 +298,26 @@ const filteredArgs = [];
|
|
|
287
298
|
process.argv.length = 2;
|
|
288
299
|
process.argv.push(...filteredArgs);
|
|
289
300
|
|
|
290
|
-
// Check if the user specified a callback port explicitly
|
|
291
|
-
const userSpecifiedPort =
|
|
292
|
-
process.argv.includes("--port") || process.argv.includes("--callback-port");
|
|
293
|
-
|
|
294
301
|
// Inject server URL as the first positional argument for mcp-remote
|
|
295
302
|
process.argv.splice(2, 0, SERVER_URL);
|
|
296
303
|
|
|
297
|
-
// If no port was specified, auto-discover a free port
|
|
304
|
+
// If no port was specified, auto-discover a free port.
|
|
298
305
|
// The guard server holds the port until mcp-remote is ready to bind.
|
|
299
306
|
let guardServer = null;
|
|
300
|
-
if (
|
|
301
|
-
const { port
|
|
307
|
+
if (requestedPort === null) {
|
|
308
|
+
const { port, server } = await findFreePort();
|
|
302
309
|
guardServer = server;
|
|
303
|
-
|
|
310
|
+
requestedPort = port;
|
|
304
311
|
if (DEBUG) {
|
|
305
|
-
console.error(
|
|
306
|
-
`Using automatically selected callback port: ${callbackPort}`
|
|
307
|
-
);
|
|
312
|
+
console.error(`Using automatically selected callback port: ${requestedPort}`);
|
|
308
313
|
}
|
|
309
314
|
}
|
|
310
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);
|
|
311
321
|
process.argv.push("--static-oauth-client-info", CLIENT_INFO);
|
|
312
322
|
process.argv.push("--static-oauth-client-metadata", CLIENT_METADATA);
|
|
313
323
|
|
package/package.json
CHANGED
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stealthmole/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "StealthMole Threat Intelligence MCP client - connect Claude Desktop, Cursor, and other MCP clients to StealthMole API",
|
|
5
5
|
"bin": {
|
|
6
|
-
"stealthmole-mcp": "
|
|
7
|
-
"stealthmole.mcp": "./bin/cli.mjs"
|
|
8
|
-
},
|
|
9
|
-
"scripts": {
|
|
10
|
-
"postinstall": "node scripts/patch-oauth-callback.mjs"
|
|
6
|
+
"stealthmole-mcp": "bin/cli.mjs"
|
|
11
7
|
},
|
|
12
8
|
"type": "module",
|
|
13
9
|
"license": "MIT",
|
|
14
10
|
"homepage": "https://www.stealthmole.com",
|
|
15
11
|
"files": [
|
|
16
|
-
"bin/"
|
|
17
|
-
"scripts/"
|
|
12
|
+
"bin/"
|
|
18
13
|
],
|
|
19
14
|
"dependencies": {
|
|
20
|
-
"mcp-remote": "0.1
|
|
15
|
+
"mcp-remote": "0.15.1"
|
|
21
16
|
},
|
|
22
17
|
"engines": {
|
|
23
|
-
"node": ">=18.
|
|
18
|
+
"node": ">=20.18.1"
|
|
24
19
|
},
|
|
25
20
|
"keywords": [
|
|
26
21
|
"mcp",
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Postinstall script that patches mcp-remote's OAuth callback page
|
|
5
|
-
* to redirect to https://www.stealthmole.com/ after successful authentication,
|
|
6
|
-
* instead of showing a "close this window" message.
|
|
7
|
-
*
|
|
8
|
-
* Safety:
|
|
9
|
-
* - Uses exact string matching (not greedy regex) to minimize unintended replacements
|
|
10
|
-
* - Verifies the result still contains the auth-code-received event emission
|
|
11
|
-
* - Non-fatal: CLI works normally even if patching fails
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { readdirSync, readFileSync, writeFileSync } from "fs";
|
|
15
|
-
import { join, dirname } from "path";
|
|
16
|
-
import { fileURLToPath } from "url";
|
|
17
|
-
import { createRequire } from "module";
|
|
18
|
-
|
|
19
|
-
// pnpm 호환: require.resolve로 mcp-remote의 실제 위치를 찾음
|
|
20
|
-
// (pnpm은 node_modules를 symlink 구조로 관리하므로 상대 경로가 동작하지 않음)
|
|
21
|
-
const require = createRequire(import.meta.url);
|
|
22
|
-
const distDir = join(dirname(require.resolve("mcp-remote/package.json")), "dist");
|
|
23
|
-
|
|
24
|
-
// Exact string that identifies the callback response in mcp-remote
|
|
25
|
-
const OLD_RESPONSE = `res.send(\`
|
|
26
|
-
Authorization successful!
|
|
27
|
-
You may close this window and return to the CLI.
|
|
28
|
-
<script>
|
|
29
|
-
// If this is a non-interactive session (no manual approval step was required) then
|
|
30
|
-
// this should automatically close the window. If not, this will have no effect and
|
|
31
|
-
// the user will see the message above.
|
|
32
|
-
window.close();
|
|
33
|
-
</script>
|
|
34
|
-
\`)`;
|
|
35
|
-
|
|
36
|
-
const NEW_HTML = [
|
|
37
|
-
"<!DOCTYPE html>",
|
|
38
|
-
'<html lang="en">',
|
|
39
|
-
"<head>",
|
|
40
|
-
' <meta charset="utf-8">',
|
|
41
|
-
' <meta http-equiv="refresh" content="3;url=https://www.stealthmole.com/">',
|
|
42
|
-
" <title>Authorization Successful - StealthMole</title>",
|
|
43
|
-
" <style>",
|
|
44
|
-
" body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;margin:0;background:#0f172a;color:#e2e8f0}",
|
|
45
|
-
" .card{text-align:center;padding:2.5rem 3rem;background:#1e293b;border-radius:16px;box-shadow:0 4px 24px rgba(0,0,0,.3)}",
|
|
46
|
-
" h1{font-size:1.4rem;margin:0 0 .8rem;color:#34d399}",
|
|
47
|
-
" p{margin:0;font-size:.95rem;color:#94a3b8}",
|
|
48
|
-
" span{color:#34d399;font-weight:600}",
|
|
49
|
-
" </style>",
|
|
50
|
-
"</head>",
|
|
51
|
-
"<body>",
|
|
52
|
-
' <div class="card">',
|
|
53
|
-
" <h1>Authorization Successful</h1>",
|
|
54
|
-
' <p>Redirecting to StealthMole in <span id="c">3</span>s\\u2026</p>',
|
|
55
|
-
" </div>",
|
|
56
|
-
" <script>",
|
|
57
|
-
' var t=3,e=document.getElementById("c"),i=setInterval(function(){if(--t<=0){clearInterval(i);location.href="https://www.stealthmole.com/"}e.textContent=t},1e3);',
|
|
58
|
-
" </script>",
|
|
59
|
-
"</body>",
|
|
60
|
-
"</html>",
|
|
61
|
-
].join("\n");
|
|
62
|
-
|
|
63
|
-
const NEW_RESPONSE = "res.send(`" + NEW_HTML + "`)";
|
|
64
|
-
|
|
65
|
-
// Post-patch integrity marker: this line must exist after the patched res.send()
|
|
66
|
-
const INTEGRITY_MARKER = 'options.events.emit("auth-code-received"';
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const files = readdirSync(distDir).filter((f) => f.endsWith(".js"));
|
|
70
|
-
let patched = false;
|
|
71
|
-
|
|
72
|
-
for (const file of files) {
|
|
73
|
-
const filePath = join(distDir, file);
|
|
74
|
-
const content = readFileSync(filePath, "utf-8");
|
|
75
|
-
|
|
76
|
-
if (!content.includes(OLD_RESPONSE)) continue;
|
|
77
|
-
|
|
78
|
-
const newContent = content.replace(OLD_RESPONSE, NEW_RESPONSE);
|
|
79
|
-
|
|
80
|
-
// Verify: the replacement should change exactly one occurrence
|
|
81
|
-
if (newContent === content) continue;
|
|
82
|
-
|
|
83
|
-
// Integrity check: auth-code-received event must still be present
|
|
84
|
-
if (!newContent.includes(INTEGRITY_MARKER)) {
|
|
85
|
-
console.error(
|
|
86
|
-
`[stealthmole.mcp] Patch integrity check failed for ${file}, skipping.`
|
|
87
|
-
);
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
writeFileSync(filePath, newContent, "utf-8");
|
|
92
|
-
console.error(
|
|
93
|
-
`[stealthmole.mcp] Patched OAuth callback page in ${file}`
|
|
94
|
-
);
|
|
95
|
-
patched = true;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!patched) {
|
|
99
|
-
console.error(
|
|
100
|
-
"[stealthmole.mcp] OAuth callback page already patched or pattern not found."
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
// Non-fatal: if patching fails the CLI still works, just shows the default page
|
|
105
|
-
console.error(
|
|
106
|
-
"[stealthmole.mcp] Could not patch OAuth callback page."
|
|
107
|
-
);
|
|
108
|
-
}
|