@cyanheads/whois-mcp-server 0.1.6 → 0.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.
Files changed (50) hide show
  1. package/AGENTS.md +21 -9
  2. package/CLAUDE.md +21 -9
  3. package/Dockerfile +7 -4
  4. package/README.md +20 -11
  5. package/changelog/0.2.x/0.2.0.md +40 -0
  6. package/dist/app.d.ts +46 -22
  7. package/dist/app.d.ts.map +1 -1
  8. package/dist/app.js +7 -5
  9. package/dist/app.js.map +1 -1
  10. package/dist/mcp-server/tools/definitions/_fqdn.d.ts +9 -3
  11. package/dist/mcp-server/tools/definitions/_fqdn.d.ts.map +1 -1
  12. package/dist/mcp-server/tools/definitions/_fqdn.js +10 -14
  13. package/dist/mcp-server/tools/definitions/_fqdn.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.d.ts +11 -5
  15. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.d.ts.map +1 -1
  16. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.js +25 -38
  17. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.js.map +1 -1
  18. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.d.ts +6 -4
  19. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.d.ts.map +1 -1
  20. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.js +25 -16
  21. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.js.map +1 -1
  22. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.d.ts +0 -5
  23. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.d.ts.map +1 -1
  24. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.js +20 -23
  25. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.js.map +1 -1
  26. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.d.ts.map +1 -1
  27. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.js +3 -0
  28. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.js.map +1 -1
  29. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.d.ts +16 -9
  30. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.d.ts.map +1 -1
  31. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.js +60 -21
  32. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.js.map +1 -1
  33. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.d.ts +14 -0
  34. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.d.ts.map +1 -1
  35. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.js +34 -6
  36. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.js.map +1 -1
  37. package/dist/services/doh/doh-service.d.ts +3 -3
  38. package/dist/services/doh/doh-service.d.ts.map +1 -1
  39. package/dist/services/doh/doh-service.js +61 -12
  40. package/dist/services/doh/doh-service.js.map +1 -1
  41. package/dist/services/doh/types.d.ts +12 -0
  42. package/dist/services/doh/types.d.ts.map +1 -1
  43. package/dist/services/rdap/rdap-service.d.ts +10 -5
  44. package/dist/services/rdap/rdap-service.d.ts.map +1 -1
  45. package/dist/services/rdap/rdap-service.js +133 -36
  46. package/dist/services/rdap/rdap-service.js.map +1 -1
  47. package/dist/services/rdap/types.d.ts +32 -2
  48. package/dist/services/rdap/types.d.ts.map +1 -1
  49. package/package.json +12 -11
  50. package/server.json +11 -25
package/AGENTS.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** whois-mcp-server
4
- **Version:** 0.1.6
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
4
+ **Version:** 0.2.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.1.0
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
8
8
  **Zod:** ^4.6.5
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
@@ -52,7 +52,7 @@ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
52
52
  import { getRdapService } from '@/services/rdap/rdap-service.js';
53
53
 
