# DomainGuard MCP — Operating Guide Endpoint: `https://nhmohio.com/api/mcp` Transport: Streamable HTTP (JSON-RPC 2.0 over `POST`) Auth: `Authorization: Bearer nhm_...` (a DomainGuard API key) Server name: `domainguard` · 196 tools · 5 prompts · 4 resources This file is written for an AI assistant. Point Claude at this URL, paste it into a project's `CLAUDE.md`, or read it through the server itself as the resource `domainguard://guide` — and it will know how to use the server well instead of guessing. The same text the server sends as `instructions` on connect is a condensed version of §3, §4 and §5 below. --- ## 1. What this server is for DomainGuard watches domains and reports on them: DNS, registration, SSL, mail authentication and the DMARC report stream, HTTP security headers, blacklists, uptime, Hijack Guard change detection, lookalike domains, vulnerabilities and open ports, software CVE/KEV alerts, dark-web breach data, on-page and whole-site SEO, audit-grade accessibility, local rankings and the Google Business Profile, reviews, citations, backlinks, AI search visibility, third-party provider status, compliance evidence and incident records. The MCP server hands every part of that to an assistant so it can answer questions about *these* domains rather than websites in general, add and set up a domain by asking the owner a few questions, and — with a write-capable key — run the scans and lookups that produce new data. **Every feature of the product is reachable here.** The only things deliberately left out are credentials, money and sessions: API keys, sign-in, billing, in-app purchase and push-device registration stay dashboard actions. **The one boundary that matters:** DomainGuard monitors these domains, it does not host them. Every tool reports, records, or measures. Nothing here edits a website or publishes a DNS record. The deliverable of a session is a diagnosis plus the exact change to make, executed by the user at their registrar, DNS provider, host, CMS, or Google Business Profile. --- ## 2. Connect ### Claude Code ```bash claude mcp add --transport http domainguard https://nhmohio.com/api/mcp \ --header "Authorization: Bearer nhm_your_key_here" ``` The five prompts then appear as slash commands: `/mcp__domainguard__onboard_domain`, `/mcp__domainguard__set_up_local_seo`, `/mcp__domainguard__review_domain`, `/mcp__domainguard__portfolio_triage`, `/mcp__domainguard__fix_email_deliverability`. ### Claude Desktop / any client that takes a JSON config ```json { "mcpServers": { "domainguard": { "type": "http", "url": "https://nhmohio.com/api/mcp", "headers": { "Authorization": "Bearer nhm_your_key_here" } } } } ``` ### Anything else Any client that can call a remote MCP server over HTTP with a custom header works. There is no OAuth flow and no event stream — `GET` is refused by design. The key is created in the DomainGuard dashboard under **Settings → API Keys** and is shown exactly once. ### Verify the connection by hand ```bash curl -s https://nhmohio.com/api/mcp \ -H "Authorization: Bearer nhm_your_key_here" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' ``` The tool list that comes back is the real answer to "what can this key do" — it is already filtered to the key's scopes and the account's plan. Trust it over this document if the two ever disagree. `prompts/list` and `resources/list` return the five prompts and four resources; `prompts/get` renders one; `resources/read` returns the text. --- ## 3. Ground rules Follow these in every session. They are what separates a useful session from an expensive one. 1. **Start with `list_domains`.** Almost every other tool takes a numeric `domain_id`, and that is where the ids come from. Never guess an id. 2. **Read stored results before running anything.** `get_checkup_history`, `get_seo_results`, `get_vuln_scan_latest`, `get_local_rank_history`, `get_geogrid_scans`, `get_domain_history` and `get_domain_setup_guide` return data already collected — free, instant, and usually enough. Scans take minutes and consume budget. 3. **Know which tools spend.** Anything named `run_*`, `start_*`, `check_*`, `verify_*`, `scan_*`, `probe_*`, `fetch_*`, `discover_*` or `assess_*` does real work: it sends traffic to a site, calls a paid data source, or spends credits. So do two innocuous-looking ones — `get_domain_email_security` and `get_scan_ownership_verification` perform live lookups and are classified as writes for that reason. 4. **Quote before you spend.** `get_local_seo_credits` for the balance, `get_local_seo_catalog` for what each action costs and requires, `quote_local_ranks`, `quote_geogrid_scan`, `quote_local_seo_endpoint`, `quote_local_seo_report`, `quote_accessibility_audit` and the `pricing` block of `get_local_seo_quickstart` for the number. Tell the user the cost and get agreement before running a paid action for the first time in a session. 5. **Always send a fresh `request_key` for a paid action.** Reuse it only to retry the exact same request after a timeout. Format: 8–100 characters of letters, digits, `.`, `_`, `-`. Avoid colons — some endpoints reject them. A good pattern: `dg-ranks-example-com-20260829-a7f3`. 6. **After a timeout, recover — do not re-run.** `get_local_seo_operation` with the same `request_key` returns the saved or in-flight result. Running again with a new key charges twice. 7. **Poll, don't wait.** `start_vuln_scan`, `start_site_audit`, `start_deep_seo_job` and `start_accessibility_audit` return an id immediately and finish in the background. Poll `get_vuln_scan_results`, `get_site_audit_status` / `get_site_audit_results`, `get_deep_seo_job`, and `get_accessibility_audit`. Leave real time between polls — a site crawl is minutes, not seconds. 8. **Confirm destructive calls in words first.** `delete_domain` erases a domain's entire history and cannot be undone. Every other `delete_*`, `remove_*`, `untrack_*` and `unsubscribe_*` is also one-way. Name the exact thing and get a yes. 9. **Report findings, then the fix, then who applies it.** The user changes DNS at their registrar, page content in their CMS, and business details in Google Business Profile. Say which one. 10. **Never invent a number — or a name.** Scores, positions, volumes, and credit prices all come from tool output. If a tool did not return it, say it is not measured rather than estimating. The same goes for a business name, a city, a phrase or an email address during onboarding: they are the user's. 11. **Add a domain by interview, not cold.** `plan_domain_onboarding` first, then the questions, then `onboard_domain` once. §4 has the whole protocol. 12. **Verify what you can, and report what the scanner got wrong.** You are the scanner's reviewer. §5 has the whole protocol. --- ## 4. Onboarding a domain — the interview This is the protocol behind the `onboard_domain` prompt and the `domainguard://onboarding` resource. The goal: the user names a domain, you ask a few plain questions, and the domain ends up monitored and configured the way they meant — without them learning the product's vocabulary. ``` 1. plan_domain_onboarding { domain } → already_monitored? · account.plan · scan_cleared · local_business.saved · credits · pricing · questions[] (ask, why, known, default, spends, maps_to) · then[] · rules[] 2. Ask the questions, in order, one or two at a time, in your own words. · confirm anything marked `known`; skip what the user already told you · if they don't know the local-business answers: discover_local_seo_details { domain_id } (needs the domain to exist; for a new domain, onboard first with no local_business, then discover, then call onboard_domain again with it) 3. onboard_domain { domain, ...answers } ← once → steps[] (done / skipped / failed, with why) · setup (the guide) · created 4. Tell the user what happened (steps) and what is left (setup.steps), in words. 5. If a local business was saved and the plan allows it: get_local_seo_quickstart { domain_id } → quote pricing → yes → run_local_seo_quickstart { domain_id, business_name, location, keywords, request_key } 6. Later: get_status_subscription_suggestions → update_status_subscriptions; get_domain_setup_guide again once the first scan finishes. ``` The questions, and what each one becomes: | Question | Argument | Default if unanswered | |---|---|---| | Does this domain send email? | `sends_email` | yes — SPF/DKIM/DMARC are checked | | Impostor check, subdomain discovery, one exposure scan at add time? | `lookalikes` `subdomains` `vulnerabilities` | all on | | Keep the WCAG 2.2 AA audit queued with the add? (5 credits) | `accessibility` | on | | Keep scanning for exposure after the first scan? | `exposure_monitoring` | on, where the plan schedules scans | | Is it a local business, and where? Name, city, storefront or service area, radius, phone, up to 5 phrases | `local_business { business_name, location, is_service_business, radius_miles, street_address, phone, targeting, keywords[] }` | skipped | | Where should alerts go? | `notification_email` | the sign-in address | | An address at the domain for the one-click ownership link | `ownership_email` | passive checks only until a proof exists | | Addresses at the domain to watch in breach data | `dark_web_emails[]` | none | | A group or client to file it under, and notes | `group` `notes` `scan_client_id` | ungrouped | `onboard_domain` spends nothing. It adds the domain (or finds it if already here), starts the add-time checks, patches the per-domain settings, saves the business profile and phrases, sets the alert email, sends the ownership link, watches the addresses, and returns the setup guide. Each step reports separately, so a step the plan refused is a sentence the user sees, not a gap the guide reveals later. It is safe to call again with more answers; it only changes what you pass. `get_domain_setup_guide` is the source of truth for what is left. It is computed from real state on every read. Never tell the user a step is done unless the guide says so, and never tick one yourself. `skip_domain_setup_step` records "not for this domain" for an optional step the user has ruled out. Ownership proof is **account-level**: proving one domain clears active scanning (deep and extreme vulnerability profiles, port scans, whole-domain breach monitoring) for every domain on the account. `get_scan_clearance_status` says whether the account is already cleared. The email link (`request_ownership_email_link`, or `ownership_email` during onboarding) is the easiest proof; the DNS/file challenge (`get_scan_ownership_verification` → `verify_scan_ownership`) is the alternative; `request_scan_authorization` is for a client's domain the account cannot prove. ### Setting up local search The protocol behind the `set_up_local_seo` prompt and `domainguard://local-seo`. ``` get_local_seo_access → is the suite on this plan? get_local_seo_quickstart { domain_id } → saved business, phrases, homepage suggestions, latest positions, last profile read, balance, pricing discover_local_seo_details { domain_id }→ name, city, address, phone, service-area?, phrases, each with source + confidence — for the user to confirm verify_local_seo_location / search_locations → when the city is unusual or not found quote: pricing.per_phrase × N + pricing.profile → get a yes run_local_seo_quickstart { ..., request_key } → local-pack AND organic position per phrase, plus the Google Business Profile read add_local_keyword_target → more phrases (25 per domain) get_local_seo_catalog → run_local_seo_action → profile_health, citations, competitor_gap, reviews, geogrid … quote each first ``` `save_local_seo_profile` and `add_local_keyword_target` are free. Every measurement spends; say the number first. On `location_not_found`, offer the suggestions the response carries and call again. On a timeout, `get_local_seo_operation` with the same key — never re-run blind. --- ## 5. Verify, then report — you are the scanner's reviewer This is the protocol behind the `review_domain` prompt and the `domainguard://review` resource. DomainGuard's results are claims about the live world, and you can check many of them yourself. **Before you present a finding as fact, verify it when you can, cheaply:** - A DNS or mail-authentication finding: resolve the record yourself (the TXT, MX, CNAME or `_dmarc` name) and compare the text the scanner quoted with what the zone serves. A truncated or non-ASCII address in an `rua=` tag is a defect the scanner should have caught. - An HTTP header or redirect finding: fetch the URL and read the response headers yourself. - A count (scripts, images, links, pages): count them in the served HTML. - A certificate finding: read the certificate's dates and names. - A ranking or review number: you usually cannot verify it; say it came from the provider and leave it. **When what you verified disagrees with what the scanner said**, report it: ``` report_scanner_issue { domain_id, category: false_positive | false_negative | wrong_value | error | other, severity: low | medium | high (how much the wrong answer misleads), check_name: "DMARC Record" (the scanner's own words), title: one line — what was wrong, detail: what it reported versus what is actually true, evidence: the record you resolved, the header you saw, the count you made, suggested_fix: what DomainGuard should change } … keep reading the domain, keep reporting … submit_scanner_review { domain_id } ← once, when you finish the domain ``` Reports for one domain collect into a review; `submit_scanner_review` sends the operators one message. Rules: report only what you verified, never a hunch. Plan limits, 402s and missing scopes are not scanner mistakes. Do not soften or hide a finding because you reported it — tell the user what the scanner said **and** what you found. Enterprise accounts only; 30 reports a day, 10 per domain, 10 per review; a repeated title inside a week is counted, not re-sent. If the tool answers 402 or 403, the account cannot report — carry on without it. The reporting endpoint accepts the account's own API keys and nothing else — a dashboard session cannot file a report — and the key must carry `scans` and `write`. That is by design: the channel pages a human, so it stays tied to a key the owner minted and can revoke. --- ## 6. Tool map — by the job you are doing ### Add and set up a domain `plan_domain_onboarding` → `onboard_domain` → `get_domain_setup_guide` · `skip_domain_setup_step` · `add_domain` · `add_domains_bulk` · `discover_local_seo_details` · `get_local_seo_quickstart` → `run_local_seo_quickstart` ### Find out what is on the account `list_domains` · `get_domain_history` · `get_dns_change_events` · `get_domain_events` · `get_domain_hijack_events` · `get_domain_event` · `get_uptime_snapshots` · `get_downtime_events` · `list_alerts` · `get_alert_activity` · `list_domain_groups` · `list_scan_clients` ### Find out what is wrong with a site `get_checkup_history` (richest single source) · `get_seo_results` · `get_vuln_scan_latest` · `get_open_ports` · `get_observed_components` · `list_software_vendors` · `get_software_vendor` · `list_software_alerts` · `list_lookalikes` · `get_lookalike_events` · `get_vulnerability_assets` ### Search performance — on-page and site-wide `get_seo_results` · `get_seo_history` · `run_seo_scan` · `start_site_audit` → `get_site_audit_status` → `get_site_audit_results` · `get_seo_intent_conflicts` · `fetch_sitemap` → `start_deep_seo_job` → `get_deep_seo_job` → `get_deep_seo_scans` → `get_deep_seo_scan_pages` · `check_page_seo` · `assess_website` · `score_page_performance` · `score_page_accessibility` · `get_seo_policy` · `email_seo_report` · `get_report_branding` · `save_report_branding` `get_seo_intent_conflicts` reads the newest crawl and says which pages compete for one search — same subject, same intent, same place — and what to do about each pair: which page to keep, and whether to redirect, merge, differentiate or split the other. It is free and instant; it fetches nothing. ### Local search — being found in a place `get_local_seo_access` · `get_local_seo_quickstart` · `run_local_seo_quickstart` · `discover_local_seo_details` · `verify_local_seo_location` · `search_locations` · `list_covered_locations` · `get_local_seo_profile` · `save_local_seo_profile` · `get_local_keyword_targets` · `add_local_keyword_target` · `delete_local_keyword_target` · `get_local_keyword_ideas` · `get_local_rank_history` · `quote_local_ranks` · `check_local_ranks` · `get_geogrid_scans` · `get_geogrid_entitlement` · `quote_geogrid_scan` · `run_geogrid_scan` · `get_backlink_snapshots` · `get_local_seo_audit_history` · `list_local_seo_reports` · `quote_local_seo_report` ### Everything else local — 39 catalogued actions `get_local_seo_catalog` → `quote_local_seo_endpoint` → `run_local_seo_action` → `get_local_seo_operation` · `get_local_seo_credits` Covers reviews, reputation, citations, keyword research, competitor intelligence, backlinks, brand mentions, AI search visibility, and full audits. See §7. ### Email that actually reaches inboxes `get_domain_email_security` · `get_dmarc_setup_status` · `get_dmarc_summary` · `get_dmarc_diagnostics` · `get_dmarc_senders` · `list_dmarc_reports` · `get_dmarc_inbox` · `rotate_dmarc_inbox` · `get_managed_email_auth` · `enable_managed_email_auth` · `check_managed_email_auth` · `set_managed_dmarc_policy` · `disable_managed_email_auth` ### Security and trust `run_domain_checkup` · `get_checkup_history` · `start_vuln_scan` · `get_vuln_scan_results` · `get_vuln_scan_history` · `discover_domain_subdomains` · `get_scan_clearance_status` · `get_scan_ownership_verification` · `verify_scan_ownership` · `verify_scan_ownership_bulk` · `request_ownership_email_link` · `request_scan_authorization` · `get_scan_authorization` · `list_scan_authorizations` · `list_vuln_dismissals` · `dismiss_vuln_finding` · `restore_vuln_dismissal` · `list_ip_targets` · `add_ip_target` · `update_ip_target` · `delete_ip_target` · `list_ip_target_scans` · `scan_ip_target` ### Software vulnerabilities (KEV / CVE) `get_software_alert_summary` · `list_software_alerts` · `update_software_alert` · `list_tracked_software` · `search_software_catalog` · `get_software_catalog` · `get_software_catalog_categories` · `track_software` · `update_tracked_software` · `untrack_software` · `get_software_advisory_history` · `get_vulnerability_feed` ### Lookalike domains `list_lookalikes` · `get_lookalike_events` · `run_lookalike_scan` · `set_lookalike_verdict` · `set_lookalike_watch` · `get_lookalike_evidence` · `act_on_lookalike_event` · `get_lookalike_alert_settings` · `update_lookalike_alert_settings` ### Hijack Guard — change events `get_domain_events` · `get_domain_hijack_events` · `get_domain_event` · `act_on_domain_event` (`authorize` = ours, promotes the baseline; `deny` = not ours, stays flagged; `acknowledge` / `investigate` / `resolve` move the state) ### Dark web `get_dark_web_status` · `get_dark_web_breach` · `run_dark_web_scan` · `add_dark_web_email` · `remove_dark_web_email` · `list_dark_web_domain_monitors` · `add_dark_web_domain_monitor` · `remove_dark_web_domain_monitor` · `run_dark_web_domain_scan` · `get_dark_web_alert_delivery` · `update_dark_web_alert_delivery` ### Accessibility (audit grade) `quote_accessibility_audit` · `start_accessibility_audit` · `get_accessibility_audit` · `list_accessibility_audits` · `record_accessibility_manual_review` · `complete_accessibility_audit` · `export_accessibility_audit` ### Uptime and dependencies `list_status_checks` · `create_status_check` · `update_status_check` · `run_status_check` · `get_status_check_runs` · `delete_status_check` · `probe_endpoint` · `get_status_services` · `get_status_incidents` · `get_status_history` · `list_status_subscriptions` · `get_status_subscription_suggestions` · `update_status_subscriptions` · `subscribe_all_status_services` · `unsubscribe_all_status_services` · `suggest_status_service` ### Compliance evidence and incidents `get_compliance_records` · `update_compliance_item` · `initialize_compliance_records` · `bulk_update_compliance` · `list_incident_records` · `create_incident_record` · `delete_incident_record` ### Account plumbing `get_account_settings` · `update_account_settings` · `mark_alerts_read` · `list_domain_groups` · `create_domain_group` · `assign_domains_to_group` · `delete_domain_group` · `list_scan_clients` · `create_scan_client` · `delete_scan_client` · `update_domain` · `delete_domain` · `run_domain_scan` · `list_support_conversations` · `get_support_conversation` · `reply_to_support_conversation` · `update_support_conversation` `mark_alerts_read` with no arguments marks every open alert on the account as read; with `ids` only those. Read alerts cannot be reopened — the History log keeps the record — so ask before clearing an inbox you did not create. ### Tell us when we are wrong `report_scanner_issue` · `submit_scanner_review` — §5. ### Operator reads — administrator only `list_error_events` · `get_self_check_status` · `list_scan_failures` · `list_scanner_reports` These need a key carrying the `admin` scope, which only the administrator's own signed-in session can mint and which reaches nothing else. They are what the nightly fix routine reads: the Worker's error log grouped by fingerprint, every self-check invariant's last verdict, the scans that did not finish, and the scanner mistakes customers' assistants reported. A customer key never sees them in `tools/list`. --- ## 7. The Local SEO catalog `get_local_seo_catalog` returns the server's authoritative list of 39 actions, each with its exact required inputs, prerequisites, price basis, and whether a saved result can be reopened for free. **Call it before `run_local_seo_action`.** The catalog is the contract; this document is a summary that can go stale. ``` run_local_seo_action { action_id: "competitor_gap", // from the catalog request_key: "dg-compgap-20260829-x1", // fresh, 8-100 chars, [A-Za-z0-9._-] input: { domain_id: 12 } // exactly the catalog's documented fields } ``` Available `action_id` values, by area: | Area | Actions | |---|---| | Profile & reputation | `profile_health` `business_profile` `business_qa` `reviews` `review_velocity` `multi_platform_reviews` `reputation_audit` | | Rankings & maps | `rank_check` `geogrid` `serp_local_finder` `serp_maps` `serp_lsa` | | Keywords | `keyword_suggestions` `keyword_search_volume` `keyword_opportunities` `keywords_for_site` `keywords_related` `keyword_trends` | | AI search | `ai_visibility` `serp_ai_overview` `serp_ai_mode` `ai_mentions` `ai_top_pages` `ai_top_sources` `ai_compare` `ai_keyword_data` `ai_llm_response` `ai_scraper` | | Authority & links | `backlinks_summary` `backlinks_gap` `local_authority` `brand_mentions` | | Competition | `competitor_gap` `competitor_ads` `business_listings` | | Audits | `local_audit` `citation_audit` `citations` `page_audit` | Some actions cache: reopening a saved result is free, refreshing buys a new one. The catalog says which, per action; `list_local_seo_reports` and `quote_local_seo_report` show what is already saved. Prefer a saved result unless the user specifically wants current data. ### `rank_check` deserves a note `check_local_ranks` is the quick path and measures the **local pack only**. `run_local_seo_quickstart` measures the local pack **and** organic for up to five phrases and reads the profile in one go. The catalogued action measures whatever you ask and costs accordingly — **one credit per phrase per surface**: ``` run_local_seo_action { action_id: "rank_check", request_key: "dg-ranks-20260829-b2", input: { domain_id: 12, surfaces: ["local_pack", "organic"], // both, or just one keywords: ["emergency plumber akron"] // omit to check every saved phrase } } ``` Omitting `keywords` checks every saved phrase, so on a domain with 20 tracked phrases and both surfaces that is 40 credits — `quote_local_ranks` first, and narrow `keywords` when the user asked about one phrase. --- ## 8. Playbooks Each playbook is a sequence that produces something the user can act on. Run the free reads first in every one of them. The five prompts the server offers are these playbooks packaged: `onboard_domain` (§4), `set_up_local_seo` (§4), `review_domain` (§5), `portfolio_triage` (8.1), `fix_email_deliverability` (8.10). ### 8.1 First session — triage the whole portfolio ``` list_domains → for each domain: get_domain_setup_guide (what was never finished) get_domain_history (is this getting worse or has it always been so?) get_checkup_history (what is actually broken, with severity) get_seo_results (title, description, headings, a11y violations) get_domain_events (anything hijack-shaped across the account) get_downtime_events (was anything actually down) list_alerts · get_software_alert_summary · get_dark_web_status ``` Output: one ranked list across the portfolio — severity first, then how many domains share the finding. Fixing one misconfiguration that appears on nine domains beats fixing nine unrelated things. Name the domains whose setup guide still has required steps open. Spend nothing in this playbook. Everything above is a stored read. ### 8.2 Make a page rank — the technical loop ``` 1. get_seo_results → title, meta description, H1/heading structure, accessibility violations that also hurt crawling 2. get_checkup_history → HTTPS, redirects, response headers, exposed files 3. get_seo_history → has the score been moving, and in which direction 4. → hand the user a specific edit list 5. (after they publish) run_seo_scan 6. get_seo_results again → confirm the finding cleared 7. get_seo_history → confirm the number moved ``` Read the SEO result for what it names, not for its score. A missing meta description, a duplicated title across templates, two H1s, or a heading level skipped is a concrete edit with an owner. "Score is 71" is not. ### 8.3 Whole site, not one page ``` start_site_audit { domain_id, max_pages } → returns audit_id get_site_audit_status { audit_id } → poll, minutes not seconds get_site_audit_results { audit_id } → page-level findings ``` Or, for a chosen set of pages: `fetch_sitemap` → `start_deep_seo_job` → `get_deep_seo_job` → `get_deep_seo_scans` → `get_deep_seo_scan_pages`. Then cluster the findings before reporting them. A crawl of 300 pages does not produce 300 problems — it produces perhaps six template defects repeated 300 times. Report the templates. Sort by pages affected × severity, and name the template file or page type where you can infer it. ### 8.4 Get into the local map pack ``` Setup (once per domain) → §4 "Setting up local search" Measure get_local_rank_history → free; is there recent data? quote_local_ranks / get_local_seo_credits check_local_ranks { request_key } → local pack, every tracked phrase quote_geogrid_scan → run_geogrid_scan { keyword, grid_size, request_key } Diagnose run_local_seo_action profile_health → what the business profile is missing run_local_seo_action citations → name/address/phone consistency run_local_seo_action competitor_gap → who is beating you and on what run_local_seo_action reviews → recent reviews and unanswered ones ``` How to read a geogrid: a business ranks best at its own pin and decays outward. A grid that is strong at the centre and dead three miles out is a proximity problem — the fix is more location-specific content and citations, not more keywords. A grid that is weak *everywhere including at the pin* is a profile or trust problem — go to `profile_health` and `citations` first. ### 8.5 Find phrases worth targeting ``` get_local_keyword_ideas → what earlier research already found (free) run_local_seo_action keyword_opportunities → local searches this business should consider run_local_seo_action keyword_suggestions → expand a seed phrase run_local_seo_action keyword_search_volume → is there real demand run_local_seo_action keywords_for_site → what this domain already associates with run_local_seo_action keywords_related → adjacent phrases run_local_seo_action keyword_trends → is interest rising or dying → add_local_keyword_target for the survivors → check_local_ranks to establish a baseline before any work starts ``` ### 8.6 Beat a specific competitor ``` run_local_seo_action competitor_gap · backlinks_gap · business_listings · competitor_ads · local_authority ``` Turn the gap into three things the user can do this month, not thirty they cannot. ### 8.7 Be visible in AI answers ``` run_local_seo_action ai_visibility · serp_ai_overview · serp_ai_mode · ai_mentions · ai_top_sources · ai_top_pages · ai_compare ``` The actionable output is almost always the *cited sources*, not the answer text. ### 8.8 Reputation ``` run_local_seo_action reviews · review_velocity · multi_platform_reviews · reputation_audit · business_qa ``` Unanswered reviews and unanswered public questions are the two cheapest wins in local search. ### 8.9 Links and mentions ``` get_backlink_snapshots (free) → run_local_seo_action backlinks_summary · backlinks_gap · brand_mentions ``` ### 8.10 Email that reaches the inbox ``` get_domain_email_security { domain_id } → live SPF, DKIM, DMARC, MX, MTA-STS, TLS-RPT + records.spf.recommended / records.dmarc.recommended get_dmarc_setup_status { domain } → the authoritative setup checklist get_dmarc_inbox → the rua= address to publish get_dmarc_summary { domain } → 30-day volume and alignment rate get_dmarc_senders { domain } → every source sending as the domain, aligned or not get_dmarc_diagnostics { domain } → ranked diagnostics list_dmarc_reports { domain } → the raw reports when a number needs checking ``` Read `get_dmarc_senders` before recommending a policy change. Moving a domain to `p=reject` while a legitimate sender is failing alignment stops that sender's mail. Every recommendation shows two records: `p=reject` first, labelled most secure, and `p=none` second, labelled safest start. If the user would rather DomainGuard publish and maintain the records: `enable_managed_email_auth` → user publishes one CNAME → `check_managed_email_auth` → later `set_managed_dmarc_policy advance` when the senders align. ### 8.11 Security findings that touch search ``` get_checkup_history → start here, it is stored and free run_domain_checkup { domain } → live; also works on a hostname you do not own, in which case only passive checks run get_vuln_scan_latest → most recent completed scan get_scan_clearance_status → is active scanning cleared for the account? start_vuln_scan → quick | standard | port-scan | deep | extreme get_vuln_scan_results { scan_id } list_software_alerts → KEV/CVE alerts with an exposure verdict ``` Active scanning (`deep`, `extreme`, port scans) requires that **one** domain on the account is proven: `request_ownership_email_link` is the easiest way. ### 8.12 Prove the work worked ``` get_seo_history · get_local_rank_history · get_geogrid_scans · get_domain_history · get_uptime_snapshots + get_downtime_events ``` Compare against the baseline, cite the dates, and say plainly where nothing moved. --- ## 9. Scopes, plans, and what a key can reach A key carries resource scopes plus, optionally, `write`. | Scope | Unlocks | |---|---| | `domains` | inventory, onboarding, setup guide, history, Hijack Guard, uptime, DMARC, alerts, settings, groups, clients, support threads | | `scans` | domain scans, security checkups, dark-web monitoring, scanner reports | | `seo` | on-page SEO, site audits, deep crawls, single-URL scoring, reports, the entire Local SEO suite | | `accessibility` | audit-grade WCAG 2.2 AA evaluations and their manual review | | `vulnscan` | vulnerability scans, ports, components, vendors, software alerts, dismissals, lookalikes, clearance, IP targets | | `compliance` | PCI / HIPAA / cyber-insurance control records, incident records | | `status` | custom endpoint checks, probes, provider status and subscriptions | | `write` | required on top of the above for anything that changes state | | `admin` | the four operator reads; mintable only from the administrator's own session | Plan gating: - **Free / Starter** — no API or MCP access. - **Pro** — API and MCP access, read only: every `R` tool the key's scopes cover. - **Enterprise** — adds `write`: every customer tool. The `admin` reads are the operator's alone. The plan is re-read on every request. An account that drops from Enterprise to Pro loses `write` on its next call, without anyone rotating the key. `tools/list` only returns tools the key can actually call, so a read-only key never sees a write tool. If an expected tool is missing, the key is missing a scope — that is a dashboard fix, not a prompt fix. --- ## 10. Errors and what to do about them | What you see | What it means | Do this | |---|---|---| | HTTP 401 | Key missing, malformed, revoked, expired, or the plan has no API access | Ask the user to check Settings → API Keys and their plan | | HTTP 401 mentioning "requires a DomainGuard API key" | A dashboard session token was used | MCP requires an `nhm_` key; sessions are refused by design | | HTTP 403 "Forbidden origin" | Origin header failed validation | Client misconfiguration, not a credential problem | | HTTP 405 on an authenticated GET | Correct — there is no SSE stream | Use POST | | HTTP 429 | Key's hourly rate limit, or a per-plan manual-scan limit | Wait; on free plans manual SEO scans are one per day | | Tool result with `isError`, "missing the … scope" | Key lacks that resource scope | Mint a key with the scope; do not retry | | Tool result with `isError`, "read-only" | Tool needs `write`, which is Enterprise | Use the read equivalent, or ask the user to upgrade | | Tool result 402 `plan_upgrade_required` | The feature is not on the plan | Say which plan has it; do not retry | | Tool result 403 on `report_scanner_issue` | Not an account API key with `scans`+`write`, or not Enterprise | Carry on without reporting | | `idempotency_mismatch` (409) | This `request_key` was already used for a *different* request | Generate a fresh key for a genuinely new run | | `location_not_found` (400) with `suggestions` | The city is not in the rank-data gazetteer as written | Offer the suggestions, or `search_locations`, then call again | | Timeout on a paid action | Unknown whether it ran | `get_local_seo_operation { request_key }` — never re-run blind | | Scan returns immediately with no findings | It is asynchronous | Poll the matching results tool | | "requires ownership verification" / scan clearance | Active scanning needs one proven domain on the account | `request_ownership_email_link`, or `get_scan_ownership_verification` → user publishes → `verify_scan_ownership` | | "Requires a saved business profile" | Local action needs profile first | `save_local_seo_profile` or `run_local_seo_quickstart` | | `onboard_domain` step `failed` | That one step was refused; the others applied | Read `detail`, tell the user, fix the input — do not repeat the whole call blind | A failed tool call comes back as a normal result carrying `isError: true` and a readable reason — read the reason and adapt rather than retrying the same call. --- ## 11. Full tool reference `R` = read, safe and free. `W` = write: needs the `write` scope, and does real work. `D` = destructive and irreversible. Inputs marked `?` are optional. The descriptions here are the first sentences of what `tools/list` returns; the tool list itself is always the fuller and more current text. Compliance statuses, for `update_compliance_item` and `bulk_update_compliance`: `unanswered` (nobody has answered yet), `met`, `partial`, `not_met` (assessed and failing — a finding, not a blank), `na` (does not apply). Never write `not_met` to mean you have not asked. ### Scope `domains` — 47 tools | Tool | | Inputs | What it does | |---|---|---|---| | `list_domains` | R | — | List every domain on this account with its current health score, last scan time, and SSL expiry. Call this first in almost any session — the numeric ids it returns are what every other domain tool takes. | | `add_domain` | W | `domain`, `onboarding?` | Start monitoring a new domain. Call this when the user names a domain that list_domains does not already return. | | `update_domain` | W | `domain_id`, `domain_group?`, `notes?`, `ignore_mail?`, `ignore_autoscan?`, `exposure_monitoring?` | Change the settings on one monitored domain: its group, notes, and whether mail checks or auto-scanning are skipped. Only the fields you pass are changed. | | `delete_domain` | W D | `domain_id` | Permanently remove a domain and its entire scan history from the account. This cannot be undone and the history cannot be recovered by re-adding the domain. | | `get_domain_history` | R | `domain_id` | Return the recorded scan results for one domain over time. Call this when the user asks whether something regressed, when a problem started, or how a score has moved. | | `get_domain_email_security` | W | `domain_id` | Return the current SPF, DKIM, DMARC, MX, MTA-STS, and TLS reporting evidence for one monitored domain. Use this instead of reducing email security to a single score. | | `discover_domain_subdomains` | W | `domain_id` | Passively discover public subdomains for one monitored domain from Certificate Transparency and DNS. This does not port-scan or probe the discovered hosts. | | `get_dns_change_events` | R | — | Return recorded DNS changes across the account so a caller can distinguish a new change from a long-standing finding. | | `get_domain_events` | R | — | Return Hijack Guard events across the account, including severity and lifecycle state for DNS, registrar, certificate, and hosting changes. | | `get_uptime_snapshots` | R | — | Return recent uptime probe observations for the monitored portfolio. | | `get_downtime_events` | R | — | Return recorded website downtime intervals and their recovery state across the monitored portfolio. | | `get_dmarc_summary` | R | `domain` | Return the 30-day aggregate-report volume and alignment rate for one monitored domain. | | `get_dmarc_diagnostics` | R | `domain` | Join the latest DNS scan, 30 days of aggregate reports, and sender attribution for one monitored domain into ranked diagnostics. | | `get_dmarc_setup_status` | R | `domain` | Return the server-authoritative SPF, DKIM, DMARC, report-arrival, and inbox-routing setup checklist for one monitored domain. | | `mark_alerts_read` | W | `ids?` | Mark the account's alerts as read (done). With no ids, every open alert is marked at once; with ids, only those. | | `plan_domain_onboarding` | R | `domain?` | Call this FIRST whenever the user wants to add or set up a domain. | | `onboard_domain` | W | `domain`, `sends_email?`, `lookalikes?`, `subdomains?`, `vulnerabilities?`, `accessibility?`, `exposure_monitoring?`, `group?`, `notes?`, `scan_client_id?`, `notification_email?`, `local_business?`, `ownership_email?`, `dark_web_emails?` | The one write that puts a domain on the account fully configured, from the answers plan_domain_onboarding told you to collect. | | `get_domain_setup_guide` | R | `domain_id` | The server-computed checklist for one domain: first scan, alerts, email authentication, DMARC reports, ownership proof, monitoring, local business, lookalikes, vulnerability scan, exposure monitoring — each done, todo, running, blocked by plan, skipped or not applicable, with how to finish it. | | `skip_domain_setup_step` | W | `domain_id`, `step`, `skipped?` | Record that a setup step does not apply to this domain (or un-skip it). Only optional steps can be skipped; the required ones cannot. | | `add_domains_bulk` | W | `domains` | Add up to 100 domains in one call. Returns which were added and which were skipped (already here, invalid). | | `list_alerts` | R | `limit?`, `before?` | The alert inbox: every notification the account was sent, newest first, with the domain, event type, channel, and whether it has been read. Read it when the user asks "what have you told me about" or before mark_alerts_read. | | `get_alert_activity` | R | `limit?`, `before?` | The History log: alerts, scans, changes and events across the account merged into one timeline. Broader than list_alerts, which is only what was sent. | | `get_account_settings` | R | — | The account's notification and scanning settings: which alerts are on, on which channel, where email goes, how often domains are scanned, and the thresholds. Read before changing anything so you change only what the user asked. | | `update_account_settings` | W | `notification_email?`, `scan_frequency?`, `notify_expiry_days?`, `notify_ssl_days?`, `security_threshold?`, `notify_expiry?`, `notify_dns_issues?`, `notify_security_issues?`, `notify_weekly_summary?`, `notify_ssl_expiry?`, `notify_scan_complete?`, `notify_scan_results_email?`, `notify_blacklist?`, `notify_status_changes?`, `notify_status_push?`, `notify_dns_record_changes?`, `notify_expiry_email?`, `notify_dns_issues_email?`, `notify_security_issues_email?`, `notify_ssl_expiry_email?`, `notify_dns_record_email?`, `notify_blacklist_email?`, `notify_status_changes_email?`, `notify_weekly_summary_email?`, `notify_uptime_push?`, `notify_uptime_email?`, `notify_dmarc_alerts?` | Change the account's alert and scan settings. Pass only the fields the user asked to change; everything else is kept. | | `list_domain_groups` | R | — | The group labels used to organise the portfolio (clients, brands, regions) with how many domains sit in each. | | `create_domain_group` | W | `name` | Create an empty group label. Assigning domains to it is assign_domains_to_group; update_domain also takes a group name. | | `delete_domain_group` | W D | `group_id` | Remove a group label. The domains in it are kept and become ungrouped. | | `assign_domains_to_group` | W | `domain_ids`, `group_name?` | Put up to 50 domains under one group name in a single call, creating the group if needed. Pass an empty group_name to ungroup them. | | `list_scan_clients` | R | — | Clients are the optional grouping a consultancy uses to hold domains and network targets per customer. Returns each with its id. | | `create_scan_client` | W | `name` | Create a client record to assign domains and IP targets to. Names are unique per account. | | `delete_scan_client` | W D | `client_id` | Remove a client record. Domains and targets assigned to it are kept, unassigned. | | `get_domain_hijack_events` | R | `domain_id` | Hijack Guard events for one domain: DNS, registrar, certificate and hosting changes with severity and lifecycle. get_domain_events is the account-wide view. | | `get_domain_event` | R | `event_id` | One Hijack Guard event in full: what changed, from what to what, when, and what has been done about it. | | `act_on_domain_event` | W | `event_id`, `action` | Move a Hijack Guard event through its lifecycle. authorize means "we made this change" and promotes it to the new baseline so it stops alerting; deny means "we did not" and keeps it flagged. | | `list_dmarc_reports` | R | `domain?`, `limit?`, `offset?` | The aggregate reports received, newest first, optionally for one monitored domain. Each names the reporter, the window, the published policy and the message counts. | | `get_dmarc_senders` | R | `domain`, `days?` | Sender attribution from the aggregate reports: each sending source, its volume, and whether it passed SPF and DKIM alignment. The list to read before tightening a DMARC policy. | | `get_dmarc_inbox` | R | — | The rua= address DomainGuard receives aggregate reports at for this account, whether it is provisioned, and any delivery error. This is the address to put in the DMARC record. | | `rotate_dmarc_inbox` | W | — | Issue a new report address and retire the old one. Every domain's DMARC record then has to be updated, so confirm with the user; there is a cooldown between rotations. | | `get_managed_email_auth` | R | — | Which domains have delegated their DMARC or SPF record to DomainGuard to publish and maintain, and whether the delegation is live. | | `enable_managed_email_auth` | W | `domain`, `service`, `source_record?` | Start managing one record: returns the CNAME the user publishes (for DMARC) or takes the SPF record to flatten and serve. After they publish, check_managed_email_auth confirms it. | | `disable_managed_email_auth` | W | `domain`, `service` | End a delegation. The user must publish their own record again or the domain will have none. | | `check_managed_email_auth` | W | `domain` | Look up DNS to confirm the delegation CNAME is published and serving. A live lookup; call it after the user says they published. | | `set_managed_dmarc_policy` | W | `domain`, `action` | For a managed DMARC record: advance moves the policy one step towards reject (none → quarantine → reject) when the reports show every legitimate sender aligned; revert steps it back. | | `list_support_conversations` | R | — | The account's support threads with NHM, newest first, with status. | | `get_support_conversation` | R | `conversation_id` | One support thread in full, including replies from NHM. | | `reply_to_support_conversation` | W | `conversation_id`, `message` | Add the user's message to an existing support thread. Opening a new thread needs the dashboard; say so if there is none. | | `update_support_conversation` | W | `conversation_id`, `status?`, `archived?` | Set a thread's status to open or closed, or archive/unarchive it. | ### Scope `scans` — 16 tools | Tool | | Inputs | What it does | |---|---|---|---| | `run_domain_scan` | W | `domain_id` | Trigger an immediate domain-health scan — DNS, registration, inferred certificate, security checkup, and single-page SEO. | | `get_checkup_history` | R | `domain_id` | Return past security checkup results for one monitored domain, including the individual findings and their severity. This is the richest source of "what is actually wrong with this site" — prefer it over a fresh checkup when recent data exists. | | `run_domain_checkup` | W | `domain` | Run a live security checkup. For a domain on this account this includes the exposed-file probe, which makes about 94 requests to the site. | | `get_dark_web_status` | R | — | Return cached breach-monitoring results for the account without making a new upstream lookup. | | `run_dark_web_scan` | W | — | Refresh every monitored email address against the breach index. This spends the account manual-scan allowance and may be rate limited. | | `list_dark_web_domain_monitors` | R | — | Return the verified domains being monitored for breached accounts and their latest scan state. | | `run_dark_web_domain_scan` | W | — | Refresh every verified whole-domain breach monitor. This uses the paid breach index and changes stored scan state. | | `report_scanner_issue` | W | `domain_id`, `category`, `severity?`, `check_name?`, `title`, `detail`, `evidence?`, `suggested_fix?` | Tell DomainGuard's operators that a scan result is wrong. | | `submit_scanner_review` | W | `domain_id` | Close the review you built with report_scanner_issue for one domain and send it to DomainGuard's operators as one message. Call it once, when you have finished reading that domain's results. | | `get_dark_web_breach` | R | `breach` | Details of one breach that a monitored address appeared in: what was exposed, when, and which monitored addresses it touched. | | `add_dark_web_email` | W | `email`, `cadence?` | Watch one address in breach data. Only addresses at the account's own domains or its verified sign-in address; cadence weekly and daily need a paid plan. | | `remove_dark_web_email` | W D | `email_hash` | Remove one monitored address by its hash, as get_dark_web_status lists it. | | `add_dark_web_domain_monitor` | W | `domain_id` | Watch every address at one monitored domain in breach data. Needs scan clearance (ownership proof) and the paid breach index. | | `remove_dark_web_domain_monitor` | W D | `monitor_id` | Remove one whole-domain monitor by the monitor id from list_dark_web_domain_monitors. | | `get_dark_web_alert_delivery` | R | — | Whether new breach hits are emailed. | | `update_dark_web_alert_delivery` | W | `email_enabled` | Turn emailed breach alerts on or off. Email alerts need a paid plan. | ### Scope `seo` — 52 tools | Tool | | Inputs | What it does | |---|---|---|---| | `get_seo_results` | R | `domain_id` | Return the most recent SEO and accessibility scan for one monitored domain: page titles, meta descriptions, heading structure, and accessibility violations. Call this when the user asks what to fix on their site for search. | | `get_seo_history` | R | `domain_id` | Return SEO scores for one monitored domain over time, to show whether changes actually moved the number. | | `run_seo_scan` | W | `domain_id` | Trigger a fresh SEO and accessibility scan of one monitored domain. Free-plan accounts are limited to one manual scan per day and will get a 429. | | `start_site_audit` | W | `domain_id`, `max_pages?` | Start a persisted crawl of a monitored site, following its sitemap or same-site links and checking every discovered page for SEO defects. Poll get_site_audit_status and then read get_site_audit_results. | | `get_site_audit_status` | R | `audit_id` | Poll a whole-site SEO audit by the audit id returned by start_site_audit. Check `truncated`: when it is true the site has more pages than the audit opened, `pagesDiscovered` says how many were seen, and the findings describe only the pages in `pagesChecked`. | | `get_seo_intent_conflicts` | R | `domain_id` | Read the newest crawl of a monitored domain and report which pages compete for the same search: pairs with one subject, one intent (informational, commercial, transactional, navigational) and one place, each with the page to keep and the steps to change the other (redirect, merge, differentiate with a proposed title, split). | | `get_site_audit_results` | R | `audit_id` | Return the page-level findings from a whole-site SEO audit. A `truncated` result covers only `pagesChecked` of the `pagesDiscovered` pages on the site, so absence of a finding is not evidence the rest are clean. | | `get_local_seo_profile` | R | `domain_id` | Return the saved business profile for one monitored domain: business name, location, service radius, and targeting mode. This profile is required before rank checks or map scans can run. | | `save_local_seo_profile` | W | `domain_id`, `business_name`, `location`, `targeting?`, `radius_miles?`, `is_service_business?`, `street_address?`, `phone?` | Create or update the business profile for a domain. Required fields are business_name and location (city, state). | | `get_local_keyword_targets` | R | `domain_id` | Return the keyword phrases being tracked for a domain in local search. These are the phrases whose rankings get checked when you run a rank check or a map scan. | | `add_local_keyword_target` | W | `domain_id`, `keyword`, `targeting?` | Add a keyword phrase to track for a domain. This is the phrase whose local ranking will be checked. | | `get_local_rank_history` | R | `domain_id` | Return recorded local ranking positions for a domain over time. Shows where the domain ranked for each tracked keyword on each check. | | `check_local_ranks` | W | `domain_id`, `request_key` | Run a live rank check for a domain: look up where it currently ranks for each tracked keyword in local search. Requires a saved business profile. | | `get_geogrid_scans` | R | `domain_id` | Return past geogrid (map) scan results for a domain. A geogrid scan checks ranking at multiple points across a grid covering the service area, showing where rankings drop off. | | `run_geogrid_scan` | W | `domain_id`, `keyword`, `grid_size?`, `request_key` | Run a geogrid scan that checks rankings at multiple points across the service area. Requires a local business profile with verified location. | | `get_local_seo_catalog` | R | — | Return the server-authoritative Local SEO catalog: every available action, its exact required inputs, price basis, prerequisites, and REST endpoints. Call this before run_local_seo_action so you can choose the right action and body. | | `run_local_seo_action` | W | `action_id`, `request_key`, `input` | Run one Local SEO action from get_local_seo_catalog without using the web interface. Pass its action_id, a fresh request_key, and exactly the action inputs as an object. | | `get_local_seo_operation` | R | `request_key` | Get the saved or in-progress result for a Local SEO action by the request_key used to start it. Use this to recover safely after a timeout instead of running the paid action again with a new key. | | `get_local_seo_credits` | R | — | Return the remaining rank-check and map-scan credits on the account. Call this before running a scan to avoid a credit-exhausted error. | | `delete_local_keyword_target` | W D | `keyword_id` | Remove a keyword phrase from tracking for a domain. Use the keyword id from get_local_keyword_targets. | | `get_seo_policy` | R | — | The account's SEO allowances: manual scans per day, crawl page caps, whether deep crawls and reports are on the plan. | | `list_manual_url_scans` | R | — | Past one-off SEO scans of individual URLs saved on the account. | | `fetch_sitemap` | W | `domain` | Read the sitemap of one monitored domain and return its URLs — the input for start_deep_seo_job. A live fetch of the site. | | `start_deep_seo_job` | W | `domain_id`, `domain`, `pages`, `sitemap_url?` | Crawl and score a list of specific pages on a monitored domain (from fetch_sitemap or the user). The plan caps how many; the response says if the list was trimmed. | | `list_deep_seo_jobs` | R | — | Every queued, running and finished deep crawl on the account with progress. | | `get_deep_seo_job` | R | `job_id` | Progress and status of one crawl job by id. | | `cancel_deep_seo_job` | W | `job_id` | Stop a queued or running crawl. Pages already scored are kept. | | `get_deep_seo_scans` | R | `domain_id` | The finished deep crawls for one domain with their scores and page counts. Each has a scan id for get_deep_seo_scan_pages. | | `get_deep_seo_scan_pages` | R | `scan_id` | Every page in one deep crawl with its title, description, headings, score and defects. | | `check_page_seo` | W | `url` | Fetch any public page and return its on-page SEO checks: title, description, headings, images, links, canonical. Works on any URL, not only monitored domains. | | `assess_website` | W | `url` | A quick combined read of any public URL: performance, accessibility and SEO signals in one result. A live fetch. | | `score_page_accessibility` | W | `url` | A quick static accessibility score of any public page. Not audit grade — start_accessibility_audit is. | | `score_page_performance` | W | `url` | A quick performance read of any public page: weight, requests, render-blocking resources. A live fetch. | | `email_seo_report` | W | `domain_id`, `to`, `note?` | Send the branded PDF SEO report for one monitored domain to an address, with an optional note. Needs a completed SEO scan. | | `get_report_branding` | R | — | The white-label branding applied to emailed reports (company name, logo, accent colour, footer) and whether the plan allows changing it. | | `save_report_branding` | W | `company_name?`, `logo_url?`, `accent_hex?`, `footer_text?` | Set the white-label branding on emailed reports. Enterprise only. | | `get_local_seo_quickstart` | R | `domain_id` | One read that shows where a domain stands in local search setup: the saved business (name, city, whether the city was confirmed), tracked phrases, suggested phrases from the homepage when nothing is saved yet, the newest local-pack and organic positions per phrase, the last Google Business Profile read, the credit balance, and the exact price of a run. | | `run_local_seo_quickstart` | W | `domain_id`, `business_name`, `location`, `keywords`, `request_key` | The whole first step in one call: saves the business name and city, saves up to 5 phrases, confirms the city against the gazetteer, then checks the local-pack AND organic position for every phrase and reads the Google Business Profile. | | `discover_local_seo_details` | W | `domain_id` | Read one monitored domain's public homepage and return candidate business name, city, street address, phone, whether it looks like a service-area business, and up to 12 candidate phrases — each with its source and a confidence. Nothing is saved. | | `verify_local_seo_location` | W | `domain_id`, `location` | Check a "City, ST" against the rank-data gazetteer before saving it, and get the resolved name or close matches. Free. | | `get_local_seo_access` | R | — | Says whether this account can use the Local SEO suite and on which plan. Check it before promising a rank check. | | `quote_local_ranks` | R | `domain_id`, `keywords?`, `surfaces?` | What check_local_ranks or a rank_check action would cost for this domain: per phrase per surface, against the current balance. Free. | | `quote_geogrid_scan` | R | `domain_id`, `keyword`, `grid_size?`, `location?` | What run_geogrid_scan would cost for one phrase and grid size, and whether the balance covers it. Free. | | `get_geogrid_entitlement` | R | `domain_id` | Free map scans remaining this period for a domain, and what more would cost. | | `quote_local_seo_endpoint` | R | `domain_id`, `endpoint` | The credit cost of one catalogued endpoint for a domain before running it. get_local_seo_catalog gives the price basis; this gives the number. | | `search_locations` | R | `q`, `state?`, `country?`, `limit?` | Find the exact "City, ST" the rank data covers, from a partial name. Use it when verify_local_seo_location or the quick start says the city was not found. | | `list_covered_locations` | R | `prefix?`, `state?`, `country?`, `limit?`, `offset?` | Page through the cities the rank data covers, optionally by state or name prefix. | | `get_local_keyword_ideas` | R | `domain_id`, `limit?` | Phrase ideas already gathered for a domain by earlier keyword research, without spending anything. | | `get_backlink_snapshots` | R | `domain_id` | Past backlink summaries recorded for a domain, so link growth can be shown over time without a new lookup. | | `get_local_seo_audit_history` | R | `domain_id`, `kind?` | Saved local, citation and reputation audits for a domain — reopening one is free. | | `list_local_seo_reports` | R | `domain_id` | Saved profile-health, reviews, AI-visibility, competitor-gap and citation reports for a domain. Reopening is free; the catalog actions refresh them. | | `quote_local_seo_report` | R | `domain_id`, `type`, `refresh?` | Whether a report of this type is already saved (free to reopen) and what a refresh would cost. | ### Scope `accessibility` — 7 tools | Tool | | Inputs | What it does | |---|---|---|---| | `quote_accessibility_audit` | R | `domain_id` | Return cost, available credits, page cap, and whether an audit can start for one monitored domain. | | `list_accessibility_audits` | R | `domain_id?`, `limit?` | Return recent audit-grade WCAG 2.2 AA accessibility evaluations, optionally narrowed to one monitored domain. | | `start_accessibility_audit` | W | `domain_id`, `request_key`, `max_pages?`, `viewport?` | Queue an audit-grade WCAG 2.2 AA browser evaluation for one monitored domain. It spends accessibility credits. | | `get_accessibility_audit` | R | `audit_id` | Return pages, axe findings, WCAG rollups, manual-review coverage, and regression data for one audit id. Automated results do not establish conformance. | | `record_accessibility_manual_review` | W | `audit_id`, `wcag_criterion`, `status`, `notes?`, `evidence_reference?` | Record the human judgement on one WCAG criterion the automated audit cannot decide. Only after the automated checks finish. | | `complete_accessibility_audit` | W | `audit_id` | Mark an audit complete once the manual reviews are recorded. It can then be exported. | | `export_accessibility_audit` | R | `audit_id` | The full audit as a JSON evidence pack: pages, findings, manual reviews, WCAG rollups. Automated results do not establish conformance; say so when handing it over. | ### Scope `vulnscan` — 47 tools | Tool | | Inputs | What it does | |---|---|---|---| | `get_vuln_scan_latest` | R | `domain_id` | Return the most recent completed vulnerability scan for one monitored domain, with its findings and severities. Check this before starting a new scan — scans take minutes and a recent one usually answers the question. | | `get_vuln_scan_results` | R | `scan_id` | Return the findings for one vulnerability scan by its scan id. Use this to poll a scan that start_vuln_scan just kicked off, since that tool returns immediately while the scan runs in the background. | | `get_vuln_scan_history` | R | `domain_id` | Return the last 20 vulnerability scans for one monitored domain, including status, profile, timing, and severity summary. | | `get_vulnerability_assets` | R | `domain_id` | Return every public hostname discovered for a monitored domain and the latest finding counts attached to each host. | | `get_open_ports` | R | `domain_id` | Return the most recent per-host port, service, banner, version, and risk observations for one monitored domain. | | `list_lookalikes` | R | `domain_id` | Return domains registered by other people that imitate one monitored domain, each with a risk band, the evidence behind it, and when it was first seen. | | `get_lookalike_events` | R | `domain_id` | Return what changed about a monitored domain's impostors over time — one appearing, one gaining the ability to receive email, one getting a certificate, one becoming more dangerous. This is the history; list_lookalikes is the current state. | | `run_lookalike_scan` | W | `domain_id` | Generate variations of a monitored domain, check which are registered, and score how dangerous each one is. Takes up to a minute and counts against the plan, so prefer list_lookalikes for stored results unless a fresh check is specifically wanted. | | `set_lookalike_verdict` | W | `domain_id`, `lookalike`, `verdict?` | Say what one impostor actually is, so it stops being reported. | | `get_observed_components` | R | — | Return the account portfolio technology inventory discovered by site-intelligence and vulnerability scans, including version confidence where observed. | | `get_scan_ownership_verification` | W | `domain_id` | Return the DNS, file, or email proof required before active vulnerability and whole-domain breach scans may run, creating a challenge when one does not exist. | | `verify_scan_ownership` | W | `domain_id` | Re-check the published DNS/file proof for one monitored domain. This records verification when the proof is found. | | `start_vuln_scan` | W | `domain_id`, `scan_type?`, `scan_profile?`, `target_hosts?` | Start a vulnerability scan against one monitored domain. Returns a scan id immediately; the scan itself runs in the background, so poll get_vuln_scan_results rather than expecting findings in the reply. | | `get_scan_clearance_status` | R | — | Account-wide view of ownership proof: how many domains carry a current proof and which. | | `verify_scan_ownership_bulk` | W | `domain_ids?` | Re-check the published DNS or file proof for the domains given (or every domain when omitted) and record what is found. | | `request_ownership_email_link` | W | `domain_id`, `address` | The easiest ownership proof: send a one-click link to an address AT the domain (postmaster@, admin@, the owner's own mailbox — never a gmail or yahoo address). When they click it the whole account is cleared for active scanning for 90 days. | | `request_scan_authorization` | W | `domain_id`, `reason`, `contact_email?` | For a domain the account cannot prove ownership of but is authorised to test (a client engagement), file a request with the reason and a contact; an operator approves or declines. Check get_scan_authorization for the answer. | | `get_scan_authorization` | R | `domain_id` | The latest operator authorisation for one domain and whether it is currently active. | | `list_scan_authorizations` | R | `limit?`, `offset?` | Every authorisation request the account has filed, newest first, with its status. | | `set_lookalike_watch` | W | `domain_id`, `enabled` | Continuous impostor-domain monitoring for one domain. Turning it on needs the plan capability; turning it off is always allowed. | | `get_lookalike_evidence` | R | `domain_id`, `lookalike` | Everything recorded about one lookalike domain — registration, DNS, mail capability, certificates, timeline — assembled for a registrar abuse report or a takedown filing. | | `act_on_lookalike_event` | W | `event_id`, `action` | Mark one lookalike change event as seen (acknowledge) or dealt with (resolve). Event ids come from get_lookalike_events. | | `get_lookalike_alert_settings` | R | — | Whether lookalike alerts go out by email and by push. | | `update_lookalike_alert_settings` | W | `email`, `push` | Turn email and push delivery of lookalike alerts on or off. Both fields are written; read them first. | | `list_vuln_dismissals` | R | `kind?` | Findings, inventory readings and software alerts the account has permanently silenced, with the reason given. | | `dismiss_vuln_finding` | W | `kind`, `target_key`, `reason`, `domain_id?`, `label?`, `note?` | Silence one thing for good, with a reason: false_positive, accepted_risk, fixed or not_applicable. target_key is the finding's stable key from the scan result (what it IS, not its row id), so the dismissal survives re-scans. | | `restore_vuln_dismissal` | W | `dismissal_id` | Remove a dismissal so the finding, component or alert is reported again. Use the id from list_vuln_dismissals. | | `list_software_vendors` | R | — | The observed technology inventory rolled up by company: every vendor seen across the account's domains with component counts and open advisories. | | `get_software_vendor` | R | `vendor` | Every component, version and domain observed for one vendor, with its advisories. vendor is the lowercase slug from list_software_vendors. | | `get_vulnerability_feed` | R | — | The global feed of recently ingested vulnerabilities (CISA KEV and NVD), not filtered to this account. Use get_software_advisory_history for one product. | | `get_software_advisory_history` | R | `product`, `vendor?` | Every known advisory for one software product, matched the same way the alert ingest matches it. Use it when a component has no status feed: "CVEs and KEVs only". | | `list_tracked_software` | R | — | The software products the account watches for new vulnerabilities, with alert preferences and open alert counts. | | `search_software_catalog` | R | `q?`, `category?` | Find a product to track by name or category. Returns slugs to pass to track_software. | | `get_software_catalog` | R | — | Every product in the catalog. Large; prefer search_software_catalog. | | `get_software_catalog_categories` | R | — | The categories the software catalog is organised by. | | `track_software` | W | `app_name`, `app_slug?`, `vendor?`, `alert_pref?`, `sources?`, `severity_filter?` | Start watching one product for new KEV and CVE entries. alert_pref is how often to be told; severity_filter narrows what counts. | | `update_tracked_software` | W | `app_id`, `alert_pref?`, `is_active?`, `severity_filter?`, `sources?` | Change how one tracked product alerts, or pause it with is_active false. Only the fields passed change. | | `untrack_software` | W D | `app_id` | Remove a tracked product and its alerts. Confirm the product name first. | | `list_software_alerts` | R | `status?`, `source?`, `limit?`, `offset?` | KEV and CVE alerts raised for tracked and observed software, each with severity and an exposure verdict: confirmed (the vulnerable version was observed), possible, not_affected or unconfirmed. Lead with confirmed ones. | | `get_software_alert_summary` | R | — | Counts of unread, critical, KEV and confirmed-exposure software alerts, per tracked product and in total. | | `update_software_alert` | W | `alert_id`, `alert_status` | Set the status of one software alert. remediated records that the fix was applied; dismissed means it will not be. | | `list_ip_targets` | R | — | The IP addresses and CIDR ranges the account port-scans daily, with their client and last result. | | `add_ip_target` | W | `target`, `attestation`, `label?`, `client_id?`, `daily_scan?` | Start daily port scanning of a public IP or small CIDR. The user must confirm they own or are authorised to scan it; pass attestation true only after they say so in words. | | `update_ip_target` | W | `target_id`, `client_id?`, `daily_scan?` | Move a target to another client or turn its daily scan on or off. | | `delete_ip_target` | W D | `target_id` | Stop scanning a target and remove it with its history. Confirm first. | | `list_ip_target_scans` | R | `target_id` | Past port scans of one target with the open ports and services seen each time. | | `scan_ip_target` | W | `target_id` | Run the port scan of one target immediately instead of waiting for the daily run. Sends traffic to the target. | ### Scope `compliance` — 7 tools | Tool | | Inputs | What it does | |---|---|---|---| | `get_compliance_records` | R | — | Return the account's compliance checklist — PCI, HIPAA, and cyber-insurance controls with their current status and notes. Each control carries one of unanswered, met, partial, not_met, na. | | `update_compliance_item` | W | `record_id`, `status`, `notes?` | Set the status and notes on one compliance control. Use the record id from get_compliance_records, not the item_id. | | `initialize_compliance_records` | W | `items` | Create compliance control records from a list of items (item_id, framework, category, requirement, optional status and notes). Existing records are kept. | | `bulk_update_compliance` | W | `items` | Set status and notes on many controls in one call, by item_id. Same status vocabulary as update_compliance_item, same rule: never write not_met for a control nobody assessed. | | `list_incident_records` | R | — | Saved incident-response records and Written Information Security Plan documents on the account. | | `create_incident_record` | W | `event_type`, `event_title`, `event_severity?`, `event_summary?`, `playbook?`, `wisp_input?`, `wisp_text` | Record an incident (type, title, severity, summary), the playbook steps taken, and the WISP text produced for it. These are compliance evidence: use the user's words. | | `delete_incident_record` | W D | `record_id` | Remove one incident record. Confirm first; it is evidence. | ### Scope `status` — 16 tools | Tool | | Inputs | What it does | |---|---|---|---| | `list_status_checks` | R | — | List the custom endpoint checks configured on this account, with their URLs and most recent result. | | `get_status_services` | R | — | Return the current status of the third-party providers this account tracks, such as Cloudflare or Google Cloud. Call this when the user reports an outage, to separate a provider incident from a problem with their own site. | | `create_status_check` | W | `name`, `url` | Add a custom endpoint check that polls a URL on a schedule and records whether it responded. | | `run_status_check` | W | `check_id` | Run one saved custom check immediately and return the result, instead of waiting for its next scheduled poll. Use the check id from list_status_checks. | | `delete_status_check` | W D | `check_id` | Permanently remove a custom endpoint check and its run history. Use the check id from list_status_checks. | | `get_status_incidents` | R | — | Open incidents across every tracked provider right now. Read with get_status_services when a user reports an outage. | | `get_status_history` | R | `limit?`, `offset?` | Past status changes across tracked providers, newest first. | | `list_status_subscriptions` | R | — | Which third-party providers this account is subscribed to for status alerts, and at what level. | | `get_status_subscription_suggestions` | R | — | Providers the account's domains were observed depending on (host, DNS, email, CDN) but is not yet subscribed to. Offer these during onboarding. | | `update_status_subscriptions` | W | `subscriptions` | Subscribe or unsubscribe from providers. Each entry names a service_id from get_status_services and a notify_level: all, major, or none. | | `subscribe_all_status_services` | W | — | Subscribe to every provider the account's domains depend on, in one call. | | `unsubscribe_all_status_services` | W | — | Clear every provider subscription. Confirm first. | | `suggest_status_service` | W | `status_page_url`, `service_name?`, `notes?` | Ask for a provider status page to be added to the catalogue. Needs its https:// status page URL. | | `probe_endpoint` | W | `url`, `method?`, `expected_http_status?`, `body_must_contain?`, `timeout_ms?`, `follow_redirects?`, `probe_mode?`, `headers?`, `body?` | Send one request to a URL from DomainGuard's network and return what came back — status, timing, headers, whether expected text was present. Paid plans; the target must be on a domain the account has proven. | | `update_status_check` | W | `check_id`, `name?`, `url?`, `method?`, `expected_http_status?`, `body_must_contain?`, `timeout_ms?`, `follow_redirects?`, `probe_mode?`, `headers?`, `body?` | Change any field of a saved check: name, URL, method, expected status, required body text, timeout, redirects, probe mode, headers, body. Only the fields passed change. | | `get_status_check_runs` | R | `check_id` | Every recorded run of one custom check: when, status, latency, and whether it passed. | ### Scope `admin` — 4 tools | Tool | | Inputs | What it does | |---|---|---|---| | `list_error_events` | R | `days?`, `source?` | The Worker's own error log (`error_events`): every recorded failure in the window, grouped by fingerprint with occurrence counts and the accounts affected, plus the newest 100 raw rows. Admin-scoped keys only. | | `get_self_check_status` | R | — | Every self-check invariant's last verdict — the failing ones first, with the observed value and the sentence the check wrote — and when each cron scope last completed a pass. Admin-scoped keys only. | | `list_scan_failures` | R | `days?` | Domain scans, exposure scans, site crawls and accessibility audits that ended in failure in the window, with their recorded reason, plus exposure scans stuck queued or running for over two hours. Admin-scoped keys only. | | `list_scanner_reports` | R | `days?`, `status?` | What customers' assistants reported wrong with the scanner, open ones first: the claim, the evidence, the suggested fix, who sent it. Admin-scoped keys only. | --- ## 12. Protocol notes - One endpoint, `POST /api/mcp`, JSON-RPC 2.0. An authenticated `GET` returns 405 with `Allow: POST`; an unauthenticated one returns 401, since the key is checked first. - Methods: `initialize`, `server/discover`, `tools/list`, `tools/call`, `prompts/list`, `prompts/get`, `resources/list`, `resources/read`, `resources/templates/list` (empty), `ping`. - Prompts: `onboard_domain`, `set_up_local_seo`, `review_domain`, `portfolio_triage`, `fix_email_deliverability`. Each takes an optional `domain` argument (`review_domain` and `fix_email_deliverability` require it) and returns one user-role message that tells the model which tools to call, in what order, and what to ask in between. - Resources: `domainguard://guide` (this document, read from the deployed file), `domainguard://onboarding`, `domainguard://local-seo`, `domainguard://review` (the three protocols in §4–§5). All `text/markdown`. - Protocol revisions supported: `2026-07-28` (stateless) and the handshake era `2025-11-25`, `2025-06-18`, `2025-03-26`. Both eras are served from the same stateless code path. - No sessions and no `Mcp-Session-Id`. Nothing is remembered between calls. - JSON-RPC batching is not supported; it was removed from the spec in 2025-06-18. - Notifications receive `202` with no body. - A tool that fails returns a normal result with `isError: true` and readable text. Only transport-level problems return a non-200 status. - `tools/list` results are cacheable, private, and scope-dependent — never share one key's tool list with another key. Prompt and resource lists are public and identical for every key. - Every tool is re-dispatched through the same route table the dashboard uses, so the tenant checks, plan gates and rate limits are the REST API's own. --- ## 13. Paste-in block for a project's CLAUDE.md ```markdown ## DomainGuard MCP The `domainguard` MCP server is connected. It reaches every part of the account: DNS, SSL, mail authentication and DMARC reports, security checkups, Hijack Guard, lookalikes, vulnerabilities and ports, software CVE/KEV alerts, dark web, on-page and site-wide SEO, accessibility, local rankings and the Google Business Profile, provider status, compliance and incident records. Rules: - Start with `list_domains`; every other tool needs the numeric `domain_id`. - Read stored results (`get_checkup_history`, `get_seo_results`, `get_vuln_scan_latest`, `get_local_rank_history`, `get_domain_setup_guide`) before running any scan. - To add or set up a domain: `plan_domain_onboarding` first, ask its questions in plain words, then `onboard_domain` once. It spends nothing. Paid steps (`run_local_seo_quickstart`, `start_accessibility_audit`) come after, on a quote the user agreed to. - Verify what you can before presenting it (resolve the record, fetch the headers, count the tags). When the scanner is wrong, `report_scanner_issue` with what it said, what is true, how you know, what to change; then `submit_scanner_review` once per domain. Report only what you verified. - `run_*`, `start_*`, `check_*`, `verify_*`, `scan_*`, `probe_*`, `fetch_*`, `discover_*` and `assess_*` cost time or credits. So do `get_domain_email_security` and `get_scan_ownership_verification`. Say the cost before spending. - Call `get_local_seo_catalog` before `run_local_seo_action`; it is the authority on inputs and price. - Every paid action needs a fresh `request_key` (8-100 chars, [A-Za-z0-9._-]). After a timeout use `get_local_seo_operation`, never a re-run. - `start_vuln_scan`, `start_site_audit`, `start_deep_seo_job` and `start_accessibility_audit` are asynchronous — poll their results tools. - Confirm every `delete_*`, `remove_*`, `untrack_*` and `unsubscribe_*` in words before calling it. - DomainGuard monitors these domains, it does not host them. Deliver the diagnosis plus the exact change, and say where the user applies it — registrar, DNS provider, host, CMS, or Google Business Profile. - Never estimate a score, position, volume, or price that a tool did not return; never invent a business name, city, phrase or email address. Full guide: https://nhmohio.com/mcp/claude-guide.md (also the MCP resource `domainguard://guide`). Prompts: /mcp__domainguard__onboard_domain, set_up_local_seo, review_domain, portfolio_triage, fix_email_deliverability. ```