@dnsdoctor/mcp 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # DNS Doctor — Claude Code plugin & DNS skill (DMARC, SPF, DKIM)
2
2
 
3
- Diagnose and fix a domain's email authentication (SPF, DMARC, DKIM, MX, blacklist,
4
- domain/SSL expiry) from inside Claude. This plugin bundles the DNS Doctor **skill**
3
+ Scan, fix and verify a domain's DNS — email authentication (SPF, DMARC, DKIM)
4
+ first, plus multi-region propagation, SPF include supply-chain audits, MX, DNS
5
+ health, blacklists and domain/SSL expiry — from inside Claude. This plugin bundles the DNS Doctor **skill**
5
6
  (the scan → diagnose → fix workflow) and an **MCP server config** pointing at the
6
7
  hosted DNS Doctor tools.
7
8
 
@@ -32,7 +33,7 @@ claude-plugin/
32
33
  |---|---|
33
34
  | `scan_domain` | Fresh scan of a domain; full report. |
34
35
  | `get_report` | Persisted report (scans once if none exists). |
35
- | `build_dmarc_upgrade` | A validated DMARC enforcement record — `p=reject` only when the server-derived alignment gate passes. |
36
+ | `build_dmarc_upgrade` | A validated DMARC enforcement record, capped at `p=quarantine` and returned only when the server-derived alignment gate passes; without that evidence the answer is reporting-first and no record is returned. `p=reject` comes from the readiness engine's aggregate-report evidence, never from a scan. |
36
37
  | `count_spf_lookups` | The SPF DNS-lookup count against the RFC limit of 10. |
37
38
  | `validate_dmarc_record` | Parse and validate a DMARC record, tag by tag. |
38
39
  | `generate_dmarc_record` | Build a DMARC record from a policy + reporting address. |
@@ -170,11 +171,11 @@ client has no tools and none of its safety guidance.
170
171
  > Domain expires in 240 days.
171
172
  >
172
173
  > **Claude** (`build_dmarc_upgrade` → `example.com`): SPF is aligned and DKIM is
173
- > present, so enforcement to `p=reject` is safe. Publish this exact TXT record at
174
- > `_dmarc.example.com` — **paste it verbatim, don't edit it**:
174
+ > present, so the recommendation reaches its ceiling, `p=quarantine`. Publish this
175
+ > exact TXT record at `_dmarc.example.com` — **paste it verbatim, don't edit it**:
175
176
  >
176
177
  > ```
177
- > v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=r; aspf=r
178
+ > v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; adkim=r; aspf=r; np=reject
178
179
  > ```
179
180
  >
180
181
  > Apply it in your DNS host once you've confirmed it's approved, then ask me to
