{
  "name": "dns-doctor",
  "description": "Scan, fix and verify a domain's DNS: email authentication (SPF, DMARC, DKIM), multi-region propagation verification, SPF include supply-chain auditing, MX, DNS health, blacklists and domain/SSL expiry — and get the exact deterministic record to fix each problem. Every record is generated and validated by a deterministic engine, never a language model.",
  "version": "1",
  "documentation": "https://dnsdoctor.dev/methodology",
  "transports": [
    {
      "type": "streamable-http",
      "url": "https://dnsdoctor.dev/mcp"
    }
  ],
  "authentication": {
    "anonymous": "The fourteen diagnosis tools are callable without auth. The two monitoring reads (get_alerts, get_readiness) and the dnsdoctor://domains resource are listed for everyone but refused without a token. The three onboarding tools (add_monitored_domain, check_domain_verification, get_domain_records) need a LINKED account: calling one without a bearer answers HTTP 401 with WWW-Authenticate, which is where a host that supports OAuth linking starts its flow. Every refusal is guidance, never a request for a credential.",
    "bearer": "Authorization: Bearer dnsd_… — unlocks the two monitoring reads (get_alerts, get_readiness) and the dnsdoctor://domains resource (the account's own monitoring data). Only the account owner can mint a token, on the dashboard's API-tokens page; an agent cannot create one for them."
  },
  "tools": [
    {
      "name": "scan_domain",
      "description": "Force-fresh scan of a domain; returns the persisted report."
    },
    {
      "name": "get_report",
      "description": "The persisted report if present, else a one-off scan."
    },
    {
      "name": "build_dmarc_upgrade",
      "description": "The next-step DMARC record, capped at p=quarantine: the alignment signal is derived server-side, never caller-asserted, and p=reject is unlocked only by aggregate-report evidence over a full reporting window, never by a scan. `record` is null when there is no honest upgrade to offer (the domain does not exist, so there is no zone to publish into; 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 the answer is to publish rua= reporting first; or the domain already applies a policy at least as strong as this scan justifies) — relay the `rationale`, never compose a record to fill the gap. `policy` describes the returned record and is null whenever `record` is; the observed policy is in `current_policy`."
    },
    {
      "name": "start_monitoring_signup",
      "description": "Returns a sign-up link to hand to the HUMAN who owns the domain — print the returned `signup_url` verbatim as a clickable markdown link on its own line, never paraphrase or describe it without printing it. Sends no email and creates nothing: they open the link, sign in there themselves, and the domain is carried over to their dashboard, already filled in, after that — monitoring starts once they prove ownership with a TXT record."
    },
    {
      "name": "count_spf_lookups",
      "description": "Validate an SPF record and count what it costs. Returns `record_valid`, per-term `findings`, `has_pass_all` (a `+all` authorizing the whole internet), `multiple_all`, the parsed `terms`, and the lookup count against the RFC 7208 limit of 10 with `over_limit`/`near_limit` and the `offending_mechanisms`. Pass EXACTLY ONE of `domain` (resolves the published record and counts recursively, through nested includes) or `record` (parses a pasted record, its own terms only). This is the SPF validator — there is no separate one. Diagnose-only: no SPF fix record is ever returned, because removing a mechanism can silently de-authorize a real sender."
    },
    {
      "name": "validate_dmarc_record",
      "description": "Validate a pasted DMARC record: parsed tags, level'd findings, and whether it 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=reject is unlocked only by aggregate-report evidence over a full reporting window, never by a scan."
    },
    {
      "name": "generate_dmarc_record",
      "description": "Build a DMARC record from scratch for a domain that has none, using the validating engine — never compose one yourself. The generated record is re-validated before it is returned. Present it verbatim; a human must approve before publishing."
    },
    {
      "name": "check_dkim_selector",
      "description": "Check ONE specific DKIM selector on a domain — the exact selector the sending platform uses, which a full scan's common-selector sweep may miss. No fix record is returned: a DKIM key is generated by the sending platform."
    },
    {
      "name": "parse_dmarc_report",
      "description": "Parse ONE DMARC aggregate (RUA) report into readable per-source aggregates: who sent mail as the domain, how much, and what share was aligned. The file's bytes go in `content_base64` (XML, .gz or .zip; up to 2 MiB decoded). Nothing is stored."
    },
    {
      "name": "check_record",
      "description": "Check whether a DNS change has landed: 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. Empty values mean the record is genuinely absent. When `in_sync` is false, `max_wait_seconds` is the largest remaining cached TTL. This samples two resolvers — never describe it as worldwide or as propagation coverage."
    },
    {
      "name": "check_propagation",
      "description": "Check whether a DNS change has propagated GLOBALLY: 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 plus a deterministic verdict comes back. 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 the exact name (www. is not stripped, _dmarc.example.com works), `record_type` is A|AAAA|CNAME|MX|TXT|NS, and the optional `expected_value` turns each cell into match or mismatch instead of agreement-only. Observation only: no record is ever composed here. A cell that did not answer is `unavailable`, which is NOT a negative result, and when fewer than three vantage points were reached the verdict downgrades to `unknown` — report vantage_reached of vantage_total rather than calling a name converged on partial coverage."
    },
    {
      "name": "lookup_registration",
      "description": "Read a domain's registration from the registry over RDAP — the WHOIS question. Returns the registrar (with its IANA id), the registration, last-changed and expiry dates, EPP status codes verbatim, the nameservers, whether the delegation is DNSSEC-signed, and an abuse contact where the registry publishes one. Most registries redact contact details post-GDPR: `redacted: true` is the normal state, not a failure. Observation only — no record is composed here, and `pendingDelete` or a near expiry is something to report, never advice to buy a domain. `status` is registered|not_registered|unknown, and `unknown` is NOT absence: the registry did not answer, and `reason` says whether that was a rate limit, a timeout, a registry error, or a TLD that publishes no RDAP service. Never tell anyone a name is free unless `status` is exactly not_registered."
    },
    {
      "name": "check_reverse_dns",
      "description": "Check one sending IP's forward-confirmed reverse DNS (FCrDNS): reads the PTR record, resolves that hostname back, and reports whether it returns to the same IP. `verdict` is 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. The fix is always made by whoever controls the IP, never in the sending domain's own DNS."
    },
    {
      "name": "audit_spf_includes",
      "description": "Audit a domain's SPF supply chain: walks every include and redirect it delegates to, and reports who can transitively send as it. 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) and spf_record_unusable (the audited domain's OWN record is missing or does not parse, so there is no chain to walk). A domain we could not verify is reported as unverified and NEVER as available, and only an include_registrable finding carrying registry_confirmed: true rests on the registry's word — on registry_confirmed: false the evidence is DNS alone, which cannot tell an unsold name from one in redemption, so never call it available. Findings are risk analysis, not instructions: no SPF fix record exists here or anywhere else in DNS Doctor. Use count_spf_lookups instead when the question is only the 10-lookup limit."
    },
    {
      "name": "build_parked_domain_records",
      "description": "Build 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. For parked, redirect and brand-defensive domains only — never for a domain that sends any mail. Do NOT set confirm_no_mail on your own judgment: only the human who owns the domain can confirm it sends nothing. That flag unlocks the question, not the answer — the server re-checks DNS itself (existence, MX, SPF, DKIM selectors) and returns records: null with a rationale when it finds evidence of mail. A lookup failure is reported as a failure, never as a pack. Present the records verbatim, in the order given, and let a human approve each one before publishing."
    },
    {
      "name": "get_alerts",
      "description": "Read the monitoring alert log for the domains the caller's account monitors, newest first. Requires a bearer token — an agent cannot create one; the account owner mints it in the dashboard. Rows carry delivery_class, so the 'dashboard_only' rows deliberately kept out of the digest mail are here too: an agent watching only a mailbox sees less than this log holds. Page down with `before` until `next_before` is null BEFORE advancing `since` (an inclusive floor — de-duplicate on id), or rows you never received are skipped. Read-only by decision: no ack, no delete — acknowledging is the human's own triage."
    },
    {
      "name": "get_readiness",
      "description": "Read the DMARC enforcement-readiness verdict for ONE monitored domain, computed from its aggregate (RUA) report window. Requires a bearer token, as above. Returns whether the domain is ready to step its policy up, the blockers that say why not, the evidence window, and next_record — engine-generated and null while blocked. That null is an answer: relay the blockers and never compose a stronger record to fill it. Read-only; a human approves any record before it is published."
    },
    {
      "name": "add_monitored_domain",
      "description": "Add a domain to the signed-in user's DNS Doctor monitoring and return the ownership-check TXT record they must publish, plus where their DNS is hosted, a provider-specific guide link and, when their provider supports it, a one-click apply URL. Needs a LINKED account (OAuth, domains:manage): on a host that supports account linking the human approves once on our page; on a host that does not, use start_monitoring_signup and print the link instead. Print every record host and value exactly as returned. Nothing is applied to anyone's DNS."
    },
    {
      "name": "check_domain_verification",
      "description": "Check whether the ownership TXT record for a domain the user has added is visible yet, and mark it verified when it is. The result says WHICH outcome occurred and which nameservers were asked, so 'not published yet', 'published with the wrong value' and 'our lookup did not complete' are distinguishable — a lookup that did not complete is TRANSIENT, never a verdict about their DNS. On success it also carries the DMARC reporting record that turns monitoring on. Needs a linked account (domains:manage)."
    },
    {
      "name": "get_domain_records",
      "description": "Read the records a domain the user monitors still needs: the ownership check while it is unverified, and once verified the DMARC reporting record plus whether we have OBSERVED that record published. Read-only. Needs a linked account (domains:manage)."
    }
  ],
  "resources": [
    {
      "uri": "dnsdoctor://domains",
      "description": "The token account's monitored domains (bearer token required)."
    }
  ]
}