@burtthecoder/mcp-dnstwist 1.0.7 → 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,181 +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.3 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
- ```
43
-
44
- 3. Add to your Claude Desktop configuration file:
45
- ```json
46
- {
47
- "mcpServers": {
48
- "dnstwist": {
49
- "command": "mcp-dnstwist"
50
- }
51
- }
52
- }
53
- ```
51
+ **Requirements:** Node.js 20.3 or newer, and either dnstwist (`pipx install "dnstwist[full]"`) or Docker. No API key.
54
52
 
55
- Configuration file location:
56
- - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
57
- - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
53
+ ```sh
54
+ # Claude Code
55
+ claude mcp add --transport stdio dnstwist -- npx -y @burtthecoder/mcp-dnstwist
58
56
 
59
- 4. Restart Claude Desktop
57
+ # Codex CLI
58
+ codex mcp add dnstwist -- npx -y @burtthecoder/mcp-dnstwist
60
59
 
61
- ## Alternative Setup (From Source)
60
+ # Gemini CLI
61
+ gemini mcp add dnstwist npx -y @burtthecoder/mcp-dnstwist
62
+ ```
62
63
 
63
- If you prefer to run from source or need to modify the code:
64
+ For Claude Desktop, VS Code, Docker, Smithery and running from source, see [Connect a client](docs/clients.md).
64
65
 
65
- 1. Clone and build:
66
- ```bash
67
- git clone <repository_url>
68
- cd mcp-dnstwist
69
- npm install
70
- npm run build
71
- ```
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).
72
67
 
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
- ```
68
+ ## The tool
84
69
 
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
- ```
70
+ One tool, `fuzz_domain`. Full parameters and real sample output are in the [tool reference](docs/tools.md).
123
71
 
124
- ## Configuration
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` |
85
+
86
+ ### Settings
125
87
 
126
88
  | Variable | Default | Description |
127
89
  |---|---|---|
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. |
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 |
129
92
 
130
- Set it in your client's `env` block, for example `"env": { "DNSTWIST_TIMEOUT_SECONDS": "900" }`.
93
+ ## Security
131
94
 
132
- ## Troubleshooting
95
+ The tool's arguments come from an AI model, which content it reads can steer, so the server treats them as hostile:
133
96
 
134
- ### Docker 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.
135
100
 
136
- 1. Verify Docker is installed and running:
137
- ```bash
138
- docker --version
139
- docker ps
140
- ```
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).
141
102
 
142
- 2. Check Docker permissions:
143
- - Ensure your user has permissions to run Docker commands
144
- - On Linux, add your user to the docker group: `sudo usermod -aG docker $USER`
145
-
146
- ### Common Issues
147
-
148
- 1. DNS resolution problems:
149
- - Verify DNS servers are accessible
150
- - Try alternative DNS servers (e.g., 8.8.8.8)
151
- - Check for rate limiting or blocking
152
-
153
- 2. Performance issues:
154
- - Adjust thread count based on system capabilities
155
- - Consider network bandwidth and latency
156
- - Monitor DNS server response times
157
-
158
- 3. After fixing any issues:
159
- - Save the configuration file
160
- - Restart Claude Desktop
103
+ ## Troubleshooting
161
104
 
162
- ## Error Messages
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`.
163
108
 
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
109
+ More in the [troubleshooting guide](docs/troubleshooting.md).
165
110
 
166
- - "Docker is not installed or not running": Install Docker and start the Docker daemon
167
- - "Failed to parse dnstwist output": Check if the domain is valid and the format is correct
168
- - "Error executing dnstwist": Check Docker logs and ensure proper permissions
169
- - "DNS server not responding": Verify DNS server accessibility and try alternative servers
111
+ ## Development
170
112
 
171
- ## 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
+ ```
172
118
 
173
- 1. Fork the repository
174
- 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
175
- 3. Commit your changes (`git commit -m 'Add amazing feature'`)
176
- 4. Push to the branch (`git push origin feature/amazing-feature`)
177
- 5. Open a Pull Request
119
+ See [CONTRIBUTING.md](CONTRIBUTING.md) and the [changelog](CHANGELOG.md).
178
120
 
179
121
  ## License
180
122
 
181
- 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
@@ -5,13 +5,13 @@ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } f
5
5
  import { execFile } from 'child_process';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { promisify } from 'util';
8
- import { validateFuzzArgs } from './validate.js';
9
- // 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,
10
10
  // so tool input can never be interpreted as shell syntax (CWE-78, issue #13).
11
11
  const execFileAsync = promisify(execFile);
12
12
  const DOCKER_IMAGE = 'elceef/dnstwist';
13
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.
14
+ // Slow resolvers (DNS-over-HTTPS), LSH and banners can take many minutes.
15
15
  const DEFAULT_TIMEOUT_SECONDS = 600;
16
16
  function scanTimeoutSeconds() {
17
17
  const raw = process.env.DNSTWIST_TIMEOUT_SECONDS;
@@ -25,20 +25,36 @@ function scanTimeoutSeconds() {
25
25
  return seconds;
26
26
  }
27
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();
28
42
  class DnstwistServer {
29
43
  constructor() {
30
- // Scans in progress: container name -> controller that stops its docker run.
31
- this.runningContainers = new Map();
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();
32
47
  this.server = new Server({ name: 'dnstwist-server', version: '0.1.0' }, { capabilities: { tools: {} } });
33
48
  this.setupToolHandlers();
34
49
  this.server.onerror = (error) => console.error('[MCP Error]', error);
35
50
  for (const signal of ['SIGINT', 'SIGTERM']) {
36
51
  process.on(signal, async () => {
37
52
  // 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)));
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)] : [])));
42
58
  await this.server.close();
43
59
  process.exit(0);
44
60
  });