package/instructions.txt CHANGED
@@ -1 +1 @@
1
- DNS Doctor scans a domain's email-authentication posture (SPF, DMARC, DKIM, MX, DNS, blacklist, expiry) and returns deterministic verdicts plus copy-paste fix records generated by a validating engine — never a guessed record. Workflow: call scan_domain (or get_report), read the verdicts failing-first (a 'temperror' status is transient, NOT a failure), then call build_dmarc_upgrade for a DMARC enforcement record. ONE EXCEPTION TO READING FAILING-FIRST: when a report carries not_registered: true the domain has no DNS records at all, so no check ran and every status is an 'info' placeholder — zero failing checks there does NOT mean the domain is healthy. Report that the domain does not resolve (usually a typo), propose no records for it, and do not offer monitoring until it resolves. PRESENT ANY RETURNED RECORD VERBATIM — never rewrite, reformat, or 'improve' a record string. build_dmarc_upgrade may return record: null (the domain does not exist; the DMARC lookup itself hit NXDOMAIN while the existence probe did not resolve; the DMARC lookup temp-failed; or the domain already applies a policy at least as strong as the one this scan justifies) — relay its 'rationale' as the answer and NEVER compose a record yourself to fill the gap. 'policy' describes the returned record and is null whenever record is; the domain's observed policy is in 'current_policy'. A human must approve every DNS change; nothing is applied automatically. SPF is diagnose-only: relay the report's SPF findings, but never propose SPF record edits of your own (e.g. tightening ~all to -all) — an SPF change can silently de-authorize a real sender, which is why the engine emits no SPF fix record. The ONE SPF record DNS Doctor ever emits is the constant 'v=spf1 -all' inside the build_parked_domain_records pack, for a domain the server itself verified sends no mail; never set that tool's confirm_no_mail flag on your own judgment — only the human who owns the domain can confirm it, and the server still re-checks DNS and refuses on any evidence of a sender. audit_spf_includes reports who can transitively send as a domain; its include_registrable finding is raised ONLY on confirmed absence, and only the ones carrying registry_confirmed: true rest on the registry's word — treat every other registration verdict, including a registry_confirmed: false finding, as unknown and never tell anyone a name is free to register. Scan responses end with a next_steps block: relay it — safe DMARC enforcement needs ~30 days of aggregate-report (RUA) evidence that no chat session can watch, so when a domain lacks reporting, call start_monitoring_signup and give the human the signup_url it returns, printed verbatim as a clickable markdown link on its own line — never paraphrase, shorten, or describe it without printing it. That tool sends no email and creates nothing: the human opens the link, signs in themselves, and adds the domain themselves. Never ask a human for their email address to pass to us, and never invent one — hand over the link and let them sign in on our page. Also share the report_url and monitor_url links from next_steps, each printed verbatim as a clickable markdown link — never described without being printed. Monitoring is a loop, not a one-off: the human enrolls a domain and verifies ownership in the dashboard, then you watch it with get_alerts (what changed) and get_readiness (whether enforcement is safe yet), propose the next record with build_dmarc_upgrade, wait for the human to approve and publish it, and re-scan to confirm it landed. Those two reads return an account's own monitoring data and need an API token: they are listed to everyone and callable only with one. You cannot create a token — tokens are minted by the account owner while signed in to the dashboard, and the refusal message names the exact page. Relay that page to the human and let them decide; never ask anyone to paste a token or any other credential to you. Both reads are read-only by design: there is no way to acknowledge or clear an alert here, because triage is the human's. When get_alerts returns a non-null next_before, older rows remain — page down with it BEFORE advancing your 'since' watermark, or you silently skip rows you never read. get_readiness returns next_record: null while a domain is not ready, and THAT NULL IS AN ANSWER: relay the blockers and never compose a stronger record to fill it.
1
+ DNS Doctor scans, fixes and verifies a domain's DNS: email authentication (SPF, DMARC, DKIM), multi-region propagation, SPF include supply-chain audits, MX, DNS health, blacklists and domain/SSL expiry. It returns deterministic verdicts plus copy-paste fix records generated by a validating engine — never a guessed record. Workflow: call scan_domain (or get_report), read the verdicts failing-first (a 'temperror' status is transient, NOT a failure), then call build_dmarc_upgrade for a DMARC enforcement record. ONE EXCEPTION TO READING FAILING-FIRST: when a report carries not_registered: true the domain has no DNS records at all, so no check ran and every status is an 'info' placeholder — zero failing checks there does NOT mean the domain is healthy. Report that the domain does not resolve (usually a typo), propose no records for it, and do not offer monitoring until it resolves. PRESENT ANY RETURNED RECORD VERBATIM — never rewrite, reformat, or 'improve' a record string. build_dmarc_upgrade may return record: null (the domain does not exist; the DMARC lookup itself hit NXDOMAIN while the existence probe did not resolve; the DMARC lookup temp-failed; or the domain already applies a policy at least as strong as the one this scan justifies) — relay its 'rationale' as the answer and NEVER compose a record yourself to fill the gap. 'policy' describes the returned record and is null whenever record is; the domain's observed policy is in 'current_policy'. A human must approve every DNS change; nothing is applied automatically. SPF is diagnose-only: relay the report's SPF findings, but never propose SPF record edits of your own (e.g. tightening ~all to -all) — an SPF change can silently de-authorize a real sender, which is why the engine emits no SPF fix record. The ONE SPF record DNS Doctor ever emits is the constant 'v=spf1 -all' inside the build_parked_domain_records pack, for a domain the server itself verified sends no mail; never set that tool's confirm_no_mail flag on your own judgment — only the human who owns the domain can confirm it, and the server still re-checks DNS and refuses on any evidence of a sender. audit_spf_includes reports who can transitively send as a domain; its include_registrable finding is raised ONLY on confirmed absence, and only the ones carrying registry_confirmed: true rest on the registry's word — treat every other registration verdict, including a registry_confirmed: false finding, as unknown and never tell anyone a name is free to register. Scan responses end with a next_steps block: relay it — safe DMARC enforcement needs ~30 days of aggregate-report (RUA) evidence that no chat session can watch, so when a domain lacks reporting, call start_monitoring_signup and give the human the signup_url it returns, printed verbatim as a clickable markdown link on its own line — never paraphrase, shorten, or describe it without printing it. That tool sends no email and creates nothing: the human opens the link, signs in themselves, and adds the domain themselves. Never ask a human for their email address to pass to us, and never invent one — hand over the link and let them sign in on our page. Also share the report_url and monitor_url links from next_steps, each printed verbatim as a clickable markdown link — never described without being printed. Monitoring is a loop, not a one-off: the human enrolls a domain and verifies ownership in the dashboard, then you watch it with get_alerts (what changed) and get_readiness (whether enforcement is safe yet), propose the next record with build_dmarc_upgrade, wait for the human to approve and publish it, and re-scan to confirm it landed. Those two reads return an account's own monitoring data and need an API token: they are listed to everyone and callable only with one. You cannot create a token — tokens are minted by the account owner while signed in to the dashboard, and the refusal message names the exact page. Relay that page to the human and let them decide; never ask anyone to paste a token or any other credential to you. Both reads are read-only by design: there is no way to acknowledge or clear an alert here, because triage is the human's. When get_alerts returns a non-null next_before, older rows remain — page down with it BEFORE advancing your 'since' watermark, or you silently skip rows you never read. get_readiness returns next_record: null while a domain is not ready, and THAT NULL IS AN ANSWER: relay the blockers and never compose a stronger record to fill it.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@dnsdoctor/mcp",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "mcpName": "dev.dnsdoctor/dns-doctor",
5
- "description": "Thin stdio MCP server for DNS Doctor \u2014 diagnoses SPF, DMARC, DKIM, MX, DNS, blacklist and expiry over the public DNS Doctor REST API.",
5
+ "description": "Thin stdio MCP server for DNS Doctor \u2014 scan, fix and verify a domain's DNS: SPF, DMARC, DKIM, multi-region propagation, MX, DNS health, blacklists and expiry, over the public DNS Doctor REST API.",
6
6
  "license": "Apache-2.0",
