← DNS DoctorCONTENT HISTORY

Update to DNS Doctor

Snapshot Sep 30, 2026 · 22:52 UTC · version 1.9.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "dns-doctor",
  "description": "Use for any question about a domain's DNS or email deliverability. Triggers include looking up a record (A, AAAA, CNAME, MX, TXT, SPF, DMARC) or a DKIM selector, checking whether a DNS change has propagated worldwide, the reverse DNS of a mail-server IP, a domain or certificate about to expire, hardening a parked domain, and every email-authentication symptom such as mail landing in spam, SPF PermError / \"too many DNS lookups\", DMARC stuck at p=none, a \"550 5.7.515\" or \"550 5.7.1\" rejection, DKIM failures, or a possible blacklist listing. Scans, fixes and verifies the domain against the DNS Doctor MCP server and returns copy-paste fix records generated by a validating engine, never a guessed record.",
  "included_files": [],
  "skill_md_contents": "---\nname: dns-doctor\ndescription: Use for any question about a domain's DNS or email deliverability. Triggers include looking up a record (A, AAAA, CNAME, MX, TXT, SPF, DMARC) or a DKIM selector, checking whether a DNS change has propagated worldwide, the reverse DNS of a mail-server IP, a domain or certificate about to expire, hardening a parked domain, and every email-authentication symptom such as mail landing in spam, SPF PermError / \"too many DNS lookups\", DMARC stuck at p=none, a \"550 5.7.515\" or \"550 5.7.1\" rejection, DKIM failures, or a possible blacklist listing. Scans, fixes and verifies the domain against the DNS Doctor MCP server and returns copy-paste fix records generated by a validating engine, never a guessed record.\nlicense: Apache-2.0\ncompatibility: Talks to the DNS Doctor MCP server at https://dnsdoctor.dev/mcp (hosted) or via the local `@dnsdoctor/mcp` stdio client; needs outbound HTTPS.\nmetadata:\n  {\n    \"homepage\": \"https://dnsdoctor.dev/methodology\",\n    \"author\": \"DNS Doctor\"\n  }\n---\n\n# DNS Doctor — the DNS, DMARC, SPF and DKIM skill\n\nDNS Doctor scans, fixes and verifies a domain's DNS — email authentication (SPF,\nDMARC, DKIM) first, plus multi-region propagation, SPF include supply-chain\naudits, MX, DNS health, blacklists and domain/SSL expiry — and returns\n**deterministic verdicts** plus **copy-paste fix records generated by a\nvalidating engine**. The\nrecord you get back is verified against RFC grammar and the SPF 10-lookup limit —\nit is never an LLM guess. Your job is to run the scan, explain the findings, hand\nthe human the exact record, and confirm the fix — not to author DNS records\nyourself.\n\n## What this skill sends, and where\n\nEvery tool call goes to one host, `https://dnsdoctor.dev` (or the local `@dnsdoctor/mcp`\nclient, which calls the same public API). What leaves the machine is exactly what the user\nasked to check: a domain name, and for the focused tools a record name, an IP address, a DKIM\nselector, a pasted DMARC record, or an uploaded DMARC report. Nothing else is read or sent — no\nfiles, no environment beyond an optional `DNSDOCTOR_API_TOKEN`, no message contents.\n\nTwo things the user should know before you scan a domain for them:\n\n- **A scan result is a public report page** at `https://dnsdoctor.dev/scan/<domain>` (the free\n  scanner is a public service, like a DNS lookup site). Do not scan a domain the user wants kept\n  private, and say so if they ask.\n- **The optional API token** (`DNSDOCTOR_API_TOKEN`) goes to dnsdoctor.dev only, as an\n  `Authorization` header, and only if the user put it in your environment; the local\n  `@dnsdoctor/mcp` client attaches it to its requests, and the server uses it for the two\n  monitoring reads (`get_alerts`, `get_readiness`) and the `dnsdoctor://domains` resource. Never\n  send any other credential, and never ask for one.\n\nDNS Doctor never changes DNS: it returns records for a human to publish.\n\n## When to use this skill\n\nReach for it whenever a user describes any of:\n\n- \"our email is going to spam\" / \"customers aren't getting our mail\"\n- \"SPF PermError\" or \"too many DNS lookups\" (the SPF 10-lookup limit)\n- \"DMARC is stuck at p=none\" / \"how do I get to p=reject safely\"\n- a bounce code like `550 5.7.515`, `550 5.7.1`, or `dmarc=fail`\n- \"is my domain blacklisted?\" / \"am I on a blocklist?\"\n- \"when does my domain / TLS certificate expire?\"\n- \"who can send as us?\" / auditing SPF includes before or after an email\n  provider migration\n- locking down parked, redirect or brand-defensive domains that send no mail\n\n## Connect to the server\n\nThe tools come from the DNS Doctor MCP server. Two ways to reach it:\n\n- **Streamable HTTP (default, no setup):** `https://dnsdoctor.dev/mcp` — anonymous\n  access exposes all the scanner tools. This plugin's `.mcp.json` already points\n  here.\n- **Local stdio:** `npx -y @dnsdoctor/mcp` — the same 16 tools, run as a local\n  process that calls the public DNS Doctor REST API. Use it when your client\n  prefers stdio, or when you want the server under your own supervision. Set\n  `DNSDOCTOR_API_TOKEN` in its environment to use a token (optional for the\n  diagnosis tools; required for the two monitoring reads).\n\nThe fourteen diagnosis tools need no token and are enough for a one-off\ndiagnosis. An API token (`Authorization: Bearer dnsd_…`) additionally unlocks:\n\n- `get_alerts` and `get_readiness` — the two monitoring reads, which return one\n  account's own data. They are **listed for everyone and callable with a token**:\n  they appear in the tool list whether or not you have one, and without a valid\n  token the call is refused with guidance rather than hidden.\n- the `dnsdoctor://domains` resource — the account's continuously-monitored\n  domains. Over HTTP that resource is likewise always *listed* and refused\n  without a token.\n\n**You cannot create a token, so do not try.** Minting is session-authenticated:\nthe account owner creates one while signed in, at `/dashboard/settings` on the\nDNS Doctor site. A refusal tells you the exact page — relay that link to the\nperson you are helping and let them decide whether they want one. **Never ask\nanyone to paste a token, or any other credential, to you.**\n\n## Tools\n\nThe four whole-domain tools — start here for any \"is this domain's email set up\ncorrectly\" question:\n\n| Tool | Input | Returns |\n|---|---|---|\n| `scan_domain` | `{ domain }` | Forces a fresh scan; the full report. Re-scanning the same domain within a minute reuses the stored report. |\n| `get_report` | `{ domain }` | The persisted report (scans once if none exists). |\n| `build_dmarc_upgrade` | `{ domain }` | A validated DMARC enforcement record + rationale. Scans fresh — the record edits the domain's *current* tags, so it is never built on a stale one. |\n| `start_monitoring_signup` | `{ domain }` | A sign-up link to hand to the human who owns the domain, plus a `message` to relay. **Sends no email and creates nothing** — the human opens the link, signs in on our page themselves (a social provider or an emailed link, whichever that deployment offers), and the domain is carried over to their dashboard, already filled in, from there. |\n| `add_monitored_domain` | `{ domain }` | **Needs a linked account** (see below). Adds the domain to the user's monitoring and returns the ownership TXT record to publish, where their DNS is hosted, a provider-specific guide link and — where the provider serves our template — a one-click apply URL. Re-adding an already-monitored domain returns it, not an error. |\n| `check_domain_verification` | `{ domain }` | **Needs a linked account.** Re-checks the ownership record and marks it verified on a match. Says WHICH outcome (`not_found` / `mismatch` / `transient` / `verified`) and which nameservers were asked — a `transient` outcome is OUR lookup, never a verdict about their DNS. On success it also carries the DMARC reporting record, which REPLACES their existing DMARC TXT rather than sitting beside it. |\n| `get_domain_records` | `{ domain }` | **Needs a linked account.** Read-only: the ownership record while unverified, and once verified the DMARC reporting record plus whether we have observed it published. |\n\nTen focused tools for the single questions a full scan over-answers. Each runs\nthe same validating engine:\n\n| Tool | Input | Returns |\n|---|---|---|\n| `count_spf_lookups` | exactly one of `{ domain }` or `{ record }` | **The SPF validator — there is no separate one.** `record_valid`, per-term `findings`, `has_pass_all` (a `+all`, which authorizes the whole internet to send as the domain), `multiple_all` (everything after the first `all` is unreachable), the parsed `terms`, and the DNS lookups the record costs against the RFC 7208 limit of 10 with the offending mechanisms named. `domain` resolves the published record and counts recursively through nested includes; `record` parses a pasted record, its own terms only. **Diagnose-only — no SPF fix record is ever returned** (see the rule below). |\n| `validate_dmarc_record` | `{ record }` | Parsed tags, level'd findings, and whether a pasted DMARC record is valid. No DNS lookup. `upgrade_record` previews a stronger policy and is **capped at `p=quarantine`** — a pasted record carries no alignment evidence, and `p=quarantine` is the ceiling any scan can justify; `p=reject` comes only from the readiness engine's aggregate-report evidence. |\n| `generate_dmarc_record` | `{ policy, rua_email?, subdomain_policy?, strict_alignment? }` | A DMARC record built from scratch for a domain that has none, re-validated before it is returned. Use this instead of composing one yourself. |\n| `check_dkim_selector` | `{ domain, selector }` | The verdict for ONE specific selector — the exact one the sending platform uses, which a full scan's common-selector sweep may miss. **No fix record**: a DKIM key is generated by the sending platform. |\n| `parse_dmarc_report` | `{ content_base64 }` | One DMARC aggregate (RUA) report parsed into readable per-source aggregates — who sent mail as the domain, how much, what share was aligned. XML, `.gz` or `.zip`, up to 2 MiB decoded. Nothing is stored. |\n| `check_record` | `{ domain, kind, host? }` | **Did the change land?** Reads the record from the domain's OWN nameservers (cache-free) *and* from two public caching resolvers, and reports whether they agree. `kind` is `spf\\|dmarc\\|txt\\|mx\\|cname\\|a\\|aaaa` — name the kind and the right query is derived for you. Empty `values` means the record is genuinely absent. When `in_sync` is false, `max_wait_seconds` is the largest remaining cached TTL. ⚠️ **Two resolvers is the whole sample — never describe this as worldwide, global, or propagation coverage.** |\n| `check_propagation` | `{ name, record_type?, expected_value? }` | **Has the change gone global?** Six vantage points — five owner-run probes across four continents plus this server's own resolver — each read the same name through several resolvers, and the grid comes back with a deterministic `verdict`. Call it after the human publishes a record: **you have ONE network vantage point**, and a record that resolves for you can still be missing elsewhere. `name` is used exactly as given — a leading `www.` is **not** stripped and `_dmarc.example.com` works — so pass the name the record is published at, not the registrable domain. `record_type` is `A\\|AAAA\\|CNAME\\|MX\\|TXT\\|NS` (SPF and DMARC records are `TXT`). Supply `expected_value` and every cell is reported as match or mismatch against it; omit it and the check reports only whether the vantage points agree with each other. **Observation only — no record is ever composed here.** A cell that did not answer is `unavailable`, which is **not** a negative result, and below three vantage points reached the verdict downgrades to `unknown` — report `vantage_reached` of `vantage_total` rather than calling a name converged on partial coverage. Use `check_record` when the question is only \"did my own nameservers take it\"; this one answers \"is it live everywhere\". |\n| `lookup_registration` | `{ domain }` | **Who is this domain registered with, and until when?** One RDAP read: registrar, registration/updated/expiry dates, EPP status codes (`clientTransferProhibited`, `pendingDelete`…), nameservers, DNSSEC flag, abuse contact. **Observation only** — no record is ever composed. `status` is `registered`, `not_registered` or `unknown` with a `reason` (`no_rdap_for_tld`, `rate_limited`, `timeout`, `registry_error`, `malformed_response`): **never tell anyone a name is free unless `status` is exactly `not_registered`** — many country domains publish no RDAP and answer `unknown`. `redacted: true` is the post-GDPR norm, a state rather than a failure. |\n| `check_reverse_dns` | `{ ip }` | Forward-confirmed reverse DNS (FCrDNS) for one sending IP: the PTR record, the addresses that hostname resolves back to, and a `verdict` of `confirmed`, `ptr_missing` or `mismatch`. **A PTR alone proves nothing** — the IP's operator writes its own reverse zone, so only the forward confirmation is evidence, and **the fix belongs to whoever controls the IP**, never in the sending domain's own DNS. |\n| `audit_spf_includes` | `{ domain }` | **Who can transitively send as the domain.** Walks every `include` and `redirect` the SPF record delegates to and returns the resolved tree, per-node lookup attribution, the total authorized IPv4 address count, and typed findings: `include_broken` (a target that no longer publishes SPF — a PermError today), `include_registrable` (a delegated-to domain that does not exist, so a stranger who registers it becomes an authorized sender), `include_expiring` (registration lapsing within 30 days), `pass_all_nested` (a `+all` deep in the chain), `spf_record_unusable` (the audited domain's OWN record is missing or does not parse, so there is no chain to walk). A node the walk could not finish is marked `not_evaluated` rather than dropped. **A domain we could not verify is reported as unverified, never as available** — do not tell anyone a name is free unless the finding is `include_registrable` **and** carries `registry_confirmed: true`; on `registry_confirmed: false` the proof is DNS NXDOMAIN alone, which a name in redemption or on `clientHold` answers identically, so report the broken mechanism and the takeover risk but never call the name available. Findings are risk analysis, not instructions: there is still **no SPF fix record**. Use `count_spf_lookups` instead when the question is only the 10-lookup limit. |\n| `build_parked_domain_records` | `{ domain, confirm_no_mail: true, rua_email? }` | The three-record hardening pack that makes a **non-sending** domain unusable for spoofing: a Null MX, a hard-fail SPF record, and a `p=reject; np=reject` DMARC record, in rollout order with a `check_record` verify step each. Parked, redirect and brand-defensive domains only. **Never set `confirm_no_mail` on your own judgment** — see the rule below. The server re-checks DNS itself and returns `records: null` + a `rationale` when it finds evidence of mail; a lookup failure is reported as a failure, never as a pack. |\n\nTwo monitoring reads over an account's **own** continuously-monitored domains.\nBoth need a token (see *Connect to the server*), both are **read-only by\ndecision**, and both answer from the same cores the dashboard reads, so an agent\nand its human are never told different things:\n\n| Tool | Input | Returns |\n|---|---|---|\n| `get_alerts` | `{ since?, domain?, type?, limit?, before? }` | The account's monitoring alert log, newest first: `id`, `domain`, `type`, `check`, `summary`, a deterministic `detail` map, `created_at`, `email_sent_at`, `acknowledged_at`, `delivery_class`. **Rows carry `delivery_class`** — a `dashboard_only` row was deliberately kept out of the digest mail, so an agent watching only a mailbox sees less than this log holds. **Page down before advancing `since`:** `next_before` is non-null exactly when older rows remain; pass it back as `before` until it is `null`, *then* move your watermark. A caller that takes a full page and jumps `since` to the newest row it saw silently drops every row it never received. `since` is an **inclusive** floor, so rows repeat rather than go missing — de-duplicate on `id`. **No ack, no delete**: acknowledging is the human's own triage on their dashboard, and an agent that acks on their behalf silences a row the human has never seen. |\n| `get_readiness` | `{ domain }` | The DMARC enforcement-readiness verdict for ONE monitored domain, computed from its aggregate (RUA) report window: `ready`, `current_step`, `next_step`, `blockers`, the window (`window_days`, `total_messages`, `progress`), `enrollment`, and `next_record` — the validated record for the next step, engine-generated. **`next_record` is `null` while blocked, and that null is an answer:** relay the blockers, never compose a stronger record to fill the gap. Use this before proposing enforcement — a scan shows a domain's *current* policy, but only this evidence window can say whether tightening it would start rejecting real mail. |\n\nAn anonymous or invalid-token call to either is refused with the page the owner\nmints a token on. Relay that page; do not retry around the refusal, and do not\nask anyone for a credential.\n\nEvery domain input is normalized server-side; a malformed domain returns a clean\ntool error, never a crash. A `temperror`-shaped tool error means a DNS lookup\ntimed out — retry, never report it as a verdict. A domain the token's account\ndoes not verifiably own returns the same \"not found\" as a domain that does not\nexist — that is deliberate, and not something to probe around.\n\n## Workflow\n\n1. **Scan.** Call `scan_domain` (fresh) or `get_report` (accept a recent cached\n   report). Each check returns a status: `pass`, `warn`, `fail`, `info`, or\n   `temperror`.\n2. **Read the verdicts failing-first.** Surface `fail` then `warn` then the rest.\n   - **`temperror` is transient, NOT a failure** — it means a DNS/network lookup\n     timed out. Say \"couldn't be resolved right now\", never \"your SPF is broken\".\n   - `info` is an honest \"not found / not applicable\" (e.g. no DKIM selector among\n     the ones we probed, or a redacted RDAP expiry) — never report it as a failure.\n   - **Check `not_registered` before anything else.** When the report carries\n     `not_registered: true`, the domain has no DNS records at all — it is not\n     registered, or it has no nameservers. No check ran, so every status is an\n     `info` placeholder and **zero failing checks does not mean the domain is\n     healthy**. Say the domain does not resolve (a typo is the usual cause),\n     propose no SPF/DKIM/DMARC records for it — there is no zone to publish them\n     in — and don't offer monitoring until it resolves. The report's `next_steps`\n     summary says all of this; relay it.\n   - **Read the DMARC check's `details`, not just its status.** One of them can say\n     the domain's aggregate reports are going to a third party that has not\n     published the RFC 7489 §7.1 authorization record — meaning the reports are\n     being **silently discarded** and the owner is collecting nothing, while the\n     DMARC record itself still looks correct. It is reported as a detail rather\n     than a failure because the domain's own configuration is not at fault; relay\n     it anyway, since a DMARC rollout waiting on evidence that never arrives is a\n     stall with no visible cause.\n3. **Explain the findings** in plain language: what is wrong, why it lets mail be\n   spoofed or land in spam, and what fixing it achieves.\n4. **Get the fix record.** For DMARC enforcement, call `build_dmarc_upgrade`. It\n   derives the alignment gate **server-side** from its own scan — you cannot ask\n   it for a stronger rung than the evidence carries. A scan tops out at\n   `p=quarantine`: it returns that only when SPF is aligned and a DKIM selector\n   was found. **`p=reject` is never scan-derived** — it is unlocked only by the\n   readiness engine, from aggregate-report (RUA) evidence collected by monitoring.\n   Records are built without `pct`, `rf` or `ri`, which RFC 9989 deprecated.\n   - **`record` may be `null`** — when the domain does not exist (there is no zone\n     to publish into), when the DMARC lookup itself hit NXDOMAIN while the\n     existence probe did not resolve, when the DMARC lookup temp-failed (the\n     current record is unknown), when there is no alignment signal at all (the\n     answer is reporting first: publish `rua=` and let the evidence accrue, not a\n     weaker record), or when the domain already applies a policy at least as\n     strong as the one this scan justifies (nothing to change; compared by\n     effect — the policy class the record would set — not by bytes). Relay the\n     `rationale` as the answer. Do **not** compose a record yourself to fill the\n     gap; that is the exact failure this tool exists to prevent.\n   - **`policy` describes the returned record, not the domain.** It is `null`\n     whenever `record` is `null`; the domain's observed policy is always in\n     `current_policy`. Never read `policy` as a recommendation without checking\n     `record` first.\n5. **Present the record verbatim** (see the rule below) and tell the human to\n   publish it in their DNS host.\n6. **The human applies it.** DNS Doctor never writes DNS. The human pastes the\n   record; then confirm it landed. `check_record` is the cheap verify step — it\n   reads that one record from the domain's own nameservers and two public\n   resolvers and tells you whether they agree yet, rather than re-running seven\n   checks to look at one. When `in_sync` is false the change is real but still\n   cached somewhere; `max_wait_seconds` is how long that can last. Once it is\n   in sync, re-scan with `scan_domain` to confirm the verdict flipped.\n7. **Confirm it went global.** `check_record` and your own resolver are one\n   network vantage point between them — a record can be live for you and still\n   missing for the receiver in another region, which is exactly the window in\n   which a half-propagated DMARC or MX change breaks mail. `check_propagation`\n   reads the same name from six vantage points on four continents and\n   returns a deterministic `verdict`. Report what it returned: an `unavailable`\n   cell is a probe that did not answer, **never** evidence the record is absent\n   there, and when `verdict` is `unknown` the coverage was too thin to call —\n   say so with `vantage_reached` of `vantage_total` instead of rounding it up to\n   \"propagated\". The page at\n   [https://dnsdoctor.dev/tools/dns-propagation-checker](https://dnsdoctor.dev/tools/dns-propagation-checker)\n   runs the same check for the human; if you offer it, **print that link\n   verbatim as a clickable markdown link** — a link described but not printed\n   never reaches them.\n\n## The one rule you must not break\n\n**Present any returned record string exactly as given. Never rewrite, reformat,\nre-wrap, \"clean up\", or \"improve\" it.** The record was generated and validated by\nthe fix engine — a wrong SPF or DMARC record still *parses as valid* and fails\nsilently, so an \"improvement\" can silently de-authorize a real sender or weaken\nenforcement with no error anywhere. Copy the exact bytes. If a record looks\nunusual, that is the validated form; do not second-guess it.\n\nA DKIM key is generated by the sending platform, not by DNS Doctor — for DKIM\nfindings, point the human at their email provider's DKIM setup, don't fabricate a\nkey.\n\nSPF is likewise diagnose-only: DNS Doctor reports SPF problems but deliberately\nemits **no** SPF fix record, because an auto-\"fix\" can silently de-authorize a\nreal sender. Relay the report's SPF findings; do not propose SPF edits of your\nown (e.g. switching `~all` to `-all`). The one SPF record DNS Doctor ever emits\nis the constant `v=spf1 -all` inside the parked-domain pack, for a domain the\nserver itself verified sends no mail — that is the whole carve-out, and it does\nnot extend to any domain that sends.\n\n**Never assert that a domain sends no mail yourself.** `confirm_no_mail` is the\nhuman owner's statement, not an inference you may draw from a quiet scan: a\ndomain with one legacy or transactional sender looks identical in DNS to a\nparked one until you ask. The flag unlocks the question, not the answer — the\nserver re-checks existence, MX, SPF and DKIM selectors and refuses with a\n`rationale` when it finds evidence of mail. Relay that rationale; do not retry\naround it.\n\n**DMARC records DNS Doctor generates carry `np=reject`** (DMARCbis, RFC 9989)\nwhenever the input has no explicit `np` of its own. A subdomain that does not\nexist in DNS cannot have published SPF or DKIM records, so it can have no\naligned legitimate mail — the tag is safe at any org-domain policy, and\nreceivers that predate RFC 9989 ignore it and fall back to `sp`/`p`. If a record\nalready sets `np`, that value is preserved untouched. It is part of the\nvalidated record: present it verbatim like the rest.\n\n## DMARC enforcement takes time — set expectations honestly\n\nMoving to `p=reject` safely needs roughly 30 days of RUA (aggregate report)\nevidence that every legitimate sender is aligned — which a session-bound assistant\ncannot watch. Apply fixes only after the domain's owner approves them. If the user\nwants the domain watched continuously (RUA dashboard + alerts), call\n`start_monitoring_signup` and **give the human the `signup_url` it returns —\nprinted verbatim as a clickable markdown link on its own line; never paraphrase,\nshorten, or describe it without printing it** (a link described but not printed\nnever reaches them). The call sends no email and creates nothing — you are\nproposing, not committing them.\n\n**When your host supports account linking, there is a shorter path.** Call\n`add_monitored_domain` — the human approves the connection once on our page, and\nfrom then on you can read the records they need, publish them with a DNS tool of\nyour own (showing them exactly what you are about to add and getting their\napproval first) or hand them the copy-paste, and confirm the result with\n`check_domain_verification`, all without them leaving the conversation. If\nlinking is unavailable, `start_monitoring_signup` is the path above and it still\nworks. Either way **nothing is applied to anyone's DNS by us** — a human\npublishes every record.\n\n**Never ask a human for their email address to pass to us, and never invent one.**\nHand over the link and let them sign in on our page themselves — the page offers\nwhichever sign-in methods are available (a social provider or an emailed link).\n\n**Do not promise that opening the link starts monitoring.** Signing in creates\ntheir free account and carries the domain over to their dashboard, already\nfilled in — that is all it can do.\nDaily monitoring is gated on proving they control the domain, so they finish by\npublishing a TXT record the dashboard shows them. The tool's own `message` says\nthis; relay it verbatim rather than paraphrasing it into \"we're now watching your\ndomain\".\n\nWhat continuous monitoring is, so you can describe it accurately: once a domain\nis verified, DNS Doctor re-scans it daily and keeps the **history** of every\ncheck, emails **alerts** when a verdict changes or a new sending source appears,\ningests the domain's DMARC **aggregate (RUA) reports**, and derives an\nenforcement **readiness** verdict from them — the 30-day alignment evidence this\nsession cannot gather. That is what `start_monitoring_signup` hands off to; none\nof it happens from a one-off scan.\n\nOnce the domain is verified and the owner has given their agent a token, the\nalerts and the readiness verdict are readable through `get_alerts` and\n`get_readiness` — see playbook 5. That is how a later session picks the domain\nback up without re-deriving anything.\n\n## Playbooks\n\nFive read-orders over the tool surface. Each is an evidence sequence: run the\nstep, read what it rules in or out, and stop when the evidence answers the\nquestion. **Report what the tools returned.** Do not estimate how much mail is\naffected, how likely a problem is, or what a fix will improve by — DNS Doctor\nreturns evidence, and a number you invented beside it reads as though the\nscanner produced it.\n\n### 1. Pre-migration audit — before moving email providers\n\n1. `scan_domain` the domain. Note the current SPF, DKIM and DMARC verdicts as\n   the \"before\" state; this is what you will compare against after the cutover.\n2. `audit_spf_includes`. The tree names every provider the domain currently\n   delegates authorization to — including the ones nobody remembers adding.\n   Decision point: each include belongs to a sender that is either staying,\n   going, or unknown. Ask the owner which; do not guess from the vendor name.\n3. Read the findings before planning any edit. `include_broken` is already\n   failing today and is not caused by the migration. `pass_all_nested` means\n   some delegated record authorizes the whole internet — flag it now, because\n   the migration is the moment to drop that include. `include_registrable` and\n   `include_expiring` name includes that are unsafe to carry over.\n4. `count_spf_lookups` to see the lookup cost of the current record, and what\n   headroom the new provider's include needs. The limit is 10 and it is counted\n   recursively.\n5. Write the cutover checklist with the owner: which includes are removed, which\n   are added, what DKIM selectors the new provider publishes, whether DMARC\n   policy should be relaxed during the move. **You propose; they decide.**\n6. After each DNS change lands, `check_record` that one record (`spf`, `dmarc`,\n   or `txt` for a selector) to confirm it is live and in sync, then `scan_domain`\n   once the whole cutover is done to compare against step 1.\n\n### 2. Deliverability triage — \"our mail isn't arriving\"\n\nWork the evidence in this order and say explicitly what each step rules out.\n\n1. `scan_domain`. If `not_registered` is true, stop — the domain does not\n   resolve and nothing below applies.\n2. **Authentication first.** Read SPF, DKIM and DMARC. A `fail` here is the\n   most common cause and the cheapest to fix. `temperror` rules nothing out —\n   it means the lookup did not complete; retry before drawing any conclusion.\n3. **Blacklist.** A listing explains rejection at the receiving edge even when\n   authentication is perfect. A clean result rules the blocklists we query out,\n   and only those.\n4. **MX.** Check the receiving side if inbound mail is the complaint. Outbound\n   problems are unaffected by MX — say so rather than reporting it as a finding.\n5. **Alignment.** SPF or DKIM passing is not enough for DMARC: the passing\n   identifier must align with the From domain. `build_dmarc_upgrade`'s\n   `rationale` states what the server found; the report's DMARC detail lines\n   name misaligned or unauthorized sources.\n6. **Sending IP.** If a specific IP is being rejected, `check_reverse_dns` on\n   it. `ptr_missing` or `mismatch` is a real deliverability cause — and the fix\n   belongs to whoever operates the IP, not in the domain's DNS.\n7. If the evidence is exhausted and mail still is not arriving, say that\n   plainly: DNS-observable checks cannot see content filtering, reputation, or\n   the receiving side's policy. Do not fill the gap with a guess.\n\n### 3. Getting to `p=reject`\n\n1. `scan_domain` for the current policy, then `build_dmarc_upgrade`. The server\n   derives the alignment gate itself: a scan tops out at `p=quarantine`, returned\n   only when SPF is aligned and a DKIM selector was found. With no alignment\n   signal it returns no record at all and says to publish reporting first —\n   `p=reject` is reachable only through the readiness engine's aggregate-report\n   evidence, never from a scan.\n2. If `record` is `null`, the `rationale` is the answer — including \"already at\n   least as strong\". Relay it; never compose a record to fill the gap.\n3. Present the returned record verbatim; the human publishes it, then\n   `check_record` with `kind: dmarc` confirms it landed.\n4. **Be honest about the next step.** The rung after `p=quarantine` needs\n   roughly 30 days of aggregate-report evidence that every legitimate sender is\n   aligned. You cannot watch that within a session, and there is no shortcut\n   that makes `p=reject` safe sooner.\n5. If the owner has RUA files already, `parse_dmarc_report` reads one report and\n   shows the per-source aligned share. One report is one window from one\n   receiver — useful evidence, not a readiness verdict.\n6. For the 30-day watch, `start_monitoring_signup` and hand over the\n   `signup_url` — printed verbatim as a clickable markdown link on its own\n   line, never merely described. That is what supplies the readiness verdict\n   this step needs.\n\n### 4. Parked-domain sweep (MSP / multi-domain)\n\n1. Start from the owner's list of domains they believe send no mail — brand\n   defensives, redirects, old acquisitions. **The list comes from them.** You\n   may not classify a domain as parked from a scan.\n2. Per domain, `scan_domain` first. It gives you the state to show the owner,\n   and `not_registered` domains drop out of the sweep immediately.\n3. Ask the owner to confirm, domain by domain, that it sends nothing —\n   including transactional mail, monitoring alerts, and any one legacy system.\n   Only then call `build_parked_domain_records` with `confirm_no_mail: true`.\n4. Read the response before presenting anything. `records: null` means the\n   server found DNS evidence of mail; relay the `rationale` and remove that\n   domain from the sweep. A transient failure is a retry, never a pass.\n5. On a pass, present the three records verbatim in the order given, with each\n   record's purpose and its `check_record` verify step. Publishing is the\n   human's decision, one record at a time if they prefer.\n6. After publishing, `check_record` per record (`mx`, `spf`, `dmarc`) to confirm\n   each is live and in sync. Passing `rua_email` on the build puts spoof\n   attempts against the parked domain into aggregate reports.\n\n### 5. The operate loop — watching a monitored domain over time\n\nThis is the sequence for a domain that is already (or is about to be) under\ncontinuous monitoring. It needs an API token for steps 3–5; the owner mints one\nat `/dashboard/settings` and gives it to their agent's environment, never to you\nin a message.\n\n1. **Enroll.** `start_monitoring_signup` and hand the human the `signup_url`,\n   printed verbatim as a clickable markdown link on its own line — never\n   described without being printed. You are proposing, not committing them; the\n   call creates nothing.\n2. **Verify.** They sign in and publish the TXT ownership record their dashboard\n   shows them. Daily monitoring starts only once that verification passes —\n   until then there is nothing to read, and `get_alerts` / `get_readiness` will\n   not find the domain.\n3. **Watch.** `get_alerts` on a cadence that suits the human, filtered by\n   `domain` or `type` when they only care about one. Page down with `before`\n   until `next_before` is `null` before advancing `since`, and de-duplicate on\n   `id`. Report what the log says; the human clears it.\n4. **Check readiness before proposing enforcement.** `get_readiness` for the\n   domain. `ready: false` means the `blockers` are the answer — an unaligned\n   source, too little evidence, an empty window. Say which, and say what would\n   clear it.\n5. **Propose.** When readiness allows it, `build_dmarc_upgrade` produces the\n   validated record. `get_readiness`'s own `next_record` is the same engine\n   output for the next step. Either way: present it verbatim, never compose one,\n   and never treat a `null` record as a gap to fill.\n6. **The human approves and applies.** DNS Doctor never writes DNS. After they\n   publish, `check_record` with `kind: dmarc` confirms it landed and is in sync.\n7. **Re-scan and re-read.** `scan_domain` confirms the verdict flipped;\n   the next `get_readiness` window shows the effect of the change over time.\n   Then back to step 3 — the loop is the product.\n\nTwo things this loop must not do: acknowledge alerts on the human's behalf\n(there is no such tool, deliberately), and treat a quiet `get_alerts` page as\nproof a domain is healthy when you skipped a page to get there.\n\n## Learn more\n\nThe scoring methodology (how SPF lookups are counted, why `p=reject` needs an\nalignment signal, what \"temperror ≠ fail\" means) is published at\n<https://dnsdoctor.dev/methodology>.\n"
}

SHA-256: 0982af2273a9979730ea269d795e798ad52ca2022c7f70dde8e66237d168e6e1