knock 1.0.0 → 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
@@ -7,7 +7,8 @@ Transparency logs** (passive, via [crt.sh](https://crt.sh)) and a bundled
7
7
  **31k-entry wordlist** (active DNS brute force), detects wildcard DNS so you
8
8
  don't drown in false positives, and verifies everything with real lookups.
9
9
 
10
- Zero runtime dependencies. Great for attack-surface audits, pen-test recon,
10
+ Use it as a CLI, a library, or an **MCP server** for AI agents. Zero
11
+ runtime dependencies. Great for attack-surface audits, pen-test recon,
11
12
  or checking what a new client actually has exposed.
12
13
 
13
14
  [![npm version](https://img.shields.io/npm/v/knock.svg)](https://www.npmjs.com/package/knock)
@@ -20,6 +21,12 @@ npm install -g knock # CLI
20
21
  npm install knock # library
21
22
  ```
22
23
 
24
+ Or run the CLI without installing:
25
+
26
+ ```bash
27
+ npx knock example.com
28
+ ```
29
+
23
30
  Requires Node.js >= 22.
24
31
 
25
32
  ## CLI
@@ -50,6 +57,7 @@ Useful flags:
50
57
  | `-w, --web` | probe http/https on each found host |
51
58
  | `-j, --json` | full report as JSON |
52
59
  | `-q, --quiet` | hostnames only (pipe-friendly) |
60
+ | `--mcp` | run as an MCP server on stdio (see below) |
53
61
 
54
62
  Found hosts stream to stdout as they resolve; diagnostics go to stderr, so
55
63
  `knock -q example.com | sort` does what you'd hope.
@@ -105,6 +113,43 @@ popularity-ordered) and `wordlists.org`, and helpers `loadWordlist(path)` /
105
113
  `parseWordlist(text)` / `normalizeDomain(input)` are exported too. Full
106
114
  TypeScript types ship with the package.
107
115
 
116
+ ## MCP server
117
+
118
+ knock ships a [Model Context Protocol](https://modelcontextprotocol.io)
119
+ server, so AI agents (Claude Code, Claude Desktop, Cursor, …) can enumerate
120
+ subdomains directly. It runs over stdio:
121
+
122
+ ```bash
123
+ claude mcp add knock -- npx -y knock --mcp
124
+ ```
125
+
126
+ Or in any client's JSON config:
127
+
128
+ ```json
129
+ {
130
+ "mcpServers": {
131
+ "knock": { "command": "npx", "args": ["-y", "knock", "--mcp"] }
132
+ }
133
+ }
134
+ ```
135
+
136
+ | Tool | Does |
137
+ | --- | --- |
138
+ | `knock_enumerate` | full run (CT + brute force, or `passive: true`), returns the report |
139
+ | `knock_ct_lookup` | Certificate Transparency names only — one HTTPS request, no DNS |
140
+ | `knock_registrable_domain` | offline Public Suffix List lookup |
141
+
142
+ All tools are read-only, return structured JSON, stream progress per found
143
+ host, and honor cancellation. The server is implemented without the MCP SDK,
144
+ so the zero-dependency promise holds.
145
+
146
+ ## AI docs
147
+
148
+ [`llms.txt`](llms.txt) is a compact, agent-oriented reference to the whole
149
+ API (library, CLI and MCP tools) and ships in the npm package at
150
+ `node_modules/knock/llms.txt`. Contributors' coding agents get repo
151
+ conventions from [`AGENTS.md`](AGENTS.md).
152
+
108
153
  ## How it works
109
154
 
110
155
  0. **Public Suffix List** — input like `https://deep.www.example.co.uk/x` is
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ Options
26
26
  -s, --server <ip> DNS server to use, repeatable
27
27
  -6, --ipv6 also resolve AAAA records
28
28
  -w, --web probe http/https on found hosts
29
+ --mcp run as an MCP server on stdio (for AI agents)
29
30
  -j, --json print the full report as JSON
30
31
  -q, --quiet hostnames only, no summary
31
32
  -h, --help show this help
@@ -35,6 +36,7 @@ Examples
35
36
  knock example.com
36
37
  knock --passive --json example.com
37
38
  knock -l org -s 1.1.1.1 -c 128 example.com
39
+ claude mcp add knock -- npx -y knock --mcp
38
40
 
39
41
  Only scan domains you own or are authorized to assess.`;
40
42
  function fail(message) {
@@ -56,6 +58,7 @@ function parseCliArgs() {
56
58
  server: { type: 'string', short: 's', multiple: true },
57
59
  ipv6: { type: 'boolean', short: '6', default: false },
58
60
  web: { type: 'boolean', short: 'w', default: false },
61
+ mcp: { type: 'boolean', default: false },
59
62
  json: { type: 'boolean', short: 'j', default: false },
60
63
  quiet: { type: 'boolean', short: 'q', default: false },
61
64
  help: { type: 'boolean', short: 'h', default: false },
@@ -76,6 +79,11 @@ if (flags.version) {
76
79
  console.log(version);
77
80
  process.exit(0);
78
81
  }
82
+ if (flags.mcp) {
83
+ const { serveMcp } = await import('./mcp.js');
84
+ await serveMcp();
85
+ process.exit(0);
86
+ }
79
87
  if (domains.length === 0) {
80
88
  fail('no domain given (try: knock example.com, or --help)');
81
89
  }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ import type { KnockOptions } from './index.ts';
3
+ /** Protocol revisions this server speaks, newest first. */
4
+ export declare const MCP_PROTOCOL_VERSIONS: string[];
5
+ export interface McpServerOptions {
6
+ input?: Readable;
7
+ output?: Writable;
8
+ /** Merged into every knock() / certSubdomains() call, mainly for testing (resolver, fetch, psl). */
9
+ knockOptions?: Pick<KnockOptions, 'resolver' | 'fetch' | 'psl'>;
10
+ }
11
+ /**
12
+ * Serve MCP over stdio (or the given streams). Resolves when the input ends.
13
+ */
14
+ export declare function serveMcp(options?: McpServerOptions): Promise<void>;
package/dist/mcp.js ADDED
@@ -0,0 +1,319 @@
1
+ /**
2
+ * knock as a Model Context Protocol server over stdio — newline-delimited
3
+ * JSON-RPC 2.0, implemented directly so the package stays dependency-free.
4
+ */
5
+ import { createInterface } from 'node:readline';
6
+ import { readFileSync } from 'node:fs';
7
+ import process from 'node:process';
8
+ import { knock, certSubdomains, normalizeDomain, wordlists, loadPublicSuffixList, getPublicSuffix, getRegistrableDomain, } from './index.js';
9
+ const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
10
+ /** Protocol revisions this server speaks, newest first. */
11
+ export const MCP_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
12
+ class InvalidParams extends Error {
13
+ }
14
+ const INSTRUCTIONS = `knock enumerates subdomains of a domain using Certificate Transparency logs (passive, via crt.sh) and a wordlist DNS brute force with wildcard detection.
15
+ Prefer knock_ct_lookup or knock_enumerate with passive: true for a quick, low-noise look; a full brute force sends ~31k DNS queries and takes roughly 30-90 seconds.
16
+ Only enumerate domains the user owns or is authorized to assess.`;
17
+ const domainProperty = {
18
+ type: 'string',
19
+ description: 'Domain, hostname or URL, e.g. "example.com" or "https://www.example.co.uk/path".',
20
+ };
21
+ const TOOLS = [
22
+ {
23
+ name: 'knock_enumerate',
24
+ title: 'Enumerate subdomains',
25
+ description: 'Find subdomains of a domain. Combines Certificate Transparency logs with a DNS brute force over a bundled wordlist, filters wildcard DNS, and verifies names with real lookups. Input is reduced to its registrable domain first (www.example.co.uk -> example.co.uk) unless fullHost is set. Returns the full report: results (name, addresses, sources), wildcard, ct (including unresolved CT names), and stats. A full run sends ~31k DNS queries; use passive: true for a CT-only lookup.',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: {
29
+ domain: domainProperty,
30
+ passive: {
31
+ type: 'boolean',
32
+ description: 'Skip the wordlist brute force; Certificate Transparency only. Default false.',
33
+ },
34
+ ct: { type: 'boolean', description: 'Query Certificate Transparency logs. Default true.' },
35
+ verify: {
36
+ type: 'boolean',
37
+ description: 'Resolve CT-discovered names over DNS. Default true.',
38
+ },
39
+ fullHost: {
40
+ type: 'boolean',
41
+ description: 'Scan the exact host given instead of its registrable domain. Default false.',
42
+ },
43
+ wordlist: {
44
+ type: 'string',
45
+ enum: ['subs', 'org'],
46
+ description: 'Bundled wordlist: "subs" (~31k labels, default) or "org".',
47
+ },
48
+ words: {
49
+ type: 'array',
50
+ items: { type: 'string' },
51
+ description: 'Custom labels to try instead of a bundled wordlist, e.g. ["www", "api"].',
52
+ },
53
+ concurrency: {
54
+ type: 'integer',
55
+ minimum: 1,
56
+ maximum: 512,
57
+ description: 'Parallel DNS queries. Default 64.',
58
+ },
59
+ timeout: {
60
+ type: 'integer',
61
+ minimum: 1,
62
+ description: 'Per-query DNS timeout in milliseconds. Default 5000.',
63
+ },
64
+ servers: {
65
+ type: 'array',
66
+ items: { type: 'string' },
67
+ description: 'DNS server IPs to query instead of the system resolvers.',
68
+ },
69
+ ipv6: { type: 'boolean', description: 'Also resolve AAAA records. Default false.' },
70
+ web: {
71
+ type: 'boolean',
72
+ description: 'Probe http/https on each found host and report status codes. Default false.',
73
+ },
74
+ },
75
+ required: ['domain'],
76
+ additionalProperties: false,
77
+ },
78
+ annotations: { readOnlyHint: true, openWorldHint: true },
79
+ },
80
+ {
81
+ name: 'knock_ct_lookup',
82
+ title: 'Certificate Transparency lookup',
83
+ description: 'List hostnames under a domain that appear in Certificate Transparency logs (crt.sh). Passive: one HTTPS request to crt.sh, no DNS traffic to the target. Names are not verified and may be stale. The domain is used exactly as given (no registrable-domain reduction).',
84
+ inputSchema: {
85
+ type: 'object',
86
+ properties: {
87
+ domain: domainProperty,
88
+ timeout: {
89
+ type: 'integer',
90
+ minimum: 1,
91
+ description: 'Request timeout in milliseconds. Default 15000.',
92
+ },
93
+ },
94
+ required: ['domain'],
95
+ additionalProperties: false,
96
+ },
97
+ annotations: { readOnlyHint: true, openWorldHint: true },
98
+ },
99
+ {
100
+ name: 'knock_registrable_domain',
101
+ title: 'Registrable domain',
102
+ description: 'Reduce a hostname or URL to its public suffix and registrable domain using the bundled Public Suffix List, e.g. "a.b.example.co.uk" -> suffix "co.uk", registrable "example.co.uk". Offline; no network access.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ host: domainProperty,
107
+ icannOnly: {
108
+ type: 'boolean',
109
+ description: 'Ignore private suffixes like github.io. Default false.',
110
+ },
111
+ },
112
+ required: ['host'],
113
+ additionalProperties: false,
114
+ },
115
+ annotations: { readOnlyHint: true, openWorldHint: false },
116
+ },
117
+ ];
118
+ function optional(args, key, check, expected) {
119
+ const value = args[key];
120
+ if (value === undefined || value === null)
121
+ return undefined;
122
+ if (!check(value))
123
+ throw new InvalidParams(`"${key}" must be ${expected}`);
124
+ return value;
125
+ }
126
+ const isBoolean = (v) => typeof v === 'boolean';
127
+ const isString = (v) => typeof v === 'string';
128
+ const isPositiveInteger = (v) => Number.isInteger(v) && v > 0;
129
+ const isStringArray = (v) => Array.isArray(v) && v.every(isString);
130
+ function required(args, key) {
131
+ const value = optional(args, key, isString, 'a string');
132
+ if (!value)
133
+ throw new InvalidParams(`"${key}" is required`);
134
+ return value;
135
+ }
136
+ /**
137
+ * Serve MCP over stdio (or the given streams). Resolves when the input ends.
138
+ */
139
+ export async function serveMcp(options = {}) {
140
+ const { input = process.stdin, output = process.stdout, knockOptions = {} } = options;
141
+ const inFlight = new Map();
142
+ const send = (message) => {
143
+ output.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
144
+ };
145
+ const reply = (id, result) => send({ id, result });
146
+ const replyError = (id, code, message) => send({ id, error: { code, message } });
147
+ const toolResult = (data, summary) => ({
148
+ content: [
149
+ ...(summary ? [{ type: 'text', text: summary }] : []),
150
+ { type: 'text', text: JSON.stringify(data) },
151
+ ],
152
+ structuredContent: data,
153
+ });
154
+ async function callTool(name, args, signal, progress) {
155
+ switch (name) {
156
+ case 'knock_enumerate': {
157
+ const domain = required(args, 'domain');
158
+ const passive = optional(args, 'passive', isBoolean, 'a boolean') ?? false;
159
+ const list = optional(args, 'wordlist', isString, 'a string');
160
+ if (list !== undefined && list !== 'subs' && list !== 'org') {
161
+ throw new InvalidParams('"wordlist" must be "subs" or "org"');
162
+ }
163
+ const run = {
164
+ ...knockOptions,
165
+ signal,
166
+ ct: optional(args, 'ct', isBoolean, 'a boolean') ?? true,
167
+ verify: optional(args, 'verify', isBoolean, 'a boolean') ?? true,
168
+ baseDomainOnly: !(optional(args, 'fullHost', isBoolean, 'a boolean') ?? false),
169
+ wordlist: wordlists[list ?? 'subs'],
170
+ words: passive ? [] : optional(args, 'words', isStringArray, 'an array of strings'),
171
+ concurrency: Math.min(optional(args, 'concurrency', isPositiveInteger, 'a positive integer') ?? 64, 512),
172
+ timeout: optional(args, 'timeout', isPositiveInteger, 'a positive integer'),
173
+ servers: optional(args, 'servers', isStringArray, 'an array of strings'),
174
+ family: optional(args, 'ipv6', isBoolean, 'a boolean') ? 'any' : 4,
175
+ web: optional(args, 'web', isBoolean, 'a boolean') ?? false,
176
+ };
177
+ if (passive && !run.ct)
178
+ throw new InvalidParams('passive with ct: false leaves nothing to do');
179
+ if (run.timeout === undefined)
180
+ delete run.timeout;
181
+ let found = 0;
182
+ run.onResult = (result) => {
183
+ found += 1;
184
+ progress(found, result.name);
185
+ };
186
+ const report = await knock(domain, run);
187
+ const { stats, wildcard, ct } = report;
188
+ const summary = `${report.domain}: ${stats.found} found from ${stats.candidates} candidates` +
189
+ (ct.enabled ? `, ${ct.names} CT names (${ct.unresolved.length} unresolved)` : '') +
190
+ (wildcard.detected ? `, wildcard DNS detected (${wildcard.addresses.join(', ')})` : '') +
191
+ (ct.error ? `, CT lookup failed: ${ct.error}` : '') +
192
+ ` in ${(stats.durationMs / 1000).toFixed(1)}s`;
193
+ return toolResult(report, summary);
194
+ }
195
+ case 'knock_ct_lookup': {
196
+ const domain = normalizeDomain(required(args, 'domain'));
197
+ const names = await certSubdomains(domain, {
198
+ signal,
199
+ fetch: knockOptions.fetch,
200
+ timeout: optional(args, 'timeout', isPositiveInteger, 'a positive integer'),
201
+ });
202
+ return toolResult({ domain, names }, `${names.length} CT names under ${domain}`);
203
+ }
204
+ case 'knock_registrable_domain': {
205
+ const host = normalizeDomain(required(args, 'host'));
206
+ const icannOnly = optional(args, 'icannOnly', isBoolean, 'a boolean') ?? false;
207
+ const rules = knockOptions.psl ?? (await loadPublicSuffixList());
208
+ return toolResult({
209
+ host,
210
+ publicSuffix: getPublicSuffix(host, rules, { icannOnly }),
211
+ registrableDomain: getRegistrableDomain(host, rules, { icannOnly }),
212
+ });
213
+ }
214
+ default:
215
+ throw new InvalidParams(`unknown tool: ${name}`);
216
+ }
217
+ }
218
+ async function handle(message) {
219
+ const { id, method } = message;
220
+ const params = (message.params ?? {});
221
+ const isRequest = typeof id === 'string' || typeof id === 'number';
222
+ if (typeof method !== 'string') {
223
+ // Responses to requests we never send; ignore. Malformed requests get an error.
224
+ if (isRequest && !('result' in message || 'error' in message)) {
225
+ replyError(id, -32600, 'invalid request');
226
+ }
227
+ return;
228
+ }
229
+ if (!isRequest) {
230
+ if (method === 'notifications/cancelled') {
231
+ inFlight.get(params.requestId)?.abort(new Error(String(params.reason ?? 'cancelled')));
232
+ }
233
+ return;
234
+ }
235
+ switch (method) {
236
+ case 'initialize': {
237
+ const requested = params.protocolVersion;
238
+ reply(id, {
239
+ protocolVersion: typeof requested === 'string' && MCP_PROTOCOL_VERSIONS.includes(requested)
240
+ ? requested
241
+ : MCP_PROTOCOL_VERSIONS[0],
242
+ capabilities: { tools: { listChanged: false } },
243
+ serverInfo: { name: 'knock', title: 'knock', version },
244
+ instructions: INSTRUCTIONS,
245
+ });
246
+ return;
247
+ }
248
+ case 'ping':
249
+ reply(id, {});
250
+ return;
251
+ case 'tools/list':
252
+ reply(id, { tools: TOOLS });
253
+ return;
254
+ case 'tools/call': {
255
+ const name = String(params.name ?? '');
256
+ const args = (params.arguments ?? {});
257
+ const progressToken = params._meta?.progressToken;
258
+ const controller = new AbortController();
259
+ inFlight.set(id, controller);
260
+ const progress = (count, text) => {
261
+ if (progressToken === undefined)
262
+ return;
263
+ send({
264
+ method: 'notifications/progress',
265
+ params: { progressToken, progress: count, message: `found ${text}` },
266
+ });
267
+ };
268
+ try {
269
+ reply(id, await callTool(name, args, controller.signal, progress));
270
+ }
271
+ catch (error) {
272
+ // A cancelled request gets no response, per the spec.
273
+ if (controller.signal.aborted)
274
+ return;
275
+ const text = error instanceof Error ? error.message : String(error);
276
+ if (error instanceof InvalidParams && text.startsWith('unknown tool')) {
277
+ replyError(id, -32602, text);
278
+ }
279
+ else {
280
+ reply(id, { content: [{ type: 'text', text }], isError: true });
281
+ }
282
+ }
283
+ finally {
284
+ inFlight.delete(id);
285
+ }
286
+ return;
287
+ }
288
+ default:
289
+ replyError(id, -32601, `method not found: ${method}`);
290
+ }
291
+ }
292
+ const lines = createInterface({ input, crlfDelay: Infinity });
293
+ const pending = new Set();
294
+ for await (const line of lines) {
295
+ if (line.trim() === '')
296
+ continue;
297
+ let message;
298
+ try {
299
+ message = JSON.parse(line);
300
+ }
301
+ catch {
302
+ replyError(null, -32700, 'parse error');
303
+ continue;
304
+ }
305
+ if (typeof message !== 'object' || message === null || Array.isArray(message)) {
306
+ replyError(null, -32600, 'invalid request');
307
+ continue;
308
+ }
309
+ // Requests run concurrently so pings and cancellations stay responsive during a scan.
310
+ const task = handle(message).catch((error) => {
311
+ process.stderr.write(`knock mcp: ${error instanceof Error ? error.stack : String(error)}\n`);
312
+ });
313
+ pending.add(task);
314
+ void task.finally(() => pending.delete(task));
315
+ }
316
+ for (const controller of inFlight.values())
317
+ controller.abort(new Error('input closed'));
318
+ await Promise.allSettled(pending);
319
+ }
package/llms.txt ADDED
@@ -0,0 +1,70 @@
1
+ # knock
2
+
3
+ > Subdomain enumeration for Node.js (>= 22), as a library, a CLI, and an MCP server. Combines Certificate Transparency logs (passive, via crt.sh) with a wordlist DNS brute force (bundled ~31k labels), detects and filters wildcard DNS, and verifies names with real lookups. ESM-only, TypeScript types included, zero runtime dependencies. Only scan domains you own or are authorized to assess.
4
+
5
+ Install with `npm install knock` (library) or run `npx knock example.com` (CLI). Targets are reduced to their registrable domain via a bundled Public Suffix List before scanning ("https://a.www.example.co.uk/x" -> "example.co.uk") unless told otherwise.
6
+
7
+ ## Library
8
+
9
+ ```js
10
+ import { knock, certSubdomains } from 'knock';
11
+
12
+ // Full run: CT + wordlist brute force. Takes ~30-90s with the default wordlist.
13
+ const report = await knock('example.com', {
14
+ concurrency: 64, // parallel DNS queries
15
+ onResult: (r) => console.log(r.name), // streamed as found
16
+ });
17
+
18
+ // Passive only: CT names, verified over DNS, no brute force.
19
+ await knock('example.com', { words: [] });
20
+
21
+ // Just the CT names, unverified: one HTTPS request, no DNS.
22
+ const names = await certSubdomains('example.com'); // string[], sorted, apex excluded
23
+ ```
24
+
25
+ `knock(domain, options?)` resolves to a `KnockReport`:
26
+
27
+ ```ts
28
+ {
29
+ domain: string; // the domain actually scanned
30
+ results: { name: string; addresses: string[]; sources: ('wordlist' | 'ct')[];
31
+ web?: { https: number | null; http: number | null } }[];
32
+ wildcard: { detected: boolean; addresses: string[] };
33
+ ct: { enabled: boolean; names: number; unresolved: string[]; error: string | null };
34
+ stats: { words: number; candidates: number; queried: number; found: number;
35
+ errors: number; durationMs: number };
36
+ }
37
+ ```
38
+
39
+ Options (all optional): `words` (string[], overrides `wordlist`; `[]` = passive), `wordlist` (file path; default `wordlists.subs`), `ct` (true), `ctTimeout` (15000), `verify` (true; resolve CT names), `concurrency` (64), `timeout` (5000 ms per DNS query), `tries` (2), `servers` (DNS server IPs), `family` (4 | 6 | 'any'; default 4), `wildcardTests` (3; 0 disables), `web` (false; HEAD http/https on found hosts), `webTimeout` (5000), `baseDomainOnly` (true), `icannOnly` (false; ignore private PSL suffixes like github.io), `signal` (AbortSignal), `onResult` (callback), and test injectables `resolver`, `fetch`, `psl`.
40
+
41
+ Behavior worth knowing:
42
+ - A CT lookup failure does not reject; it is reported in `report.ct.error` and the run continues with the wordlist.
43
+ - Wordlist hits that only return the wildcard DNS answer are dropped; CT-sourced names are kept even under a wildcard.
44
+ - `report.ct.unresolved` lists CT names that no longer resolve (stale or internal hosts).
45
+ - `knock()` rejects with the abort reason when `signal` aborts, and with a `TypeError` for unusable input.
46
+
47
+ Other exports: `normalizeDomain(input)`, `parseWordlist(text)`, `loadWordlist(path)`, `wordlists` (`{ subs, org }` absolute paths), `getRegistrableDomain(host, rules, { icannOnly })`, `getPublicSuffix(host, rules, opts)`, `loadPublicSuffixList(path?)`, `parsePublicSuffixList(text)`, `publicSuffixListPath`, and all the types (`KnockOptions`, `KnockReport`, `KnockResult`, ...). Default export is `knock`.
48
+
49
+ ## CLI
50
+
51
+ ```
52
+ knock [options] <domain> [domain ...]
53
+ ```
54
+
55
+ `-p/--passive` (CT only), `--no-ct`, `--no-verify`, `--full-host` (skip registrable-domain reduction), `-l/--list <path|subs|org>`, `-c/--concurrency <n>`, `-t/--timeout <ms>`, `-s/--server <ip>` (repeatable), `-6/--ipv6`, `-w/--web`, `-j/--json` (full report), `-q/--quiet` (hostnames only), `--mcp`. Results go to stdout, diagnostics to stderr; exit code 1 if any domain failed.
56
+
57
+ ## MCP server
58
+
59
+ `knock --mcp` serves the Model Context Protocol over stdio. Register it with, for example, `claude mcp add knock -- npx -y knock --mcp`, or in a JSON client config: `{"command": "npx", "args": ["-y", "knock", "--mcp"]}`.
60
+
61
+ Tools (all read-only; results include `structuredContent` plus a JSON text block):
62
+ - `knock_enumerate` — full `knock()` run. Args: `domain` (required), `passive`, `ct`, `verify`, `fullHost`, `wordlist` ("subs" | "org"), `words`, `concurrency`, `timeout`, `servers`, `ipv6`, `web`. Returns the `KnockReport`. Sends progress notifications per found host when the call carries a progress token, and supports cancellation.
63
+ - `knock_ct_lookup` — `certSubdomains()`. Args: `domain`, `timeout`. Returns `{ domain, names }`.
64
+ - `knock_registrable_domain` — offline PSL lookup. Args: `host`, `icannOnly`. Returns `{ host, publicSuffix, registrableDomain }`.
65
+
66
+ ## Links
67
+
68
+ - [README](https://github.com/neopunisher/node-knock#readme): full documentation
69
+ - [Changelog](https://github.com/neopunisher/node-knock/blob/main/CHANGELOG.md)
70
+ - [npm](https://www.npmjs.com/package/knock)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "knock",
3
- "version": "1.0.0",
4
- "description": "Knock, knock. Who's there? Wordlist-based subdomain enumeration with wildcard DNS detection. Zero dependencies.",
3
+ "version": "1.1.0",
4
+ "description": "Knock, knock. Who's there? Subdomain enumeration via Certificate Transparency and wordlist DNS brute force, with wildcard detection. CLI, library and MCP server. Zero dependencies.",
5
5
  "keywords": [
6
6
  "domain",
7
7
  "subdomain",
@@ -12,7 +12,10 @@
12
12
  "pentest",
13
13
  "security",
14
14
  "util",
15
- "utility"
15
+ "utility",
16
+ "mcp",
17
+ "mcp-server",
18
+ "model-context-protocol"
16
19
  ],
17
20
  "author": "Carter Cole <node@cartercole.com>",
18
21
  "license": "MIT",
@@ -39,7 +42,8 @@
39
42
  },
40
43
  "files": [
41
44
  "dist/",
42
- "lists/"
45
+ "lists/",
46
+ "llms.txt"
43
47
  ],
44
48
  "engines": {
45
49
  "node": ">=22"