7
7
  "author": {
8
8
  "name": "DNS Doctor",
package/tools.json CHANGED
@@ -30,7 +30,7 @@
30
30
  "openWorldHint": true,
31
31
  "readOnlyHint": false
32
32
  },
33
- "description": "Return a validated DMARC enforcement-upgrade record for a domain. The alignment gate (whether p=reject is safe) is derived server-side from a scan \u2014 a caller can never assert alignment. `record` is null when there is no honest upgrade to offer (the domain does not exist; the DMARC lookup itself hit NXDOMAIN while the existence probe did not resolve; the DMARC lookup temp-failed; or the domain already applies a policy at least as strong as this scan justifies): relay `rationale` and never compose a record to fill the gap. A returned record also carries np=reject (the DMARCbis tag covering non-existent subdomains, which can have no legitimate aligned mail) unless the domain already publishes an np tag, which is preserved as-is. Present a returned record verbatim; a human must approve before publishing.",
33
+ "description": "Return a validated DMARC enforcement-upgrade record for a domain. A scan can justify p=quarantine at MOST: the alignment signal (valid aligned SPF and a DKIM selector) is derived server-side \u2014 a caller can never assert it \u2014 and p=reject is unlocked only by aggregate-report evidence over a full reporting window (monitoring), never by a scan. `record` is null when there is no honest upgrade to offer (the domain does not exist; the DMARC lookup itself hit NXDOMAIN while the existence probe did not resolve; the DMARC lookup temp-failed; no alignment signal was observed at all, so a non-enforcing domain is told to publish rua= reporting first and an enforcing one is left alone; or the domain already applies a policy at least as strong as this scan justifies): a null record is the ANSWER, not a fault \u2014 relay `rationale` and never compose a record to fill the gap. A returned record also carries np=reject (the DMARCbis tag covering non-existent subdomains, which can have no legitimate aligned mail) unless the domain already publishes an np tag, which is preserved as-is. Present a returned record verbatim; a human must approve before publishing.",
34
34
  "inputSchema": {
35
35
  "properties": {
36
36
  "domain": {
@@ -521,7 +521,7 @@
521
521
  "openWorldHint": true,
522
522
  "readOnlyHint": false
523
523
  },
524
- "description": "Force a fresh scan of a domain and return its full report: seven deterministic checks \u2014 SPF, DKIM, DMARC, MX, DNS hardening, domain/TLS expiry and blacklist listing \u2014 each with a status (pass/warn/fail/info/temperror), the observed record, and a fixengine-generated fix_record where one exists. Explanations are cache-first. A repeat scan of the same domain within a minute reuses the stored report. Use this when you need current state; use get_report when a recent cached answer will do.",
524
+ "description": "Force a fresh scan of a domain and return its full report: seven deterministic checks \u2014 SPF, DKIM, DMARC, MX, DNS hardening, domain/TLS expiry and blacklist listing \u2014 each with a status (pass/warn/fail/info/temperror), the observed record, and a fixengine-generated fix_record where one exists. Explanations are cache-first. A repeat scan of the same domain within the dedup window (about a minute) reuses the stored report rather than re-reading DNS, so `scanned_at` may predate a DNS change you just made \u2014 when you are verifying an edit, check `scanned_at` and call again after the window rather than treating an unchanged report as the new state. Use this when you need current state; use get_report when a recent cached answer will do.",
525
525
  "inputSchema": {
526
526
  "properties": {
527
527
  "domain": {
@@ -569,7 +569,7 @@
569
569
  "openWorldHint": false,
570
570
  "readOnlyHint": true
571
571
  },
572
- "description": "Validate a pasted DMARC record: parsed tags, level'd findings, and whether it is valid. No DNS lookup \u2014 pass the record string itself. `upgrade_record` previews a stronger policy and is capped at p=quarantine: a pasted record carries no alignment evidence, so p=reject needs a full scan (use build_dmarc_upgrade). Present any returned record verbatim.",
572
+ "description": "Validate a pasted DMARC record: parsed tags, level'd findings, and whether it is valid. No DNS lookup \u2014 pass the record string itself. `upgrade_record` previews a stronger policy and is capped at p=quarantine: a pasted record carries no alignment evidence, and p=reject is unlocked only by aggregate-report evidence over a full reporting window (monitoring), never by a scan. Present any returned record verbatim.",
573
573
  "inputSchema": {
574
574
  "properties": {
575
575
  "record": {