54
54
  export const whoisLookupDomain = tool('whois_lookup_domain', {
55
- description: 'Look up a domain\'s registration record — registrar, created/expiry dates, nameservers, EPP status codes, DNSSEC flag, and registrant org (where not privacy-redacted). RDAP-first via IANA auto-bootstrap.',
55
+ description: 'Look up registry registration data with provider DNSSEC and privacy evidence. Public registrant org is independent of contact redaction.',
56
56
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
57
57
  input: z.object({
58
58
  domain: z.string().describe('Domain name to look up (e.g. "github.com")'),
@@ -67,8 +67,8 @@ export const whoisLookupDomain = tool('whois_lookup_domain', {
67
67
  when: 'TLD has no RDAP server in IANA bootstrap',
68
68
  recovery: 'Use whois_check_availability instead or try a different TLD.' },
69
69
  { reason: 'domain_not_found', code: JsonRpcErrorCode.NotFound,
70
- when: 'RDAP returned 404 — domain not registered',
71
- recovery: 'Use whois_check_availability to confirm the domain is available.' },
70
+ when: 'RDAP returned 404 — no registry object found',
71
+ recovery: 'Use whois_check_availability for record status; no eligibility guarantee is implied.' },
72
72
  ],
73
73
  async handler(input, ctx) {
74
74
  const rdap = getRdapService();
@@ -110,6 +110,8 @@ export function getServerConfig() {
110
110
 
111
111
  For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
112
112
 
113
+ Call `getServerConfig()` in `createApp()`'s `setup()` before initializing services so invalid environment values fail startup before the transport binds.
114
+
113
115
  ### Server identity and instructions
114
116
 
115
117
  `createApp()` accepts optional identity fields forwarded to the SDK's `initialize` response and the server manifest (`/.well-known/mcp.json`):
@@ -225,16 +227,26 @@ src/
225
227
  mcp-server/
226
228
  tools/definitions/
227
229
  whois-lookup-domain.tool.ts # Domain registration record via RDAP
228
- whois-check-availability.tool.ts # Domain availability check (RDAP 404 = available)
230
+ whois-check-availability.tool.ts # Registry record status; no eligibility guarantee
229
231
  whois-get-dns.tool.ts # DNS records via DoH (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR)
230
232
  whois-lookup-ip.tool.ts # IP/CIDR netblock, org, abuse contact via RIR RDAP
231
233
  whois-lookup-asn.tool.ts # ASN to org/RIR resolution via RIR RDAP
232
234
  whois-get-dossier.tool.ts # One-call domain triage — registration + DNS in parallel
233
- _fqdn.ts # Shared FQDN validation helper
235
+ _fqdn.ts # Shared registration/DNS/ASN/IP syntax patterns
234
236
  ```
235
237
 
236
238
  ---
237
239
 
240
+ ## Lookup Contracts
241
+
242
+ Registry object absence never establishes registration eligibility. `whois_check_availability` reports `registration_status: registered | not_found | unknown`, with `available: false` only for a found record and null otherwise. ICANN-only suffix recognition rejects known subdomains without silently querying their parent; private suffixes do not create registry boundaries.
243
+
244
+ Registrant org comes from jCard `org`; personal `fn` is not organization data. Explicit registrant redaction and missing evidence yield true and null respectively; public org survives independently. DNSSEC preserves provider `delegationSigned`, with missing/no-coverage evidence null. Domain and dossier formatters render unknown states explicitly. Optional `include_registrar` detail stays nested, never overriding registry authority; one referral, no retry/recursion, five guarded redirects and one total RDAP deadline. Destination checks are best-effort against DNS rebinding.
245
+
246
+ DNS owner syntax separately permits underscores and one normalized root dot. Failure RCODEs and truncated replies never become empty successes; CAA does not repeat NextDNS as fallback, and cancellation starts no further requests. TXT uses complete literal fences, not an injection boundary. PTR selects only reachable owner-matched records, allows five aliases under one DNS deadline, and discloses found/not_found/failed plus failure detail. IP output retains all ordered `cidrs`, with scalar `cidr` only for one component. A missing IANA route is non-retryable `ip_no_rdap_route`; bootstrap fetch failure stays operational.
247
+
248
+ Serialized patterns are the syntax gate (`InvalidParams`/`invalid_arguments`); IP/ASN semantic parsing and exclusions retain typed reasons. Preserve existing ASN whitespace/plus/zero-padding and CIDR padding without arbitrary input ceilings. See `docs/design.md` for the domain decisions.
249
+
238
250
  ## Naming
239
251
 
240
252
  | What | Convention | Example |
@@ -364,7 +376,7 @@ security: false # optional — true ONLY for a source
364
376
 
365
377
  ## Publishing
366
378
 
367
- **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
379
+ **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
368
380
 
369
381
  ---
370
382
 
package/CLAUDE.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # Developer Protocol
2
2
 
3
3
  **Server:** whois-mcp-server
4
- **Version:** 0.1.6
5
- **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.10`
4
+ **Version:** 0.2.0
5
+ **Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core) `^0.13.15`
6
6
  **Engines:** Bun ≥1.4.0, Node ≥24.0.0
7
- **MCP SDK:** `@modelcontextprotocol/server` ^2.1.0
7
+ **MCP SDK:** `@modelcontextprotocol/server` ^2.3.1
8
8
  **Zod:** ^4.6.5
9
9
 
10
10
  > **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
@@ -52,7 +52,7 @@ import { JsonRpcErrorCode } from '@cyanheads/mcp-ts-core/errors';
52
52
  import { getRdapService } from '@/services/rdap/rdap-service.js';
53
53
 
54
54
  export const whoisLookupDomain = tool('whois_lookup_domain', {
55
- description: 'Look up a domain\'s registration record — registrar, created/expiry dates, nameservers, EPP status codes, DNSSEC flag, and registrant org (where not privacy-redacted). RDAP-first via IANA auto-bootstrap.',
55
+ description: 'Look up registry registration data with provider DNSSEC and privacy evidence. Public registrant org is independent of contact redaction.',
56
56
  annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
57
57
  input: z.object({
58
58
  domain: z.string().describe('Domain name to look up (e.g. "github.com")'),
@@ -67,8 +67,8 @@ export const whoisLookupDomain = tool('whois_lookup_domain', {
67
67
  when: 'TLD has no RDAP server in IANA bootstrap',
68
68
  recovery: 'Use whois_check_availability instead or try a different TLD.' },
69
69
  { reason: 'domain_not_found', code: JsonRpcErrorCode.NotFound,
70
- when: 'RDAP returned 404 — domain not registered',
71
- recovery: 'Use whois_check_availability to confirm the domain is available.' },
70
+ when: 'RDAP returned 404 — no registry object found',
71
+ recovery: 'Use whois_check_availability for record status; no eligibility guarantee is implied.' },
72
72
  ],
73
73
  async handler(input, ctx) {
74
74
  const rdap = getRdapService();
@@ -110,6 +110,8 @@ export function getServerConfig() {
110
110
 
111
111
  For env booleans use `z.stringbool()`, never `z.coerce.boolean()` — `Boolean("false")` is `true`, so a coerced flag can't be disabled through the environment. `z.stringbool()` parses `true/false/1/0/yes/no/on/off` and rejects anything else, so `=false` actually disables.
112
112
 
113
+ Call `getServerConfig()` in `createApp()`'s `setup()` before initializing services so invalid environment values fail startup before the transport binds.
114
+
113
115
  ### Server identity and instructions
114
116
 
115
117
  `createApp()` accepts optional identity fields forwarded to the SDK's `initialize` response and the server manifest (`/.well-known/mcp.json`):
@@ -225,16 +227,26 @@ src/
225
227
  mcp-server/
226
228
  tools/definitions/
227
229
  whois-lookup-domain.tool.ts # Domain registration record via RDAP
228
- whois-check-availability.tool.ts # Domain availability check (RDAP 404 = available)
230
+ whois-check-availability.tool.ts # Registry record status; no eligibility guarantee
229
231
  whois-get-dns.tool.ts # DNS records via DoH (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR)
230
232
  whois-lookup-ip.tool.ts # IP/CIDR netblock, org, abuse contact via RIR RDAP
231
233
  whois-lookup-asn.tool.ts # ASN to org/RIR resolution via RIR RDAP
232
234
  whois-get-dossier.tool.ts # One-call domain triage — registration + DNS in parallel
233
- _fqdn.ts # Shared FQDN validation helper
235
+ _fqdn.ts # Shared registration/DNS/ASN/IP syntax patterns
234
236
  ```
235
237
 
236
238
  ---
237
239
 
240
+ ## Lookup Contracts
241
+
242
+ Registry object absence never establishes registration eligibility. `whois_check_availability` reports `registration_status: registered | not_found | unknown`, with `available: false` only for a found record and null otherwise. ICANN-only suffix recognition rejects known subdomains without silently querying their parent; private suffixes do not create registry boundaries.
243
+
244
+ Registrant org comes from jCard `org`; personal `fn` is not organization data. Explicit registrant redaction and missing evidence yield true and null respectively; public org survives independently. DNSSEC preserves provider `delegationSigned`, with missing/no-coverage evidence null. Domain and dossier formatters render unknown states explicitly. Optional `include_registrar` detail stays nested, never overriding registry authority; one referral, no retry/recursion, five guarded redirects and one total RDAP deadline. Destination checks are best-effort against DNS rebinding.
245
+
246
+ DNS owner syntax separately permits underscores and one normalized root dot. Failure RCODEs and truncated replies never become empty successes; CAA does not repeat NextDNS as fallback, and cancellation starts no further requests. TXT uses complete literal fences, not an injection boundary. PTR selects only reachable owner-matched records, allows five aliases under one DNS deadline, and discloses found/not_found/failed plus failure detail. IP output retains all ordered `cidrs`, with scalar `cidr` only for one component. A missing IANA route is non-retryable `ip_no_rdap_route`; bootstrap fetch failure stays operational.
247
+
248
+ Serialized patterns are the syntax gate (`InvalidParams`/`invalid_arguments`); IP/ASN semantic parsing and exclusions retain typed reasons. Preserve existing ASN whitespace/plus/zero-padding and CIDR padding without arbitrary input ceilings. See `docs/design.md` for the domain decisions.
249
+
238
250
  ## Naming
239
251
 
240
252
  | What | Convention | Example |
@@ -364,7 +376,7 @@ security: false # optional — true ONLY for a source
364
376
 
365
377
  ## Publishing
366
378
 
367
- **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the changelog entry plus a gates section); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
379
+ **Every release goes through a release PR, straight-through** — `git-wrapup`'s "Release PR mode", mode `straight-through`. One run: `git-wrapup` lands the commit stack on `release/<version>`, pushes it, and opens the PR (title = the release commit subject, body = the release digest: theme line, `## Changes`, `## Gates`, changelog link last); `release-and-publish` then fast-forwards `main` locally with `git merge --ff-only`, creates the tag on `main`'s tip, pushes `main` and the tag, deletes the branch, and publishes. A caller's brief may run a given release as `gated` instead — a `release-pr-review` pass on the open PR before `release-and-publish`. **Never merge through the GitHub UI or `gh pr merge`**: squash and rebase-merge are disabled in the repo settings because both rewrite the stack (rebase-merge also strips the SSH signatures), and a merge commit breaks the linear history.
368
380
 
369
381
  ---
370
382
 
package/Dockerfile CHANGED
@@ -4,7 +4,7 @@
4
4
  # This stage installs all dependencies (including dev), builds the TypeScript
5
5
  # source code into JavaScript, and prepares the production assets.
6
6
  # ==============================================================================
7
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS build
7
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS build
8
8
 
9
9
  WORKDIR /usr/src/app
10
10
 
@@ -29,7 +29,7 @@ RUN bun run build
29
29
  # Run the scanner and OTel installer natively, cross-installing dependencies for
30
30
  # the target architecture. Only node_modules enters the production image.
31
31
  # ==============================================================================
32
- FROM --platform=$BUILDPLATFORM oven/bun:1.4.2 AS deps
32
+ FROM --platform=$BUILDPLATFORM oven/bun:1.4.3 AS deps
33
33
 
34
34
  WORKDIR /usr/src/app
35
35
 
@@ -59,6 +59,9 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \
59
59
  bun scripts/install-otel.ts --os="$TARGETOS" --cpu="$(cat .bun-cpu)"; \
60
60
  fi
61
61
 
62
+ COPY scripts/prune-musl-packages.ts ./scripts/
63
+ RUN bun scripts/prune-musl-packages.ts
64
+
62
65
  RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
63
66
 
64
67
 
@@ -69,7 +72,7 @@ RUN rm -rf node_modules/@socketsecurity/bun-security-scanner
69
72
  # application. It uses a slim base image and only includes production
70
73
  # dependencies and build artifacts.
71
74
  # ==============================================================================
72
- FROM oven/bun:1.4.2-slim AS production
75
+ FROM oven/bun:1.4.3-slim AS production
73
76
 
74
77
  WORKDIR /usr/src/app
75
78
 
@@ -80,7 +83,7 @@ ENV NODE_ENV=production
80
83
  # OCI image metadata (https://github.com/opencontainers/image-spec/blob/main/annotations.md)
81
84
  ARG APP_VERSION
82
85
  LABEL org.opencontainers.image.title="whois-mcp-server"
83
- LABEL org.opencontainers.image.description="Look up domain registration, check availability, fetch DNS records, and resolve IPs and ASNs via RDAP and DNS-over-HTTPS via MCP. STDIO or Streamable HTTP."
86
+ LABEL org.opencontainers.image.description="Look up domain registration, check registry record status, fetch DNS records, and resolve IPs and ASNs over RDAP and DNS-over-HTTPS via MCP. STDIO or Streamable HTTP."
84
87
  LABEL org.opencontainers.image.source="https://github.com/cyanheads/whois-mcp-server"
85
88
  LABEL org.opencontainers.image.licenses="Apache-2.0"
86
89
  LABEL org.opencontainers.image.version="${APP_VERSION}"
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  <div align="center">
2
2
  <h1>@cyanheads/whois-mcp-server</h1>
3
- <p><b>Look up domain registration, check availability, fetch DNS records, and resolve IPs and ASNs via RDAP and DNS-over-HTTPS via MCP. STDIO or Streamable HTTP.</b>
3
+ <p><b>Look up domain registration, check registry record status, fetch DNS records, and resolve IPs and ASNs over RDAP and DNS-over-HTTPS via MCP. STDIO or Streamable HTTP.</b>
4
4
  <div>6 Tools</div>
5
5
  </p>
6
6
  </div>
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.1.6-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/whois-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.1.0-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/whois-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/whois-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.2-blueviolet.svg?style=flat-square)](https://bun.sh/)
10
+ [![Version](https://img.shields.io/badge/Version-0.2.0-blue.svg?style=flat-square)](./CHANGELOG.md) [![License](https://img.shields.io/badge/License-Apache%202.0-orange.svg?style=flat-square)](./LICENSE) [![Docker](https://img.shields.io/badge/Docker-ghcr.io-2496ED?style=flat-square&logo=docker&logoColor=white)](https://github.com/users/cyanheads/packages/container/package/whois-mcp-server) [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-^2.3.1-green.svg?style=flat-square)](https://modelcontextprotocol.io/) [![npm](https://img.shields.io/npm/v/@cyanheads/whois-mcp-server?style=flat-square&logo=npm&logoColor=white)](https://www.npmjs.com/package/@cyanheads/whois-mcp-server) [![TypeScript](https://img.shields.io/badge/TypeScript-^7.0.2-3178C6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Bun](https://img.shields.io/badge/Bun-v1.4.3-blueviolet.svg?style=flat-square)](https://bun.sh/)
11
11
 
12
12
  </div>
13
13
 
@@ -23,15 +23,15 @@
23
23
 
24
24
  ## Overview
25
25
 
26
- Domain and network intelligence via RDAP and DNS-over-HTTPS. Look up domain registrations, check availability, fetch DNS records, and resolve IPs and ASNs to their registries — all via public, keyless data sources. Runs as a stdio process or a local Streamable HTTP server.
26
+ Domain and network intelligence via RDAP and DNS-over-HTTPS. Look up domain registrations, check registry record status, fetch DNS records, and resolve IPs and ASNs to their registries — all via public, keyless data sources. Runs as a stdio process or a local Streamable HTTP server.
27
27
 
28
28
  ### Tools
29
29
 
30
30
  | Tool | Description |
31
31
  |:-----|:------------|
32
32
  | `whois_lookup_domain` | Full domain registration record — registrar, created/expiry dates, nameservers, EPP status, DNSSEC, registrant org |
33
- | `whois_check_availability` | Check whether a domain is registered or available for registration |
34
- | `whois_get_dns` | DNS records for any hostname via DNS-over-HTTPS (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR) |
33
+ | `whois_check_availability` | Check registry record presence; missing records do not establish registration eligibility |
34
+ | `whois_get_dns` | DNS owner records via DNS-over-HTTPS (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR) |
35
35
  | `whois_lookup_ip` | IP or CIDR netblock, org, country, abuse contact, and reverse DNS via RIR RDAP |
36
36
  | `whois_lookup_asn` | Resolve an ASN to its org name, country, and RIR source |
37
37
  | `whois_get_dossier` | One-call domain triage — registration + DNS in parallel, normalized into a single record with factual signals |
@@ -44,34 +44,40 @@ Domain and network intelligence via RDAP and DNS-over-HTTPS. Look up domain regi
44
44
 
45
45
  - Accepts a fully qualified domain name and selects the registry RDAP server through IANA bootstrap.
46
46
  - Returns registrar, registration dates, nameservers, EPP status, DNSSEC, and `registrant_redacted`; throws `rdap_no_coverage` or `domain_not_found` when no record can be returned.
47
+ - Absent DNSSEC and privacy evidence stay `null`. Public jCard `org` is independent of redacted contact fields; a person's formatted name is not an organization.
48
+ - `include_registrar: true` follows one registrar RDAP referral and returns separate `registrar_detail` with `found`, `no_referral`, or `failed` status and source URL. Registry dates, nameservers, status, and DNSSEC remain authoritative. Default lookups make no registrar request. The optional exchange has no retries, at most five guarded redirects, and one `RDAP_TIMEOUT_MS` deadline; failure retains the registry result, cancellation propagates. Destination checks are best-effort against DNS rebinding.
47
49
 
48
50
  ---
49
51
 
50
52
  ### `whois_check_availability` <sub>tool</sub>
51
53
 
52
54
  - Accepts a fully qualified domain name for a registration check.
53
- - Returns `available: true` for RDAP 404, `false` with registrar and expiry for a registered domain, or `null` with `rdap_coverage: false` when coverage is absent.
55
+ - Returns `registration_status: registered | not_found | unknown`. `available` is `false` for a found registration and `null` otherwise; it never returns `true`. RDAP 404 establishes record absence, not eligibility to register.
56
+ - Known ICANN subdomains return `non_registrable_domain` with their registrable parent; the query is never silently rewritten. Private suffixes do not create independent registration boundaries, and unlisted suffixes retain IANA coverage handling.
54
57
 
55
58
  ---
56
59
 
57
60
  ### `whois_get_dns` <sub>tool</sub>
58
61
 
59
- - Accepts a hostname and optional nonempty `types`: A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR; omission defaults to A, AAAA, MX, TXT, NS. Duplicate types are queried once.
62
+ - Accepts an ASCII DNS owner name, including underscore labels (`_dmarc.example.com`) and one terminal root dot. Names normalize to at most 253 characters, with nonempty 1–63-character labels; spaces, URLs, consecutive dots, and multiple terminal dots are rejected. Optional nonempty `types`: A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR; omission defaults to A, AAAA, MX, TXT, NS. Duplicate types are queried once.
60
63
  - Returns records with TTLs and ordered `query_sources: [{type, source}]`, including empty answers and NXDOMAIN. Legacy `source` is `cloudflare` if any query consumed a successful Cloudflare response, otherwise `nextdns`; a nonexistent domain returns `nxdomain: true` as data.
64
+ - NOERROR/NODATA stays empty data; resolver failure codes and truncated responses return errors after retries/fallback. CAA queries use NextDNS once per retry sequence. TXT values, including duplicates, empty strings and newlines, remain complete on both surfaces and use content-sized Markdown fences for literal presentation.
61
65
 
62
66
  ---
63
67
 
64
68
  ### `whois_lookup_ip` <sub>tool</sub>
65
69
 
66
70
  - Accepts a complete IPv4/IPv6 address with at most one decimal CIDR prefix (0–32 / 0–128); no whitespace, zone IDs, or brackets. Queries the base address and echoes the original `ip`. IPv4-mapped IPv6 uses the embedded IPv4 for policy, RDAP, and PTR; other IPv6 uses 32 reversed PTR nibbles.
67
- - Returns netblock CIDR, organization, country, abuse contact, and best-effort PTR (`null` on failure); throws `invalid_ip` for malformed input or `ip_not_found` on an RIR RDAP 404.
71
+ - Returns every provider CIDR in ordered `cidrs`; `cidr` appears only for exactly one component. Start/end addresses remain authoritative; missing CIDR extensions produce an empty list rather than an inferred prefix.
72
+ - Best-effort PTR uses `ptr_status: found | not_found | failed`; failures include `ptr_source_error` and retain RDAP data. Only reachable owner-matched PTR records are selected, through at most five alias transitions under one `DOH_TIMEOUT_MS` deadline. Cancellation propagates.
73
+ - Semantic input failures return `invalid_ip`; RIR 404 returns `ip_not_found`. A successfully loaded bootstrap with no covering route returns non-retryable `ip_no_rdap_route`, distinct from bootstrap failure.
68
74
  - `private_range` enforces an explicit policy: IPv4 `0.0.0.0/8`, `10.0.0.0/8`, `100.64.0.0/10`, `127.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`, `192.168.0.0/16`, `255.0.0.0/8`; IPv6 `::1/128`, `fc00::/7`, `fe80::/10`. Other special-use addresses remain eligible for registry records or normal no-coverage/not-found outcomes.
69
75
 
70
76
  ---
71
77
 
72
78
  ### `whois_lookup_asn` <sub>tool</sub>
73
79
 
74
- - Accepts a decimal ASN from 1 to 4294967295 with an optional case-insensitive `AS` prefix and leading `+` (e.g., `AS15169`, `AS 15169`, `+15169`). Outer whitespace and whitespace after `AS` are allowed; whitespace inside digits or after `+`, suffixes, decimals, and out-of-range values return `invalid_asn`.
80
+ - Accepts a decimal ASN from 1 to 4294967295 with an optional case-insensitive `AS` prefix and leading `+` (e.g., `AS15169`, `AS 15169`, `+15169`). Outer whitespace, whitespace after `AS`, and arbitrary zero-padding are allowed; whitespace inside digits or after `+`, suffixes, and decimals fail syntax validation. Out-of-range numbers retain `invalid_asn`.
75
81
  - Returns `name`, `org_name`, `country`, `rir`, `start_autnum`, and `end_autnum`; throws `asn_not_found` when no ASN record exists.
76
82
 
77
83
  ---
@@ -80,6 +86,9 @@ Domain and network intelligence via RDAP and DNS-over-HTTPS. Look up domain regi
80
86
 
81
87
  - Accepts a fully qualified domain name for parallel registration and A/MX/NS/TXT lookups.
82
88
  - Returns registration, DNS, domain age, privacy status, and inferred NS/MX providers. Individual failures remain partial data in `rdap_source_error` or `dns_source_error`; `both_legs_failed` means neither source succeeded.
89
+ - Missing RDAP evidence, no coverage, and RDAP failure keep DNSSEC/privacy unknown. A registry miss is record absence without an eligibility guarantee; DNS failure codes and truncation populate `dns_source_error`.
90
+
91
+ Identifier syntax is advertised in JSON Schema and rejected with `InvalidParams` / `invalid_arguments` before network calls. Complete IP parsing, prefix ranges, numeric ASN bounds, and exclusion policy retain typed semantic errors. Registration names use hostname grammar separately from DNS underscore/root-dot names; ASN and CIDR inputs have no arbitrary length ceiling.
83
92
 
84
93
  ---
85
94
 
@@ -97,7 +106,7 @@ RDAP / DNS-specific:
97
106
  Agent-friendly output:
98
107
 
99
108
  - Coverage signaled two ways — `rdap_coverage: false` returned as data by `whois_check_availability` and `whois_get_dossier`, while `whois_lookup_domain` throws `rdap_no_coverage` for the same case
100
- - Privacy redaction surfaced as a field — `registrant_redacted: true` rather than silently absent contact data
109
+ - Privacy redaction surfaced from explicit registrant evidence — `true` for redaction and `null` when evidence is absent; organization data is independent
101
110
  - Partial failure model — `whois_get_dossier` marks individual legs with a `source_error` field and continues; only both-legs-fail escalates to an error
102
111
  - Factual signals, not scores — `age_days`, `privacy_redacted`, `ns_provider`, `mx_provider` are real data, not synthesized risk scores
103
112
 
@@ -163,7 +172,7 @@ For Streamable HTTP, set the transport and start the server:
163
172
 
164
173
  ```sh
165
174
  MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
166
- # Server listens at http://localhost:3010/mcp
175
+ # Server listens at http://127.0.0.1:3010/mcp
167
176
  ```
168
177
 
169
178
  ### Prerequisites
@@ -0,0 +1,40 @@
1
+ ---
2
+ summary: "DNS and RDAP results preserve uncertainty, with optional registrar contact detail."
3
+ breaking: true
4
+ security: false
5
+ ---
6
+
7
+ # 0.2.0 — 2026-10-13
8
+
9
+ ## Added
10
+
11
+ - **`include_registrar`** opts `whois_lookup_domain` into one bounded registrar referral, returning separate `registrar_detail` without replacing registry fields; failed detail retains the registry record. ([#25](https://github.com/cyanheads/whois-mcp-server/issues/25))
12
+ - **DNS owner names** accept underscore labels and one terminal root dot, normalized before lookup. ([#17](https://github.com/cyanheads/whois-mcp-server/issues/17))
13
+
14
+ ## Changed
15
+
16
+ - **Registration status** is `registered`, `not_found`, or `unknown`; `available` is now `false` or `null`, never `true`. Migrate eligibility checks to registrar confirmation. Known ICANN subdomains return `non_registrable_domain` with their parent. ([#18](https://github.com/cyanheads/whois-mcp-server/issues/18))
17
+ - **Registrant evidence** reads public jCard `org` independently of contact redaction; personal `fn` is no longer organization data. Missing privacy evidence is `null`, so consumers must handle unknown separately from false. ([#19](https://github.com/cyanheads/whois-mcp-server/issues/19))
18
+ - **DNSSEC evidence** preserves explicit provider booleans and returns `null` when absent, including dossier no-coverage results; handle unknown separately from unsigned. ([#20](https://github.com/cyanheads/whois-mcp-server/issues/20))
19
+ - **IP allocations** expose all ordered `cidrs`; legacy `cidr` is populated only for one component. Read `cidrs` for multi-component allocations. ([#21](https://github.com/cyanheads/whois-mcp-server/issues/21))
20
+ - **Identifier validation** is advertised in JSON Schema on all six tools; syntax failures now return `InvalidParams` / `invalid_arguments`, while semantic IP/ASN errors retain typed reasons. ([#26](https://github.com/cyanheads/whois-mcp-server/issues/26))
21
+ - **Framework tooling** syncs skill mirrors, packaging launch checks, CodeQL categories and configuration examples; Docker uses Bun 1.4.2 → 1.4.3 and prunes unused musl dependencies from glibc images.
22
+
23
+ ## Fixed
24
+
25
+ - **IP routing** returns non-retryable `ip_no_rdap_route` for a successfully loaded bootstrap without a covering route. ([#15](https://github.com/cyanheads/whois-mcp-server/issues/15))
26
+ - **DNS failures** and truncation return `dns_response_failed` after retries/fallback; CAA avoids duplicate fallback, cancellation stops further requests, and dossiers retain partial data. ([#16](https://github.com/cyanheads/whois-mcp-server/issues/16))
27
+ - **PTR lookup** selects only reachable owner-matched records, follows at most five aliases under one deadline, and reports `ptr_status` as `found`, `not_found`, or `failed` with failure detail. ([#22](https://github.com/cyanheads/whois-mcp-server/issues/22))
28
+ - **TXT presentation** preserves complete, duplicate, empty and multiline values in literal Markdown fences sized for embedded backticks. ([#23](https://github.com/cyanheads/whois-mcp-server/issues/23))
29
+ - **Startup configuration** validates server environment values before opening the transport; MCP Registry entries launch the package bin through `npx` and select HTTP through a fixed environment value.
30
+
31
+ ## Dependencies
32
+
33
+ - `@cyanheads/mcp-ts-core` ^0.13.10 → ^0.13.15
34
+ - `pino-pretty` ^13.1.3 → ^13.2.0
35
+ - `tldts` added at ^7.4.18
36
+ - `@biomejs/biome` ^2.5.14 → ^2.5.15
37
+ - `@types/node` ^26.6.3 → ^26.6.5
38
+ - `ignore` ^7.0.10 → ^7.0.12
39
+ - `tsc-alias` ^1.9.5 → ^1.9.7
40
+ - `vitest` ^5.0.2 → ^5.0.3
package/dist/app.d.ts CHANGED
@@ -18,15 +18,21 @@ export declare const appOptions: {
18
18
  domain: import("zod").ZodString;
19
19
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
20
20
  domain: import("zod").ZodString;
21
- available: import("zod").ZodNullable<import("zod").ZodBoolean>;
21
+ registration_status: import("zod").ZodEnum<{
22
+ not_found: "not_found";
23
+ registered: "registered";
24
+ unknown: "unknown";
25
+ }>;
26
+ available: import("zod").ZodNullable<import("zod").ZodLiteral<false>>;
22
27
  rdap_coverage: import("zod").ZodBoolean;
23
28
  registrar: import("zod").ZodOptional<import("zod").ZodString>;
24
29
  expiry_date: import("zod").ZodOptional<import("zod").ZodString>;
25
30
  }, import("zod/v4/core").$strip>, readonly [{
26
- readonly reason: "invalid_domain";
31
+ readonly reason: "non_registrable_domain";
27
32
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
28
- readonly when: "Input is not a valid FQDN.";
29
- readonly recovery: "Provide a valid fully-qualified domain name like \"example.com\" or \"sub.example.org\".";
33
+ readonly when: "An ICANN-recognized suffix identifies the query as a subdomain.";
34
+ readonly recovery: "Query the registrable parent named in the error instead; no eligibility is implied.";
35
+ readonly thrownBy: "service";
30
36
  }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
31
37
  domain: import("zod").ZodString;
32
38
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
@@ -51,18 +57,25 @@ export declare const appOptions: {
51
57
  rdap_source_error: import("zod").ZodOptional<import("zod").ZodString>;
52
58
  dns_source_error: import("zod").ZodOptional<import("zod").ZodString>;
53
59
  }, import("zod/v4/core").$strip>, readonly [{
54
- readonly reason: "invalid_domain";
55
- readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
56
- readonly when: "Input is not a valid FQDN.";
57
- readonly recovery: "Provide a valid fully-qualified domain name like \"example.com\" or \"sub.example.org\".";
58
- }, {
59
60
  readonly reason: "both_legs_failed";
60
61
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ServiceUnavailable;
61
62
  readonly when: "Both RDAP and DoH legs failed — no data could be retrieved.";
62
63
  readonly recovery: "Both RDAP and DNS services are unavailable. Retry in a few minutes or check network connectivity.";
63
64
  }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
65
+ include_registrar: import("zod").ZodDefault<import("zod").ZodBoolean>;
64
66
  domain: import("zod").ZodString;
65
67
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
68
+ registrar_detail: import("zod").ZodOptional<import("zod").ZodObject<{
69
+ status: import("zod").ZodEnum<{
70
+ failed: "failed";
71
+ found: "found";
72
+ no_referral: "no_referral";
73
+ }>;
74
+ source: import("zod").ZodOptional<import("zod").ZodString>;
75
+ registrant_org: import("zod").ZodOptional<import("zod").ZodString>;
76
+ registrant_redacted: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodBoolean>>;
77
+ source_error: import("zod").ZodOptional<import("zod").ZodString>;
78
+ }, import("zod/v4/core").$strip>>;
66
79
  domain: import("zod").ZodString;
67
80
  rdap_coverage: import("zod").ZodBoolean;
68
81
  handle: import("zod").ZodOptional<import("zod").ZodString>;
@@ -74,15 +87,10 @@ export declare const appOptions: {
74
87
  rdap_last_updated: import("zod").ZodOptional<import("zod").ZodString>;
75
88
  status: import("zod").ZodArray<import("zod").ZodString>;
76
89
  nameservers: import("zod").ZodArray<import("zod").ZodString>;
77
- dnssec_signed: import("zod").ZodBoolean;
90
+ dnssec_signed: import("zod").ZodNullable<import("zod").ZodBoolean>;
78
91
  registrant_org: import("zod").ZodOptional<import("zod").ZodString>;
79
- registrant_redacted: import("zod").ZodBoolean;
92
+ registrant_redacted: import("zod").ZodNullable<import("zod").ZodBoolean>;
80
93
  }, import("zod/v4/core").$strip>, readonly [{
81
- readonly reason: "invalid_domain";
82
- readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
83
- readonly when: "Input is not a valid FQDN.";
84
- readonly recovery: "Provide a valid fully-qualified domain name like \"example.com\" or \"sub.example.org\".";
85
- }, {
86
94
  readonly reason: "rdap_no_coverage";
87
95
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
88
96
  readonly when: "TLD has no RDAP server in the IANA bootstrap — cannot perform lookup.";
@@ -90,8 +98,8 @@ export declare const appOptions: {
90
98
  }, {
91
99
  readonly reason: "domain_not_found";
92
100
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
93
- readonly when: "RDAP server returned 404 — domain is not registered.";
94
- readonly recovery: "The domain is not registered. Use whois_check_availability to confirm availability for registration.";
101
+ readonly when: "RDAP server returned 404 — no domain record found.";
102
+ readonly recovery: "Use whois_check_availability for record status; a missing record does not establish registration eligibility.";
95
103
  }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
96
104
  domain: import("zod").ZodString;
97
105
  types: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodEnum<{
@@ -146,10 +154,12 @@ export declare const appOptions: {
146
154
  }>;
147
155
  }, import("zod/v4/core").$strip>>;
148
156
  }, import("zod/v4/core").$strip>, readonly [{
149
- readonly reason: "invalid_domain";
150
- readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
151
- readonly when: "Input is not a valid FQDN.";
152
- readonly recovery: "Provide a valid fully-qualified domain name like \"example.com\" or \"sub.example.org\".";
157
+ readonly reason: "dns_response_failed";
158
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ServiceUnavailable;
159
+ readonly when: "Resolvers return a DNS failure status or truncated response after retry and fallback.";
160
+ readonly recovery: "Retry the DNS query later or choose another record type.";
161
+ readonly thrownBy: "service";
162
+ readonly retryable: true;
153
163
  }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
154
164
  ip: import("zod").ZodString;
155
165
  }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
@@ -158,6 +168,7 @@ export declare const appOptions: {
158
168
  start_address: import("zod").ZodOptional<import("zod").ZodString>;
159
169
  end_address: import("zod").ZodOptional<import("zod").ZodString>;
160
170
  cidr: import("zod").ZodOptional<import("zod").ZodString>;
171
+ cidrs: import("zod").ZodArray<import("zod").ZodString>;
161
172
  ip_version: import("zod").ZodOptional<import("zod").ZodString>;
162
173
  name: import("zod").ZodOptional<import("zod").ZodString>;
163
174
  type: import("zod").ZodOptional<import("zod").ZodString>;
@@ -165,8 +176,21 @@ export declare const appOptions: {
165
176
  org_name: import("zod").ZodOptional<import("zod").ZodString>;
166
177
  abuse_email: import("zod").ZodOptional<import("zod").ZodString>;
167
178
  ptr: import("zod").ZodNullable<import("zod").ZodString>;
179
+ ptr_status: import("zod").ZodEnum<{
180
+ failed: "failed";
181
+ found: "found";
182
+ not_found: "not_found";
183
+ }>;
184
+ ptr_source_error: import("zod").ZodOptional<import("zod").ZodString>;
168
185
  rdap_source: import("zod").ZodOptional<import("zod").ZodString>;
169
186
  }, import("zod/v4/core").$strip>, readonly [{
187
+ readonly reason: "ip_no_rdap_route";
188
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
189
+ readonly when: "A successfully loaded IANA bootstrap has no covering RIR route.";
190
+ readonly recovery: "Try another address; this range has no RDAP route in the IANA bootstrap.";
191
+ readonly retryable: false;
192
+ readonly thrownBy: "service";
193
+ }, {
170
194
  readonly reason: "invalid_ip";
171
195
  readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
172
196
  readonly when: "Input is not a valid IPv4, IPv6, or CIDR address.";
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAYH,eAAO,MAAM,UAAU;;;IAGrB;;;;;;OAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwBuB,CAAC"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,eAAO,MAAM,UAAU;;;IAGrB;;;;;;OAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyBuB,CAAC"}
package/dist/app.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * the entry point starts and tests assert against.
4
4
  * @module app
5
5
  */
6
+ import { getServerConfig } from './config/server-config.js';
6
7
  import { whoisCheckAvailability } from './mcp-server/tools/definitions/whois-check-availability.tool.js';
7
8
  import { whoisGetDns } from './mcp-server/tools/definitions/whois-get-dns.tool.js';
8
9
  import { whoisGetDossier } from './mcp-server/tools/definitions/whois-get-dossier.tool.js';
@@ -33,13 +34,14 @@ export const appOptions = {
33
34
  resources: [],
34
35
  prompts: [],
35
36
  instructions: 'whois-mcp-server: domain and IP intelligence via RDAP and DNS-over-HTTPS. No API keys required.\n' +
36
- '- whois_lookup_domain: full registration record (registrar, dates, status, nameservers)\n' +
37
- '- whois_check_availability: is a domain available to register? (RDAP 404 = available)\n' +
38
- '- whois_get_dns: DNS records for any hostname (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR)\n' +
39
- '- whois_lookup_ip: IP/CIDR netblock, org, abuse contact, and PTR via RIR RDAP\n' +
37
+ '- whois_lookup_domain: registry record; include_registrar opts into separate registrar contact detail\n' +
38
+ '- whois_check_availability: registered/not_found/unknown RDAP record status; absence does not establish registration eligibility\n' +
39
+ '- whois_get_dns: ASCII DNS owner names, including underscores and one root dot (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR)\n' +
40
+ '- whois_lookup_ip: IP/CIDR netblock CIDRs, org, abuse contact, and found/not_found/failed PTR outcome\n' +
40
41
  '- whois_lookup_asn: ASN to org/RIR resolution\n' +
41
- '- whois_get_dossier: one-call domain triage — registration + DNS in parallel with inferred signals',
42
+ '- whois_get_dossier: domain triage with partial source errors; absent DNSSEC/privacy evidence stays unknown',
42
43
  setup(core) {
44
+ getServerConfig();
43
45
  initRdapService(core.config, core.storage);
44
46
  initDohService(core.config, core.storage);
45
47
  },
package/dist/app.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,WAAW,EAAE,MAAM,sDAAsD,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,0DAA0D,CAAC;AAC3F,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,wDAAwD,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,kBAAkB;IACxB,KAAK,EAAE,kBAAkB;IACzB;;;;;;OAMG;IACH,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,iBAAiB;QACjB,sBAAsB;QACtB,WAAW;QACX,aAAa;QACb,cAAc;QACd,eAAe;KAChB;IACD,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;IACX,YAAY,EACV,mGAAmG;QACnG,2FAA2F;QAC3F,yFAAyF;QACzF,8FAA8F;QAC9F,iFAAiF;QACjF,iDAAiD;QACjD,oGAAoG;IACtG,KAAK,CAAC,IAAI;QACR,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;CACyB,CAAC"}
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,iEAAiE,CAAC;AACzG,OAAO,EAAE,WAAW,EAAE,MAAM,sDAAsD,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,0DAA0D,CAAC;AAC3F,OAAO,EAAE,cAAc,EAAE,MAAM,yDAAyD,CAAC;AACzF,OAAO,EAAE,iBAAiB,EAAE,MAAM,4DAA4D,CAAC;AAC/F,OAAO,EAAE,aAAa,EAAE,MAAM,wDAAwD,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAElE,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,kBAAkB;IACxB,KAAK,EAAE,kBAAkB;IACzB;;;;;;OAMG;IACH,WAAW,EAAE,WAAW;IACxB,KAAK,EAAE;QACL,iBAAiB;QACjB,sBAAsB;QACtB,WAAW;QACX,aAAa;QACb,cAAc;QACd,eAAe;KAChB;IACD,SAAS,EAAE,EAAE;IACb,OAAO,EAAE,EAAE;IACX,YAAY,EACV,mGAAmG;QACnG,yGAAyG;QACzG,oIAAoI;QACpI,+HAA+H;QAC/H,yGAAyG;QACzG,iDAAiD;QACjD,6GAA6G;IAC/G,KAAK,CAAC,IAAI;QACR,eAAe,EAAE,CAAC;QAClB,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QAC3C,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC;CACyB,CAAC"}
@@ -1,7 +1,13 @@
1
1
  /**
2
- * @fileoverview Shared FQDN validation used across tool handlers.
2
+ * @fileoverview Serializable identifier grammars shared by lookup schemas.
3
3
  * @module mcp-server/tools/definitions/_fqdn
4
4
  */
5
- /** Simple FQDN validation: labels separated by dots, no consecutive dots, length limits */
6
- export declare function isValidFqdn(domain: string): boolean;
5
+ /** Registration hostname grammar, with exact end-of-input matching. */
6
+ export declare const REGISTRATION_NAME_PATTERN: RegExp;
7
+ /** DNS presentation-name grammar: underscore labels and one optional root dot. */
8
+ export declare const DNS_NAME_PATTERN: RegExp;
9
+ /** Conservative IP/CIDR shape; complete address and prefix validity are semantic. */
10
+ export declare const IP_PATTERN: RegExp;
11
+ /** Accepted ASN spelling, preserving padding, case and surrounding whitespace. */
12
+ export declare const ASN_PATTERN: RegExp;
7
13
  //# sourceMappingURL=_fqdn.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"_fqdn.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_fqdn.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAQnD"}
1
+ {"version":3,"file":"_fqdn.d.ts","sourceRoot":"","sources":["../../../../src/mcp-server/tools/definitions/_fqdn.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,uEAAuE;AACvE,eAAO,MAAM,yBAAyB,QAErC,CAAC;AACF,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,QACwE,CAAC;AACtG,qFAAqF;AACrF,eAAO,MAAM,UAAU,QAA2C,CAAC;AACnE,kFAAkF;AAClF,eAAO,MAAM,WAAW,QAA+C,CAAC"}
@@ -1,18 +1,14 @@
1
1
  /**
2
- * @fileoverview Shared FQDN validation used across tool handlers.
2
+ * @fileoverview Serializable identifier grammars shared by lookup schemas.
3
3
  * @module mcp-server/tools/definitions/_fqdn
4
4
  */
5
- /** Simple FQDN validation: labels separated by dots, no consecutive dots, length limits */
6
- export function isValidFqdn(domain) {
7
- if (!domain || domain.length > 253)
8
- return false;
9
- const labels = domain.split('.');
10
- if (labels.length < 2)
11
- return false;
12
- return labels.every((label) => {
13
- if (!label || label.length > 63)
14
- return false;
15
- return /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/.test(label);
16
- });
17
- }
5
+ const HOST_LABEL = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?';
6
+ /** Registration hostname grammar, with exact end-of-input matching. */
7
+ export const REGISTRATION_NAME_PATTERN = new RegExp(`^${HOST_LABEL}(?:\\.${HOST_LABEL})+(?![\\s\\S])`);
8
+ /** DNS presentation-name grammar: underscore labels and one optional root dot. */
9
+ export const DNS_NAME_PATTERN = /^(?=[A-Za-z0-9_.-]{1,253}\.?(?![\s\S]))[A-Za-z0-9_-]{1,63}(?:\.[A-Za-z0-9_-]{1,63})+\.?(?![\s\S])/;
10
+ /** Conservative IP/CIDR shape; complete address and prefix validity are semantic. */
11
+ export const IP_PATTERN = /^[0-9A-Fa-f:.]+(?:\/[0-9]+)?(?![\s\S])/;
12
+ /** Accepted ASN spelling, preserving padding, case and surrounding whitespace. */
13
+ export const ASN_PATTERN = /^\s*(?:[Aa][Ss]\s*)?\+?[0-9]+\s*(?![\s\S])/;
18
14
  //# sourceMappingURL=_fqdn.js.map