@cyanheads/whois-mcp-server 0.1.1
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/AGENTS.md +349 -0
- package/CLAUDE.md +349 -0
- package/Dockerfile +99 -0
- package/LICENSE +201 -0
- package/README.md +316 -0
- package/changelog/0.1.x/0.1.1.md +32 -0
- package/changelog/template.md +127 -0
- package/dist/config/server-config.d.ts +16 -0
- package/dist/config/server-config.d.ts.map +1 -0
- package/dist/config/server-config.js +30 -0
- package/dist/config/server-config.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-server/tools/definitions/_fqdn.d.ts +7 -0
- package/dist/mcp-server/tools/definitions/_fqdn.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/_fqdn.js +18 -0
- package/dist/mcp-server/tools/definitions/_fqdn.js.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-check-availability.tool.d.ts +26 -0
- package/dist/mcp-server/tools/definitions/whois-check-availability.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-check-availability.tool.js +102 -0
- package/dist/mcp-server/tools/definitions/whois-check-availability.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-get-dns.tool.d.ts +49 -0
- package/dist/mcp-server/tools/definitions/whois-get-dns.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-get-dns.tool.js +97 -0
- package/dist/mcp-server/tools/definitions/whois-get-dns.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.d.ts +41 -0
- package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.js +332 -0
- package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.d.ts +30 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.js +83 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.d.ts +40 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.js +124 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.js.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.d.ts +39 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.d.ts.map +1 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.js +111 -0
- package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.js.map +1 -0
- package/dist/services/doh/doh-service.d.ts +30 -0
- package/dist/services/doh/doh-service.d.ts.map +1 -0
- package/dist/services/doh/doh-service.js +114 -0
- package/dist/services/doh/doh-service.js.map +1 -0
- package/dist/services/doh/types.d.ts +45 -0
- package/dist/services/doh/types.d.ts.map +1 -0
- package/dist/services/doh/types.js +17 -0
- package/dist/services/doh/types.js.map +1 -0
- package/dist/services/rdap/rdap-service.d.ts +54 -0
- package/dist/services/rdap/rdap-service.d.ts.map +1 -0
- package/dist/services/rdap/rdap-service.js +609 -0
- package/dist/services/rdap/rdap-service.js.map +1 -0
- package/dist/services/rdap/types.d.ts +140 -0
- package/dist/services/rdap/types.d.ts.map +1 -0
- package/dist/services/rdap/types.js +6 -0
- package/dist/services/rdap/types.js.map +1 -0
- package/package.json +103 -0
- package/server.json +161 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview whois_check_availability tool — domain availability check via RDAP 404 semantics.
|
|
3
|
+
* @module mcp-server/tools/definitions/whois-check-availability.tool
|
|
4
|
+
*/
|
|
5
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { getRdapService } from '../../../services/rdap/rdap-service.js';
|
|
8
|
+
import { isValidFqdn } from './_fqdn.js';
|
|
9
|
+
export const whoisCheckAvailability = tool('whois_check_availability', {
|
|
10
|
+
title: 'Domain Availability Check',
|
|
11
|
+
description: 'Check whether a domain name is registered or available for registration. RDAP 404 = available — ' +
|
|
12
|
+
'this is the RDAP spec behavior, modeled as data (available: true) not an error. Returns available: false ' +
|
|
13
|
+
'with registrar and expiry_date when the domain is registered. When the TLD has no RDAP coverage, returns ' +
|
|
14
|
+
'available: null with rdap_coverage: false — availability cannot be determined. Designed for "can I register X" ' +
|
|
15
|
+
'and bulk name sweeps. For the full registration record use whois_lookup_domain.',
|
|
16
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
17
|
+
input: z.object({
|
|
18
|
+
domain: z
|
|
19
|
+
.string()
|
|
20
|
+
.describe('Fully qualified domain name to check (e.g., "myfuturename.com"). ' +
|
|
21
|
+
'Must be a valid FQDN — labels separated by dots.'),
|
|
22
|
+
}),
|
|
23
|
+
output: z.object({
|
|
24
|
+
domain: z.string().describe('Normalized domain name checked.'),
|
|
25
|
+
available: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.nullable()
|
|
28
|
+
.describe('True = available for registration (RDAP 404). False = registered. ' +
|
|
29
|
+
'Null = rdap_coverage is false — cannot determine availability for this TLD.'),
|
|
30
|
+
rdap_coverage: z.boolean().describe('True when a RDAP server was found for this TLD.'),
|
|
31
|
+
registrar: z.string().optional().describe('Registrar name when available: false.'),
|
|
32
|
+
expiry_date: z.string().optional().describe('Expiry date when available: false.'),
|
|
33
|
+
}),
|
|
34
|
+
errors: [
|
|
35
|
+
{
|
|
36
|
+
reason: 'invalid_domain',
|
|
37
|
+
code: JsonRpcErrorCode.InvalidParams,
|
|
38
|
+
when: 'Input is not a valid FQDN.',
|
|
39
|
+
recovery: 'Provide a valid fully-qualified domain name like "example.com" or "sub.example.org".',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
reason: 'rdap_no_coverage',
|
|
43
|
+
code: JsonRpcErrorCode.NotFound,
|
|
44
|
+
when: 'TLD has no RDAP server — available is null, cannot determine registration status.',
|
|
45
|
+
recovery: 'This TLD has no RDAP coverage; availability cannot be determined programmatically.',
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
async handler(input, ctx) {
|
|
49
|
+
if (!isValidFqdn(input.domain)) {
|
|
50
|
+
throw ctx.fail('invalid_domain', `"${input.domain}" is not a valid FQDN.`, {
|
|
51
|
+
...ctx.recoveryFor('invalid_domain'),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
ctx.log.info('RDAP availability check', { domain: input.domain });
|
|
55
|
+
const result = await getRdapService().checkAvailability(input.domain, ctx);
|
|
56
|
+
if (result.available === null) {
|
|
57
|
+
// No RDAP coverage — surface as data, not an error
|
|
58
|
+
return {
|
|
59
|
+
domain: input.domain.toLowerCase(),
|
|
60
|
+
available: null,
|
|
61
|
+
rdap_coverage: false,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (result.available === true) {
|
|
65
|
+
return {
|
|
66
|
+
domain: input.domain.toLowerCase(),
|
|
67
|
+
available: true,
|
|
68
|
+
rdap_coverage: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Registered
|
|
72
|
+
return {
|
|
73
|
+
domain: result.record.domain,
|
|
74
|
+
available: false,
|
|
75
|
+
rdap_coverage: true,
|
|
76
|
+
registrar: result.record.registrar,
|
|
77
|
+
expiry_date: result.record.expiry_date,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
format: (result) => {
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push(`# Domain: ${result.domain}`);
|
|
83
|
+
if (result.available === null) {
|
|
84
|
+
lines.push(`**Available:** Unknown (no RDAP coverage for this TLD)`);
|
|
85
|
+
lines.push(`**RDAP Coverage:** No`);
|
|
86
|
+
}
|
|
87
|
+
else if (result.available === true) {
|
|
88
|
+
lines.push(`**Available:** Yes — not currently registered`);
|
|
89
|
+
lines.push(`**RDAP Coverage:** Yes`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
lines.push(`**Available:** No — registered`);
|
|
93
|
+
lines.push(`**RDAP Coverage:** Yes`);
|
|
94
|
+
}
|
|
95
|
+
if (result.registrar)
|
|
96
|
+
lines.push(`**Registrar:** ${result.registrar}`);
|
|
97
|
+
if (result.expiry_date)
|
|
98
|
+
lines.push(`**Expires:** ${result.expiry_date}`);
|
|
99
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=whois-check-availability.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whois-check-availability.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/whois-check-availability.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview whois_get_dns tool — multi-type DNS record lookup via DNS-over-HTTPS.
|
|
3
|
+
* @module mcp-server/tools/definitions/whois-get-dns.tool
|
|
4
|
+
*/
|
|
5
|
+
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
export declare const whoisGetDns: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
8
|
+
domain: z.ZodString;
|
|
9
|
+
types: z.ZodDefault<z.ZodArray<z.ZodEnum<{
|
|
10
|
+
A: "A";
|
|
11
|
+
AAAA: "AAAA";
|
|
12
|
+
MX: "MX";
|
|
13
|
+
TXT: "TXT";
|
|
14
|
+
NS: "NS";
|
|
15
|
+
CNAME: "CNAME";
|
|
16
|
+
SOA: "SOA";
|
|
17
|
+
CAA: "CAA";
|
|
18
|
+
PTR: "PTR";
|
|
19
|
+
}>>>;
|
|
20
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
21
|
+
domain: z.ZodString;
|
|
22
|
+
nxdomain: z.ZodBoolean;
|
|
23
|
+
records: z.ZodArray<z.ZodObject<{
|
|
24
|
+
type: z.ZodEnum<{
|
|
25
|
+
A: "A";
|
|
26
|
+
AAAA: "AAAA";
|
|
27
|
+
MX: "MX";
|
|
28
|
+
TXT: "TXT";
|
|
29
|
+
NS: "NS";
|
|
30
|
+
CNAME: "CNAME";
|
|
31
|
+
SOA: "SOA";
|
|
32
|
+
CAA: "CAA";
|
|
33
|
+
PTR: "PTR";
|
|
34
|
+
}>;
|
|
35
|
+
name: z.ZodString;
|
|
36
|
+
ttl: z.ZodNumber;
|
|
37
|
+
data: z.ZodString;
|
|
38
|
+
}, z.core.$strip>>;
|
|
39
|
+
source: z.ZodEnum<{
|
|
40
|
+
cloudflare: "cloudflare";
|
|
41
|
+
google: "google";
|
|
42
|
+
}>;
|
|
43
|
+
}, z.core.$strip>, readonly [{
|
|
44
|
+
readonly reason: "invalid_domain";
|
|
45
|
+
readonly code: JsonRpcErrorCode.InvalidParams;
|
|
46
|
+
readonly when: "Input is not a valid FQDN.";
|
|
47
|
+
readonly recovery: "Provide a valid fully-qualified domain name like \"example.com\" or \"sub.example.org\".";
|
|
48
|
+
}], undefined>;
|
|
49
|
+
//# sourceMappingURL=whois-get-dns.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whois-get-dns.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/whois-get-dns.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAOjE,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgHtB,CAAC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview whois_get_dns tool — multi-type DNS record lookup via DNS-over-HTTPS.
|
|
3
|
+
* @module mcp-server/tools/definitions/whois-get-dns.tool
|
|
4
|
+
*/
|
|
5
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { getDohService } from '../../../services/doh/doh-service.js';
|
|
8
|
+
import { isValidFqdn } from './_fqdn.js';
|
|
9
|
+
const DNS_RECORD_TYPES = ['A', 'AAAA', 'MX', 'TXT', 'NS', 'CNAME', 'SOA', 'CAA', 'PTR'];
|
|
10
|
+
export const whoisGetDns = tool('whois_get_dns', {
|
|
11
|
+
title: 'DNS Record Lookup',
|
|
12
|
+
description: 'Fetch DNS records for a domain via DNS-over-HTTPS. Uses Cloudflare 1.1.1.1 as primary resolver ' +
|
|
13
|
+
'with Google 8.8.8.8 as fallback (and primary for CAA records, since Cloudflare returns raw hex ' +
|
|
14
|
+
'wire format for those). Supports A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR. Multiple types ' +
|
|
15
|
+
'are fetched in parallel. NXDOMAIN is returned as nxdomain: true in the result, not as an error — ' +
|
|
16
|
+
'it means the domain does not exist in DNS.',
|
|
17
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
18
|
+
input: z.object({
|
|
19
|
+
domain: z
|
|
20
|
+
.string()
|
|
21
|
+
.describe('Fully qualified domain name or hostname to query (e.g., "github.com", "mail.example.com").'),
|
|
22
|
+
types: z
|
|
23
|
+
.array(z.enum(DNS_RECORD_TYPES).describe('A DNS record type to fetch.'))
|
|
24
|
+
.default(['A', 'AAAA', 'MX', 'TXT', 'NS'])
|
|
25
|
+
.describe('DNS record types to fetch. Defaults to [A, AAAA, MX, TXT, NS]. ' +
|
|
26
|
+
'Specify more types to expand coverage (e.g., add CAA to check certificate authority authorization).'),
|
|
27
|
+
}),
|
|
28
|
+
output: z.object({
|
|
29
|
+
domain: z.string().describe('Domain queried.'),
|
|
30
|
+
nxdomain: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.describe('True when the domain does not exist in DNS (NXDOMAIN / Status 3). ' +
|
|
33
|
+
'Records will be empty. This is a valid data signal, not an error.'),
|
|
34
|
+
records: z
|
|
35
|
+
.array(z
|
|
36
|
+
.object({
|
|
37
|
+
type: z.enum(DNS_RECORD_TYPES).describe('DNS record type.'),
|
|
38
|
+
name: z.string().describe('Record owner name.'),
|
|
39
|
+
ttl: z.number().describe('Time-to-live in seconds.'),
|
|
40
|
+
data: z.string().describe('Record data (IP address, hostname, text, etc.).'),
|
|
41
|
+
})
|
|
42
|
+
.describe('A single DNS resource record.'))
|
|
43
|
+
.describe('DNS records returned for the requested types.'),
|
|
44
|
+
source: z
|
|
45
|
+
.enum(['cloudflare', 'google'])
|
|
46
|
+
.describe('The DoH resolver that provided results (cloudflare = primary, google = fallback or CAA).'),
|
|
47
|
+
}),
|
|
48
|
+
errors: [
|
|
49
|
+
{
|
|
50
|
+
reason: 'invalid_domain',
|
|
51
|
+
code: JsonRpcErrorCode.InvalidParams,
|
|
52
|
+
when: 'Input is not a valid FQDN.',
|
|
53
|
+
recovery: 'Provide a valid fully-qualified domain name like "example.com" or "sub.example.org".',
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
async handler(input, ctx) {
|
|
57
|
+
if (!isValidFqdn(input.domain)) {
|
|
58
|
+
throw ctx.fail('invalid_domain', `"${input.domain}" is not a valid FQDN.`, {
|
|
59
|
+
...ctx.recoveryFor('invalid_domain'),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
ctx.log.info('DNS lookup via DoH', { domain: input.domain, types: input.types });
|
|
63
|
+
const result = await getDohService().lookup(input.domain, input.types, ctx);
|
|
64
|
+
return {
|
|
65
|
+
domain: result.domain,
|
|
66
|
+
nxdomain: result.nxdomain,
|
|
67
|
+
records: result.records,
|
|
68
|
+
source: result.source,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
format: (result) => {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(`# DNS Records: ${result.domain}`);
|
|
74
|
+
lines.push(`**Source:** ${result.source}`);
|
|
75
|
+
lines.push(`**NXDOMAIN:** ${result.nxdomain === true ? 'Yes — domain does not exist in DNS' : 'No'}`);
|
|
76
|
+
// Group records by type and render all fields (name, ttl, data)
|
|
77
|
+
const byType = new Map();
|
|
78
|
+
for (const rec of result.records) {
|
|
79
|
+
const list = byType.get(rec.type) ?? [];
|
|
80
|
+
list.push(rec);
|
|
81
|
+
byType.set(rec.type, list);
|
|
82
|
+
}
|
|
83
|
+
for (const [type, recs] of byType) {
|
|
84
|
+
lines.push(`\n## ${type}`);
|
|
85
|
+
for (const rec of recs) {
|
|
86
|
+
// TXT records are attacker-controlled free-text and must be fenced to prevent prompt injection
|
|
87
|
+
const data = type === 'TXT' ? `\`${rec.data}\`` : rec.data;
|
|
88
|
+
lines.push(`- **${data}** | name: ${rec.name} | TTL: ${rec.ttl}s`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (result.records.length === 0 && result.nxdomain !== true) {
|
|
92
|
+
lines.push('\nNo records found for the requested types.');
|
|
93
|
+
}
|
|
94
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=whois-get-dns.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whois-get-dns.tool.js","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/whois-get-dns.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview whois_get_dossier tool — one-call domain triage combining RDAP + DNS in parallel.
|
|
3
|
+
* @module mcp-server/tools/definitions/whois-get-dossier.tool
|
|
4
|
+
*/
|
|
5
|
+
import { z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
export declare const whoisGetDossier: import("@cyanheads/mcp-ts-core").ToolDefinition<z.ZodObject<{
|
|
8
|
+
domain: z.ZodString;
|
|
9
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
10
|
+
domain: z.ZodString;
|
|
11
|
+
rdap_coverage: z.ZodNullable<z.ZodBoolean>;
|
|
12
|
+
registered: z.ZodNullable<z.ZodBoolean>;
|
|
13
|
+
registrar: z.ZodOptional<z.ZodString>;
|
|
14
|
+
created_date: z.ZodOptional<z.ZodString>;
|
|
15
|
+
expiry_date: z.ZodOptional<z.ZodString>;
|
|
16
|
+
age_days: z.ZodNullable<z.ZodNumber>;
|
|
17
|
+
dnssec_signed: z.ZodNullable<z.ZodBoolean>;
|
|
18
|
+
privacy_redacted: z.ZodNullable<z.ZodBoolean>;
|
|
19
|
+
status: z.ZodArray<z.ZodString>;
|
|
20
|
+
nameservers: z.ZodArray<z.ZodString>;
|
|
21
|
+
a_records: z.ZodArray<z.ZodString>;
|
|
22
|
+
mx_records: z.ZodArray<z.ZodString>;
|
|
23
|
+
ns_records: z.ZodArray<z.ZodString>;
|
|
24
|
+
txt_records: z.ZodArray<z.ZodString>;
|
|
25
|
+
dns_nxdomain: z.ZodNullable<z.ZodBoolean>;
|
|
26
|
+
ns_provider: z.ZodNullable<z.ZodString>;
|
|
27
|
+
mx_provider: z.ZodNullable<z.ZodString>;
|
|
28
|
+
rdap_source_error: z.ZodOptional<z.ZodString>;
|
|
29
|
+
dns_source_error: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strip>, readonly [{
|
|
31
|
+
readonly reason: "invalid_domain";
|
|
32
|
+
readonly code: JsonRpcErrorCode.InvalidParams;
|
|
33
|
+
readonly when: "Input is not a valid FQDN.";
|
|
34
|
+
readonly recovery: "Provide a valid fully-qualified domain name like \"example.com\" or \"sub.example.org\".";
|
|
35
|
+
}, {
|
|
36
|
+
readonly reason: "both_legs_failed";
|
|
37
|
+
readonly code: JsonRpcErrorCode.ServiceUnavailable;
|
|
38
|
+
readonly when: "Both RDAP and DoH legs failed — no data could be retrieved.";
|
|
39
|
+
readonly recovery: "Both RDAP and DNS services are unavailable. Retry in a few minutes or check network connectivity.";
|
|
40
|
+
}], undefined>;
|
|
41
|
+
//# sourceMappingURL=whois-get-dossier.tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whois-get-dossier.tool.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/whois-get-dossier.tool.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAQ,CAAC,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAY,MAAM,+BAA+B,CAAC;AAyE3E,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAiR1B,CAAC"}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview whois_get_dossier tool — one-call domain triage combining RDAP + DNS in parallel.
|
|
3
|
+
* @module mcp-server/tools/definitions/whois-get-dossier.tool
|
|
4
|
+
*/
|
|
5
|
+
import { tool, z } from '@cyanheads/mcp-ts-core';
|
|
6
|
+
import { JsonRpcErrorCode, McpError } from '@cyanheads/mcp-ts-core/errors';
|
|
7
|
+
import { getDohService } from '../../../services/doh/doh-service.js';
|
|
8
|
+
import { getRdapService } from '../../../services/rdap/rdap-service.js';
|
|
9
|
+
import { isValidFqdn } from './_fqdn.js';
|
|
10
|
+
/** True when the error is a typed domain_not_found from rdap-service */
|
|
11
|
+
function isDomainNotFound(err) {
|
|
12
|
+
return (err instanceof McpError &&
|
|
13
|
+
typeof err.data?.reason === 'string' &&
|
|
14
|
+
err.data.reason === 'domain_not_found');
|
|
15
|
+
}
|
|
16
|
+
/** Infer NS provider from the first NS record data (e.g., ns1.cloudflare.com → cloudflare) */
|
|
17
|
+
function inferNsProvider(nsRecords) {
|
|
18
|
+
if (nsRecords.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
const ns = (nsRecords[0]?.data ?? '').toLowerCase().replace(/\.$/, '');
|
|
21
|
+
// Common providers
|
|
22
|
+
if (ns.includes('cloudflare'))
|
|
23
|
+
return 'Cloudflare';
|
|
24
|
+
if (ns.includes('awsdns') || ns.includes('amazonaws'))
|
|
25
|
+
return 'AWS Route 53';
|
|
26
|
+
if (ns.includes('google'))
|
|
27
|
+
return 'Google Cloud DNS';
|
|
28
|
+
if (ns.includes('azure-dns') || ns.includes('microsoftdns'))
|
|
29
|
+
return 'Azure DNS';
|
|
30
|
+
if (ns.includes('namebrightdns') || ns.includes('namebright'))
|
|
31
|
+
return 'NameBright';
|
|
32
|
+
if (ns.includes('nsone') || ns.includes('ns1.com'))
|
|
33
|
+
return 'NS1';
|
|
34
|
+
if (ns.includes('dnsimple'))
|
|
35
|
+
return 'DNSimple';
|
|
36
|
+
if (ns.includes('domaincontrol'))
|
|
37
|
+
return 'GoDaddy';
|
|
38
|
+
if (ns.includes('name.com'))
|
|
39
|
+
return 'Name.com';
|
|
40
|
+
if (ns.includes('registrar-servers'))
|
|
41
|
+
return 'Namecheap';
|
|
42
|
+
// Fall back to the second-level domain of the NS server
|
|
43
|
+
const parts = ns.split('.');
|
|
44
|
+
if (parts.length >= 2)
|
|
45
|
+
return parts[parts.length - 2];
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
/** Infer MX provider from the first MX record data */
|
|
49
|
+
function inferMxProvider(mxRecords) {
|
|
50
|
+
if (mxRecords.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
const mx = (mxRecords[0]?.data ?? '').toLowerCase().replace(/\.$/, '');
|
|
53
|
+
if (mx.includes('google') || mx.includes('googlemail') || mx.includes('aspmx'))
|
|
54
|
+
return 'Google Workspace';
|
|
55
|
+
if (mx.includes('outlook') ||
|
|
56
|
+
mx.includes('microsoft') ||
|
|
57
|
+
mx.includes('office365') ||
|
|
58
|
+
mx.includes('protection.outlook'))
|
|
59
|
+
return 'Microsoft 365';
|
|
60
|
+
if (mx.includes('mxroute'))
|
|
61
|
+
return 'MXroute';
|
|
62
|
+
if (mx.includes('mailchannels'))
|
|
63
|
+
return 'MailChannels';
|
|
64
|
+
if (mx.includes('amazonses') || mx.includes('amazonaws'))
|
|
65
|
+
return 'Amazon SES';
|
|
66
|
+
if (mx.includes('fastmail'))
|
|
67
|
+
return 'Fastmail';
|
|
68
|
+
if (mx.includes('protonmail'))
|
|
69
|
+
return 'ProtonMail';
|
|
70
|
+
if (mx.includes('zoho'))
|
|
71
|
+
return 'Zoho Mail';
|
|
72
|
+
if (mx.includes('sendgrid'))
|
|
73
|
+
return 'SendGrid';
|
|
74
|
+
if (mx.includes('mailgun'))
|
|
75
|
+
return 'Mailgun';
|
|
76
|
+
// Fall back to second-level domain
|
|
77
|
+
const parts = mx.split('.');
|
|
78
|
+
if (parts.length >= 2)
|
|
79
|
+
return parts[parts.length - 2];
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
/** Compute domain age in days from creation date string */
|
|
83
|
+
function ageDaysFromCreated(createdDate) {
|
|
84
|
+
if (!createdDate)
|
|
85
|
+
return;
|
|
86
|
+
const created = new Date(createdDate);
|
|
87
|
+
if (Number.isNaN(created.getTime()))
|
|
88
|
+
return;
|
|
89
|
+
const diffMs = Date.now() - created.getTime();
|
|
90
|
+
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
91
|
+
}
|
|
92
|
+
export const whoisGetDossier = tool('whois_get_dossier', {
|
|
93
|
+
title: 'Domain Dossier',
|
|
94
|
+
description: 'One-call domain triage: fetches registration record (RDAP) and DNS records (A, MX, NS, TXT) in ' +
|
|
95
|
+
'parallel, returning a single normalized record with factual signals — domain age in days, ' +
|
|
96
|
+
'privacy-redacted flag, registrar, NS provider inferred from NS records, mail provider inferred ' +
|
|
97
|
+
'from MX records. No synthesized scores — factual signals only. Partial results are surfaced when ' +
|
|
98
|
+
'one leg fails (registration or DNS marked with source_error); only when both legs fail does the ' +
|
|
99
|
+
'tool throw both_legs_failed. For the full registration record use whois_lookup_domain. For DNS ' +
|
|
100
|
+
'types beyond A/MX/NS/TXT (e.g., CNAME, CAA, SOA) use whois_get_dns.',
|
|
101
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
|
|
102
|
+
input: z.object({
|
|
103
|
+
domain: z
|
|
104
|
+
.string()
|
|
105
|
+
.describe('Fully qualified domain name for the triage (e.g., "github.com"). Must be a valid FQDN.'),
|
|
106
|
+
}),
|
|
107
|
+
output: z.object({
|
|
108
|
+
domain: z.string().describe('Normalized domain name.'),
|
|
109
|
+
// Registration section
|
|
110
|
+
rdap_coverage: z
|
|
111
|
+
.boolean()
|
|
112
|
+
.nullable()
|
|
113
|
+
.describe('True = RDAP server found. False = no RDAP coverage. Null = RDAP leg failed (source_error set).'),
|
|
114
|
+
registered: z
|
|
115
|
+
.boolean()
|
|
116
|
+
.nullable()
|
|
117
|
+
.describe('True when the domain has a registration record. False = RDAP 404 (not registered). ' +
|
|
118
|
+
'Null when RDAP leg failed.'),
|
|
119
|
+
registrar: z.string().optional().describe('Registrar name from registration record.'),
|
|
120
|
+
created_date: z.string().optional().describe('ISO 8601 registration creation date.'),
|
|
121
|
+
expiry_date: z.string().optional().describe('ISO 8601 registration expiry date.'),
|
|
122
|
+
age_days: z
|
|
123
|
+
.number()
|
|
124
|
+
.nullable()
|
|
125
|
+
.describe('Domain age in days since creation_date. Null when created_date is unavailable.'),
|
|
126
|
+
dnssec_signed: z
|
|
127
|
+
.boolean()
|
|
128
|
+
.nullable()
|
|
129
|
+
.describe('True when delegation-signed. Null when RDAP leg unavailable.'),
|
|
130
|
+
privacy_redacted: z
|
|
131
|
+
.boolean()
|
|
132
|
+
.nullable()
|
|
133
|
+
.describe('True when registrant contact info is privacy-redacted. Null when RDAP leg unavailable.'),
|
|
134
|
+
status: z.array(z.string()).describe('EPP status codes. Empty when RDAP leg failed.'),
|
|
135
|
+
nameservers: z
|
|
136
|
+
.array(z.string())
|
|
137
|
+
.describe('Authoritative nameservers. Empty when RDAP leg failed.'),
|
|
138
|
+
// DNS section
|
|
139
|
+
a_records: z
|
|
140
|
+
.array(z.string())
|
|
141
|
+
.describe('IPv4 addresses (A records). Empty when DNS leg failed or NXDOMAIN.'),
|
|
142
|
+
mx_records: z
|
|
143
|
+
.array(z.string())
|
|
144
|
+
.describe('Mail exchange hostnames (MX data). Empty when DNS leg failed or NXDOMAIN.'),
|
|
145
|
+
ns_records: z
|
|
146
|
+
.array(z.string())
|
|
147
|
+
.describe('DNS nameservers from live DNS (NS data). Empty when DNS leg failed or NXDOMAIN.'),
|
|
148
|
+
txt_records: z
|
|
149
|
+
.array(z.string())
|
|
150
|
+
.describe('TXT record values (SPF, DKIM hints, etc.). Empty when DNS leg failed or NXDOMAIN.'),
|
|
151
|
+
dns_nxdomain: z
|
|
152
|
+
.boolean()
|
|
153
|
+
.nullable()
|
|
154
|
+
.describe('True when DNS says domain does not exist (NXDOMAIN). Null when DNS leg failed.'),
|
|
155
|
+
// Inferred signals
|
|
156
|
+
ns_provider: z
|
|
157
|
+
.string()
|
|
158
|
+
.nullable()
|
|
159
|
+
.describe('DNS provider inferred from NS record (e.g., "Cloudflare", "AWS Route 53"). Null when unknown or DNS leg failed.'),
|
|
160
|
+
mx_provider: z
|
|
161
|
+
.string()
|
|
162
|
+
.nullable()
|
|
163
|
+
.describe('Mail provider inferred from MX record (e.g., "Google Workspace", "Microsoft 365"). Null when no MX or DNS leg failed.'),
|
|
164
|
+
// Error signals
|
|
165
|
+
rdap_source_error: z
|
|
166
|
+
.string()
|
|
167
|
+
.optional()
|
|
168
|
+
.describe('Error message from the RDAP leg when it failed. Omitted on success.'),
|
|
169
|
+
dns_source_error: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.describe('Error message from the DNS leg when it failed. Omitted on success.'),
|
|
173
|
+
}),
|
|
174
|
+
errors: [
|
|
175
|
+
{
|
|
176
|
+
reason: 'invalid_domain',
|
|
177
|
+
code: JsonRpcErrorCode.InvalidParams,
|
|
178
|
+
when: 'Input is not a valid FQDN.',
|
|
179
|
+
recovery: 'Provide a valid fully-qualified domain name like "example.com" or "sub.example.org".',
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
reason: 'both_legs_failed',
|
|
183
|
+
code: JsonRpcErrorCode.ServiceUnavailable,
|
|
184
|
+
when: 'Both RDAP and DoH legs failed — no data could be retrieved.',
|
|
185
|
+
recovery: 'Both RDAP and DNS services are unavailable. Retry in a few minutes or check network connectivity.',
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
async handler(input, ctx) {
|
|
189
|
+
if (!isValidFqdn(input.domain)) {
|
|
190
|
+
throw ctx.fail('invalid_domain', `"${input.domain}" is not a valid FQDN.`, {
|
|
191
|
+
...ctx.recoveryFor('invalid_domain'),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
ctx.log.info('Domain dossier lookup', { domain: input.domain });
|
|
195
|
+
// Fan out RDAP and DNS lookups in parallel
|
|
196
|
+
const [rdapSettled, dnsSettled] = await Promise.allSettled([
|
|
197
|
+
getRdapService().lookupDomain(input.domain, ctx),
|
|
198
|
+
getDohService().lookup(input.domain, ['A', 'MX', 'NS', 'TXT'], ctx),
|
|
199
|
+
]);
|
|
200
|
+
const rdapOk = rdapSettled.status === 'fulfilled';
|
|
201
|
+
const dnsOk = dnsSettled.status === 'fulfilled';
|
|
202
|
+
// domain_not_found is a valid data signal (RDAP 404 = not registered), not a leg failure
|
|
203
|
+
const rdapNotFound = rdapSettled.status === 'rejected' && isDomainNotFound(rdapSettled.reason);
|
|
204
|
+
if (!rdapOk && !rdapNotFound && !dnsOk) {
|
|
205
|
+
throw ctx.fail('both_legs_failed', 'Both RDAP and DNS lookups failed — no data available.', {
|
|
206
|
+
rdap_error: rdapSettled.status === 'rejected' ? String(rdapSettled.reason) : undefined,
|
|
207
|
+
dns_error: dnsSettled.status === 'rejected' ? String(dnsSettled.reason) : undefined,
|
|
208
|
+
...ctx.recoveryFor('both_legs_failed'),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// Build result from available legs
|
|
212
|
+
let reg;
|
|
213
|
+
let rdapSourceError;
|
|
214
|
+
if (rdapOk) {
|
|
215
|
+
reg = rdapSettled.value;
|
|
216
|
+
}
|
|
217
|
+
else if (!rdapNotFound) {
|
|
218
|
+
rdapSourceError =
|
|
219
|
+
rdapSettled.reason instanceof Error
|
|
220
|
+
? rdapSettled.reason.message
|
|
221
|
+
: String(rdapSettled.reason);
|
|
222
|
+
}
|
|
223
|
+
let dnsRecords = [];
|
|
224
|
+
let dnsNxdomain = null;
|
|
225
|
+
let dnsSourceError;
|
|
226
|
+
if (dnsOk) {
|
|
227
|
+
dnsRecords = dnsSettled.value.records;
|
|
228
|
+
dnsNxdomain = dnsSettled.value.nxdomain;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
dnsSourceError =
|
|
232
|
+
dnsSettled.reason instanceof Error ? dnsSettled.reason.message : String(dnsSettled.reason);
|
|
233
|
+
}
|
|
234
|
+
const nsRecords = dnsRecords.filter((r) => r.type === 'NS');
|
|
235
|
+
const mxRecords = dnsRecords.filter((r) => r.type === 'MX');
|
|
236
|
+
const aRecords = dnsRecords.filter((r) => r.type === 'A');
|
|
237
|
+
const txtRecords = dnsRecords.filter((r) => r.type === 'TXT');
|
|
238
|
+
const ageDays = reg ? (ageDaysFromCreated(reg.created_date) ?? null) : null;
|
|
239
|
+
const nsProvider = dnsOk ? (inferNsProvider(nsRecords) ?? null) : null;
|
|
240
|
+
const mxProvider = dnsOk ? (inferMxProvider(mxRecords) ?? null) : null;
|
|
241
|
+
// registered:
|
|
242
|
+
// true — RDAP returned a record (domain exists)
|
|
243
|
+
// false — RDAP returned 404 (domain_not_found = not registered)
|
|
244
|
+
// null — RDAP leg errored for another reason, or rdap_coverage: false
|
|
245
|
+
const registered = rdapNotFound
|
|
246
|
+
? false
|
|
247
|
+
: rdapOk && reg
|
|
248
|
+
? reg.rdap_coverage
|
|
249
|
+
? true
|
|
250
|
+
: null
|
|
251
|
+
: null;
|
|
252
|
+
return {
|
|
253
|
+
domain: input.domain.toLowerCase(),
|
|
254
|
+
// rdapNotFound means RDAP server was reachable (coverage: true) but domain doesn't exist
|
|
255
|
+
rdap_coverage: rdapNotFound ? true : rdapOk ? (reg?.rdap_coverage ?? null) : null,
|
|
256
|
+
registered,
|
|
257
|
+
registrar: reg?.registrar,
|
|
258
|
+
created_date: reg?.created_date,
|
|
259
|
+
expiry_date: reg?.expiry_date,
|
|
260
|
+
age_days: ageDays,
|
|
261
|
+
dnssec_signed: rdapOk ? (reg?.dnssec_signed ?? null) : null,
|
|
262
|
+
privacy_redacted: rdapOk ? (reg?.registrant_redacted ?? null) : null,
|
|
263
|
+
status: reg?.status ?? [],
|
|
264
|
+
nameservers: reg?.nameservers ?? [],
|
|
265
|
+
a_records: aRecords.map((r) => r.data),
|
|
266
|
+
mx_records: mxRecords.map((r) => r.data.replace(/\.$/, '')),
|
|
267
|
+
ns_records: nsRecords.map((r) => r.data.replace(/\.$/, '')),
|
|
268
|
+
txt_records: txtRecords.map((r) => r.data),
|
|
269
|
+
dns_nxdomain: dnsNxdomain,
|
|
270
|
+
ns_provider: nsProvider,
|
|
271
|
+
mx_provider: mxProvider,
|
|
272
|
+
...(rdapSourceError ? { rdap_source_error: rdapSourceError } : {}),
|
|
273
|
+
...(dnsSourceError ? { dns_source_error: dnsSourceError } : {}),
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
format: (result) => {
|
|
277
|
+
const lines = [];
|
|
278
|
+
lines.push(`# Domain Dossier: ${result.domain}`);
|
|
279
|
+
// Registration section
|
|
280
|
+
lines.push('\n## Registration');
|
|
281
|
+
if (result.rdap_coverage === null) {
|
|
282
|
+
lines.push(`**RDAP:** Failed`);
|
|
283
|
+
}
|
|
284
|
+
else if (!result.rdap_coverage) {
|
|
285
|
+
lines.push('**RDAP Coverage:** None — TLD has no RDAP server');
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
lines.push(`**Registered:** ${result.registered ? 'Yes' : result.registered === false ? 'No' : 'Unknown'}`);
|
|
289
|
+
if (result.created_date)
|
|
290
|
+
lines.push(`**Created:** ${result.created_date}`);
|
|
291
|
+
if (result.expiry_date)
|
|
292
|
+
lines.push(`**Expires:** ${result.expiry_date}`);
|
|
293
|
+
if (result.age_days !== null)
|
|
294
|
+
lines.push(`**Age:** ${result.age_days} days`);
|
|
295
|
+
lines.push(`**DNSSEC:** ${result.dnssec_signed ? 'Signed' : result.dnssec_signed === false ? 'Not signed' : 'Unknown'}`);
|
|
296
|
+
lines.push(`**Privacy Redacted:** ${result.privacy_redacted ? 'Yes' : result.privacy_redacted === false ? 'No' : 'Unknown'}`);
|
|
297
|
+
if (result.status.length > 0)
|
|
298
|
+
lines.push(`**Status:** ${result.status.join(', ')}`);
|
|
299
|
+
if (result.nameservers.length > 0)
|
|
300
|
+
lines.push(`**Nameservers (RDAP):** ${result.nameservers.join(', ')}`);
|
|
301
|
+
}
|
|
302
|
+
if (result.registrar)
|
|
303
|
+
lines.push(`**Registrar:** ${result.registrar}`);
|
|
304
|
+
if (result.rdap_source_error)
|
|
305
|
+
lines.push(`**RDAP Error:** ${result.rdap_source_error}`);
|
|
306
|
+
// DNS section
|
|
307
|
+
lines.push('\n## DNS');
|
|
308
|
+
if (result.dns_source_error) {
|
|
309
|
+
lines.push(`**DNS:** Failed — ${result.dns_source_error}`);
|
|
310
|
+
}
|
|
311
|
+
else if (result.dns_nxdomain) {
|
|
312
|
+
lines.push('**NXDOMAIN:** Domain does not exist in DNS.');
|
|
313
|
+
}
|
|
314
|
+
if (result.a_records.length > 0)
|
|
315
|
+
lines.push(`**A:** ${result.a_records.join(', ')}`);
|
|
316
|
+
if (result.ns_records.length > 0)
|
|
317
|
+
lines.push(`**NS:** ${result.ns_records.join(', ')}`);
|
|
318
|
+
if (result.mx_records.length > 0)
|
|
319
|
+
lines.push(`**MX:** ${result.mx_records.join(', ')}`);
|
|
320
|
+
if (result.txt_records.length > 0) {
|
|
321
|
+
// TXT records are attacker-controlled free-text; backtick-fence each value to prevent prompt injection
|
|
322
|
+
const fenced = result.txt_records.slice(0, 3).map((v) => `\`${v}\``);
|
|
323
|
+
lines.push(`**TXT:** ${fenced.join(' | ')}`);
|
|
324
|
+
}
|
|
325
|
+
// Inferred signals
|
|
326
|
+
lines.push('\n## Signals');
|
|
327
|
+
lines.push(`**NS Provider:** ${result.ns_provider ?? 'Unknown'}`);
|
|
328
|
+
lines.push(`**Mail Provider:** ${result.mx_provider ?? 'None / Unknown'}`);
|
|
329
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
//# sourceMappingURL=whois-get-dossier.tool.js.map
|