@burtthecoder/mcp-dnstwist 1.0.6 → 1.0.7
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 +11 -1
- package/build/index.js +73 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ This tool is designed for legitimate security research purposes. Please:
|
|
|
17
17
|
|
|
18
18
|
## Requirements
|
|
19
19
|
|
|
20
|
-
- Node.js (v20 or later)
|
|
20
|
+
- Node.js (v20.3 or later)
|
|
21
21
|
- Docker
|
|
22
22
|
- macOS, Linux, or Windows with Docker Desktop installed
|
|
23
23
|
|
|
@@ -121,6 +121,14 @@ Example:
|
|
|
121
121
|
}
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
| Variable | Default | Description |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `DNSTWIST_TIMEOUT_SECONDS` | `600` | Longest a scan may run. When it's reached, or when the client cancels the call, the scan's container is stopped and removed. |
|
|
129
|
+
|
|
130
|
+
Set it in your client's `env` block, for example `"env": { "DNSTWIST_TIMEOUT_SECONDS": "900" }`.
|
|
131
|
+
|
|
124
132
|
## Troubleshooting
|
|
125
133
|
|
|
126
134
|
### Docker Issues
|
|
@@ -153,6 +161,8 @@ docker ps
|
|
|
153
161
|
|
|
154
162
|
## Error Messages
|
|
155
163
|
|
|
164
|
+
- "dnstwist did not finish within N seconds": the scan hit `DNSTWIST_TIMEOUT_SECONDS`. Use an IPv4 nameserver (DNS-over-HTTPS is slow), turn off `ssdeep` or `banners`, or raise the limit
|
|
165
|
+
|
|
156
166
|
- "Docker is not installed or not running": Install Docker and start the Docker daemon
|
|
157
167
|
- "Failed to parse dnstwist output": Check if the domain is valid and the format is correct
|
|
158
168
|
- "Error executing dnstwist": Check Docker logs and ensure proper permissions
|
package/build/index.js
CHANGED
|
@@ -3,32 +3,58 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { execFile } from 'child_process';
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
6
7
|
import { promisify } from 'util';
|
|
7
8
|
import { validateFuzzArgs } from './validate.js';
|
|
8
9
|
// execFile runs docker directly with an argument array. No shell is involved,
|
|
9
10
|
// so tool input can never be interpreted as shell syntax (CWE-78, issue #13).
|
|
10
11
|
const execFileAsync = promisify(execFile);
|
|
11
12
|
const DOCKER_IMAGE = 'elceef/dnstwist';
|
|
13
|
+
// A scan that runs longer than this is stopped and its container removed.
|
|
14
|
+
// Slow resolvers (DNS-over-HTTPS) and --ssdeep/--banners can take many minutes.
|
|
15
|
+
const DEFAULT_TIMEOUT_SECONDS = 600;
|
|
16
|
+
function scanTimeoutSeconds() {
|
|
17
|
+
const raw = process.env.DNSTWIST_TIMEOUT_SECONDS;
|
|
18
|
+
if (raw === undefined || raw === '')
|
|
19
|
+
return DEFAULT_TIMEOUT_SECONDS;
|
|
20
|
+
const seconds = Number(raw);
|
|
21
|
+
if (!Number.isInteger(seconds) || seconds < 1 || seconds > 86400) {
|
|
22
|
+
console.error(`DNSTWIST_TIMEOUT_SECONDS must be an integer from 1 to 86400, got ${JSON.stringify(raw)}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
return seconds;
|
|
26
|
+
}
|
|
27
|
+
const TIMEOUT_SECONDS = scanTimeoutSeconds();
|
|
12
28
|
class DnstwistServer {
|
|
13
29
|
constructor() {
|
|
30
|
+
// Scans in progress: container name -> controller that stops its docker run.
|
|
31
|
+
this.runningContainers = new Map();
|
|
14
32
|
this.server = new Server({ name: 'dnstwist-server', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
15
33
|
this.setupToolHandlers();
|
|
16
34
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
35
|
+
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
36
|
+
process.on(signal, async () => {
|
|
37
|
+
// Don't leave scans running after the server is gone: stop each
|
|
38
|
+
// in-flight docker run first, then remove its container.
|
|
39
|
+
for (const controller of this.runningContainers.values())
|
|
40
|
+
controller.abort();
|
|
41
|
+
await Promise.all([...this.runningContainers.keys()].map((name) => this.removeContainer(name)));
|
|
42
|
+
await this.server.close();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
21
46
|
// Trigger setup immediately
|
|
22
47
|
this.ensureSetup().catch(error => {
|
|
23
48
|
console.error('Failed to setup dnstwist:', error);
|
|
24
49
|
process.exit(1);
|
|
25
50
|
});
|
|
26
51
|
}
|
|
27
|
-
async execCommand(file, args) {
|
|
52
|
+
async execCommand(file, args, options = {}) {
|
|
28
53
|
console.error('Executing:', file, JSON.stringify(args));
|
|
29
54
|
try {
|
|
30
55
|
const result = await execFileAsync(file, args, {
|
|
31
|
-
maxBuffer: 10 * 1024 * 1024
|
|
56
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
57
|
+
...options
|
|
32
58
|
});
|
|
33
59
|
console.error('Command output:', result.stdout);
|
|
34
60
|
if (result.stderr)
|
|
@@ -40,6 +66,22 @@ class DnstwistServer {
|
|
|
40
66
|
throw error;
|
|
41
67
|
}
|
|
42
68
|
}
|
|
69
|
+
// Killing the docker CLI doesn't stop the container it started, so a scan
|
|
70
|
+
// that times out or is cancelled is removed by name. Right after the CLI
|
|
71
|
+
// starts, the daemon may not have created the container yet ("No such
|
|
72
|
+
// container"), so keep trying for a few seconds until a removal succeeds.
|
|
73
|
+
async removeContainer(name, attempts = 10, delayMs = 500) {
|
|
74
|
+
for (let i = 0; i < attempts; i++) {
|
|
75
|
+
try {
|
|
76
|
+
await execFileAsync('docker', ['rm', '-f', name]);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
console.error(`Could not confirm removal of container ${name}`);
|
|
84
|
+
}
|
|
43
85
|
async ensureSetup() {
|
|
44
86
|
try {
|
|
45
87
|
console.error('Checking Docker...');
|
|
@@ -122,7 +164,7 @@ class DnstwistServer {
|
|
|
122
164
|
}
|
|
123
165
|
]
|
|
124
166
|
}));
|
|
125
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
167
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
126
168
|
try {
|
|
127
169
|
await this.ensureSetup();
|
|
128
170
|
if (request.params.name !== 'fuzz_domain') {
|
|
@@ -147,7 +189,30 @@ class DnstwistServer {
|
|
|
147
189
|
if (banners)
|
|
148
190
|
args.push('-b');
|
|
149
191
|
// Run dnstwist in Docker
|
|
150
|
-
const
|
|
192
|
+
const container = `mcp-dnstwist-${randomUUID()}`;
|
|
193
|
+
const stop = new AbortController();
|
|
194
|
+
this.runningContainers.set(container, stop);
|
|
195
|
+
let stdout;
|
|
196
|
+
try {
|
|
197
|
+
({ stdout } = await this.execCommand('docker', ['run', '--rm', '--name', container, DOCKER_IMAGE, ...args], { timeout: TIMEOUT_SECONDS * 1000, signal: AbortSignal.any([extra.signal, stop.signal]) }));
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const stopped = extra.signal.aborted || stop.signal.aborted;
|
|
201
|
+
const timedOut = Boolean(error.killed) && !stopped;
|
|
202
|
+
// If dnstwist exited on its own, --rm already removed the container.
|
|
203
|
+
if (timedOut || stopped)
|
|
204
|
+
await this.removeContainer(container);
|
|
205
|
+
if (timedOut) {
|
|
206
|
+
const unit = TIMEOUT_SECONDS === 1 ? 'second' : 'seconds';
|
|
207
|
+
throw new Error(`dnstwist did not finish within ${TIMEOUT_SECONDS} ${unit} and was stopped. ` +
|
|
208
|
+
'Use an IPv4 nameserver (DNS-over-HTTPS is slow), turn off ssdeep or banners, ' +
|
|
209
|
+
'or raise DNSTWIST_TIMEOUT_SECONDS.');
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.runningContainers.delete(container);
|
|
215
|
+
}
|
|
151
216
|
// csv and list output are returned as dnstwist printed them.
|
|
152
217
|
if (format !== 'json') {
|
|
153
218
|
return { content: [{ type: 'text', text: stdout }] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@burtthecoder/mcp-dnstwist",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "MCP server for dnstwist - DNS fuzzing to detect typosquatting, phishing and corporate espionage",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"author": "burtmacklin",
|
|
43
43
|
"license": "MIT",
|
|
44
44
|
"engines": {
|
|
45
|
-
"node": ">=20"
|
|
45
|
+
"node": ">=20.3"
|
|
46
46
|
}
|
|
47
47
|
}
|