@burtthecoder/mcp-dnstwist 1.0.6 → 1.1.0

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
@@ -1,171 +1,123 @@
1
- # DNStwist MCP Server
2
- [![smithery badge](https://smithery.ai/badge/@burtthecoder/mcp-dnstwist)](https://smithery.ai/server/@burtthecoder/mcp-dnstwist)
3
-
4
- A Model Context Protocol (MCP) server for [dnstwist](https://github.com/elceef/dnstwist), a powerful DNS fuzzing tool that helps detect typosquatting, phishing, and corporate espionage. This server provides tools for analyzing domain permutations and identifying potentially malicious domains. It is designed to integrate seamlessly with MCP-compatible applications like [Claude Desktop](https://claude.ai).
5
-
6
- <a href="https://glama.ai/mcp/servers/it7izu3ufb"><img width="380" height="200" src="https://glama.ai/mcp/servers/it7izu3ufb/badge" alt="mcp-dnstwist MCP server" /></a>
7
-
8
-
9
- ## ⚠️ Warning
10
-
11
- This tool is designed for legitimate security research purposes. Please:
12
- - Only analyze domains you own or have permission to test
13
- - Respect rate limits and DNS server policies
14
- - Use responsibly and ethically
15
- - Be aware that some DNS servers may rate-limit or block automated queries
16
- - Consider the impact on DNS infrastructure when running large scans
17
-
18
- ## Requirements
19
-
20
- - Node.js (v20 or later)
21
- - Docker
22
- - macOS, Linux, or Windows with Docker Desktop installed
23
-
24
- ## Quick Start
25
-
26
- ### Installing via Smithery
27
-
28
- To install DNStwist for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@burtthecoder/mcp-dnstwist):
29
-
30
- ```bash
31
- npx -y @smithery/cli install @burtthecoder/mcp-dnstwist --client claude
1
+ <p align="center">
2
+ <img alt="MCP dnstwist: typosquatting detection for AI agents. Find lookalike, homoglyph and phishing domains with dnstwist over the Model Context Protocol." src="docs/assets/banner.webp" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@burtthecoder/mcp-dnstwist"><img alt="npm version" src="https://img.shields.io/npm/v/@burtthecoder/mcp-dnstwist"></a>
7
+ <a href="https://github.com/w0h1v/mcp-dnstwist/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/w0h1v/mcp-dnstwist/actions/workflows/ci.yml/badge.svg"></a>
8
+ <a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-blue"></a>
9
+ <img alt="Node 20.3 or newer" src="https://img.shields.io/badge/node-%E2%89%A520.3-339933">
10
+ <a href="https://registry.modelcontextprotocol.io"><img alt="MCP Registry" src="https://img.shields.io/badge/MCP-Registry-7c3aed"></a>
11
+ <a href="https://smithery.ai/server/@burtthecoder/mcp-dnstwist"><img alt="Smithery" src="https://img.shields.io/badge/Smithery-listed-ff5601"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="#quickstart">Quickstart</a> ·
16
+ <a href="#the-tool">Tool</a> ·
17
+ <a href="#security">Security</a> ·
18
+ <a href="#troubleshooting">Troubleshooting</a> ·
19
+ <a href="https://w0h1v.github.io/mcp-dnstwist/">Docs</a>
20
+ </p>
21
+
22
+ **mcp-dnstwist** is an [MCP](https://modelcontextprotocol.io) server that lets AI agents run [dnstwist](https://github.com/elceef/dnstwist) to find domains that imitate yours: typos, homoglyphs, bit flips, swapped TLDs and more. For each registered lookalike it can report DNS records, whether its mail server could intercept your email, WHOIS registration details, and how closely its web page copies yours.
23
+
24
+ > [!NOTE]
25
+ > Independent project, not affiliated with dnstwist. Only scan domains you own or are authorized to assess.
26
+
27
+ > [!IMPORTANT]
28
+ > Versions up to 1.0.4 have a command injection vulnerability ([GHSA-p4jr-h3rj-2rw4](https://github.com/w0h1v/mcp-dnstwist/security/advisories/GHSA-p4jr-h3rj-2rw4)) and are deprecated. Use 1.0.5 or later; `npx -y @burtthecoder/mcp-dnstwist` always fetches the latest.
29
+
30
+ ## Highlights
31
+
32
+ - **Lookalike discovery.** All of dnstwist's permutation algorithms, or only the ones you pick.
33
+ - **Threat signals.** DNS records, open mail relays that could intercept email (`mxcheck`), WHOIS creation date and registrar, HTTP and SMTP banners, and web-page similarity (ssdeep or TLSH) to spot cloned phishing sites.
34
+ - **Runs your way.** A local dnstwist install, the official dnstwist Docker image, or a self-contained mcp-dnstwist image.
35
+ - **Hardened.** No shell, strict input validation, and time limits that stop and clean up long scans.
36
+ - **Tested.** End-to-end tests with fake `docker` and `dnstwist` executables run in CI on Node 20, 22 and 24.
37
+
38
+ ## How it works
39
+
40
+ ```mermaid
41
+ flowchart LR
42
+ A["AI agent<br/>(Claude, Codex, Gemini, VS Code)"] -->|"MCP over stdio"| S["mcp-dnstwist"]
43
+ S -->|"validated argv, no shell,<br/>time limit"| D["dnstwist<br/>(local or Docker)"]
44
+ D -->|"DNS, WHOIS, HTTP, SMTP"| I["Lookalike domains"]
45
+ D -->|"JSON"| S
46
+ S -->|"results"| A
32
47
  ```
33
48
 
34
- ### Installing Manually
35
- 1. Install Docker:
36
- - macOS: Install [Docker Desktop](https://www.docker.com/products/docker-desktop)
37
- - Linux: Follow the [Docker Engine installation guide](https://docs.docker.com/engine/install/)
49
+ ## Quickstart
38
50
 
39
- 2. Install the server globally via npm:
40
- ```bash
41
- npm install -g @burtthecoder/mcp-dnstwist
42
- ```
51
+ **Requirements:** Node.js 20.3 or newer, and either dnstwist (`pipx install "dnstwist[full]"`) or Docker. No API key.
43
52
 
44
- 3. Add to your Claude Desktop configuration file:
45
- ```json
46
- {
47
- "mcpServers": {
48
- "dnstwist": {
49
- "command": "mcp-dnstwist"
50
- }
51
- }
52
- }
53
- ```
53
+ ```sh
54
+ # Claude Code
55
+ claude mcp add --transport stdio dnstwist -- npx -y @burtthecoder/mcp-dnstwist
54
56
 
55
- Configuration file location:
56
- - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
57
- - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
57
+ # Codex CLI
58
+ codex mcp add dnstwist -- npx -y @burtthecoder/mcp-dnstwist
58
59
 
59
- 4. Restart Claude Desktop
60
+ # Gemini CLI
61
+ gemini mcp add dnstwist npx -y @burtthecoder/mcp-dnstwist
62
+ ```
60
63
 
61
- ## Alternative Setup (From Source)
64
+ For Claude Desktop, VS Code, Docker, Smithery and running from source, see [Connect a client](docs/clients.md).
62
65
 
63
- If you prefer to run from source or need to modify the code:
66
+ Then ask your agent: *"Which lookalike domains of example.com are registered, and could any of them intercept email?"* More prompts are in [example workflows](docs/examples.md).
64
67
 
65
- 1. Clone and build:
66
- ```bash
67
- git clone <repository_url>
68
- cd mcp-dnstwist
69
- npm install
70
- npm run build
71
- ```
68
+ ## The tool
72
69
 
73
- 2. Add to your Claude Desktop configuration:
74
- ```json
75
- {
76
- "mcpServers": {
77
- "dnstwist": {
78
- "command": "node",
79
- "args": ["/absolute/path/to/mcp-dnstwist/build/index.js"]
80
- }
81
- }
82
- }
83
- ```
70
+ One tool, `fuzz_domain`. Full parameters and real sample output are in the [tool reference](docs/tools.md).
84
71
 
85
- ## Features
86
-
87
- - **Domain Fuzzing**: Generate domain permutations using various algorithms
88
- - **Registration Check**: Verify if permutated domains are registered
89
- - **DNS Analysis**: Check A, AAAA, MX, and NS records
90
- - **Web Presence**: Capture HTTP banner information
91
- - **WHOIS Data**: Retrieve registration dates and registrar information
92
- - **Phishing Detection**: Generate fuzzy hashes of web pages
93
- - **Configurable**: Custom DNS servers and parallel processing
94
- - **Multiple Formats**: Support for json, csv, and list output formats
95
-
96
- ## Tools
97
-
98
- ### Domain Fuzzing Tool
99
- - Name: `fuzz_domain`
100
- - Description: Generate and analyze domain permutations to detect potential typosquatting, phishing, and brand impersonation
101
- - Parameters:
102
- * `domain` (required): Domain name to analyze (e.g., example.com)
103
- * `nameservers` (optional, default: "1.1.1.1"): Comma-separated DNS servers, up to 10. Each must be an IPv4 address or a DNS-over-HTTPS URL such as `https://cloudflare-dns.com/dns-query` (the forms dnstwist accepts). DNS-over-HTTPS is much slower: a DoH-only scan can take over 10 minutes, so include an IPv4 resolver unless you need DoH
104
- * `threads` (optional, default: 50): Number of threads for parallel processing (1-100)
105
- * `format` (optional, default: "json"): Output format (json, csv, list)
106
- * `registered_only` (optional, default: true): Show only registered domains
107
- * `mxcheck` (optional, default: true): Check for MX records
108
- * `ssdeep` (optional, default: false): Generate fuzzy hashes of web pages
109
- * `banners` (optional, default: true): Capture HTTP banner information
110
-
111
- Example:
112
- ```json
113
- {
114
- "domain": "example.com",
115
- "nameservers": "1.1.1.1,8.8.8.8",
116
- "threads": 50,
117
- "format": "json",
118
- "registered_only": true,
119
- "mxcheck": true,
120
- "banners": true
121
- }
122
- ```
72
+ | Parameter | Default | What it does |
73
+ |---|---|---|
74
+ | `domain` | *(required)* | The domain to imitate, for example `example.com` |
75
+ | `registered_only` | `true` | Only return lookalikes that exist |
76
+ | `fuzzers` | all | Limit to some algorithms, for example `["homoglyph", "bitsquatting"]` |
77
+ | `mxcheck` | `true` | Flag mail servers that would accept email for any address |
78
+ | `banners` | `true` | Capture HTTP and SMTP banners |
79
+ | `whois` | `false` | Registration date and registrar |
80
+ | `lsh` | off | Web-page similarity to the original: `ssdeep` or `tlsh` |
81
+ | `all_records` | `false` | All DNS records, not only the first of each type |
82
+ | `nameservers` | `1.1.1.1` | Up to 10 IPv4 addresses or `https://<host>/dns-query` URLs |
83
+ | `threads` | `50` | Parallel lookups, 1 to 100 |
84
+ | `format` | `json` | `json`, `csv` or `list` |
123
85
 
124
- ## Troubleshooting
86
+ ### Settings
125
87
 
126
- ### Docker Issues
88
+ | Variable | Default | Description |
89
+ |---|---|---|
90
+ | `DNSTWIST_RUNNER` | `auto` | `local` (dnstwist on `PATH`), `docker` (the `elceef/dnstwist` image), or `auto` (local if available) |
91
+ | `DNSTWIST_TIMEOUT_SECONDS` | `600` | Longest a scan may run. On timeout, cancellation or shutdown the scan is stopped and cleaned up |
127
92
 
128
- 1. Verify Docker is installed and running:
129
- ```bash
130
- docker --version
131
- docker ps
132
- ```
93
+ ## Security
133
94
 
134
- 2. Check Docker permissions:
135
- - Ensure your user has permissions to run Docker commands
136
- - On Linux, add your user to the docker group: `sudo usermod -aG docker $USER`
95
+ The tool's arguments come from an AI model, which content it reads can steer, so the server treats them as hostile:
137
96
 
138
- ### Common Issues
97
+ - **No shell.** dnstwist starts with `execFile` and an argument array; a test fails the build if a shell-based API appears.
98
+ - **Strict validation.** Domains must be hostnames, nameservers IPv4 or DoH URLs, numbers and names from fixed sets. No value can start with `-`.
99
+ - **Bounded runs.** Time limits, and cleanup of processes and containers on timeout, cancellation and shutdown.
139
100
 
140
- 1. DNS resolution problems:
141
- - Verify DNS servers are accessible
142
- - Try alternative DNS servers (e.g., 8.8.8.8)
143
- - Check for rate limiting or blocking
101
+ The Docker runner needs access to the Docker daemon (root-equivalent on most systems). The local runner or the self-contained image avoid that. Details in the [security model](docs/security.md). Report vulnerabilities privately; see [SECURITY.md](SECURITY.md).
144
102
 
145
- 2. Performance issues:
146
- - Adjust thread count based on system capabilities
147
- - Consider network bandwidth and latency
148
- - Monitor DNS server response times
103
+ ## Troubleshooting
149
104
 
150
- 3. After fixing any issues:
151
- - Save the configuration file
152
- - Restart Claude Desktop
105
+ - **"Neither dnstwist nor Docker is available":** install dnstwist (`pipx install "dnstwist[full]"`) or start Docker. Desktop clients may not see your shell's `PATH`.
106
+ - **Slow on Apple Silicon:** the dnstwist Docker image is amd64-only. Install dnstwist locally; it's used automatically.
107
+ - **"did not finish within N seconds":** include an IPv4 nameserver (DNS-over-HTTPS is slow), turn off `whois`, `lsh` or `banners`, or raise `DNSTWIST_TIMEOUT_SECONDS`.
153
108
 
154
- ## Error Messages
109
+ More in the [troubleshooting guide](docs/troubleshooting.md).
155
110
 
156
- - "Docker is not installed or not running": Install Docker and start the Docker daemon
157
- - "Failed to parse dnstwist output": Check if the domain is valid and the format is correct
158
- - "Error executing dnstwist": Check Docker logs and ensure proper permissions
159
- - "DNS server not responding": Verify DNS server accessibility and try alternative servers
111
+ ## Development
160
112
 
161
- ## Contributing
113
+ ```sh
114
+ git clone https://github.com/w0h1v/mcp-dnstwist && cd mcp-dnstwist
115
+ npm ci
116
+ npm test # fake docker and dnstwist executables: no Docker or network needed
117
+ ```
162
118
 
163
- 1. Fork the repository
164
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
165
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
166
- 4. Push to the branch (`git push origin feature/amazing-feature`)
167
- 5. Open a Pull Request
119
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and the [changelog](CHANGELOG.md).
168
120
 
169
121
  ## License
170
122
 
171
- This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
123
+ MIT. See [LICENSE](LICENSE).
package/build/index.js CHANGED
@@ -3,33 +3,93 @@ 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
- import { validateFuzzArgs } from './validate.js';
8
- // execFile runs docker directly with an argument array. No shell is involved,
8
+ import { FUZZERS, LSH_ALGORITHMS, validateFuzzArgs } from './validate.js';
9
+ // execFile runs docker (or dnstwist) 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), LSH and 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();
28
+ const RUNNER_SETTINGS = ['docker', 'local', 'auto'];
29
+ // docker: run the elceef/dnstwist image. local: run a dnstwist on PATH.
30
+ // auto (default): local when a dnstwist executable works, otherwise docker.
31
+ function runnerSetting() {
32
+ const raw = process.env.DNSTWIST_RUNNER;
33
+ if (raw === undefined || raw === '')
34
+ return 'auto';
35
+ if (!RUNNER_SETTINGS.includes(raw)) {
36
+ console.error(`DNSTWIST_RUNNER must be one of ${RUNNER_SETTINGS.join(', ')}, got ${JSON.stringify(raw)}`);
37
+ process.exit(1);
38
+ }
39
+ return raw;
40
+ }
41
+ const RUNNER_SETTING = runnerSetting();
12
42
  class DnstwistServer {
13
43
  constructor() {
44
+ // Scans in progress: id -> controller that stops the child process, plus the
45
+ // container name when the docker runner started one.
46
+ this.runningScans = new Map();
14
47
  this.server = new Server({ name: 'dnstwist-server', version: '0.1.0' }, { capabilities: { tools: {} } });
15
48
  this.setupToolHandlers();
16
49
  this.server.onerror = (error) => console.error('[MCP Error]', error);
17
- process.on('SIGINT', async () => {
18
- await this.server.close();
19
- process.exit(0);
20
- });
50
+ for (const signal of ['SIGINT', 'SIGTERM']) {
51
+ process.on(signal, async () => {
52
+ // Don't leave scans running after the server is gone: stop each
53
+ // in-flight child process first, then remove any docker container.
54
+ const scans = [...this.runningScans.values()];
55
+ for (const scan of scans)
56
+ scan.stop.abort();
57
+ await Promise.all(scans.flatMap((scan) => (scan.container ? [this.removeContainer(scan.container)] : [])));
58
+ await this.server.close();
59
+ process.exit(0);
60
+ });
61
+ }
21
62
  // Trigger setup immediately
22
63
  this.ensureSetup().catch(error => {
23
64
  console.error('Failed to setup dnstwist:', error);
24
65
  process.exit(1);
25
66
  });
26
67
  }
27
- async execCommand(file, args) {
68
+ async execCommand(file, args, options = {}) {
28
69
  console.error('Executing:', file, JSON.stringify(args));
29
70
  try {
30
- const result = await execFileAsync(file, args, {
31
- maxBuffer: 10 * 1024 * 1024
71
+ const { killOnAbort, ...execOptions } = options;
72
+ const pending = execFileAsync(file, args, {
73
+ maxBuffer: 10 * 1024 * 1024,
74
+ ...execOptions,
75
+ // execFile's own `signal` support always sends SIGTERM; when the process
76
+ // must be killed outright, handle the abort here instead.
77
+ ...(killOnAbort ? { signal: undefined } : {})
32
78
  });
79
+ const abort = () => pending.child.kill('SIGKILL');
80
+ if (killOnAbort && options.signal) {
81
+ if (options.signal.aborted)
82
+ abort();
83
+ else
84
+ options.signal.addEventListener('abort', abort, { once: true });
85
+ }
86
+ let result;
87
+ try {
88
+ result = await pending;
89
+ }
90
+ finally {
91
+ options.signal?.removeEventListener('abort', abort);
92
+ }
33
93
  console.error('Command output:', result.stdout);
34
94
  if (result.stderr)
35
95
  console.error('Command stderr:', result.stderr);
@@ -40,26 +100,84 @@ class DnstwistServer {
40
100
  throw error;
41
101
  }
42
102
  }
43
- async ensureSetup() {
44
- try {
45
- console.error('Checking Docker...');
46
- try {
47
- await this.execCommand('docker', ['--version']);
48
- }
49
- catch (error) {
50
- throw new Error('Docker is not installed or not running. Please install Docker and try again.');
51
- }
52
- console.error('Checking if dnstwist image exists...');
103
+ // Killing the docker CLI doesn't stop the container it started, so a scan
104
+ // that times out or is cancelled is removed by name. Right after the CLI
105
+ // starts, the daemon may not have created the container yet ("No such
106
+ // container"), so keep trying for a few seconds until a removal succeeds.
107
+ async removeContainer(name, attempts = 10, delayMs = 500) {
108
+ for (let i = 0; i < attempts; i++) {
53
109
  try {
54
- await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
55
- console.error('Dnstwist image found');
110
+ await execFileAsync('docker', ['rm', '-f', name]);
111
+ return;
56
112
  }
57
- catch (error) {
58
- console.error('Dnstwist image not found, pulling...');
59
- await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
60
- console.error('Dnstwist image pulled successfully');
113
+ catch {
114
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
61
115
  }
62
116
  }
117
+ console.error(`Could not confirm removal of container ${name}`);
118
+ }
119
+ // Decided once. A failed check is not cached, so a later call can retry.
120
+ getRunner() {
121
+ this.runnerPromise ?? (this.runnerPromise = this.selectRunner().catch((error) => {
122
+ this.runnerPromise = undefined;
123
+ throw error;
124
+ }));
125
+ return this.runnerPromise;
126
+ }
127
+ async checkLocal() {
128
+ try {
129
+ await this.execCommand('dnstwist', ['--version']);
130
+ }
131
+ catch (error) {
132
+ throw new Error('The dnstwist executable was not found or does not work (`dnstwist --version` failed). ' +
133
+ 'Install it (for example `pipx install "dnstwist[full]"`) or set DNSTWIST_RUNNER=docker.');
134
+ }
135
+ }
136
+ async selectRunner() {
137
+ if (RUNNER_SETTING === 'docker')
138
+ return 'docker';
139
+ if (RUNNER_SETTING === 'local') {
140
+ await this.checkLocal();
141
+ console.error('Using the local dnstwist executable (DNSTWIST_RUNNER=local)');
142
+ return 'local';
143
+ }
144
+ try {
145
+ await this.checkLocal();
146
+ console.error('Using the local dnstwist executable (DNSTWIST_RUNNER=auto)');
147
+ return 'local';
148
+ }
149
+ catch {
150
+ console.error('No working dnstwist executable on PATH; using Docker (DNSTWIST_RUNNER=auto)');
151
+ return 'docker';
152
+ }
153
+ }
154
+ async checkDocker() {
155
+ console.error('Checking Docker...');
156
+ try {
157
+ await this.execCommand('docker', ['--version']);
158
+ }
159
+ catch (error) {
160
+ throw new Error('Docker is not installed or not running. Please install Docker and try again.');
161
+ }
162
+ console.error('Checking if dnstwist image exists...');
163
+ try {
164
+ await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
165
+ console.error('Dnstwist image found');
166
+ }
167
+ catch (error) {
168
+ console.error('Dnstwist image not found, pulling...');
169
+ await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
170
+ console.error('Dnstwist image pulled successfully');
171
+ }
172
+ }
173
+ async ensureSetup() {
174
+ try {
175
+ const runner = await this.getRunner();
176
+ // Docker is re-checked per call: the image or daemon can disappear.
177
+ if (runner === 'docker')
178
+ await this.checkDocker();
179
+ return runner;
180
+ }
63
181
  catch (error) {
64
182
  console.error('Setup failed:', error);
65
183
  throw error;
@@ -85,7 +203,7 @@ class DnstwistServer {
85
203
  },
86
204
  threads: {
87
205
  type: 'number',
88
- description: 'Number of threads to use for parallel processing',
206
+ description: 'Number of parallel lookups (1-100)',
89
207
  minimum: 1,
90
208
  maximum: 100,
91
209
  default: 50
@@ -103,17 +221,40 @@ class DnstwistServer {
103
221
  },
104
222
  mxcheck: {
105
223
  type: 'boolean',
106
- description: 'Check for MX records',
224
+ description: 'Check whether each mail server would accept email for any address, so it could intercept mail sent to typos of your domain',
107
225
  default: true
108
226
  },
109
227
  ssdeep: {
110
228
  type: 'boolean',
111
- description: 'Generate fuzzy hashes of web pages to detect phishing',
229
+ description: 'Deprecated alias for lsh: "ssdeep" (ignored when lsh is set)',
112
230
  default: false
113
231
  },
232
+ lsh: {
233
+ type: 'string',
234
+ enum: [...LSH_ALGORITHMS],
235
+ description: 'Compare each permutation\'s web page with the original using a fuzzy hash (slow) to spot cloned phishing pages'
236
+ },
237
+ whois: {
238
+ type: 'boolean',
239
+ description: 'Look up WHOIS creation date and registrar for each domain (slower)',
240
+ default: false
241
+ },
242
+ all_records: {
243
+ type: 'boolean',
244
+ description: 'Return all DNS records instead of only the first of each type',
245
+ default: false
246
+ },
247
+ fuzzers: {
248
+ type: 'array',
249
+ items: { type: 'string', enum: [...FUZZERS] },
250
+ minItems: 1,
251
+ maxItems: FUZZERS.length,
252
+ uniqueItems: true,
253
+ description: 'Only use these fuzzing algorithms (default: all)'
254
+ },
114
255
  banners: {
115
256
  type: 'boolean',
116
- description: 'Capture HTTP banner information',
257
+ description: 'Capture HTTP and SMTP server banners',
117
258
  default: true
118
259
  }
119
260
  },
@@ -122,13 +263,13 @@ class DnstwistServer {
122
263
  }
123
264
  ]
124
265
  }));
125
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
266
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
126
267
  try {
127
- await this.ensureSetup();
268
+ const runner = await this.ensureSetup();
128
269
  if (request.params.name !== 'fuzz_domain') {
129
270
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
130
271
  }
131
- const { domain, nameservers, threads, format, registered_only, mxcheck, ssdeep, banners } = validateFuzzArgs(request.params.arguments);
272
+ const { domain, nameservers, threads, format, registered_only, mxcheck, banners, whois, all_records, fuzzers, lsh } = validateFuzzArgs(request.params.arguments);
132
273
  // Build command arguments
133
274
  const args = [
134
275
  domain,
@@ -142,12 +283,45 @@ class DnstwistServer {
142
283
  args.push('-t', threads.toString());
143
284
  if (mxcheck)
144
285
  args.push('--mxcheck');
145
- if (ssdeep)
146
- args.push('--ssdeep');
286
+ if (lsh)
287
+ args.push('--lsh', lsh);
147
288
  if (banners)
148
289
  args.push('-b');
149
- // Run dnstwist in Docker
150
- const { stdout } = await this.execCommand('docker', ['run', '--rm', DOCKER_IMAGE, ...args]);
290
+ if (whois)
291
+ args.push('-w');
292
+ if (all_records)
293
+ args.push('-a');
294
+ if (fuzzers)
295
+ args.push('--fuzzers', fuzzers.join(','));
296
+ const id = randomUUID();
297
+ const container = runner === 'docker' ? `mcp-dnstwist-${id}` : undefined;
298
+ const stop = new AbortController();
299
+ this.runningScans.set(id, { stop, container });
300
+ let stdout;
301
+ try {
302
+ const options = { timeout: TIMEOUT_SECONDS * 1000, signal: AbortSignal.any([extra.signal, stop.signal]) };
303
+ ({ stdout } = container
304
+ ? await this.execCommand('docker', ['run', '--rm', '--name', container, DOCKER_IMAGE, ...args], options)
305
+ // dnstwist handles SIGTERM itself and may wind down slowly: kill it outright.
306
+ : await this.execCommand('dnstwist', args, { ...options, killSignal: 'SIGKILL', killOnAbort: true }));
307
+ }
308
+ catch (error) {
309
+ const stopped = extra.signal.aborted || stop.signal.aborted;
310
+ const timedOut = Boolean(error.killed) && !stopped;
311
+ // If dnstwist exited on its own, --rm already removed the container.
312
+ if (container && (timedOut || stopped))
313
+ await this.removeContainer(container);
314
+ if (timedOut) {
315
+ const unit = TIMEOUT_SECONDS === 1 ? 'second' : 'seconds';
316
+ throw new Error(`dnstwist did not finish within ${TIMEOUT_SECONDS} ${unit} and was stopped. ` +
317
+ 'Use an IPv4 nameserver (DNS-over-HTTPS is slow), turn off lsh, whois or banners, ' +
318
+ 'or raise DNSTWIST_TIMEOUT_SECONDS.');
319
+ }
320
+ throw error;
321
+ }
322
+ finally {
323
+ this.runningScans.delete(id);
324
+ }
151
325
  // csv and list output are returned as dnstwist printed them.
152
326
  if (format !== 'json') {
153
327
  return { content: [{ type: 'text', text: stdout }] };
package/build/validate.js CHANGED
@@ -6,6 +6,13 @@ import { domainToASCII } from 'url';
6
6
  // mistaken for a dnstwist option either.
7
7
  const HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{2,63}|xn--[a-z0-9-]{1,59})$/i;
8
8
  const FORMATS = ['json', 'csv', 'list'];
9
+ // dnstwist's fuzzing algorithms, minus dictionary and tld-swap: those need a
10
+ // word or TLD list file, which this tool doesn't take.
11
+ export const FUZZERS = [
12
+ 'addition', 'bitsquatting', 'cyrillic', 'homoglyph', 'hyphenation', 'insertion', 'omission',
13
+ 'plural', 'repetition', 'replacement', 'subdomain', 'transposition', 'vowel-swap', 'various',
14
+ ];
15
+ export const LSH_ALGORITHMS = ['ssdeep', 'tlsh'];
9
16
  const MAX_NAMESERVERS = 10;
10
17
  // The DNS-over-HTTPS form dnstwist accepts (dnstwist.py --nameservers parsing).
11
18
  const DOH = /^https:\/\/([a-z0-9.-]{4,253})\/dns-query$/;
@@ -65,6 +72,22 @@ export function validateFuzzArgs(args) {
65
72
  if (!FORMATS.includes(format)) {
66
73
  invalid(`format must be one of ${FORMATS.join(', ')}`);
67
74
  }
75
+ let fuzzers;
76
+ if (a.fuzzers !== undefined) {
77
+ const list = a.fuzzers;
78
+ if (!Array.isArray(list) || list.length < 1 || list.length > FUZZERS.length ||
79
+ list.some((f) => typeof f !== 'string' || !FUZZERS.includes(f)) ||
80
+ new Set(list).size !== list.length) {
81
+ invalid(`fuzzers must be 1 to ${FUZZERS.length} unique names from: ${FUZZERS.join(', ')}`);
82
+ }
83
+ fuzzers = list;
84
+ }
85
+ if (a.lsh !== undefined && !LSH_ALGORITHMS.includes(a.lsh)) {
86
+ invalid(`lsh must be one of ${LSH_ALGORITHMS.join(', ')}`);
87
+ }
88
+ const ssdeep = optionalBoolean(a, 'ssdeep', false);
89
+ // `ssdeep: true` is the older spelling of `lsh: 'ssdeep'`; an explicit lsh wins.
90
+ const lsh = a.lsh ?? (ssdeep ? 'ssdeep' : undefined);
68
91
  return {
69
92
  domain: domain,
70
93
  nameservers: servers.join(','),
@@ -72,7 +95,11 @@ export function validateFuzzArgs(args) {
72
95
  format: format,
73
96
  registered_only: optionalBoolean(a, 'registered_only', true),
74
97
  mxcheck: optionalBoolean(a, 'mxcheck', true),
75
- ssdeep: optionalBoolean(a, 'ssdeep', false),
98
+ ssdeep,
76
99
  banners: optionalBoolean(a, 'banners', true),
100
+ whois: optionalBoolean(a, 'whois', false),
101
+ all_records: optionalBoolean(a, 'all_records', false),
102
+ ...(fuzzers ? { fuzzers } : {}),
103
+ ...(lsh ? { lsh } : {}),
77
104
  };
78
105
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@burtthecoder/mcp-dnstwist",
3
- "version": "1.0.6",
4
- "description": "MCP server for dnstwist - DNS fuzzing to detect typosquatting, phishing and corporate espionage",
3
+ "version": "1.1.0",
4
+ "description": "MCP server for dnstwist: find typosquatting, homoglyph and lookalike domains used for phishing and brand impersonation",
5
5
  "main": "build/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -14,6 +14,8 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "build": "tsc && chmod +x build/index.js",
17
+ "start": "node build/index.js",
18
+ "dev": "tsc -w",
17
19
  "prepublishOnly": "npm run build",
18
20
  "test": "npm run build && node --test tests/*.test.mjs"
19
21
  },
@@ -37,11 +39,22 @@
37
39
  "typosquatting",
38
40
  "domain-security",
39
41
  "claude",
40
- "claude-desktop"
42
+ "claude-desktop",
43
+ "mcp-server",
44
+ "model-context-protocol",
45
+ "brand-protection",
46
+ "threat-intelligence",
47
+ "dfir",
48
+ "phishing-detection"
41
49
  ],
42
- "author": "burtmacklin",
50
+ "author": "w0h1v",
43
51
  "license": "MIT",
44
52
  "engines": {
45
- "node": ">=20"
46
- }
53
+ "node": ">=20.3"
54
+ },
55
+ "homepage": "https://w0h1v.github.io/mcp-dnstwist/",
56
+ "bugs": {
57
+ "url": "https://github.com/w0h1v/mcp-dnstwist/issues"
58
+ },
59
+ "mcpName": "io.github.w0h1v/dnstwist"
47
60
  }