@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 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
- process.on('SIGINT', async () => {
18
- await this.server.close();
19
- process.exit(0);
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 { stdout } = await this.execCommand('docker', ['run', '--rm', DOCKER_IMAGE, ...args]);
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.6",
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
  }