@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.
- package/README.md +164 -10
- package/bin/cli.mjs +332 -3
- package/package.json +5 -6
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
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
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.
|
|
4
|
-
"description": "StealthMole Threat Intelligence MCP client
|
|
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": "
|
|
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
|
-
"
|
|
15
|
+
"mcp-remote": "0.15.1"
|
|
17
16
|
},
|
|
18
17
|
"engines": {
|
|
19
|
-
"node": ">=18.
|
|
18
|
+
"node": ">=20.18.1"
|
|
20
19
|
},
|
|
21
20
|
"keywords": [
|
|
22
21
|
"mcp",
|