@cyanheads/whois-mcp-server 0.1.4 → 0.1.6

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 (45) hide show
  1. package/AGENTS.md +61 -28
  2. package/CLAUDE.md +61 -28
  3. package/Dockerfile +44 -33
  4. package/README.md +41 -65
  5. package/changelog/0.1.x/0.1.5.md +35 -0
  6. package/changelog/0.1.x/0.1.6.md +41 -0
  7. package/changelog/template.md +9 -26
  8. package/dist/app.d.ts +216 -0
  9. package/dist/app.d.ts.map +1 -0
  10. package/dist/app.js +47 -0
  11. package/dist/app.js.map +1 -0
  12. package/dist/index.js +2 -33
  13. package/dist/index.js.map +1 -1
  14. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.d.ts.map +1 -1
  15. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.js +1 -3
  16. package/dist/mcp-server/tools/definitions/whois-check-availability.tool.js.map +1 -1
  17. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.d.ts +17 -0
  18. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.d.ts.map +1 -1
  19. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.js +21 -14
  20. package/dist/mcp-server/tools/definitions/whois-get-dns.tool.js.map +1 -1
  21. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.js +39 -31
  23. package/dist/mcp-server/tools/definitions/whois-get-dossier.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.d.ts +4 -2
  25. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.js +8 -5
  27. package/dist/mcp-server/tools/definitions/whois-lookup-asn.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.d.ts.map +1 -1
  29. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.js +2 -5
  30. package/dist/mcp-server/tools/definitions/whois-lookup-domain.tool.js.map +1 -1
  31. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.d.ts +4 -2
  32. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.d.ts.map +1 -1
  33. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.js +13 -12
  34. package/dist/mcp-server/tools/definitions/whois-lookup-ip.tool.js.map +1 -1
  35. package/dist/services/doh/doh-service.d.ts.map +1 -1
  36. package/dist/services/doh/doh-service.js +10 -12
  37. package/dist/services/doh/doh-service.js.map +1 -1
  38. package/dist/services/doh/types.d.ts +4 -0
  39. package/dist/services/doh/types.d.ts.map +1 -1
  40. package/dist/services/rdap/rdap-service.d.ts +10 -3
  41. package/dist/services/rdap/rdap-service.d.ts.map +1 -1
  42. package/dist/services/rdap/rdap-service.js +67 -109
  43. package/dist/services/rdap/rdap-service.js.map +1 -1
  44. package/package.json +13 -11
  45. package/server.json +3 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  <div align="center">
9
9
 