@@ -52,10 +68,28 @@ class DnstwistServer {
52
68
  async execCommand(file, args, options = {}) {
53
69
  console.error('Executing:', file, JSON.stringify(args));
54
70
  try {
55
- const result = await execFileAsync(file, args, {
71
+ const { killOnAbort, ...execOptions } = options;
72
+ const pending = execFileAsync(file, args, {
56
73
  maxBuffer: 10 * 1024 * 1024,
57
- ...options
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 } : {})
58
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
+ }
59
93
  console.error('Command output:', result.stdout);
60
94
  if (result.stderr)
61
95
  console.error('Command stderr:', result.stderr);
@@ -82,25 +116,67 @@ class DnstwistServer {
82
116
  }
83
117
  console.error(`Could not confirm removal of container ${name}`);
84
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
+ }
85
173
  async ensureSetup() {
86
174
  try {
87
- console.error('Checking Docker...');
88
- try {
89
- await this.execCommand('docker', ['--version']);
90
- }
91
- catch (error) {
92
- throw new Error('Docker is not installed or not running. Please install Docker and try again.');
93
- }
94
- console.error('Checking if dnstwist image exists...');
95
- try {
96
- await this.execCommand('docker', ['image', 'inspect', DOCKER_IMAGE]);
97
- console.error('Dnstwist image found');
98
- }
99
- catch (error) {
100
- console.error('Dnstwist image not found, pulling...');
101
- await this.execCommand('docker', ['pull', DOCKER_IMAGE]);
102
- console.error('Dnstwist image pulled successfully');
103
- }
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;
104
180
  }
105
181
  catch (error) {
106
182
  console.error('Setup failed:', error);
@@ -127,7 +203,7 @@ class DnstwistServer {
127
203
  },
128
204
  threads: {
129
205
  type: 'number',
130
- description: 'Number of threads to use for parallel processing',
206
+ description: 'Number of parallel lookups (1-100)',
131
207
  minimum: 1,
132
208
  maximum: 100,
133
209
  default: 50
@@ -145,17 +221,40 @@ class DnstwistServer {
145
221
  },
146
222
  mxcheck: {
147
223
  type: 'boolean',
148
- 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',
149
225
  default: true
150
226
  },
151
227
  ssdeep: {
152
228
  type: 'boolean',
153
- description: 'Generate fuzzy hashes of web pages to detect phishing',
229
+ description: 'Deprecated alias for lsh: "ssdeep" (ignored when lsh is set)',
154
230
  default: false
155
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
+ },
156
255
  banners: {
157
256
  type: 'boolean',
158
- description: 'Capture HTTP banner information',
257
+ description: 'Capture HTTP and SMTP server banners',
159
258
  default: true
160
259
  }
161
260
  },
@@ -166,11 +265,11 @@ class DnstwistServer {
166
265
  }));
167
266
  this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
168
267
  try {
169
- await this.ensureSetup();
268
+ const runner = await this.ensureSetup();
170
269
  if (request.params.name !== 'fuzz_domain') {
171
270
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
172
271
  }
173
- 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);
174
273
  // Build command arguments
175
274
  const args = [
176
275
  domain,
@@ -184,34 +283,44 @@ class DnstwistServer {
184
283
  args.push('-t', threads.toString());
185
284
  if (mxcheck)
186
285
  args.push('--mxcheck');
187
- if (ssdeep)
188
- args.push('--ssdeep');
286
+ if (lsh)
287
+ args.push('--lsh', lsh);
189
288
  if (banners)
190
289
  args.push('-b');
191
- // Run dnstwist in Docker
192
- const container = `mcp-dnstwist-${randomUUID()}`;
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;
193
298
  const stop = new AbortController();
194
- this.runningContainers.set(container, stop);
299
+ this.runningScans.set(id, { stop, container });
195
300
  let stdout;
196
301
  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]) }));
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 }));
198
307
  }
199
308
  catch (error) {
200
309
  const stopped = extra.signal.aborted || stop.signal.aborted;
201
310
  const timedOut = Boolean(error.killed) && !stopped;
202
311
  // If dnstwist exited on its own, --rm already removed the container.
203
- if (timedOut || stopped)
312
+ if (container && (timedOut || stopped))
204
313
  await this.removeContainer(container);
205
314
  if (timedOut) {
206
315
  const unit = TIMEOUT_SECONDS === 1 ? 'second' : 'seconds';
207
316
  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, ' +
317
+ 'Use an IPv4 nameserver (DNS-over-HTTPS is slow), turn off lsh, whois or banners, ' +
209
318
  'or raise DNSTWIST_TIMEOUT_SECONDS.');
210
319
  }
211
320
  throw error;
212
321
  }
213
322
  finally {
214
- this.runningContainers.delete(container);
323
+ this.runningScans.delete(id);
215
324
  }
216
325
  // csv and list output are returned as dnstwist printed them.
217
326
  if (format !== 'json') {
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.7",
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
53
  "node": ">=20.3"
46
- }
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
  }