@paulgit/mcp-dns-tools 2.2.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/dist/index.js ADDED
@@ -0,0 +1,1439 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/cli.ts
7
+ import { parseArgs } from "util";
8
+ function parseCliArgs(args) {
9
+ const { values } = parseArgs({
10
+ args,
11
+ options: {
12
+ transport: {
13
+ type: "string",
14
+ default: "stdio"
15
+ }
16
+ },
17
+ strict: true
18
+ });
19
+ if (values.transport !== "stdio") {
20
+ throw new Error(
21
+ `Unsupported transport: ${values.transport}. Only stdio is supported.`
22
+ );
23
+ }
24
+ return { transport: "stdio" };
25
+ }
26
+
27
+ // src/dns.ts
28
+ import dns2 from "dns/promises";
29
+ import { setServers } from "dns";
30
+ import net2 from "net";
31
+
32
+ // src/registration.ts
33
+ import dns from "dns/promises";
34
+ import net from "net";
35
+ import { domainToASCII } from "url";
36
+ import { Agent, fetch as undiciFetch } from "undici";
37
+ var RDAP_DNS_BOOTSTRAP = "https://data.iana.org/rdap/dns.json";
38
+ var RDAP_IPV4_BOOTSTRAP = "https://data.iana.org/rdap/ipv4.json";
39
+ var RDAP_IPV6_BOOTSTRAP = "https://data.iana.org/rdap/ipv6.json";
40
+ var BOOTSTRAP_TTL_MS = 24 * 60 * 60 * 1e3;
41
+ var REQUEST_TIMEOUT_MS = 1e4;
42
+ var MAX_REDIRECTS = 3;
43
+ var MAX_RDAP_BYTES = 1024 * 1024;
44
+ var MAX_BOOTSTRAP_BYTES = 2 * 1024 * 1024;
45
+ var MAX_WHOIS_BYTES = 1024 * 1024;
46
+ var MAX_WHOIS_QUERIES = 3;
47
+ var RegistrationLookupError = class extends Error {
48
+ constructor(message, kind, source, reason) {
49
+ super(message);
50
+ this.kind = kind;
51
+ this.source = source;
52
+ this.reason = reason;
53
+ this.name = "RegistrationLookupError";
54
+ }
55
+ };
56
+ var bootstrapCache = /* @__PURE__ */ new Map();
57
+ var blockedAddresses = new net.BlockList();
58
+ var syntheticHttpsProxyAddresses = new net.BlockList();
59
+ syntheticHttpsProxyAddresses.addSubnet("198.18.0.0", 15, "ipv4");
60
+ for (const [address, prefix] of [
61
+ ["0.0.0.0", 8],
62
+ ["10.0.0.0", 8],
63
+ ["100.64.0.0", 10],
64
+ ["127.0.0.0", 8],
65
+ ["169.254.0.0", 16],
66
+ ["172.16.0.0", 12],
67
+ ["192.0.0.0", 24],
68
+ ["192.0.2.0", 24],
69
+ ["192.168.0.0", 16],
70
+ ["198.18.0.0", 15],
71
+ ["198.51.100.0", 24],
72
+ ["203.0.113.0", 24],
73
+ ["224.0.0.0", 4],
74
+ ["240.0.0.0", 4]
75
+ ]) {
76
+ blockedAddresses.addSubnet(address, prefix, "ipv4");
77
+ }
78
+ for (const [address, prefix] of [
79
+ ["::", 128],
80
+ ["::1", 128],
81
+ ["64:ff9b:1::", 48],
82
+ ["100::", 64],
83
+ ["2001:db8::", 32],
84
+ ["fc00::", 7],
85
+ ["fe80::", 10],
86
+ ["fec0::", 10],
87
+ ["ff00::", 8]
88
+ ]) {
89
+ blockedAddresses.addSubnet(address, prefix, "ipv6");
90
+ }
91
+ function isRecord(value) {
92
+ return typeof value === "object" && value !== null && !Array.isArray(value);
93
+ }
94
+ function stringValue(value) {
95
+ if (typeof value === "string" && value.trim()) return value.trim();
96
+ if (Array.isArray(value)) {
97
+ const values = value.map((entry) => stringValue(entry)).filter((entry) => entry !== void 0);
98
+ return values.length > 0 ? values.join(", ") : void 0;
99
+ }
100
+ return void 0;
101
+ }
102
+ function stringArray(value) {
103
+ if (!Array.isArray(value)) return [];
104
+ return value.filter(
105
+ (entry) => typeof entry === "string" && entry.length > 0
106
+ );
107
+ }
108
+ function isBlockedAddress(address) {
109
+ const family = net.isIP(address);
110
+ if (family === 4) return blockedAddresses.check(address, "ipv4");
111
+ if (family === 6) return blockedAddresses.check(address, "ipv6");
112
+ return true;
113
+ }
114
+ async function resolvePublicHostname(hostname, allowSyntheticNetworkProxy = false) {
115
+ const normalised = hostname.replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
116
+ if (!normalised || normalised === "localhost" || normalised.endsWith(".local")) {
117
+ throw new RegistrationLookupError(
118
+ `Refusing registration-data connection to ${hostname}`,
119
+ "security"
120
+ );
121
+ }
122
+ if (net.isIP(normalised) !== 0) {
123
+ if (isBlockedAddress(normalised)) {
124
+ throw new RegistrationLookupError(
125
+ `Refusing registration-data connection to non-public address ${hostname}`,
126
+ "security"
127
+ );
128
+ }
129
+ return [
130
+ {
131
+ address: normalised,
132
+ family: net.isIP(normalised)
133
+ }
134
+ ];
135
+ }
136
+ let addresses;
137
+ try {
138
+ addresses = await dns.lookup(normalised, {
139
+ all: true,
140
+ verbatim: true
141
+ });
142
+ } catch (error) {
143
+ throw new RegistrationLookupError(
144
+ `Unable to resolve registration-data service ${hostname}: ${errorMessage(error)}`,
145
+ "network"
146
+ );
147
+ }
148
+ const resolved = addresses.filter(
149
+ (entry) => entry.family === 4 || entry.family === 6
150
+ );
151
+ if (resolved.length === 0 || resolved.some(
152
+ (entry) => isBlockedAddress(entry.address) && !(allowSyntheticNetworkProxy && entry.family === 4 && syntheticHttpsProxyAddresses.check(entry.address, "ipv4"))
153
+ )) {
154
+ throw new RegistrationLookupError(
155
+ `Refusing registration-data connection to ${hostname}: destination is not public`,
156
+ "security"
157
+ );
158
+ }
159
+ return resolved;
160
+ }
161
+ function pinnedLookup(addresses) {
162
+ return (_hostname, options, callback) => {
163
+ const requestedFamily = options.family ?? 0;
164
+ const candidates = addresses.filter(
165
+ (entry) => requestedFamily === 0 || entry.family === requestedFamily
166
+ );
167
+ if (candidates.length === 0) {
168
+ const error = new Error(
169
+ `No validated address for requested family ${requestedFamily}`
170
+ );
171
+ error.code = "EAI_ADDRFAMILY";
172
+ callback(error, "");
173
+ return;
174
+ }
175
+ if (options.all) {
176
+ callback(null, candidates);
177
+ return;
178
+ }
179
+ const [selected] = candidates;
180
+ if (!selected) {
181
+ callback(new Error("No validated address available"), "");
182
+ return;
183
+ }
184
+ callback(null, selected.address, selected.family);
185
+ };
186
+ }
187
+ function errorMessage(error) {
188
+ return error instanceof Error ? error.message : "unknown error";
189
+ }
190
+ function errorCode(error) {
191
+ return error instanceof Error ? error.code : void 0;
192
+ }
193
+ function reasonForError(protocol, error) {
194
+ if (error instanceof RegistrationLookupError && error.reason)
195
+ return error.reason;
196
+ const message = errorMessage(error).toLowerCase();
197
+ const code = errorCode(error);
198
+ if (message.includes("exceeds") && message.includes("bytes"))
199
+ return "RESPONSE_TOO_LARGE";
200
+ if (message.includes("referral loop")) return "REFERRAL_LOOP";
201
+ if (message.includes("referral limit")) return "REFERRAL_LIMIT_REACHED";
202
+ if (protocol === "RDAP") {
203
+ if (error instanceof RegistrationLookupError && error.kind === "not-found")
204
+ return "RDAP_NOT_FOUND";
205
+ if (error instanceof RegistrationLookupError && error.kind === "rate-limited")
206
+ return "RDAP_RATE_LIMITED";
207
+ if (message.includes("timed out") || code === "ETIMEDOUT")
208
+ return "RDAP_TIMEOUT";
209
+ if (message.includes("http ")) return "RDAP_HTTP_ERROR";
210
+ if (error instanceof RegistrationLookupError && error.kind === "security")
211
+ return "RDAP_DESTINATION_REJECTED";
212
+ if (error instanceof RegistrationLookupError && (error.kind === "invalid-response" || error.kind === "unsupported"))
213
+ return error.kind === "unsupported" ? "RDAP_NOT_AVAILABLE" : "RDAP_INVALID_RESPONSE";
214
+ return "RDAP_NETWORK_ERROR";
215
+ }
216
+ if (code === "ECONNREFUSED") return "WHOIS_CONNECTION_REFUSED";
217
+ if (code === "ECONNRESET") return "WHOIS_CONNECTION_RESET";
218
+ if (code === "ENOTFOUND" || code === "EAI_AGAIN") return "WHOIS_DNS_FAILURE";
219
+ if (message.includes("unable to resolve registration-data service"))
220
+ return "WHOIS_DNS_FAILURE";
221
+ if (message.includes("timed out") || code === "ETIMEDOUT")
222
+ return "WHOIS_TIMEOUT";
223
+ if (message.includes("no whois data")) return "WHOIS_EMPTY_RESPONSE";
224
+ if (error instanceof RegistrationLookupError && error.kind === "unsupported")
225
+ return "WHOIS_NOT_ADVERTISED";
226
+ if (error instanceof RegistrationLookupError && error.kind === "rate-limited")
227
+ return "WHOIS_RATE_LIMITED";
228
+ if (error instanceof RegistrationLookupError && error.kind === "security")
229
+ return "REFERRAL_REJECTED";
230
+ return "UNKNOWN_ERROR";
231
+ }
232
+ function warningText(warning) {
233
+ return `${warning.reason}: ${warning.detail}`;
234
+ }
235
+ function failureSuggestion(rdapReason, whoisReason) {
236
+ if (whoisReason === "WHOIS_WEB_ONLY")
237
+ return "Use the registry's web-based registration lookup service.";
238
+ if (rdapReason === "RDAP_RATE_LIMITED" || whoisReason === "WHOIS_RATE_LIMITED")
239
+ return "Retry later and reduce lookup frequency.";
240
+ if (whoisReason === "WHOIS_SERVICE_RETIRED")
241
+ return "Use the registry's RDAP or web-based replacement service.";
242
+ if (whoisReason === "WHOIS_CONNECTION_REFUSED" || whoisReason === "WHOIS_CONNECTION_RESET" || whoisReason === "WHOIS_TIMEOUT" || whoisReason === "WHOIS_DNS_FAILURE")
243
+ return "Check outbound HTTPS and TCP port 43 access, then retry.";
244
+ if (whoisReason === "WHOIS_NOT_ADVERTISED" || whoisReason === "WHOIS_UNSUPPORTED_RESPONSE")
245
+ return "Consult the TLD registry website for its supported lookup service.";
246
+ if (whoisReason === "REFERRAL_REJECTED" || whoisReason === "REFERRAL_LOOP")
247
+ return "Use the last authoritative source shown or consult the registry website.";
248
+ return "Retry later or consult the registry's registration-data service.";
249
+ }
250
+ function formatLookupFailure(queryType, query, rdapError, whoisError) {
251
+ const rdapReason = reasonForError("RDAP", rdapError);
252
+ const whoisReason = reasonForError("WHOIS", whoisError);
253
+ const rdapSource = rdapError instanceof RegistrationLookupError ? rdapError.source : void 0;
254
+ const whoisSource = whoisError instanceof RegistrationLookupError ? whoisError.source : void 0;
255
+ return [
256
+ "Status: failed",
257
+ `${queryType}: ${query}`,
258
+ "RDAP status: failed",
259
+ `RDAP reason: ${rdapReason}`,
260
+ ...rdapSource ? [`RDAP source: ${rdapSource}`] : [],
261
+ `RDAP detail: ${errorMessage(rdapError)}`,
262
+ "WHOIS status: failed",
263
+ `WHOIS reason: ${whoisReason}`,
264
+ ...whoisSource ? [`WHOIS source: ${whoisSource}`] : [],
265
+ `WHOIS detail: ${errorMessage(whoisError)}`,
266
+ `Suggestion: ${failureSuggestion(rdapReason, whoisReason)}`
267
+ ].join("\n");
268
+ }
269
+ async function readResponseBody(response, maximumBytes) {
270
+ const contentLength = response.headers.get("content-length");
271
+ if (contentLength && Number(contentLength) > maximumBytes) {
272
+ await response.body?.cancel();
273
+ throw new RegistrationLookupError(
274
+ `Registration-data response exceeds ${maximumBytes} bytes`,
275
+ "invalid-response"
276
+ );
277
+ }
278
+ if (!response.body) return "";
279
+ const reader = response.body.getReader();
280
+ const chunks = [];
281
+ let total = 0;
282
+ while (true) {
283
+ const { done, value } = await reader.read();
284
+ if (done) break;
285
+ total += value.byteLength;
286
+ if (total > maximumBytes) {
287
+ try {
288
+ await reader.cancel();
289
+ } catch {
290
+ }
291
+ throw new RegistrationLookupError(
292
+ `Registration-data response exceeds ${maximumBytes} bytes`,
293
+ "invalid-response"
294
+ );
295
+ }
296
+ chunks.push(value);
297
+ }
298
+ const body = new Uint8Array(total);
299
+ let offset = 0;
300
+ for (const chunk of chunks) {
301
+ body.set(chunk, offset);
302
+ offset += chunk.byteLength;
303
+ }
304
+ return new TextDecoder().decode(body);
305
+ }
306
+ function validateHttpsUrl(value) {
307
+ let url;
308
+ try {
309
+ url = new URL(value);
310
+ } catch {
311
+ throw new RegistrationLookupError(
312
+ `Invalid RDAP service URL: ${value}`,
313
+ "invalid-response"
314
+ );
315
+ }
316
+ if (url.protocol !== "https:" || url.username || url.password || url.port && url.port !== "443") {
317
+ throw new RegistrationLookupError(
318
+ `Refusing non-standard RDAP service URL: ${value}`,
319
+ "security"
320
+ );
321
+ }
322
+ return url;
323
+ }
324
+ async function fetchJson(initialUrl, maximumBytes) {
325
+ let url = validateHttpsUrl(initialUrl);
326
+ for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects += 1) {
327
+ const addresses = await resolvePublicHostname(url.hostname, true);
328
+ const dispatcher = new Agent({
329
+ connect: { lookup: pinnedLookup(addresses) }
330
+ });
331
+ const controller = new AbortController();
332
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
333
+ try {
334
+ const response = await undiciFetch(url, {
335
+ headers: {
336
+ accept: "application/rdap+json, application/json",
337
+ "user-agent": "mcp-dns-tools/registration-lookup"
338
+ },
339
+ dispatcher,
340
+ redirect: "manual",
341
+ signal: controller.signal
342
+ });
343
+ if ([301, 302, 303, 307, 308].includes(response.status)) {
344
+ const location = response.headers.get("location");
345
+ if (!location) {
346
+ throw new RegistrationLookupError(
347
+ `RDAP redirect from ${url.hostname} has no location`,
348
+ "invalid-response"
349
+ );
350
+ }
351
+ if (redirects === MAX_REDIRECTS) {
352
+ throw new RegistrationLookupError(
353
+ "RDAP redirect limit exceeded",
354
+ "invalid-response"
355
+ );
356
+ }
357
+ await response.body?.cancel();
358
+ url = validateHttpsUrl(new URL(location, url).href);
359
+ continue;
360
+ }
361
+ const body = await readResponseBody(response, maximumBytes);
362
+ if (response.status === 404) {
363
+ throw new RegistrationLookupError(
364
+ "Registration data not found",
365
+ "not-found",
366
+ url.href
367
+ );
368
+ }
369
+ if (response.status === 429) {
370
+ const retryAfter = response.headers.get("retry-after");
371
+ throw new RegistrationLookupError(
372
+ `RDAP service rate limited the request${retryAfter ? `; retry after ${retryAfter}` : ""}`,
373
+ "rate-limited"
374
+ );
375
+ }
376
+ if (!response.ok) {
377
+ throw new RegistrationLookupError(
378
+ `RDAP service returned HTTP ${response.status}`,
379
+ "network"
380
+ );
381
+ }
382
+ let parsed;
383
+ try {
384
+ parsed = JSON.parse(body);
385
+ } catch {
386
+ throw new RegistrationLookupError(
387
+ `RDAP service ${url.hostname} returned invalid JSON`,
388
+ "invalid-response"
389
+ );
390
+ }
391
+ if (!isRecord(parsed)) {
392
+ throw new RegistrationLookupError(
393
+ `RDAP service ${url.hostname} returned an invalid object`,
394
+ "invalid-response"
395
+ );
396
+ }
397
+ return { data: parsed, source: url.href };
398
+ } catch (error) {
399
+ if (controller.signal.aborted) {
400
+ throw new RegistrationLookupError(
401
+ `Request to ${url.hostname} timed out`,
402
+ "network",
403
+ url.href,
404
+ "RDAP_TIMEOUT"
405
+ );
406
+ }
407
+ if (error instanceof RegistrationLookupError) throw error;
408
+ throw new RegistrationLookupError(
409
+ `Request to ${url.hostname} failed: ${errorMessage(error)}`,
410
+ "network"
411
+ );
412
+ } finally {
413
+ clearTimeout(timeout);
414
+ await dispatcher.close();
415
+ }
416
+ }
417
+ throw new RegistrationLookupError("RDAP request failed", "network");
418
+ }
419
+ async function getBootstrap(url) {
420
+ const cached = bootstrapCache.get(url);
421
+ if (cached && cached.expiresAt > Date.now()) return cached.data;
422
+ const response = await fetchJson(url, MAX_BOOTSTRAP_BYTES);
423
+ const services = response.data.services;
424
+ if (!Array.isArray(services)) {
425
+ throw new RegistrationLookupError(
426
+ "IANA RDAP bootstrap response has no services array",
427
+ "invalid-response"
428
+ );
429
+ }
430
+ const data = { services };
431
+ bootstrapCache.set(url, { data, expiresAt: Date.now() + BOOTSTRAP_TTL_MS });
432
+ return data;
433
+ }
434
+ function bootstrapServices(data) {
435
+ const services = [];
436
+ for (const service of data.services) {
437
+ if (!Array.isArray(service) || service.length !== 2) continue;
438
+ const entries = stringArray(service[0]).map((entry) => entry.toLowerCase());
439
+ const urls = stringArray(service[1]).filter(
440
+ (url) => url.startsWith("https://")
441
+ );
442
+ if (entries.length > 0 && urls.length > 0) services.push({ entries, urls });
443
+ }
444
+ return services;
445
+ }
446
+ function domainServiceUrls(data, domain) {
447
+ let longestMatch = -1;
448
+ let urls = [];
449
+ for (const service of bootstrapServices(data)) {
450
+ for (const entry of service.entries) {
451
+ if (domain !== entry && !domain.endsWith(`.${entry}`)) continue;
452
+ const labels = entry.split(".").length;
453
+ if (labels > longestMatch) {
454
+ longestMatch = labels;
455
+ urls = [...service.urls];
456
+ } else if (labels === longestMatch) {
457
+ urls.push(...service.urls);
458
+ }
459
+ }
460
+ }
461
+ return [...new Set(urls)];
462
+ }
463
+ function ipServiceUrls(data, ip) {
464
+ const family = net.isIP(ip);
465
+ const type = family === 4 ? "ipv4" : "ipv6";
466
+ let longestPrefix = -1;
467
+ let urls = [];
468
+ for (const service of bootstrapServices(data)) {
469
+ for (const entry of service.entries) {
470
+ const [network, rawPrefix] = entry.split("/");
471
+ const prefix = Number(rawPrefix);
472
+ if (!network || !Number.isInteger(prefix) || net.isIP(network) !== family)
473
+ continue;
474
+ try {
475
+ const block = new net.BlockList();
476
+ block.addSubnet(network, prefix, type);
477
+ if (!block.check(ip, type)) continue;
478
+ } catch {
479
+ continue;
480
+ }
481
+ if (prefix > longestPrefix) {
482
+ longestPrefix = prefix;
483
+ urls = [...service.urls];
484
+ } else if (prefix === longestPrefix) {
485
+ urls.push(...service.urls);
486
+ }
487
+ }
488
+ }
489
+ return [...new Set(urls)];
490
+ }
491
+ async function queryFirstRdapService(baseUrls, path) {
492
+ let lastError;
493
+ for (const baseUrl of baseUrls) {
494
+ try {
495
+ const base = validateHttpsUrl(baseUrl);
496
+ if (!base.pathname.endsWith("/")) base.pathname += "/";
497
+ const queryUrl = new URL(path, base);
498
+ return await fetchJson(queryUrl.href, MAX_RDAP_BYTES);
499
+ } catch (error) {
500
+ if (error instanceof RegistrationLookupError && error.kind === "not-found") {
501
+ throw error;
502
+ }
503
+ lastError = error;
504
+ }
505
+ }
506
+ throw lastError ?? new RegistrationLookupError("No usable RDAP service", "unsupported");
507
+ }
508
+ function relatedRdapUrl(data) {
509
+ if (!Array.isArray(data.links)) return void 0;
510
+ for (const value of data.links) {
511
+ if (!isRecord(value) || value.rel !== "related") continue;
512
+ const href = stringValue(value.href);
513
+ const type = stringValue(value.type);
514
+ if (href && type?.toLowerCase().includes("application/rdap+json"))
515
+ return href;
516
+ }
517
+ return void 0;
518
+ }
519
+ function eventLines(data) {
520
+ if (!Array.isArray(data.events)) return [];
521
+ const labels = {
522
+ registration: "Registered",
523
+ expiration: "Expires",
524
+ "last changed": "Updated",
525
+ "last update of RDAP database": "RDAP database updated"
526
+ };
527
+ const lines = [];
528
+ for (const event of data.events) {
529
+ if (!isRecord(event)) continue;
530
+ const action = stringValue(event.eventAction);
531
+ const date = stringValue(event.eventDate);
532
+ if (action && date) lines.push(`${labels[action] ?? action}: ${date}`);
533
+ }
534
+ return lines;
535
+ }
536
+ function vcardFields(entity) {
537
+ const fields = /* @__PURE__ */ new Map();
538
+ if (!Array.isArray(entity.vcardArray) || !Array.isArray(entity.vcardArray[1]))
539
+ return fields;
540
+ for (const property of entity.vcardArray[1]) {
541
+ if (!Array.isArray(property) || property.length < 4) continue;
542
+ const name = stringValue(property[0])?.toLowerCase();
543
+ const value = stringValue(property[3]);
544
+ if (!name || !value) continue;
545
+ fields.set(name, [...fields.get(name) ?? [], value]);
546
+ }
547
+ return fields;
548
+ }
549
+ function entityLines(data) {
550
+ if (!Array.isArray(data.entities)) return [];
551
+ const lines = [];
552
+ const visit = (entity) => {
553
+ if (!isRecord(entity)) return;
554
+ const roles = stringArray(entity.roles);
555
+ const fields = vcardFields(entity);
556
+ const name = fields.get("fn")?.[0] ?? fields.get("org")?.[0];
557
+ const handle = stringValue(entity.handle);
558
+ const identity = name ?? handle;
559
+ if (identity) {
560
+ const publicIds = Array.isArray(entity.publicIds) ? entity.publicIds.map((value) => {
561
+ if (!isRecord(value)) return void 0;
562
+ const type = stringValue(value.type);
563
+ const identifier = stringValue(value.identifier);
564
+ return type && identifier ? `${type} ${identifier}` : void 0;
565
+ }).filter((value) => value !== void 0) : [];
566
+ const details = [
567
+ ...publicIds,
568
+ ...(fields.get("email") ?? []).map((value) => `email ${value}`),
569
+ ...(fields.get("tel") ?? []).map((value) => `telephone ${value}`)
570
+ ];
571
+ lines.push(
572
+ `Entity${roles.length > 0 ? ` (${roles.join(", ")})` : ""}: ${identity}${details.length > 0 ? `; ${details.join("; ")}` : ""}`
573
+ );
574
+ }
575
+ if (Array.isArray(entity.entities)) entity.entities.forEach(visit);
576
+ };
577
+ data.entities.forEach(visit);
578
+ return [...new Set(lines)];
579
+ }
580
+ function noticeTitles(data) {
581
+ if (!Array.isArray(data.notices)) return [];
582
+ return data.notices.map((notice) => isRecord(notice) ? stringValue(notice.title) : void 0).filter((title) => title !== void 0);
583
+ }
584
+ function formatDomainRdap(responses, requestedDomain, warnings) {
585
+ const data = Object.assign(
586
+ {},
587
+ ...responses.slice().reverse().map((response) => response.data)
588
+ );
589
+ data.entities = responses.flatMap(
590
+ (response) => Array.isArray(response.data.entities) ? response.data.entities : []
591
+ );
592
+ data.notices = responses.flatMap(
593
+ (response) => Array.isArray(response.data.notices) ? response.data.notices : []
594
+ );
595
+ data.events = responses.flatMap(
596
+ (response) => Array.isArray(response.data.events) ? response.data.events : []
597
+ );
598
+ const lines = [
599
+ "Protocol: RDAP",
600
+ ...responses.map((response) => `Source: ${response.source}`),
601
+ `Domain: ${stringValue(data.unicodeName) ?? stringValue(data.ldhName) ?? requestedDomain}`
602
+ ];
603
+ const handle = stringValue(data.handle);
604
+ if (handle) lines.push(`Registry ID: ${handle}`);
605
+ lines.push(...eventLines(data));
606
+ const statuses = stringArray(data.status);
607
+ if (statuses.length > 0) lines.push(`Status: ${statuses.join(", ")}`);
608
+ if (Array.isArray(data.nameservers)) {
609
+ const nameservers = data.nameservers.map(
610
+ (value) => isRecord(value) ? stringValue(value.unicodeName) ?? stringValue(value.ldhName) : void 0
611
+ ).filter((value) => value !== void 0);
612
+ if (nameservers.length > 0)
613
+ lines.push(`Nameservers: ${nameservers.join(", ")}`);
614
+ }
615
+ if (isRecord(data.secureDNS)) {
616
+ const signed = data.secureDNS.delegationSigned;
617
+ if (typeof signed === "boolean")
618
+ lines.push(`DNSSEC: ${signed ? "signed" : "unsigned"}`);
619
+ }
620
+ lines.push(...entityLines(data));
621
+ const notices = noticeTitles(data);
622
+ if (notices.length > 0) lines.push(`Notices: ${notices.join("; ")}`);
623
+ lines.push(...warnings.map((warning) => `Warning: ${warning}`));
624
+ return lines.join("\n");
625
+ }
626
+ function formatIpRdap(response, ip) {
627
+ const { data } = response;
628
+ const lines = ["Protocol: RDAP", `Source: ${response.source}`, `IP: ${ip}`];
629
+ for (const [label, key] of [
630
+ ["Network", "name"],
631
+ ["Handle", "handle"],
632
+ ["Start address", "startAddress"],
633
+ ["End address", "endAddress"],
634
+ ["IP version", "ipVersion"],
635
+ ["Type", "type"],
636
+ ["Country", "country"],
637
+ ["Parent handle", "parentHandle"]
638
+ ]) {
639
+ const value = stringValue(data[key]);
640
+ if (value) lines.push(`${label}: ${value}`);
641
+ }
642
+ const statuses = stringArray(data.status);
643
+ if (statuses.length > 0) lines.push(`Status: ${statuses.join(", ")}`);
644
+ lines.push(...eventLines(data), ...entityLines(data));
645
+ const notices = noticeTitles(data);
646
+ if (notices.length > 0) lines.push(`Notices: ${notices.join("; ")}`);
647
+ return lines.join("\n");
648
+ }
649
+ function normaliseRegistrationDomain(domain) {
650
+ const candidate = domain.trim().replace(/\.$/, "");
651
+ const containsControlCharacter = [...candidate].some((character) => {
652
+ const codePoint = character.codePointAt(0) ?? 0;
653
+ return codePoint <= 31 || codePoint === 127;
654
+ });
655
+ if (!candidate || containsControlCharacter) {
656
+ throw new Error("Invalid domain");
657
+ }
658
+ const ascii = domainToASCII(candidate).toLowerCase();
659
+ if (!ascii || ascii.length > 253) throw new Error("Invalid domain");
660
+ const labels = ascii.split(".");
661
+ if (labels.some(
662
+ (label) => label.length === 0 || label.length > 63 || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)
663
+ )) {
664
+ throw new Error("Invalid domain");
665
+ }
666
+ return ascii;
667
+ }
668
+ function parseWhoisEndpoint(value, allowWebScheme = false) {
669
+ const candidate = value.includes("://") ? value : `whois://${value}`;
670
+ let url;
671
+ try {
672
+ url = new URL(candidate);
673
+ } catch {
674
+ throw new RegistrationLookupError(
675
+ `Invalid WHOIS referral: ${value}`,
676
+ "invalid-response",
677
+ void 0,
678
+ "REFERRAL_REJECTED"
679
+ );
680
+ }
681
+ const webScheme = url.protocol === "http:" || url.protocol === "https:";
682
+ const port = webScheme ? 43 : url.port ? Number(url.port) : 43;
683
+ if (url.protocol !== "whois:" && !(allowWebScheme && webScheme) || !url.hostname || url.username || url.password || url.pathname && url.pathname !== "/" || url.search || url.hash || !webScheme && url.port && url.port !== "43") {
684
+ throw new RegistrationLookupError(
685
+ `Refusing unsafe WHOIS referral: ${value}`,
686
+ "security",
687
+ void 0,
688
+ "REFERRAL_REJECTED"
689
+ );
690
+ }
691
+ return { host: url.hostname.replace(/^\[|\]$/g, "").toLowerCase(), port };
692
+ }
693
+ function whoisReferral(response) {
694
+ for (const [pattern, allowWebScheme] of [
695
+ [/^refer:[ \t]*(\S+)/im, false],
696
+ [/^whois:[ \t]*(\S+)/im, false],
697
+ [/^Registrar WHOIS Server:[ \t]*(\S+)/im, true],
698
+ [/^ReferralServer:[ \t]*(\S+)/im, false]
699
+ ]) {
700
+ const match = response.match(pattern);
701
+ if (match?.[1]) return parseWhoisEndpoint(match[1].trim(), allowWebScheme);
702
+ }
703
+ return void 0;
704
+ }
705
+ function inspectWhoisResponse(response) {
706
+ const hasRegistrationData = /^(?:Domain Name|Domain|domain|NetRange|CIDR|inetnum|network|route):[ \t]*\S+/imu.test(
707
+ response
708
+ );
709
+ if (!hasRegistrationData && /(?:rate limit|query limit|quota exceeded|too many quer(?:y|ies)|maximum number of quer(?:y|ies)|try again later)/iu.test(
710
+ response
711
+ )) {
712
+ throw new RegistrationLookupError(
713
+ "WHOIS service rate limited the request",
714
+ "rate-limited",
715
+ void 0,
716
+ "WHOIS_RATE_LIMITED"
717
+ );
718
+ }
719
+ if (!hasRegistrationData && /(?:whois service (?:has been |is )?(?:retired|discontinued)|whois is no longer available)/iu.test(
720
+ response
721
+ )) {
722
+ throw new RegistrationLookupError(
723
+ "WHOIS service reports that it has been retired",
724
+ "unsupported",
725
+ void 0,
726
+ "WHOIS_SERVICE_RETIRED"
727
+ );
728
+ }
729
+ if (!hasRegistrationData && /(?:web[- ]based whois|web whois|visit .{0,80}whois)/isu.test(response)) {
730
+ throw new RegistrationLookupError(
731
+ "Registry directs queries to a web-based WHOIS service",
732
+ "unsupported",
733
+ void 0,
734
+ "WHOIS_WEB_ONLY"
735
+ );
736
+ }
737
+ if (!hasRegistrationData && /(?:invalid query|malformed query|unsupported query|syntax error|^usage:)/imu.test(
738
+ response
739
+ )) {
740
+ throw new RegistrationLookupError(
741
+ "WHOIS service rejected the query syntax",
742
+ "invalid-response",
743
+ void 0,
744
+ "WHOIS_UNSUPPORTED_RESPONSE"
745
+ );
746
+ }
747
+ const warnings = [];
748
+ if (response.includes("\uFFFD")) {
749
+ warnings.push({
750
+ reason: "WHOIS_INVALID_ENCODING",
751
+ detail: "WHOIS response contains bytes that are not valid UTF-8"
752
+ });
753
+ }
754
+ return {
755
+ notFound: !hasRegistrationData && /(?:no match for|not found|no entries found|no object found)/iu.test(
756
+ response
757
+ ),
758
+ warnings
759
+ };
760
+ }
761
+ function whoisQueryValue(host, query) {
762
+ if (host === "whois.jprs.jp" && !query.endsWith("/e")) return `${query}/e`;
763
+ if (host === "whois.denic.de") return `-T dn,ace ${query}`;
764
+ return query;
765
+ }
766
+ async function whoisQuery(endpoint, query) {
767
+ const addresses = await resolvePublicHostname(endpoint.host, true);
768
+ return await new Promise((resolve, reject) => {
769
+ let settled = false;
770
+ let total = 0;
771
+ const chunks = [];
772
+ const timeoutError = () => new RegistrationLookupError(
773
+ "WHOIS query timed out",
774
+ "network",
775
+ void 0,
776
+ "WHOIS_TIMEOUT"
777
+ );
778
+ const socket = net.createConnection(
779
+ { ...endpoint, lookup: pinnedLookup(addresses) },
780
+ () => {
781
+ socket.write(`${whoisQueryValue(endpoint.host, query)}\r
782
+ `);
783
+ }
784
+ );
785
+ const deadline = setTimeout(
786
+ () => socket.destroy(timeoutError()),
787
+ REQUEST_TIMEOUT_MS
788
+ );
789
+ const finish = (error) => {
790
+ if (settled) return;
791
+ settled = true;
792
+ clearTimeout(deadline);
793
+ if (error) reject(error);
794
+ else resolve(Buffer.concat(chunks).toString("utf-8"));
795
+ };
796
+ socket.on("data", (chunk) => {
797
+ total += chunk.byteLength;
798
+ if (total > MAX_WHOIS_BYTES) {
799
+ socket.destroy(
800
+ new RegistrationLookupError(
801
+ `WHOIS response exceeds ${MAX_WHOIS_BYTES} bytes`,
802
+ "invalid-response",
803
+ void 0,
804
+ "RESPONSE_TOO_LARGE"
805
+ )
806
+ );
807
+ return;
808
+ }
809
+ chunks.push(chunk);
810
+ });
811
+ socket.on("end", () => finish());
812
+ socket.on("error", (error) => finish(error));
813
+ socket.setTimeout(REQUEST_TIMEOUT_MS, () => {
814
+ socket.destroy(timeoutError());
815
+ });
816
+ });
817
+ }
818
+ async function legacyWhois(query) {
819
+ let endpoint = { host: "whois.iana.org", port: 43 };
820
+ const visited = /* @__PURE__ */ new Set();
821
+ let response = "";
822
+ const warnings = [];
823
+ for (let index = 0; index < MAX_WHOIS_QUERIES; index += 1) {
824
+ const key = `${endpoint.host}:${endpoint.port}`;
825
+ if (visited.has(key)) {
826
+ throw new RegistrationLookupError(
827
+ "WHOIS referral loop detected",
828
+ "invalid-response",
829
+ `whois://${endpoint.host}:43`,
830
+ "REFERRAL_LOOP"
831
+ );
832
+ }
833
+ visited.add(key);
834
+ try {
835
+ response = (await whoisQuery(endpoint, query)).trim();
836
+ } catch (error) {
837
+ throw new RegistrationLookupError(
838
+ errorMessage(error),
839
+ error instanceof RegistrationLookupError ? error.kind : "network",
840
+ `whois://${endpoint.host}:43`,
841
+ reasonForError("WHOIS", error)
842
+ );
843
+ }
844
+ let referral;
845
+ try {
846
+ referral = whoisReferral(response);
847
+ } catch (error) {
848
+ if (index === 0) throw error;
849
+ warnings.push({
850
+ reason: reasonForError("WHOIS", error),
851
+ detail: errorMessage(error)
852
+ });
853
+ break;
854
+ }
855
+ if (!referral) {
856
+ if (index === 0) {
857
+ throw new RegistrationLookupError(
858
+ "IANA does not advertise a WHOIS service for this query",
859
+ "unsupported",
860
+ "whois://whois.iana.org:43",
861
+ "WHOIS_NOT_ADVERTISED"
862
+ );
863
+ }
864
+ break;
865
+ }
866
+ const referralKey = `${referral.host}:${referral.port}`;
867
+ if (visited.has(referralKey)) {
868
+ if (referralKey === key) break;
869
+ throw new RegistrationLookupError(
870
+ "WHOIS referral loop detected",
871
+ "invalid-response",
872
+ `whois://${endpoint.host}:43`,
873
+ "REFERRAL_LOOP"
874
+ );
875
+ }
876
+ if (index === MAX_WHOIS_QUERIES - 1) {
877
+ warnings.push({
878
+ reason: "REFERRAL_LIMIT_REACHED",
879
+ detail: `WHOIS referral limit of ${MAX_WHOIS_QUERIES} queries reached`
880
+ });
881
+ break;
882
+ }
883
+ try {
884
+ await resolvePublicHostname(referral.host, true);
885
+ } catch (error) {
886
+ if (index === 0) throw error;
887
+ warnings.push({
888
+ reason: reasonForError("WHOIS", error),
889
+ detail: errorMessage(error)
890
+ });
891
+ break;
892
+ }
893
+ endpoint = referral;
894
+ }
895
+ if (!response)
896
+ throw new RegistrationLookupError(
897
+ "No WHOIS data returned",
898
+ "network",
899
+ `whois://${endpoint.host}:43`,
900
+ "WHOIS_EMPTY_RESPONSE"
901
+ );
902
+ let inspection;
903
+ try {
904
+ inspection = inspectWhoisResponse(response);
905
+ } catch (error) {
906
+ throw new RegistrationLookupError(
907
+ errorMessage(error),
908
+ error instanceof RegistrationLookupError ? error.kind : "invalid-response",
909
+ `whois://${endpoint.host}:43`,
910
+ reasonForError("WHOIS", error)
911
+ );
912
+ }
913
+ warnings.push(...inspection.warnings);
914
+ return [
915
+ "Protocol: WHOIS",
916
+ `Source: whois://${endpoint.host}:43`,
917
+ ...inspection.notFound ? ["Status: not registered or not found", "Reason: WHOIS_NOT_FOUND"] : [],
918
+ ...warnings.map((warning) => `Warning: ${warningText(warning)}`),
919
+ "",
920
+ response
921
+ ].join("\n");
922
+ }
923
+ function fallbackWarning(error) {
924
+ return `${reasonForError("RDAP", error)}: ${errorMessage(error)}; used WHOIS fallback`;
925
+ }
926
+ async function domainRegistrationLookupResult(domain) {
927
+ const normalised = normaliseRegistrationDomain(domain);
928
+ let rdapError;
929
+ try {
930
+ const bootstrap = await getBootstrap(RDAP_DNS_BOOTSTRAP);
931
+ const urls = domainServiceUrls(bootstrap, normalised);
932
+ if (urls.length > 0) {
933
+ try {
934
+ const registry = await queryFirstRdapService(
935
+ urls,
936
+ `domain/${encodeURIComponent(normalised)}`
937
+ );
938
+ const responses = [registry];
939
+ const warnings = [];
940
+ const related = relatedRdapUrl(registry.data);
941
+ if (related) {
942
+ try {
943
+ responses.push(await fetchJson(related, MAX_RDAP_BYTES));
944
+ } catch (error) {
945
+ warnings.push(
946
+ `${reasonForError("RDAP", error)}: registrar RDAP lookup failed: ${errorMessage(error)}`
947
+ );
948
+ }
949
+ }
950
+ return {
951
+ text: formatDomainRdap(responses, normalised, warnings),
952
+ failed: false
953
+ };
954
+ } catch (error) {
955
+ if (error instanceof RegistrationLookupError && error.kind === "not-found") {
956
+ return {
957
+ text: `Protocol: RDAP
958
+ Source: ${error.source ?? "authoritative RDAP service"}
959
+ Domain: ${normalised}
960
+ Status: not registered or not found
961
+ Reason: RDAP_NOT_FOUND`,
962
+ failed: false
963
+ };
964
+ }
965
+ rdapError = error;
966
+ }
967
+ } else {
968
+ rdapError = new RegistrationLookupError(
969
+ "IANA has no RDAP bootstrap service for this TLD",
970
+ "unsupported"
971
+ );
972
+ }
973
+ } catch (error) {
974
+ rdapError = error;
975
+ }
976
+ try {
977
+ const result = await legacyWhois(normalised);
978
+ return {
979
+ text: rdapError ? `${result}
980
+
981
+ Warning: ${fallbackWarning(rdapError)}` : result,
982
+ failed: false
983
+ };
984
+ } catch (whoisError) {
985
+ if (rdapError) {
986
+ return {
987
+ text: formatLookupFailure("Domain", normalised, rdapError, whoisError),
988
+ failed: true
989
+ };
990
+ }
991
+ return {
992
+ text: formatLookupFailure(
993
+ "Domain",
994
+ normalised,
995
+ new RegistrationLookupError(
996
+ "RDAP was not attempted",
997
+ "unsupported",
998
+ void 0,
999
+ "RDAP_NOT_AVAILABLE"
1000
+ ),
1001
+ whoisError
1002
+ ),
1003
+ failed: true
1004
+ };
1005
+ }
1006
+ }
1007
+ async function ipRegistrationLookupResult(ip) {
1008
+ const family = net.isIP(ip);
1009
+ if (family === 0) throw new Error("Invalid IP address");
1010
+ let rdapError;
1011
+ try {
1012
+ const bootstrap = await getBootstrap(
1013
+ family === 4 ? RDAP_IPV4_BOOTSTRAP : RDAP_IPV6_BOOTSTRAP
1014
+ );
1015
+ const urls = ipServiceUrls(bootstrap, ip);
1016
+ if (urls.length > 0) {
1017
+ try {
1018
+ const response = await queryFirstRdapService(
1019
+ urls,
1020
+ `ip/${encodeURIComponent(ip)}`
1021
+ );
1022
+ return { text: formatIpRdap(response, ip), failed: false };
1023
+ } catch (error) {
1024
+ if (error instanceof RegistrationLookupError && error.kind === "not-found") {
1025
+ return {
1026
+ text: `Protocol: RDAP
1027
+ Source: ${error.source ?? "authoritative RDAP service"}
1028
+ IP: ${ip}
1029
+ Status: registration data not found
1030
+ Reason: RDAP_NOT_FOUND`,
1031
+ failed: false
1032
+ };
1033
+ }
1034
+ rdapError = error;
1035
+ }
1036
+ } else {
1037
+ rdapError = new RegistrationLookupError(
1038
+ "IANA has no RDAP bootstrap service for this address",
1039
+ "unsupported"
1040
+ );
1041
+ }
1042
+ } catch (error) {
1043
+ rdapError = error;
1044
+ }
1045
+ try {
1046
+ const result = await legacyWhois(ip);
1047
+ return {
1048
+ text: rdapError ? `${result}
1049
+
1050
+ Warning: ${fallbackWarning(rdapError)}` : result,
1051
+ failed: false
1052
+ };
1053
+ } catch (whoisError) {
1054
+ if (rdapError) {
1055
+ return {
1056
+ text: formatLookupFailure("IP", ip, rdapError, whoisError),
1057
+ failed: true
1058
+ };
1059
+ }
1060
+ return {
1061
+ text: formatLookupFailure(
1062
+ "IP",
1063
+ ip,
1064
+ new RegistrationLookupError(
1065
+ "RDAP was not attempted",
1066
+ "unsupported",
1067
+ void 0,
1068
+ "RDAP_NOT_AVAILABLE"
1069
+ ),
1070
+ whoisError
1071
+ ),
1072
+ failed: true
1073
+ };
1074
+ }
1075
+ }
1076
+
1077
+ // src/dns.ts
1078
+ function configureDnsServers(env = process.env) {
1079
+ const raw = env.DNS_SERVERS;
1080
+ if (!raw) return;
1081
+ const servers = raw.split(",").map((s) => s.trim()).filter((s) => net2.isIP(s) !== 0);
1082
+ if (servers.length > 0) {
1083
+ setServers(servers);
1084
+ }
1085
+ }
1086
+ function mapDnsError(error, type, target) {
1087
+ const code = error.code;
1088
+ switch (code) {
1089
+ case "ENODATA":
1090
+ return `No ${type} records found for ${target}.`;
1091
+ case "ENOTFOUND":
1092
+ return `Domain not found: ${target}`;
1093
+ case "EREFUSED":
1094
+ return `DNS server refused the ${type} query for ${target}. The local resolver may not support this record type; set DNS_SERVERS to use a different resolver.`;
1095
+ case "ETIMEOUT":
1096
+ return `DNS query for ${target} (${type}) timed out.`;
1097
+ default:
1098
+ return `DNS lookup failed for ${target} (${type}): ${code ?? "unknown error"}`;
1099
+ }
1100
+ }
1101
+ var DOMAIN_RE = /^(?=.{1,253}$)[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9_]([a-zA-Z0-9_-]{0,61}[a-zA-Z0-9])?)*\.?$/;
1102
+ function assertValidDomain(domain) {
1103
+ if (!DOMAIN_RE.test(domain)) {
1104
+ throw new Error("Invalid domain");
1105
+ }
1106
+ }
1107
+ async function dnsLookup(domain, type) {
1108
+ assertValidDomain(domain);
1109
+ let records;
1110
+ try {
1111
+ records = await dns2.resolve(domain, type);
1112
+ } catch (error) {
1113
+ return mapDnsError(error, type, domain);
1114
+ }
1115
+ switch (type) {
1116
+ case "MX": {
1117
+ const mx = records;
1118
+ if (mx.length === 0) return "No MX records found.";
1119
+ return mx.sort((a, b) => a.priority - b.priority).map((r) => `${r.priority} ${r.exchange}`).join("\n");
1120
+ }
1121
+ case "TXT": {
1122
+ const txt = records;
1123
+ if (txt.length === 0) return "No TXT records found.";
1124
+ return txt.map((entries) => entries.join("")).join("\n");
1125
+ }
1126
+ case "CAA": {
1127
+ const caa = records;
1128
+ if (caa.length === 0) return "No CAA records found.";
1129
+ const formatted = caa.flatMap(
1130
+ ({ critical, ...tags }) => Object.entries(tags).filter(
1131
+ (entry) => entry[0] !== "type" && typeof entry[1] === "string"
1132
+ ).map(([tag, value]) => `${critical} ${tag} ${JSON.stringify(value)}`)
1133
+ );
1134
+ return formatted.length > 0 ? formatted.join("\n") : "No CAA records found.";
1135
+ }
1136
+ case "SOA": {
1137
+ const soa = records;
1138
+ const s = soa[0];
1139
+ if (!s) return "No SOA record found.";
1140
+ return [
1141
+ `nsname: ${s.nsname}`,
1142
+ `hostmaster: ${s.hostmaster}`,
1143
+ `serial: ${s.serial}`,
1144
+ `refresh: ${s.refresh}`,
1145
+ `retry: ${s.retry}`,
1146
+ `expire: ${s.expire}`,
1147
+ `minttl: ${s.minttl}`
1148
+ ].join("\n");
1149
+ }
1150
+ case "SRV": {
1151
+ const srv = records;
1152
+ if (srv.length === 0) return "No SRV records found.";
1153
+ return srv.map((r) => `${r.priority} ${r.weight} ${r.port} ${r.name}`).join("\n");
1154
+ }
1155
+ default: {
1156
+ const arr = records;
1157
+ if (arr.length === 0) return `No ${type} records found.`;
1158
+ return arr.join("\n");
1159
+ }
1160
+ }
1161
+ }
1162
+ async function reverseDns(ip) {
1163
+ if (net2.isIP(ip) === 0) {
1164
+ throw new Error("Invalid IP address");
1165
+ }
1166
+ let hostnames;
1167
+ try {
1168
+ hostnames = await dns2.reverse(ip);
1169
+ } catch (error) {
1170
+ return mapDnsError(error, "PTR", ip);
1171
+ }
1172
+ return hostnames.length > 0 ? hostnames.join("\n") : "No PTR records found.";
1173
+ }
1174
+ async function resolveAll(domain) {
1175
+ assertValidDomain(domain);
1176
+ const types = [
1177
+ { name: "A", type: "A" },
1178
+ { name: "AAAA", type: "AAAA" },
1179
+ { name: "MX", type: "MX" },
1180
+ { name: "TXT", type: "TXT" },
1181
+ { name: "CAA", type: "CAA" },
1182
+ { name: "NS", type: "NS" },
1183
+ { name: "CNAME", type: "CNAME" }
1184
+ ];
1185
+ const results = [];
1186
+ for (const { name, type } of types) {
1187
+ try {
1188
+ const records = await dnsLookup(domain, type);
1189
+ results.push(`=== ${name} ===
1190
+ ${records}`);
1191
+ } catch {
1192
+ results.push(`=== ${name} ===
1193
+ (no records or error)`);
1194
+ }
1195
+ }
1196
+ return results.join("\n\n");
1197
+ }
1198
+ async function checkNameservers(domain) {
1199
+ assertValidDomain(domain);
1200
+ let nameservers;
1201
+ try {
1202
+ nameservers = await dns2.resolveNs(domain);
1203
+ } catch (error) {
1204
+ return mapDnsError(error, "NS", domain);
1205
+ }
1206
+ return nameservers.length > 0 ? nameservers.join("\n") : "No nameserver records found.";
1207
+ }
1208
+ async function whoisLookupResult(domain) {
1209
+ return await domainRegistrationLookupResult(domain);
1210
+ }
1211
+ async function ipWhoisResult(ip) {
1212
+ return await ipRegistrationLookupResult(ip);
1213
+ }
1214
+
1215
+ // src/server.ts
1216
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1217
+ import { z } from "zod";
1218
+
1219
+ // package.json
1220
+ var package_default = {
1221
+ name: "@paulgit/mcp-dns-tools",
1222
+ mcpName: "it.paulg/dns-tools",
1223
+ version: "2.2.0",
1224
+ description: "MCP server for DNS lookups, including CAA checks, reverse DNS, and RDAP-first domain or IP registration data with WHOIS fallback; zero auth, zero config",
1225
+ type: "module",
1226
+ publishConfig: {
1227
+ access: "public"
1228
+ },
1229
+ engines: {
1230
+ node: ">=24"
1231
+ },
1232
+ bin: {
1233
+ "mcp-dns-tools": "./dist/index.js"
1234
+ },
1235
+ files: [
1236
+ "dist",
1237
+ "skills"
1238
+ ],
1239
+ scripts: {
1240
+ build: "tsup",
1241
+ typecheck: "tsc --noEmit",
1242
+ test: "vitest run",
1243
+ "test:watch": "vitest",
1244
+ "test:coverage": "vitest run --coverage",
1245
+ lint: "eslint . && prettier --check .",
1246
+ format: "prettier --write .",
1247
+ "audit:deps": "npm audit --omit=dev --audit-level=high",
1248
+ prepack: "npm run build",
1249
+ prepublishOnly: "npm run lint && npm run typecheck && npm test",
1250
+ prepare: "husky"
1251
+ },
1252
+ keywords: [
1253
+ "mcp",
1254
+ "mcp-server",
1255
+ "model-context-protocol",
1256
+ "dns",
1257
+ "whois",
1258
+ "domain",
1259
+ "network",
1260
+ "lookup",
1261
+ "nameserver",
1262
+ "caa",
1263
+ "tls",
1264
+ "certificate-authority",
1265
+ "ai",
1266
+ "llm",
1267
+ "claude",
1268
+ "cursor"
1269
+ ],
1270
+ author: "Paul Git (https://code.paulg.it/paulgit)",
1271
+ license: "MIT",
1272
+ repository: {
1273
+ type: "git",
1274
+ url: "https://code.paulg.it/paulgit/mcp-dns-tools.git"
1275
+ },
1276
+ bugs: {
1277
+ url: "https://code.paulg.it/paulgit/mcp-dns-tools/issues"
1278
+ },
1279
+ homepage: "https://code.paulg.it/paulgit/mcp-dns-tools#readme",
1280
+ dependencies: {
1281
+ "@modelcontextprotocol/sdk": "^1.26.0",
1282
+ undici: "^8.10.0",
1283
+ zod: "^3.25.0"
1284
+ },
1285
+ devDependencies: {
1286
+ "@eslint/js": "^9.0.0",
1287
+ "@types/node": "^22.0.0",
1288
+ eslint: "^9.0.0",
1289
+ "eslint-config-prettier": "^10.0.0",
1290
+ husky: "^9.0.0",
1291
+ "lint-staged": "^15.0.0",
1292
+ prettier: "^3.0.0",
1293
+ tsup: "^8.0.0",
1294
+ typescript: "^5.7.0",
1295
+ "typescript-eslint": "^8.0.0",
1296
+ vitest: "^3.2.0"
1297
+ },
1298
+ "lint-staged": {
1299
+ "*.{ts,tsx,js}": [
1300
+ "eslint --fix",
1301
+ "prettier --write"
1302
+ ],
1303
+ "*.{json,md,yml,yaml}": [
1304
+ "prettier --write"
1305
+ ]
1306
+ }
1307
+ };
1308
+
1309
+ // src/server.ts
1310
+ var recordTypeSchema = z.enum(["A", "AAAA", "MX", "TXT", "CNAME", "NS", "SOA", "SRV", "CAA", "PTR"]).default("A");
1311
+ var readOnlyAnnotations = {
1312
+ readOnlyHint: true,
1313
+ destructiveHint: false
1314
+ };
1315
+ function createMcpServer() {
1316
+ const server = new McpServer(
1317
+ {
1318
+ name: "mcp-dns-tools",
1319
+ version: package_default.version
1320
+ },
1321
+ {
1322
+ instructions: "When the user names one DNS record type, call dns_lookup and pass that type, except for NS or nameserver-only requests, which must use check_nameservers. For CAA requests, call dns_lookup with type CAA. Use resolve_all only when the user asks for all records, multiple record types, a DNS overview, or a general investigation. Never use resolve_all for one named record type. Use whois for domain registration, registrar, registration status, or registration-date questions. Use ip_whois for IP allocation and network ownership questions."
1323
+ }
1324
+ );
1325
+ server.registerTool(
1326
+ "dns_lookup",
1327
+ {
1328
+ title: "DNS record lookup",
1329
+ description: "Use this when the user requests one specific DNS record type, except for NS or nameserver requests, which must use check_nameservers. For CAA or certificate authority issuance questions, call this tool with type CAA. Supports A, AAAA, MX, TXT, CNAME, SOA, SRV, CAA, PTR, and NS for backwards compatibility. Do not use resolve_all when the user names one record type.",
1330
+ inputSchema: {
1331
+ domain: z.string().describe("Domain name to look up (e.g. example.com)"),
1332
+ type: recordTypeSchema.describe(
1333
+ "Specific record type to return: A, AAAA, MX, TXT, CNAME, NS, SOA, SRV, CAA, or PTR. Use CAA for certificate authority issuance policy."
1334
+ )
1335
+ },
1336
+ annotations: readOnlyAnnotations
1337
+ },
1338
+ async ({ domain, type }) => {
1339
+ const result = await dnsLookup(domain, type);
1340
+ return { content: [{ type: "text", text: result }] };
1341
+ }
1342
+ );
1343
+ server.registerTool(
1344
+ "reverse_dns",
1345
+ {
1346
+ title: "Reverse DNS lookup",
1347
+ description: "Perform a reverse DNS (PTR) lookup on an IP address.",
1348
+ inputSchema: {
1349
+ ip: z.string().describe("IP address (e.g. 8.8.8.8)")
1350
+ },
1351
+ annotations: readOnlyAnnotations
1352
+ },
1353
+ async ({ ip }) => {
1354
+ const result = await reverseDns(ip);
1355
+ return { content: [{ type: "text", text: result }] };
1356
+ }
1357
+ );
1358
+ server.registerTool(
1359
+ "resolve_all",
1360
+ {
1361
+ title: "DNS overview",
1362
+ description: "Use this only when the user requests all common DNS records, multiple record types, a DNS overview, or a general domain investigation. Returns A, AAAA, MX, TXT, CAA, NS, and CNAME. Do not use this for one named record type.",
1363
+ inputSchema: {
1364
+ domain: z.string().describe("Domain name to resolve (e.g. example.com)")
1365
+ },
1366
+ annotations: readOnlyAnnotations
1367
+ },
1368
+ async ({ domain }) => {
1369
+ const result = await resolveAll(domain);
1370
+ return { content: [{ type: "text", text: result }] };
1371
+ }
1372
+ );
1373
+ server.registerTool(
1374
+ "check_nameservers",
1375
+ {
1376
+ title: "Nameserver lookup",
1377
+ description: "Use this whenever the user requests NS records, nameservers, authoritative nameservers, or domain delegation nameservers.",
1378
+ inputSchema: {
1379
+ domain: z.string().describe("Domain name (e.g. example.com)")
1380
+ },
1381
+ annotations: readOnlyAnnotations
1382
+ },
1383
+ async ({ domain }) => {
1384
+ const result = await checkNameservers(domain);
1385
+ return { content: [{ type: "text", text: result }] };
1386
+ }
1387
+ );
1388
+ server.registerTool(
1389
+ "whois",
1390
+ {
1391
+ title: "Domain registration lookup",
1392
+ description: "Query authoritative domain registration data, including registrar, status, dates, nameservers, and DNSSEC. Uses IANA RDAP discovery over HTTPS and follows a registrar RDAP link when available, with bounded legacy WHOIS fallback for TLDs without working RDAP. Supports Unicode domain names. Failures identify each attempted protocol, endpoint, stable reason code, detail, and suggested next action.",
1393
+ inputSchema: {
1394
+ domain: z.string().describe("Domain name for registration lookup (e.g. example.com)")
1395
+ },
1396
+ annotations: readOnlyAnnotations
1397
+ },
1398
+ async ({ domain }) => {
1399
+ const result = await whoisLookupResult(domain);
1400
+ return {
1401
+ content: [{ type: "text", text: result.text }],
1402
+ isError: result.failed
1403
+ };
1404
+ }
1405
+ );
1406
+ server.registerTool(
1407
+ "ip_whois",
1408
+ {
1409
+ title: "IP registration lookup",
1410
+ description: "Query authoritative IPv4 or IPv6 allocation and network ownership data. Uses IANA RDAP bootstrap discovery over HTTPS, with bounded legacy WHOIS fallback to the appropriate Regional Internet Registry. Failures identify each attempted protocol, endpoint, stable reason code, detail, and suggested next action.",
1411
+ inputSchema: {
1412
+ ip: z.string().describe("IP address (e.g. 8.8.8.8 or 2001:4860:4860::8888)")
1413
+ },
1414
+ annotations: readOnlyAnnotations
1415
+ },
1416
+ async ({ ip }) => {
1417
+ const result = await ipWhoisResult(ip);
1418
+ return {
1419
+ content: [{ type: "text", text: result.text }],
1420
+ isError: result.failed
1421
+ };
1422
+ }
1423
+ );
1424
+ return server;
1425
+ }
1426
+
1427
+ // src/index.ts
1428
+ configureDnsServers();
1429
+ async function main() {
1430
+ parseCliArgs(process.argv.slice(2));
1431
+ const server = createMcpServer();
1432
+ const transport = new StdioServerTransport();
1433
+ await server.connect(transport);
1434
+ }
1435
+ main().catch((error) => {
1436
+ console.error("Fatal error:", error);
1437
+ process.exit(1);
1438
+ });
1439
+ //# sourceMappingURL=index.js.map