10
- [![Version](https://img.shields.io/badge/Version-0.1.4-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.0.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.0-blueviolet.svg?style=flat-square)](https://bun.sh/)
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/)
11
11
 
12
12
  </div>
13
13
 
@@ -21,9 +21,11 @@
21
21
 
22
22
  ---
23
23
 
24
- ## Tools
24
+ ## Overview
25
25
 
26
- Six tools covering domain intelligence, DNS, and IP/ASN resolution:
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.
27
+
28
+ ### Tools
27
29
 
28
30
  | Tool | Description |
29
31
  |:-----|:------------|
@@ -34,97 +36,70 @@ Six tools covering domain intelligence, DNS, and IP/ASN resolution:
34
36
  | `whois_lookup_asn` | Resolve an ASN to its org name, country, and RIR source |
35
37
  | `whois_get_dossier` | One-call domain triage — registration + DNS in parallel, normalized into a single record with factual signals |
36
38
 
37
- ### `whois_lookup_domain`
39
+ ---
38
40
 
39
- Look up a domain's full RDAP registration record.
41
+ ## Capability reference
40
42
 
41
- - RDAP-first via IANA auto-bootstrap — automatically selects the correct registry RDAP server per TLD
42
- - Returns registrar, creation/expiry dates, nameservers, EPP status codes, DNSSEC delegation flag
43
- - Surfaces `registrant_redacted: true` explicitly when privacy redaction is in effect (standard post-GDPR for gTLDs)
44
- - Returns `rdap_coverage: false` for TLDs without RDAP coverage rather than silently failing
45
- - Includes `last_update_of_rdap_db` event timestamp for data freshness transparency
43
+ ### `whois_lookup_domain` <sub>tool</sub>
46
44
 
47
- ---
45
+ - Accepts a fully qualified domain name and selects the registry RDAP server through IANA bootstrap.
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.
48
47
 
49
- ### `whois_check_availability`
48
+ ---
50
49
 
51
- Check whether a domain name is registered or available to register.
50
+ ### `whois_check_availability` <sub>tool</sub>
52
51
 
53
- - RDAP 404 = available (`available: true`) — exploits the RDAP spec's intended behavior
54
- - Returns `available: false` with `registrar` and `expiry_date` when registered
55
- - Returns `available: null` with `rdap_coverage: false` for TLDs without RDAP — cannot determine availability
56
- - Optimized for bulk name sweeps — thin response, no unnecessary fields
52
+ - 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.
57
54
 
58
55
  ---
59
56
 
60
- ### `whois_get_dns`
61
-
62
- Fetch DNS records via DNS-over-HTTPS.
57
+ ### `whois_get_dns` <sub>tool</sub>
63
58
 
64
- - Cloudflare primary, NextDNS fallback (CAA records always use NextDNS — Cloudflare returns raw hex wire format for them)
65
- - Supports A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, PTR — multiple types in one call
66
- - Returns records with TTLs and the resolving source (`cloudflare` or `nextdns`)
67
- - `nxdomain: true` in result (not an error) when the domain doesn't exist in DNS
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.
60
+ - 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.
68
61
 
69
62
  ---
70
63
 
71
- ### `whois_lookup_ip`
64
+ ### `whois_lookup_ip` <sub>tool</sub>
72
65
 
73
- Look up an IP address or CIDR block via RIR RDAP.
74
-
75
- - Auto-routes to the correct RIR (ARIN, RIPE, APNIC, LACNIC, AFRINIC) via IANA IP bootstrap
76
- - Returns netblock CIDR, org name, country, abuse contact email
77
- - Fetches PTR (reverse DNS) via DoH as a best-effort step — `ptr: null` on failure, not an error
78
- - Validates and rejects private/reserved ranges (RFC 1918, loopback, link-local) with a clear error
66
+ - 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.
68
+ - `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.
79
69
 
80
70
  ---
81
71
 
82
- ### `whois_lookup_asn`
83
-
84
- Resolve an ASN to its org name, country, and RIR.
72
+ ### `whois_lookup_asn` <sub>tool</sub>
85
73
 
86
- - Accepts `AS15169` or bare integer `15169` format
87
- - Routes to the correct RIR RDAP endpoint via IANA ASN bootstrap
88
- - Returns `name`, `org`, `country`, `rir`, `start_autnum`, `end_autnum`
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`.
75
+ - Returns `name`, `org_name`, `country`, `rir`, `start_autnum`, and `end_autnum`; throws `asn_not_found` when no ASN record exists.
89
76
 
90
77
  ---
91
78
 
92
- ### `whois_get_dossier`
79
+ ### `whois_get_dossier` <sub>tool</sub>
93
80
 
94
- One-call domain triage aggregating registration and DNS data in parallel.
95
-
96
- - Runs RDAP domain lookup and DoH (A, MX, NS, TXT) in parallel via `Promise.allSettled`
97
- - Inferred signals: `age_days`, `privacy_redacted`, `registrar`, `ns_provider` (from NS records), `mx_provider` (from MX records)
98
- - No synthesized risk scores — factual signals only; the agent decides the verdict
99
- - Partial results surfaced when one leg fails (`source_error` on the failed leg)
100
- - Both-legs-fail throws `ServiceUnavailable`; individual leg failures are data, not errors
81
+ - Accepts a fully qualified domain name for parallel registration and A/MX/NS/TXT lookups.
82
+ - 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.
101
83
 
102
84
  ---
103
85
 
104
86
  ## Features
105
87
 
106
- Built on [`@cyanheads/mcp-ts-core`](https://www.npmjs.com/package/@cyanheads/mcp-ts-core):
107
-
108
- - Declarative tool definitions — single file per tool, framework handles registration and validation
109
- - Unified error handling — handlers throw, framework catches, classifies, and formats
110
- - Pluggable auth: `none`, `jwt`, `oauth`
111
- - Swappable storage backends: `in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`
112
- - Structured logging with optional OpenTelemetry tracing
113
- - STDIO and Streamable HTTP transports
88
+ Built on [`@cyanheads/mcp-ts-core`](https://github.com/cyanheads/mcp-ts-core): stdio and Streamable HTTP transports, pluggable auth (`none` / `jwt` / `oauth`), swappable storage (`in-memory`, `filesystem`, `Supabase`, `Cloudflare KV/R2/D1`), structured logging with optional OpenTelemetry tracing.
114
89
 
115
- Domain and network intelligence:
90
+ RDAP / DNS-specific:
116
91
 
117
- - RDAP over HTTPS — no port-43 TCP dependency, runs on Node, Bun, and Cloudflare Workers
118
- - IANA bootstrap auto-selection — correct registry RDAP server picked per TLD, RIR, or ASN range; bootstrap JSON cached (TTL 24h) in tenant state
119
- - DNS-over-HTTPS via Cloudflare and NextDNS — resilient dual-provider with per-type routing (NextDNS for CAA; Cloudflare for all others)
92
+ - RDAP over HTTPS — no port-43 TCP dependency
93
+ - IANA bootstrap auto-selection — correct registry RDAP server picked per TLD, RIR, or ASN range; bootstrap JSON cached in-memory for 24h
94
+ - DNS-over-HTTPS via Cloudflare and NextDNS — dual-provider with per-type routing (NextDNS for CAA; Cloudflare for all others) and automatic fallback
120
95
  - No API keys required — all sources (IANA, registry RDAP endpoints, RIR RDAP, Cloudflare DoH, NextDNS DoH) are public and keyless
121
96
 
122
97
  Agent-friendly output:
123
98
 
124
- - Explicit coverage signals — `rdap_coverage: false` tells the agent the TLD lacks RDAP rather than returning a confusing error
99
+ - 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
125
100
  - Privacy redaction surfaced as a field — `registrant_redacted: true` rather than silently absent contact data
126
- - Partial failure model — `whois_get_dossier` marks individual legs with `source_error` and continues; only both-legs-fail escalates to an error
127
- - Factual signals, not scores — `age_days`, `privacy_redacted`, `ns_provider`, `mx_provider` are real data; agents chain into threat-intel or risk servers for enrichment
101
+ - 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
+ - Factual signals, not scores — `age_days`, `privacy_redacted`, `ns_provider`, `mx_provider` are real data, not synthesized risk scores
128
103
 
129
104
  ---
130
105
 
@@ -193,7 +168,7 @@ MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
193
168
 
194
169
  ### Prerequisites
195
170
 
196
- - [Bun v1.3.0](https://bun.sh/) or higher (or Node.js v24+).
171
+ - [Bun v1.4.0](https://bun.sh/) or higher (or Node.js v24+).
197
172
  - No API keys required — all data sources are public.
198
173
 
199
174
  ### Installation
@@ -279,7 +254,8 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
279
254
 
280
255
  | Path | Purpose |
281
256
  |:-----|:--------|
282
- | `src/index.ts` | `createApp()` entry point — registers tools and inits services. |
257
+ | `src/index.ts` | Entry point — starts the app. |
258
+ | `src/app.ts` | `createApp()` options — registers tools, inits services, declares the session mode. |
283
259
  | `src/config/` | Server-specific environment variable parsing and validation (Zod). |
284
260
  | `src/services/rdap/` | RDAP client — IANA bootstrap cache, domain/IP/ASN lookup, retry. |
285
261
  | `src/services/doh/` | DNS-over-HTTPS client — Cloudflare primary, NextDNS fallback. |
@@ -294,15 +270,15 @@ The Dockerfile defaults to HTTP transport, stateless session mode, and logs to `
294
270
  See [`CLAUDE.md`](./CLAUDE.md) for development guidelines and architectural rules. The short version:
295
271
 
296
272
  - Handlers throw, framework catches — no `try/catch` in tool logic
297
- - Use `ctx.log` for request-scoped logging, `ctx.state` for tenant-scoped storage (IANA bootstrap cache)
298
- - Register new tools via `src/index.ts` tools array
273
+ - Use `ctx.log` for request-scoped logging
274
+ - Register new tools via `src/app.ts`'s `tools` array
299
275
  - Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields
300
276
 
301
277
  ---
302
278
 
303
279
  ## Contributing
304
280
 
305
- Issues and pull requests are welcome. Run checks and tests before submitting:
281
+ Issues are welcome. Run checks and tests before submitting:
306
282
 
307
283
  ```sh
308
284
  bun run devcheck
@@ -0,0 +1,35 @@
1
+ ---
2
+ summary: "Adopt mcp-ts-core 0.13.6 — the server resolves to a stateless session posture however it is launched, and an out-of-schema argument fails as InvalidParams (-32602) with a Recovery line instead of ValidationError (-32007). The Bun floor rises to 1.4.0."
3
+ breaking: true
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.5 — 2026-09-20
8
+
9
+ ## Added
10
+
11
+ - **`.github/FUNDING.yml`** — GitHub Sponsors and Buy Me a Coffee.
12
+ - **`bun run audit:fix`** — `bun audit fix`, the first response to a transitive advisory, ahead of `audit:refresh`.
13
+ - **`devcheck.config.json` `lint` block** — where `lint.truncationAllowlist` now lives, in place of a per-script `MCP_LINT_TRUNCATION_ALLOWLIST` env var.
14
+ - **`tests/app.test.ts`** — asserts the resolved session posture and the registered tool surface against a live `createApp()`.
15
+
16
+ ## Changed
17
+
18
+ - **The session mode is `stateless` however the server is launched** ([#11](https://github.com/cyanheads/whois-mcp-server/issues/11)). `src/app.ts` declares `createApp({ sessionMode: 'stateless' })` and `.env.example` sets the variable actively, so the `Dockerfile` line restates the posture rather than overriding it; previously only the container was stateless and a `bunx`, `npm start`, or from-source run resolved to `stateful`. No tool holds per-session state or calls `ctx.requestInput`.
19
+ - **An out-of-schema argument now fails as `InvalidParams` (`-32602`), not `ValidationError` (`-32007`).** The rejection carries `data.reason: "invalid_arguments"` and its text ends with a `Recovery: …` line naming the field to fix. A client branching on the old code needs updating.
20
+ - **Tool error text closes with `(reason <reason> · not retryable)`** whenever the error carries a `reason` or a boolean `retryable`. `structuredContent` is unchanged.
21
+ - **A caller that disconnects mid-call classifies as `RequestCancelled` (`-32011`)** rather than `InternalError`, and logs at `info`. An upstream 500 on a DNS-over-HTTPS query now retries instead of failing on the first attempt.
22
+ - **Bun engines floor `>=1.3.0` → `>=1.4.0`**, required by the framework.
23
+ - **`whois_lookup_ip` and `whois_lookup_asn` mark their service-thrown contract entries `thrownBy: 'service'`** — `private_range`, `ip_not_found`, `invalid_asn`, `asn_not_found` (lint-only metadata; the advertised error surface is unchanged).
24
+ - **The framework skill tree moved from `skills/` to `framework-skills/`** — Claude Code and Codex auto-load a plugin's root `skills/`, so this server's plugin manifests no longer hand its development skills to every agent that installs it.
25
+ - **`createApp()` options live in `src/app.ts` as `appOptions`**; `src/index.ts` is the entry point that starts them.
26
+
27
+ ## Dependencies
28
+
29
+ - `@cyanheads/mcp-ts-core` `^0.12.3` → `^0.13.6`
30
+ - `zod` `^4.4.3` → `^4.6.5`
31
+ - `vitest` (dev) `^4.1.11` → `^5.0.1`
32
+ - `@biomejs/biome` (dev) `^2.5.9` → `^2.5.14`
33
+ - `@types/node` (dev) `^26` → `^26.6.1`
34
+ - `ignore` (dev) `^7.0.6` → `^7.0.9`
35
+ - `tsc-alias` (dev) `^1.9.2` → `^1.9.5`
@@ -0,0 +1,41 @@
1
+ ---
2
+ summary: "Lookup inputs are validated whole before any request, DNS results attribute each query to its resolver, and the dossier renders every TXT record and labels providers by domain."
3
+ breaking: false
4
+ security: false
5
+ ---
6
+
7
+ # 0.1.6 — 2026-09-30
8
+
9
+ ## Added
10
+
11
+ - **`whois_get_dns` `query_sources`** ([#10](https://github.com/cyanheads/whois-mcp-server/issues/10)) — one `{type, source}` entry per requested type, in request order, naming the resolver whose response was consumed, empty answers and NXDOMAIN included. Duplicate types are queried once; `source` stays the aggregate (`cloudflare` when any query used Cloudflare, otherwise `nextdns`).
12
+ - **Contract tests over the real services** — `tests/helpers/upstream.ts` replaces only outbound HTTP, and `tests/fixtures/` holds captured IANA, ARIN, and APNIC responses that the IP suite replays.
13
+
14
+ ## Changed
15
+
16
+ - **Framework `@cyanheads/mcp-ts-core` `^0.13.6` → `^0.13.10`** — tool errors carry `data.requestId` and their text closes with `request <id>`, an integer sent for a string argument is repaired to its digits, and framework-built errors keep stack traces and request context out of `data`.
17
+ - **Recovery hints reach the wire through the framework's contract fill** — throws whose `errors[]` entry supplies the hint no longer forward `ctx.recoveryFor`.
18
+ - **Docker image** — production dependencies and the OpenTelemetry peers install in a `deps` stage on the build platform through `scripts/install-otel.ts`, at the framework's declared peer ranges, so no target-platform stage runs Bun while building. The unread `MCP_FORCE_CONSOLE_LOGGING` is gone.
19
+ - **Packaging** — `package.json` `files` carries `!dist/*.mcpb`, keeping a built bundle out of the npm tarball, and `.mcpbignore` excludes `docs/idea.md` and `logs/` from the bundle.
20
+ - **Tooling synced from the framework** — `lint:packaging` checks the bundle exclusion, `manifest.json` version parity, and the Dockerfile build platform; `lint:mcp` fails on a definition it cannot import; `release:github -- --check` validates the tag annotation.
21
+ - **Docs** — README tool sections state each tool's input and output contract, `docs/design.md` records the IP exclusion policy, and `CLAUDE.md` / `AGENTS.md` plus 31 framework skills follow the 0.13.10 templates.
22
+
23
+ ## Fixed
24
+
25
+ - **`whois_lookup_asn` validates the whole token** ([#5](https://github.com/cyanheads/whois-mcp-server/issues/5)) — a decimal ASN from 1 to 4294967295 with an optional `AS` prefix and leading `+`. `AS15169abc`, `1.5`, and out-of-range values fail as `invalid_asn` before any request instead of resolving a truncated number.
26
+ - **`whois_lookup_ip` parses IP and CIDR input completely** ([#6](https://github.com/cyanheads/whois-mcp-server/issues/6)) — extra slashes, trailing text, whitespace, zone IDs, brackets, and malformed IPv6 fail as `invalid_ip` before any request. A CIDR prefix is one decimal within 0–32 or 0–128.
27
+ - **`private_range` follows the numeric address** — equivalent IPv6 spellings classify alike (`0:0:0:0:0:0:0:1`, `fe90::1`), `::10` is no longer excluded, and the tool description lists the excluded ranges.
28
+ - **IPv6 with a dotted IPv4 tail is accepted** — `2001:db8::192.0.2.1` resolves with a 32-nibble PTR name, and IPv4-mapped addresses (`::ffff:8.8.8.8`) are classified, routed, and PTR-resolved as their embedded IPv4.
29
+ - **`whois_get_dossier` renders every TXT value** ([#8](https://github.com/cyanheads/whois-mcp-server/issues/8)) — `content[]` dropped values past the third. Each value now sits in its own fence, longer than any backtick run inside it.
30
+ - **`whois_get_dns` rejects `types: []`** ([#9](https://github.com/cyanheads/whois-mcp-server/issues/9)) — it fails as `InvalidParams` (`-32602`) before any query instead of reporting a NextDNS lookup that never ran. Omitting `types` still fetches A, AAAA, MX, TXT, NS.
31
+ - **Provider and registry labels match whole domains** ([#12](https://github.com/cyanheads/whois-mcp-server/issues/12)) — `ns_provider` and `mx_provider` compare hostnames at label boundaries with the MX priority removed, and `rdap_source` / `rir` read only the self link's hostname. `ns1.hostname.com` no longer reads as Name.com; Fastmail's `messagingengine.com` is recognized.
32
+ - **`whois_lookup_domain` description** — a TLD without RDAP coverage throws `rdap_no_coverage`; the description previously promised `rdap_coverage: false`.
33
+
34
+ ## Dependencies
35
+
36
+ - `@cyanheads/mcp-ts-core` `^0.13.6` → `^0.13.10`
37
+ - `@socketsecurity/bun-security-scanner` (dev) `^1.1.2` → `^1.1.3`
38
+ - `@types/node` (dev) `^26.6.1` → `^26.6.3`
39
+ - `ignore` (dev) `^7.0.9` → `^7.0.10`
40
+ - `vitest` (dev) `^5.0.1` → `^5.0.2`
41
+ - Bun `1.4.0` → `1.4.2` (`packageManager`, Docker base images)
@@ -6,8 +6,8 @@
6
6
 
7
7
  # Required. One-line GitHub Release-style headline. 350 character cap — a
8
8
  # ceiling, not a target. Default short and scannable. Don't pad, don't stitch
9
- # unrelated changes with commas/semicolons into an inventory — pick the
10
- # headline, like a tag's theme line. Quotes required: unquoted YAML treats
9
+ # unrelated changes with commas/semicolons into an inventory — pick the one
10
+ # headline the release is about. Quotes required: unquoted YAML treats
11
11
  # `: ` inside the value as a key separator and fails GitHub's strict parser.
12
12
  summary: ""
13
13
 
@@ -117,30 +117,13 @@ security: false
117
117
  in that unrelated item's metadata.
118
118
 
119
119
  TAG ANNOTATIONS — the annotated tag body renders as the GitHub Release body
120
- via `gh release create --notes-from-tag`. The tag is a derivative of this
121
- changelog entry — a condensed, scannable version, not a copy. Format:
122
-
123
- <theme — omit version number, GitHub prepends it>
124
- ← blank line
125
- <1-2 sentence context: what this release does>
126
- ← blank line
127
- Dependency bumps: ← section header
128
- ← blank line
129
- - `@cyanheads/mcp-ts-core` ^0.9.1 → ^0.9.6 ← bullet
130
- ← blank line
131
- Changed: ← only sections with entries
132
- ← blank line
133
- - `format()` output includes `query` in text mode
134
- ← blank line
135
- Added:
136
- ← blank line
137
- - `manifest.json` scaffolded for MCPB bundle support
138
- - Install badges (Claude Desktop, Cursor, VS Code)
139
- ← blank line
140
- <N> tests pass; `bun run devcheck` clean. ← footer
141
-
142
- Never a flat comma-separated string. Always structured markdown with
143
- sections. The tag must scan well as a rendered GitHub Release page.
120
+ via `gh release create --notes-from-tag`. It is a condensed digest of this
121
+ entry, never a copy, and its format is owned by the `release-and-publish`
122
+ skill (step 4, "Create the annotated tag"): a short subject line without the
123
+ version, flat headline bullets — no Keep-a-Changelog section headers, no
124
+ gates line — at most one deps line, issue backlinks, and the changelog link
125
+ last. In release-PR mode the `git-wrapup` skill authors those bullets as the
126
+ PR body's `## Changes` and the tag copies them.
144
127
  -->
145
128
 
146
129
  ## Added
package/dist/app.d.ts ADDED
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @fileoverview whois-mcp-server application wiring — the `createApp()` options
3
+ * the entry point starts and tests assert against.
4
+ * @module app
5
+ */
6
+ export declare const appOptions: {
7
+ name: string;
8
+ title: string;
9
+ /**
10
+ * No per-session state, and no handler calls `ctx.requestInput` — the session
11
+ * store and the per-session `McpServer` allocation are pure overhead, and the
12
+ * process scales horizontally without them. Declared here so the posture holds
13
+ * however the server is launched; a deployment's `MCP_SESSION_MODE` still wins
14
+ * when it carries a meaningful value.
15
+ */
16
+ sessionMode: "stateless";
17
+ tools: (import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
18
+ domain: import("zod").ZodString;
19
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
20
+ domain: import("zod").ZodString;
21
+ available: import("zod").ZodNullable<import("zod").ZodBoolean>;
22
+ rdap_coverage: import("zod").ZodBoolean;
23
+ registrar: import("zod").ZodOptional<import("zod").ZodString>;
24
+ expiry_date: import("zod").ZodOptional<import("zod").ZodString>;
25
+ }, import("zod/v4/core").$strip>, readonly [{
26
+ readonly reason: "invalid_domain";
27
+ 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\".";
30
+ }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
31
+ domain: import("zod").ZodString;
32
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
33
+ domain: import("zod").ZodString;
34
+ rdap_coverage: import("zod").ZodNullable<import("zod").ZodBoolean>;
35
+ registered: import("zod").ZodNullable<import("zod").ZodBoolean>;
36
+ registrar: import("zod").ZodOptional<import("zod").ZodString>;
37
+ created_date: import("zod").ZodOptional<import("zod").ZodString>;
38
+ expiry_date: import("zod").ZodOptional<import("zod").ZodString>;
39
+ age_days: import("zod").ZodNullable<import("zod").ZodNumber>;
40
+ dnssec_signed: import("zod").ZodNullable<import("zod").ZodBoolean>;
41
+ privacy_redacted: import("zod").ZodNullable<import("zod").ZodBoolean>;
42
+ status: import("zod").ZodArray<import("zod").ZodString>;
43
+ nameservers: import("zod").ZodArray<import("zod").ZodString>;
44
+ a_records: import("zod").ZodArray<import("zod").ZodString>;
45
+ mx_records: import("zod").ZodArray<import("zod").ZodString>;
46
+ ns_records: import("zod").ZodArray<import("zod").ZodString>;
47
+ txt_records: import("zod").ZodArray<import("zod").ZodString>;
48
+ dns_nxdomain: import("zod").ZodNullable<import("zod").ZodBoolean>;
49
+ ns_provider: import("zod").ZodNullable<import("zod").ZodString>;
50
+ mx_provider: import("zod").ZodNullable<import("zod").ZodString>;
51
+ rdap_source_error: import("zod").ZodOptional<import("zod").ZodString>;
52
+ dns_source_error: import("zod").ZodOptional<import("zod").ZodString>;
53
+ }, 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
+ readonly reason: "both_legs_failed";
60
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ServiceUnavailable;
61
+ readonly when: "Both RDAP and DoH legs failed — no data could be retrieved.";
62
+ readonly recovery: "Both RDAP and DNS services are unavailable. Retry in a few minutes or check network connectivity.";
63
+ }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
64
+ domain: import("zod").ZodString;
65
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
66
+ domain: import("zod").ZodString;
67
+ rdap_coverage: import("zod").ZodBoolean;
68
+ handle: import("zod").ZodOptional<import("zod").ZodString>;
69
+ registrar: import("zod").ZodOptional<import("zod").ZodString>;
70
+ registrar_iana_id: import("zod").ZodOptional<import("zod").ZodString>;
71
+ created_date: import("zod").ZodOptional<import("zod").ZodString>;
72
+ updated_date: import("zod").ZodOptional<import("zod").ZodString>;
73
+ expiry_date: import("zod").ZodOptional<import("zod").ZodString>;
74
+ rdap_last_updated: import("zod").ZodOptional<import("zod").ZodString>;
75
+ status: import("zod").ZodArray<import("zod").ZodString>;
76
+ nameservers: import("zod").ZodArray<import("zod").ZodString>;
77
+ dnssec_signed: import("zod").ZodBoolean;
78
+ registrant_org: import("zod").ZodOptional<import("zod").ZodString>;
79
+ registrant_redacted: import("zod").ZodBoolean;
80
+ }, 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
+ readonly reason: "rdap_no_coverage";
87
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
88
+ readonly when: "TLD has no RDAP server in the IANA bootstrap — cannot perform lookup.";
89
+ readonly recovery: "This TLD has no RDAP coverage. Use whois_check_availability to test registration status when possible.";
90
+ }, {
91
+ readonly reason: "domain_not_found";
92
+ 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.";
95
+ }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
96
+ domain: import("zod").ZodString;
97
+ types: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodEnum<{
98
+ A: "A";
99
+ AAAA: "AAAA";
100
+ CAA: "CAA";
101
+ CNAME: "CNAME";
102
+ MX: "MX";
103
+ NS: "NS";
104
+ PTR: "PTR";
105
+ SOA: "SOA";
106
+ TXT: "TXT";
107
+ }>>>;
108
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
109
+ domain: import("zod").ZodString;
110
+ nxdomain: import("zod").ZodBoolean;
111
+ records: import("zod").ZodArray<import("zod").ZodObject<{
112
+ type: import("zod").ZodEnum<{
113
+ A: "A";
114
+ AAAA: "AAAA";
115
+ CAA: "CAA";
116
+ CNAME: "CNAME";
117
+ MX: "MX";
118
+ NS: "NS";
119
+ PTR: "PTR";
120
+ SOA: "SOA";
121
+ TXT: "TXT";
122
+ }>;
123
+ name: import("zod").ZodString;
124
+ ttl: import("zod").ZodNumber;
125
+ data: import("zod").ZodString;
126
+ }, import("zod/v4/core").$strip>>;
127
+ source: import("zod").ZodEnum<{
128
+ cloudflare: "cloudflare";
129
+ nextdns: "nextdns";
130
+ }>;
131
+ query_sources: import("zod").ZodArray<import("zod").ZodObject<{
132
+ type: import("zod").ZodEnum<{
133
+ A: "A";
134
+ AAAA: "AAAA";
135
+ CAA: "CAA";
136
+ CNAME: "CNAME";
137
+ MX: "MX";
138
+ NS: "NS";
139
+ PTR: "PTR";
140
+ SOA: "SOA";
141
+ TXT: "TXT";
142
+ }>;
143
+ source: import("zod").ZodEnum<{
144
+ cloudflare: "cloudflare";
145
+ nextdns: "nextdns";
146
+ }>;
147
+ }, import("zod/v4/core").$strip>>;
148
+ }, 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\".";
153
+ }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
154
+ ip: import("zod").ZodString;
155
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
156
+ ip: import("zod").ZodString;
157
+ handle: import("zod").ZodOptional<import("zod").ZodString>;
158
+ start_address: import("zod").ZodOptional<import("zod").ZodString>;
159
+ end_address: import("zod").ZodOptional<import("zod").ZodString>;
160
+ cidr: import("zod").ZodOptional<import("zod").ZodString>;
161
+ ip_version: import("zod").ZodOptional<import("zod").ZodString>;
162
+ name: import("zod").ZodOptional<import("zod").ZodString>;
163
+ type: import("zod").ZodOptional<import("zod").ZodString>;
164
+ country: import("zod").ZodOptional<import("zod").ZodString>;
165
+ org_name: import("zod").ZodOptional<import("zod").ZodString>;
166
+ abuse_email: import("zod").ZodOptional<import("zod").ZodString>;
167
+ ptr: import("zod").ZodNullable<import("zod").ZodString>;
168
+ rdap_source: import("zod").ZodOptional<import("zod").ZodString>;
169
+ }, import("zod/v4/core").$strip>, readonly [{
170
+ readonly reason: "invalid_ip";
171
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
172
+ readonly when: "Input is not a valid IPv4, IPv6, or CIDR address.";
173
+ readonly recovery: "Provide a valid IPv4 (e.g., \"8.8.8.8\"), IPv6 (e.g., \"2001:db8::1\"), or CIDR (e.g., \"192.0.2.0/24\").";
174
+ }, {
175
+ readonly reason: "private_range";
176
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
177
+ readonly when: "The numeric address falls within the server’s explicit excluded ranges.";
178
+ readonly recovery: "Choose an address outside the excluded ranges listed in this tool’s description.";
179
+ readonly thrownBy: "service";
180
+ }, {
181
+ readonly reason: "ip_not_found";
182
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
183
+ readonly when: "RIR RDAP returned 404 — no netblock record for this address.";
184
+ readonly recovery: "The IP address is not found in any RIR RDAP database. Try a different IP or verify it is globally routable.";
185
+ readonly thrownBy: "service";
186
+ }], undefined> | import("@cyanheads/mcp-ts-core").ToolDefinition<import("zod").ZodObject<{
187
+ asn: import("zod").ZodString;
188
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
189
+ asn: import("zod").ZodString;
190
+ handle: import("zod").ZodOptional<import("zod").ZodString>;
191
+ start_autnum: import("zod").ZodOptional<import("zod").ZodNumber>;
192
+ end_autnum: import("zod").ZodOptional<import("zod").ZodNumber>;
193
+ name: import("zod").ZodOptional<import("zod").ZodString>;
194
+ type: import("zod").ZodOptional<import("zod").ZodString>;
195
+ country: import("zod").ZodOptional<import("zod").ZodString>;
196
+ org_name: import("zod").ZodOptional<import("zod").ZodString>;
197
+ rir: import("zod").ZodOptional<import("zod").ZodString>;
198
+ }, import("zod/v4/core").$strip>, readonly [{
199
+ readonly reason: "invalid_asn";
200
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.ValidationError;
201
+ readonly when: "The complete token is not a decimal ASN from 1 to 4294967295.";
202
+ readonly recovery: "Provide a decimal integer from 1 to 4294967295, optionally prefixed with AS and +, such as \"AS15169\" or \"+15169\".";
203
+ readonly thrownBy: "service";
204
+ }, {
205
+ readonly reason: "asn_not_found";
206
+ readonly code: import("@cyanheads/mcp-ts-core/errors").JsonRpcErrorCode.NotFound;
207
+ readonly when: "RDAP returned 404 — ASN not found in any RIR.";
208
+ readonly recovery: "The ASN is not found in any RIR RDAP database. Verify the ASN number is correct and assigned.";
209
+ readonly thrownBy: "service";
210
+ }], undefined>)[];
211
+ resources: never[];
212
+ prompts: never[];
213
+ instructions: string;
214
+ setup(core: import("@cyanheads/mcp-ts-core").CoreServices<object>): void;
215
+ };
216
+ //# sourceMappingURL=app.d.ts.map
@@ -0,0 +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"}
package/dist/app.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @fileoverview whois-mcp-server application wiring — the `createApp()` options
3
+ * the entry point starts and tests assert against.
4
+ * @module app
5
+ */
6
+ import { whoisCheckAvailability } from './mcp-server/tools/definitions/whois-check-availability.tool.js';
7
+ import { whoisGetDns } from './mcp-server/tools/definitions/whois-get-dns.tool.js';
8
+ import { whoisGetDossier } from './mcp-server/tools/definitions/whois-get-dossier.tool.js';
9
+ import { whoisLookupAsn } from './mcp-server/tools/definitions/whois-lookup-asn.tool.js';
10
+ import { whoisLookupDomain } from './mcp-server/tools/definitions/whois-lookup-domain.tool.js';
11
+ import { whoisLookupIp } from './mcp-server/tools/definitions/whois-lookup-ip.tool.js';
12
+ import { initDohService } from './services/doh/doh-service.js';
13
+ import { initRdapService } from './services/rdap/rdap-service.js';
14
+ export const appOptions = {
15
+ name: 'whois-mcp-server',
16
+ title: 'whois-mcp-server',
17
+ /**
18
+ * No per-session state, and no handler calls `ctx.requestInput` — the session
19
+ * store and the per-session `McpServer` allocation are pure overhead, and the
20
+ * process scales horizontally without them. Declared here so the posture holds
21
+ * however the server is launched; a deployment's `MCP_SESSION_MODE` still wins
22
+ * when it carries a meaningful value.
23
+ */
24
+ sessionMode: 'stateless',
25
+ tools: [
26
+ whoisLookupDomain,
27
+ whoisCheckAvailability,
28
+ whoisGetDns,
29
+ whoisLookupIp,
30
+ whoisLookupAsn,
31
+ whoisGetDossier,
32
+ ],
33
+ resources: [],
34
+ prompts: [],
35
+ 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' +
40
+ '- 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
+ setup(core) {
43
+ initRdapService(core.config, core.storage);
44
+ initDohService(core.config, core.storage);
45
+ },
46
+ };
47
+ //# sourceMappingURL=app.js.map
@@ -0,0 +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"}