{
  "markdown": "<div align=\"center\">\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"assets/wordmark-dark.svg\">\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"assets/wordmark-light.svg\">\n  <img alt=\"SEOMonster\" src=\"assets/wordmark-light.svg\" width=\"440\">\n</picture>\n\n<h3>Don't learn another SEO dashboard. Just ask.</h3>\n\n<p>The open-source SEO analyst that lives inside the AI assistant you already use —<br>working from <b>your own</b> Search Console, Analytics, and PageSpeed data, on your machine.</p>\n\n<p>\n  <a href=\"https://pypi.org/project/seo-monster/\"><img src=\"https://img.shields.io/pypi/v/seo-monster?color=2BD9A8&label=pypi\" alt=\"PyPI version\"></a>\n  <a href=\"https://pypi.org/project/seo-monster/\"><img src=\"https://img.shields.io/pypi/pyversions/seo-monster\" alt=\"Python versions\"></a>\n  <a href=\"https://pypi.org/project/seo-monster/\"><img src=\"https://img.shields.io/pypi/dm/seo-monster?color=2BD9A8\" alt=\"Downloads\"></a>\n  <a href=\"https://github.com/avansaber/seo-monster/actions\"><img src=\"https://img.shields.io/badge/tests-575%20passing-2BD9A8\" alt=\"Tests\"></a>\n  <img src=\"https://img.shields.io/badge/license-MIT-green\" alt=\"License: MIT\">\n  <a href=\"https://github.com/avansaber/seo-monster/stargazers\"><img src=\"https://img.shields.io/github/stars/avansaber/seo-monster?style=flat&color=2BD9A8\" alt=\"GitHub stars\"></a>\n</p>\n\n<p>\n  <a href=\"https://cursor.com/en/install-mcp?name=seomonster&amp;config=eyJjb21tYW5kIjoidXZ4IiwiYXJncyI6WyJzZW8tbW9uc3RlciJdfQ==\"><img src=\"https://cursor.com/deeplink/mcp-install-dark.svg\" alt=\"Add to Cursor\" height=\"32\"></a>\n  &nbsp;\n  <a href=\"https://insiders.vscode.dev/redirect/mcp/install?name=seomonster&amp;config=%7B%22command%22%3A%22uvx%22%2C%22args%22%3A%5B%22seo-monster%22%5D%7D\"><img src=\"https://img.shields.io/badge/VS_Code-Install_server-0098FF?style=flat&logo=visualstudiocode&logoColor=white\" alt=\"Install in VS Code\" height=\"32\"></a>\n  &nbsp;\n  <a href=\"https://seomonster.avansaber.com\"><img src=\"https://img.shields.io/badge/Website-seomonster.avansaber.com-555\" alt=\"Website\" height=\"32\"></a>\n</p>\n\n</div>\n\n---\n\nYou don't open a new tool — you just ask, in plain English, inside **Claude** (or Cursor / Cline / Codex):\n\n> **You** &nbsp; *\"What should I write about next?\"*\n>\n> **🦖 SEOMonster** &nbsp; Pulls your Search Console and surfaces three topics you're *almost* ranking for — positions 8–20 with real demand — ranked by opportunity. Say the word and it drafts the brief.\n\n| You ask … | … SEOMonster does |\n| :-- | :-- |\n| *\"What should I write about next?\"* | Surfaces near-ranking topics from **your own** Search Console demand |\n| *\"Is this page ready to publish?\"* | Runs the technical-SEO + structured-data checks before you ship |\n| *\"Get this indexed.\"* | Nudges Google (Indexing API) and Bing / Yandex (IndexNow) |\n| *\"Did my change actually move rankings?\"* | Before/after attribution vs a matched control group — with a confidence interval, not a guess |\n| *\"Is ChatGPT recommending us, or our competitors?\"* | Tracks your brand's **share of voice** across the AI answer engines |\n\n**70 tools** across Search Console, GA4, PageSpeed, Cloudflare, AI-citation tracking, keyword discovery, and technical SEO — every one returning the same JSON result envelope, every one driven by your own credentials. No new dashboard to learn; you chat with the assistant you already use.\n\n> [!NOTE]\n> Published on PyPI as **`seo-monster`** (so the command is `uvx seo-monster`). The import package is `seo_mcp`; `seo-mcp` is a dev/local alias. The package ships **zero secrets** — every credential is resolved at runtime from your own environment or config file.\n\n- **Your data, your machine.** User-credential-driven; nothing is sent to any third party.\n- **Read-first, safe by default.** Reads always work; the two routine writes (sitemap submit, indexing request) are on by default; the Cloudflare write tools (cache purge, redirects, settings, managed robots.txt) are gated behind `SEO_MCP_ALLOW_DESTRUCTIVE`, and the riskier ones also need a per-call `confirm` token.\n- **Lean.** Standard library + the `mcp` SDK + the Google client libraries; PageSpeed and Cloudflare ride on `urllib`, no extra HTTP dependency.\n\n## Quickstart\n\n> [!TIP]\n> Fastest path: the **one-click install** buttons above. Or add it to any MCP client by hand:\n\n```json\n{\n  \"mcpServers\": {\n    \"seomonster\": { \"command\": \"uvx\", \"args\": [\"seo-monster\"] }\n  }\n}\n```\n\nRun the one-time Google sign-in once — `uvx --from seo-monster seo-monster auth` — then ask your assistant to call **`system_status`** to confirm what's connected. Full per-client setup and credentials are in [Install](#install) and [Auth](#auth).\n\n## How it works\n\n```mermaid\nflowchart LR\n  U[\"You — plain English\"] --> H[\"Claude · Cursor · Cline · Codex\"]\n  H -- \"MCP · stdio\" --> S[\"SEOMonster<br/>70 tools\"]\n  S --> GSC[\"Search Console · GA4\"]\n  S --> PSI[\"PageSpeed · CrUX\"]\n  S --> CF[\"Cloudflare · IndexNow\"]\n  S --> AI[\"AI engines · SERP<br/>(optional)\"]\n  S -. \"your creds · your machine\" .-> U\n```\n\nSEOMonster is a stdio **MCP server**: your AI host launches it, it calls Google / Cloudflare / the AI engines with *your* credentials, and returns a consistent JSON envelope the assistant reads back to you in plain language.\n\n## Requirements\n\nFor the **`.mcpb` bundle path** (Claude Desktop): just Claude Desktop on macOS\nor Windows. The bundle declares Python 3.11+ as a runtime; Claude Desktop\nmaterializes the environment for you. No prior `uv` install needed.\n\nFor the **`uvx` path** (Cursor, Cline, Codex, advanced Claude Desktop): Python\n3.11 or newer plus [`uv`](https://docs.astral.sh/uv/) (which provides `uvx`).\nFind the absolute path to `uvx` with `which uvx`; GUI hosts do not read your\nshell profile, so MCP configs need the full path.\n\n## Tools\n\n70 tools, grouped by service. All return the same result envelope (see\n[Result envelope](#result-envelope)). Call `system_status` first if unsure what\nis configured. The server also publishes thirteen named [workflow prompts](#workflow-prompts).\n\n**Cross-service**\n- `system_status` - which services are configured/reachable, the Google auth\n  method and scopes, whether destructive mode is on, the full tool catalog,\n  and the list of registered prompts.\n\n<details>\n<summary><b>📂 Browse all 70 tools</b> — grouped by service (GSC · GA4 · AI/GEO · keyword discovery · content · PageSpeed/CrUX · Cloudflare · IndexNow · technical · schema). Click to expand.</summary>\n<br>\n\n<a name=\"gsc\"></a>\n**Google Search Console (18)**\n\n*Workhorses*\n- `gsc_list_properties` - properties the credentials can see, with permission\n  level and a derived `writable` flag (true for `siteOwner` / `siteFullUser`).\n- `gsc_search_analytics` - the workhorse: clicks/impressions/CTR/position by\n  dimensions, date range, filters, and `data_state`.\n- `gsc_top_queries` / `gsc_top_pages` - convenience top-N wrappers.\n- `gsc_compare_periods` - current vs prior window with per-key deltas.\n  v0.2.0 added `sort_by`, `sort_dir`, `min_delta_clicks` / `_impressions` /\n  `_position`, `anomalies_only` + `sigma_threshold`, and `top` for one-call\n  movers / losers / outliers reporting.\n- `gsc_inspect_url` - URL Inspection (index verdict, coverage, canonicals).\n- `gsc_batch_inspect_urls` - inspect up to 25 URLs, per-URL failures collected.\n- `gsc_list_sitemaps` - registered sitemaps and their status.\n- `gsc_submit_sitemap` - submit a sitemap (write, un-gated; needs the writable\n  scope). Accepts either `sitemap_url` (friendly) or `feedpath` (raw API field).\n- `gsc_request_indexing` - request (re)crawl via the Indexing API (write,\n  un-gated). Accepts singular `url` or `urls`.\n\n*Query intelligence (v0.2.0)*\n- `gsc_query_opportunities` - queries already ranking top N with below-target\n  CTR. Title and meta optimization candidates.\n- `gsc_query_gaps` - queries that draw impressions but barely any clicks.\n  Content opportunity signal.\n- `gsc_new_queries` - queries appearing in the current window with no prior\n  impressions. Emerging topics.\n- `gsc_top_pages_by_query` - which pages rank for a specific query. The\n  cannibalization audit input.\n\n*Multi-property + lifecycle (v0.5.0)*\n- `gsc_portfolio_summary(days, include?, exclude?)` - multi-property fleet\n  view. Per-property one-row summary (clicks, impressions, CTR, position)\n  for the last N days, plus a portfolio-level rollup. Honors optional\n  `include` / `exclude` filters. The single fastest answer to \"how is the\n  whole portfolio doing?\" across agency or multi-brand setups.\n- `gsc_trending_pages(days, limit)` - pages whose impressions grew most over\n  the last N days vs the prior N days. Wrapper on `gsc_compare_periods` with\n  `dimensions=[\"page\"], sort_by=\"delta_impressions\", sort_dir=\"desc\"`.\n- `gsc_decaying_pages(days, limit)` - same wrapper, ascending sort. Pages\n  to rescue.\n- `gsc_coverage_audit(urls, site_url?)` - heuristic coverage audit. The GSC\n  Index Coverage report is not exposed in the API; this tool takes a user-\n  supplied URL list (typically pulled from a sitemap) and bulk-inspects\n  each, then rolls up verdicts (PASS / PARTIAL / FAIL) and coverage_state\n  frequencies.\n\n<a name=\"content\"></a>\n**Content intelligence (4)**\n- `content_opportunities(site_url?, days?, count?, impressions_min?)` - ranks\n  data-grounded content topics from your own Search Console data: fuses\n  CTR-vs-expected gap (curve self-calibrated from your own per-position CTR),\n  striking-distance position, demand, and momentum into a transparent\n  opportunity score; flags cannibalization. If a GA4 property is configured, it\n  also weights each topic by the organic conversions its top page already drives\n  (up to +50%), so topics that convert rank higher; `filters_applied.ga4_value_status`\n  reports whether that ran and why (`applied` / `no_ga4_property` /\n  `ga4_unreachable` / `no_conversions`). Prioritizes demand you already have;\n  does not do cold-start keyword research or write the content. Pairs with the\n  content workflow prompts below. (GA4 weighting v0.7.3) v0.9.0 adds an additive\n  per-candidate `winnability` block (banded: striking-distance + topical-\n  proximity, GSC-personalization tier; existing fields unchanged).\n- `content_brief_data(target_query, competitor_urls?, topic?, site_url?, days?)` -\n  data-wired backing for a content brief: fetches the competitor pages (or your\n  own GSC-ranking pages as a fallback) and returns the heading union, median\n  word-count floor, schema types, and entity coverage, plus the 2026 GEO writing\n  directives and validation rules. The host writes the prose; SEOMonster brings\n  rules + evidence. Backs the `content_brief` prompt. (v0.9.0)\n- `topic_cluster_map(cluster_path | pillar_url, site_url?, days?, impressions_min?)`\n  - maps a content cluster from your own GSC data and surfaces missing subtopics.\n  Classifies each cluster query into defend / optimize / create / monitor by\n  demand and best position; the create quadrant is your missing-subtopic list.\n  Flags cannibalization. GSC-only; honest about the ~47% query anonymization. (v0.9.0)\n- `rank_change_attribution(url | urls, change_date, query?, site_url?, pre_days?, post_days?, gap_days?, control_scope?)`\n  - estimates whether an on-site change moved a page's clicks via\n  difference-in-differences against a matched control group (never a naked\n  before/after). Returns an estimated lift with a 95% CI, a three-state verdict\n  (likely_positive / likely_negative / inconclusive), and a confounders block\n  that auto-detects the 2025 GSC data-regime breaks (impression bug, num=100) and\n  downgrades position reliability. Observational, not causal -- a server-side\n  split test is the only true causal test. GSC-only. (v0.9.0)\n\n<a name=\"ai\"></a>\n**AI / GEO citation (3, v0.9.0)** - whether the AI answer surfaces reach and cite you.\n- `ai_citation_readiness(url)` - is a page structured to be extracted/cited by\n  LLM answer engines? Leads with a render-blindness check (GPTBot / ClaudeBot /\n  PerplexityBot fetch but do not run JS, so a client-rendered SPA is invisible to\n  them), then scores evidence-backed signals (statistics, quotations, cited\n  sources, no keyword-stuffing). schema.org / FAQ / llms.txt are reported as\n  informational only -- the 2026 evidence does not support them as AI-citation\n  drivers, so they are not scored. Free, HTTP-only.\n- `ai_referral_overview(property_id?, site_url?, days?)` - first-party AI traffic:\n  GA4 referral sessions from AI apps (the native `ai-assistant` channel plus a\n  configurable source-host regex) and AI-crawler robots coverage (GPTBot,\n  ClaudeBot, PerplexityBot, ...). Surfaces the ~70% dark-traffic undercount and\n  keeps AI-Overview clicks (counted as Organic) separate. Free.\n- `ai_citation_track(prompts, brand, brand_domains?, competitors?, engines?, samples?)`\n  - sampled brand mention + citation share-of-voice across AI engines\n  (Perplexity / OpenAI / Anthropic / Gemini APIs + Google AI Overviews via\n  DataForSEO) for a managed prompt set, vs competitors. N samples/prompt (default\n  7) with a 95% CI, share-of-voice, and run-to-run volatility -- NOT an \"AI rank\"\n  (single runs are statistically meaningless). Discloses that developer-API output\n  differs from the logged-in consumer UI and that AIO has no API. Paid +\n  non-deterministic. Needs at least one engine key and/or DataForSEO.\n\n<a name=\"discovery\"></a>\n**Keyword discovery (3, v0.9.0)** - find terms you don't already rank for.\n- `gsc_keyword_expand(candidates, site_url?, days?, impressions_min?)` - you\n  (the host) brainstorm candidate terms from your winning queries; this grounds\n  each against your own Search Console data (footprint covered / thin / none) with\n  a sibling-strength confidence band. \"none\" = no VISIBLE footprint (GSC hides\n  ~75% of impressions), so net-new terms are scored hypotheses. Free.\n- `serp_adjacency_expand(seeds, include_paa?)` - expand seed terms into adjacent\n  terms. FREE core: Google Autocomplete (no key). Optional People-Also-Ask +\n  related searches via DataForSEO. Returns per-seed suggestions plus the\n  aggregated net-new terms; degrades gracefully without a key.\n- `keyword_universe(target_domain?, competitors?, keywords?, limit?)` - optional,\n  paid. Core value: the competitor keyword GAP (DataForSEO Domain Intersection;\n  no Google equivalent). Optional search volume / difficulty / intent via a\n  provider chain (DataForSEO, else Google Ads volume-only). External volume is a\n  degraded directional signal -- a tiebreaker, never a gate.\n\n<a name=\"ga4\"></a>\n**Google Analytics 4 (7)**\n- `ga4_run_report` - the workhorse: arbitrary dimensions/metrics/date range,\n  optional dimension filter and ordering.\n- `ga4_top_landing_pages` - top landing pages, organic-only by default.\n- `ga4_traffic_by_channel` - sessions/engagement/conversions by channel group.\n- `ga4_organic_search_overview` - organic totals plus a day-by-day trend.\n- `ga4_setup_audit(property_id?)` - read-only SEO-measurement-readiness audit:\n  web data stream, key events, data retention, content-group dimensions, and\n  (v0.7.4) enhanced measurement, internal site search, and Google Signals.\n  Severity-graded with a benign exception per finding. Uses the GA4 Admin API\n  over REST (analytics.readonly; no extra dependency). (v0.7.0)\n- `ga4_site_search(days?, limit?)` - internal site-search query report (a\n  direct content-gap signal); honest envelope when no real search terms. (v0.7.1)\n- `ga4_landing_page_conversions(days?, organic_only?, limit?)` - organic\n  landing pages ranked by conversions. (v0.7.1)\n\n<a name=\"psi\"></a>\n**PageSpeed Insights (2)**\n- `psi_analyze` - Lighthouse scores, lab Core Web Vitals, and field (CrUX) Core\n  Web Vitals for a URL. Defaults to the mobile strategy. Field data carries a\n  `field_data_note`: Google is deprecating PSI field data, so use `crux_snapshot`\n  / `crux_history` for durable field metrics.\n- `psi_opportunities(url, strategy?)` - the actionable Lighthouse \"opportunity\"\n  audits (with estimated savings) plus the SEO-category audits, severity-graded.\n  Lab data only. An on-page-basics checklist, not a ranking predictor. (v0.7.1)\n\n<a name=\"cf\"></a>\n**Cloudflare (13)**\n- `cf_list_zones` - zones the token can see.\n- `cf_zone_info` - status, plan, name servers for a zone.\n- `cf_list_dns` - DNS records (read-only); useful for verifying canonical host\n  and TXT verification records during migrations.\n- `cf_web_analytics` - read-only edge Web Analytics (RUM), to compare against\n  GA4. Cloudflare returns `host: null` for some sites; pass the `site_tag` to\n  look those up explicitly.\n- `cf_purge_cache` - purge specific URLs (gated).\n- `cf_purge_cache_all` - purge an entire zone (gated + confirm token).\n- `cf_settings_audit(zone?)` - read-only audit of SEO-relevant Cloudflare zone\n  settings (SSL mode, Always-Use-HTTPS, HSTS, Automatic HTTPS Rewrites, Brotli,\n  cache TTL). Severity-graded with a \"verify, not fail\" discipline because CF\n  cannot see the origin; HSTS is never a hard failure. Needs Zone Settings Read\n  on the token. Each finding carries a machine-readable `fix` hint (the exact\n  `cf_settings_update` setting + recommended value) to chain audit -> fix. (v0.7.1)\n- `cf_settings_update(settings, zone?, confirm?, acknowledge_hsts_risk?, dry_run?)`\n  - write the SEO/crawl/security settings the audit grades (SSL mode, Always-Use-\n  HTTPS, Automatic HTTPS Rewrites, Brotli, browser cache TTL, HSTS), closing the\n  audit -> remediate loop. Gated. ssl_mode or any HSTS-raise needs `confirm=<zone>`\n  (HSTS-raise also needs `acknowledge_hsts_risk=true`); validates locally, supports\n  `dry_run`, and re-runs the audit so you see the finding clear. Needs Zone\n  Settings:Edit (vs the audit's Read). (v0.7.10)\n- `cf_list_redirects(zone?)` - list a zone's single (dynamic) redirect rules\n  plus the account's Bulk Redirect lists (read-only). Call before any redirect\n  write so nothing is clobbered. (v0.7.8; bulk lists added v0.7.9)\n- `cf_create_redirect(source, target, status_code?, ...)` - create one edge\n  redirect (e.g. a 301 for a renamed URL). Gated. Pre-flights the target (no\n  redirecting to a dead URL), refuses loops/duplicates, supports `dry_run`. (v0.7.8)\n- `cf_delete_redirect(rule_id, zone?)` - remove a single-redirect rule by id\n  (rollback for cf_create_redirect). Gated. (v0.7.8)\n- `cf_bulk_redirect_upsert(items, list_name, confirm, ...)` - create/append many\n  redirects at once via an account-level Bulk Redirect List (for migrations).\n  Gated + a confirm token equal to `list_name`. Validates every item locally\n  first and rejects the whole batch on any bad item (never half-applies);\n  supports `dry_run`. (v0.7.9)\n- `cf_managed_robots(action, zone?, ...)` - get / configure / disable\n  Cloudflare's managed robots.txt and Content-Signals policy (these ride on the\n  zone's Bot Management config). `action=\"get\"` reads the current state\n  (read-only, un-gated). `action=\"configure\"` sets the managed robots.txt\n  (`managed_robots`), the Content-Signals variant (`cf_robots_variant`:\n  off / policy_only), and the AI-bot blocking levers (`ai_bots_protection`,\n  `content_bots_protection`, `crawler_protection`). `action=\"disable\"` turns the\n  managed robots.txt and the policy back off. **Managed robots.txt and the\n  Content-Signals policy are mutually exclusive in Cloudflare**, so the valid\n  combinations are `managed_robots=true` + `cf_robots_variant=\"off\"` (managed\n  robots.txt) OR `managed_robots=false` + `cf_robots_variant=\"policy_only\"` (the\n  policy); the tool rejects the invalid combo locally with `INVALID_INPUT`. A\n  custom Content-Signal line (e.g. from `robots_ai_posture`) is not a managed\n  option - put that in your origin robots.txt. Writes are gated, need\n  `confirm=<zone>`, and support `dry_run`; reads are safe (GET -> overlay -> PUT,\n  so nothing else in the config is clobbered). Every response carries a caveat\n  separating the stated-preference signals (Content-Signals, honored only by\n  adopting crawlers and ignored by Googlebot) from the levers that actually\n  enforce at the edge. Needs Bot Management:Edit for writes (Read for get).\n  (v0.8.2)\n\n<a name=\"indexnow\"></a>\n**IndexNow (2, v0.2.0)**\n- `indexnow_submit(url)` - submit a single URL to Bing, Yandex, Naver, Seznam,\n  Yep. Complements (does not replace) `gsc_request_indexing`, which only talks\n  to Google. Requires `SEO_MCP_INDEXNOW_KEY` plus a verification file at\n  `https://<your-host>/<key>.txt` (see [IndexNow setup](#indexnow) for the full\n  key + file format + same-host rules).\n- `indexnow_bulk_submit(urls)` - up to 10,000 URLs sharing one host in a\n  single POST. Mixed-host batches are rejected client-side with\n  `INVALID_INPUT` before any network call. The `SEO_MCP_INDEXNOW_KEY_LOCATION`\n  env var overrides the default verification-file URL when your CDN rewrites\n  `/key.txt` paths.\n\n<a name=\"technical\"></a>\n**Technical SEO (8, v0.3.0)** - no credentials needed; built-in HTTP client.\n- `inspect_meta(url)` - on-page surface in one call: title, meta description,\n  meta robots, canonical, Open Graph + Twitter Card tags, hreflang, H1 count.\n- `check_canonical(url)` - canonical-link audit: self-referential / cross-host\n  / protocol-mismatched / trailing-slash drift / canonical target reachable.\n- `mixed_content_check(url)` - parses an HTTPS page and flags any `http://`\n  references (img / script / iframe / form action / srcset). No-op for `http://`.\n- `redirect_chain_audit(url, max_redirects=10)` - walks the chain hop by hop.\n  Flags long chains, protocol downgrades, loops, non-2xx terminus.\n- `robots_txt_validate(site_url, probes?)` - parses robots.txt (per-group\n  rules + sitemaps + Content-Signals), optionally verdicts (user_agent, url)\n  probes using RFC 9309 longest-match (matches what Google + Bing actually do,\n  not stdlib's first-match). Also detects a **stale edge-cached** robots.txt\n  (cache-bust comparison; re-parses from the fresh content) and a **Cloudflare\n  Managed robots.txt / Content-Signals** policy overriding your origin -\n  catching false-clean robots on migrated/CF-fronted sites. (v0.8.0)\n- `sitemap_validate(sitemap_url)` - validates a sitemap or sitemap-index XML,\n  counts entries, flags oversize + cross-host + missing lastmod. `.gz`\n  transparent.\n- `sitemap_health(sitemap_url, sample_size=25)` - sample-HEAD audit. Status\n  histogram + first non-2xx examples.\n- `robots_ai_posture(goal?, sitemap_url?)` - deterministic, offline advisor for\n  the Content-Signals levers (`search` / `ai-input` / `ai-train`). Takes a\n  business goal (`content_authority` default / `maximize_visibility` /\n  `protect_ip`), recommends a posture with a plain-language rationale, lays out\n  the trade-off alternatives, and emits a ready-to-apply artifact: the\n  `Content-Signal:` directive line plus a full suggested robots.txt. No network,\n  no writes. Every response carries the mandatory caveat that Content-Signal is\n  honored only by adopting crawlers, is ignored by Googlebot, and is not a\n  ranking factor. (v0.8.1)\n\n<a name=\"crux\"></a>\n**Chrome UX Report (2)**\n- `crux_history(url? | origin?, form_factor?, metrics?)` - 25 weeks of p75\n  Core Web Vitals via the CrUX History API. Reuses `PSI_API_KEY`; works\n  anonymously at a tighter rate limit when no key is configured.\n- `crux_snapshot(url? | origin?, form_factor?)` - the current p75 Core Web\n  Vitals (point-in-time, vs the history). Each metric reports a category\n  (GOOD / NEEDS_IMPROVEMENT / POOR); the rolled-up rating is `overall_category`.\n  Time metrics use `p75_ms`; the unitless CLS uses `p75`. Small origins return\n  a `no_data` envelope. (v0.7.1)\n\n**Structured data + cross-site + on-page (7)** - no new credentials (the v0.9 SERP auto-fetch is optional).\n- `inspect_schema(url)` - extract every JSON-LD block from a page; report\n  the schema.org @type counts and a sample entity per type.\n- `validate_schema(url, types?)` - verdict each JSON-LD entity against the\n  Google Rich Results required-field set. Covers Article, NewsArticle,\n  BlogPosting, Product, FAQPage, BreadcrumbList, Organization, LocalBusiness,\n  Event, Review, Recipe. Per-entity verdict plus missing_required and\n  missing_recommended lists.\n- `hreflang_consistency_check(urls)` - cross-page hreflang audit on a\n  user-supplied URL set. Flags missing reciprocity, broken hreflang\n  targets, duplicate hreflang on one page, missing self-link, missing\n  x-default when there are 3+ language variants.\n- `internal_link_graph(start_url, max_depth=2, max_pages=50)` - small\n  BFS crawl within the same host. Per-page in-degree + out-degree,\n  orphan pages, broken internal links, depth distribution. Hard caps\n  (max_depth <= 4, max_pages <= 200) so a misuse never melts the host.\n- `lighthouse_budget(url, budget)` - wraps `psi_analyze` and verdicts\n  the results against a budget dict, e.g.\n  `{performance: 80, LCP_ms: 2500, CLS: 0.1}`. Per-metric pass/fail and\n  an overall verdict. Useful as a CI / pre-deploy gate inside an LLM\n  session. Reuses `PSI_API_KEY`.\n- `internal_link_recommend(start_url, site_url?, days?, position_min?, position_max?, relevance_floor?, limit?)`\n  - recommends specific source->target internal links from high-in-degree pages\n  to GSC striking-distance pages (default position 8-20 with real impressions),\n  with anchor text. Ranks sources by lexical relevance + internal authority, skips\n  pages that already link the target, balances anchor text, and never suggests\n  nofollow. Built on `internal_link_graph` + GSC. Free. (v0.9.0)\n- `onpage_serp_gap(target_url, query?, competitor_urls?, max_competitors?)` - the\n  headings, entities, and schema the top SERP results have that a target page\n  lacks, turned into on-page actions. FREE with caller-supplied `competitor_urls`;\n  optional DataForSEO SERP auto-fetch by `query`, which also returns winnability\n  signals (`serp_composition` AI-Overview / UGC zero-click risk) and, with Open\n  PageRank, competitor domain authority. Surfaces information gain, not just\n  parity. Boilerplate/nav headings are filtered out (pattern-class chrome filter). (v0.9.0; F6 fix v0.9.1, F1 generalized v0.9.2)\n\nEvery tool's `tools/list` entry carries the MCP standard annotations\n(`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so MCP\nhosts can decide what to auto-approve and what to confirm.\n\n</details>\n\n## Workflow prompts\n\nThe server publishes thirteen named MCP prompts (via `prompts/list` /\n`prompts/get`) that chain the granular tools into common SEO workflows. Hosts\nthat surface prompts (Claude Desktop's slash menu, Cursor's command palette,\nCline's prompt picker) advertise them automatically.\n\n| Prompt | Arguments | Chains |\n|---|---|---|\n| `post_deploy_verify` | `urls`, `zone?`, `skip_psi?` | `cf_purge_cache` -> `gsc_request_indexing` -> `indexnow_bulk_submit` -> `psi_analyze` |\n| `weekly_review` | `days?`, `site_url?` | `gsc_compare_periods` (gainers + losers via sort_dir) -> `gsc_query_opportunities` -> `gsc_query_gaps` -> `ga4_organic_search_overview` |\n| `content_audit` | `site_url?`, `days?`, `top_n_queries?` | `gsc_top_queries` -> per-query `gsc_top_pages_by_query` -> cannibalization recommendation |\n| `migration_check` | `urls`, `site_url?` | `gsc_batch_inspect_urls` -> `gsc_list_sitemaps` -> canonical-agreement table -> remediation list |\n| `technical_seo_audit` | `url` | `inspect_meta` -> `check_canonical` -> `redirect_chain_audit` -> `mixed_content_check` -> `robots_txt_validate` -> `sitemap_health` -> severity-ranked triage list |\n| `structured_data_audit` | `urls` | per-URL `inspect_schema` -> `validate_schema` -> (if 2+ URLs) `hreflang_consistency_check` -> per-URL + cross-URL report |\n| `pre_deploy_check` | `urls` | `robots_txt_validate` -> per-URL `inspect_meta` -> `check_canonical` -> `validate_schema` -> `redirect_chain_audit` -> `mixed_content_check` -> deploy-gate verdict (block on critical issues, approve otherwise) |\n| `content_brief` (v0.7.1) | `topic`, `target_query`, `site_url?` | `gsc_top_pages_by_query` -> `inspect_meta` / `inspect_schema` on top rankers -> brief with required sections + validation rules |\n| `content_outline` (v0.7.1) | `brief` | outline with rules: >=5 H2, >=70% target-query coverage, H1 has the primary keyword |\n| `content_article` (v0.7.1) | `outline`, `brief` | article with rules: word count within +/-15%, per-section minimum, internal links, inline JSON-LD hint, no em-dashes |\n| `content_workflow` (v0.7.1) | `site_url?`, `days?` | `content_opportunities` -> brief -> outline -> article -> `pre_deploy_check` -> `gsc_request_indexing` + `indexnow_submit` -> scheduled `content_performance` |\n| `content_performance` (v0.7.1) | `url`, `target_queries?`, `site_url?` | `gsc_compare_periods` + `gsc_search_analytics` before / after the publish window |\n| `seo_setup_audit` (v0.7.1) | `site_url?`, `property_id?` | `ga4_setup_audit` -> `cf_settings_audit` -> `psi_opportunities` -> `robots_txt_validate` -> consolidated stack-config report |\n\nWhy prompts and not megatools: composability. A failed step inside a megatool\npoisons the megatool's envelope and the host loses the ability to retry just\nthe failing leg. Prompts hand the host a recipe; each step's envelope arrives\nintact at the LLM.\n\n## Install\n\nSEOMonster ships **two install paths**, both fully local:\n\n- **`.mcpb` bundle** for Claude Desktop. One-click install, GUI form for\n  credentials, secret-typed inputs stored in the OS keychain. Recommended for\n  most users.\n- **`uvx`** for Cursor, Cline, Codex, and Claude Desktop power users who prefer\n  to hand-edit MCP config files.\n\nBoth paths run the same Python package (`seo_mcp`) and expose the same\n52-tool surface. The difference is only how the host launches the server\nand how it collects credentials.\n\n### Claude Desktop (recommended): `.mcpb` bundle\n\nThree short steps. The OAuth consent is **run once from a terminal** (the GUI\nflow inside Claude Desktop's MCP subprocess times out before a real user can\nfinish; see [Why pre-flight auth?](#why-pre-flight-auth) below).\n\n**1. Install the bundle.** Download\n[`seo-monster-0.2.0.mcpb`](https://github.com/avansaber/seo-monster/releases/latest)\nfrom GitHub releases (or, when listed, from the [Claude\nDirectory](https://claude.ai/directory)) and double-click it. Claude Desktop\nverifies the bundle, runs `uv` to materialize the Python environment, and\nshows a configuration form:\n\n| Field                          | Type           | Required | Notes                                                                 |\n|--------------------------------|----------------|----------|-----------------------------------------------------------------------|\n| Google OAuth Client Secrets    | file picker    | yes      | Desktop-app client-secrets JSON from Google Cloud Console.            |\n| Google OAuth Token Cache Path  | string         | yes      | Defaults to `~/.config/seo-monster/token.json`. Written on consent.   |\n| GSC Default Property           | string         | no       | e.g. `sc-domain:example.com` or `https://www.example.com/`.           |\n| GA4 Default Property ID        | string         | no       | `properties/123456789` or bare `123456789`.                           |\n| PageSpeed Insights API Key     | string, secret | no       | Stored in the OS keychain. **Strongly recommended** ([why?](#pagespeed-insights)). |\n| Cloudflare API Token           | string, secret | no       | Stored in the OS keychain. Required only for the Cloudflare tools.    |\n| Cloudflare Default Zone        | string         | no       | e.g. `example.com`.                                                   |\n| IndexNow Key                   | string, secret | no       | Required only for IndexNow tools. Any 8-128 hex string you generate.  |\n| IndexNow Key File URL          | string         | no       | Override the default verification location (`https://<host>/<key>.txt`). |\n\nFill the fields, click **Save**, then **toggle the extension on**. Quit Claude\nDesktop completely (⌘Q on macOS) and reopen.\n\n**2. Run the one-time OAuth consent from a terminal.** Before using any\nGoogle-backed tool, run:\n\n```sh\nuvx seo-monster auth\n```\n\nA browser opens. Approve the requested scopes. The command writes\n`token.json` to the path you configured (default `~/.config/seo-monster/token.json`)\nwith `0600` permissions, then exits. This step is the recommended pattern; it\nsidesteps the timeout that Claude Desktop imposes on every tool call.\n\n**3. Start a new chat in Claude Desktop and use the tools.** Click the 🔧\ntools icon in the input box; you should see 52 SEOMonster tools. Try\n`system_status` first to verify everything is configured.\n\n#### Why pre-flight auth?\n\nThe OAuth installed-app flow opens a local browser and waits for the user to\nfinish the consent screen. Inside Claude Desktop, MCP servers are launched as\nsubprocesses whose tool calls have a ~30-60 second timeout. Real users do not\ncomplete browser consent that fast, so the originating call times out, and\nsince every Google tool retries the flow until a token exists, every call\ntimes out in turn. Running `uvx seo-monster auth` once from a terminal puts\nthe token on disk; from that point on, Claude Desktop's MCP server just reads\nthe cached token and silently refreshes it as needed.\n\n### `uvx` for Cursor, Cline, Codex (and Claude Desktop power users)\n\n`uvx` runs the published PyPI package `seo-monster` in an ephemeral\nenvironment. Add the snippet for your host below, using the **absolute path**\nto `uvx` (find it with `which uvx`; GUI hosts do not read your shell profile).\n\n#### Cursor (`~/.cursor/mcp.json` or project `.cursor/mcp.json`)\n\n```json\n{\n  \"mcpServers\": {\n    \"seomonster\": {\n      \"command\": \"/Users/me/.local/bin/uvx\",\n      \"args\": [\"seo-monster\"],\n      \"env\": {\n        \"SEO_MCP_GOOGLE_OAUTH_CLIENT\": \"/Users/me/.config/seo-monster/client_secret.json\",\n        \"SEO_MCP_GOOGLE_TOKEN\": \"/Users/me/.config/seo-monster/token.json\",\n        \"SEO_MCP_GA4_PROPERTY_ID\": \"properties/123456789\",\n        \"PSI_API_KEY\": \"AIza...\",\n        \"CF_API_TOKEN\": \"...\"\n      }\n    }\n  }\n}\n```\n\n#### Cline (`cline_mcp_settings.json`)\n\n```json\n{\n  \"mcpServers\": {\n    \"seomonster\": {\n      \"command\": \"/Users/me/.local/bin/uvx\",\n      \"args\": [\"seo-monster\"],\n      \"env\": {\n        \"SEO_MCP_GOOGLE_OAUTH_CLIENT\": \"/Users/me/.config/seo-monster/client_secret.json\",\n        \"SEO_MCP_GOOGLE_TOKEN\": \"/Users/me/.config/seo-monster/token.json\"\n      },\n      \"alwaysAllow\": [\"system_status\", \"gsc_search_analytics\", \"ga4_run_report\", \"psi_analyze\"]\n    }\n  }\n}\n```\n\n`alwaysAllow` lists read tools so Cline does not prompt on each call. Leave the\ncache-purge tools off so they always prompt.\n\n#### Codex (`~/.codex/config.toml`)\n\n```toml\n[mcp_servers.seomonster]\ncommand = \"/Users/me/.local/bin/uvx\"\nargs = [\"seo-monster\"]\n\n[mcp_servers.seomonster.env]\nSEO_MCP_GOOGLE_OAUTH_CLIENT = \"/Users/me/.config/seo-monster/client_secret.json\"\nSEO_MCP_GOOGLE_TOKEN = \"/Users/me/.config/seo-monster/token.json\"\nSEO_MCP_GA4_PROPERTY_ID = \"properties/123456789\"\n```\n\n#### Claude Desktop, direct `uvx` (advanced)\n\nIf you prefer to hand-edit `claude_desktop_config.json` instead of using the\n`.mcpb` bundle, the same snippet shape as Cursor above works.\n\n<a name=\"auth\"></a>\n## Auth\n\nThe four services authenticate independently. Configure only the ones you use;\na tool for an unconfigured service returns a clear `AUTH_MISSING` error rather\nthan failing the server.\n\n### Quick setup (recommended): `seo-monster setup`\n\nRun `seo-monster setup` once from a terminal. It interactively collects your\nCloudflare token, PageSpeed Insights key, IndexNow key, and the default GSC and\nGA4 properties, validates what it can against the live APIs, and writes them to\n`~/.config/seo-mcp/config.toml` with `0600` permissions. Your MCP host config\nthen needs no secrets in it:\n\n```json\n{ \"command\": \"uvx\", \"args\": [\"seo-monster\"] }\n```\n\nTwo things `setup` does not do, by design:\n\n- **Google OAuth** still uses the separate one-time browser step. After `setup`,\n  run `seo-monster auth` to complete Google consent (see the next section).\n- It never overrides environment variables. Anything set in your host's `env`\n  block still wins over the config file, so CI and Docker keep using env vars.\n\n`setup` is re-runnable: existing values are shown as defaults and kept when you\nleave a field blank. The sections below document the per-service env vars, which\nare what `setup` writes for you and what CI pipelines can set directly.\n\n### Google (Search Console + Analytics 4) - OAuth, recommended\n\nThis is the lower-friction path: no Cloud service account, no per-property email\ngrants.\n\n1. In the [Google Cloud Console](https://console.cloud.google.com/), create (or\n   pick) a project and **enable the APIs** you will use:\n   - Search Console API\n   - Indexing API (for `gsc_request_indexing`)\n   - Google Analytics Data API (for the GA4 tools)\n   - PageSpeed Insights API (only if you want a PSI key; see below)\n2. Create an OAuth client of type **Desktop app** and download the client-secrets\n   JSON.\n3. Point the server at it and at a writable token path:\n   - `SEO_MCP_GOOGLE_OAUTH_CLIENT` = path to the client-secrets JSON\n   - `SEO_MCP_GOOGLE_TOKEN` = a writable path where the token will be cached\n4. **One-time:** run `uvx seo-monster auth` from a terminal. A browser opens;\n   approve the scopes. The command writes `token.json` (`0600`) and exits.\n5. Subsequent runs (server-side) refresh the token silently. **The server\n   never opens a browser**; if the cached token is missing, tools return\n   `AUTH_MISSING` pointing back at the `auth` command.\n\nThe signed-in Google account must have access to the Search Console properties\nand GA4 properties you query.\n\n**Token-cache hardening.** The cached token is refresh-capable and equivalent\nto a long-lived credential for the requested scopes. The server writes it with\n`0600` and its parent directory with `0700`. Keep `SEO_MCP_GOOGLE_TOKEN` under\na directory you control (e.g. `~/.config/seo-monster/`) and do not put it on a\nshared filesystem.\n\n### Google - service account (advanced, headless)\n\nFor fully headless or server deployments where a browser is not available:\n\n1. Create a service account and download its JSON key.\n2. Set `SEO_MCP_GOOGLE_CREDENTIALS` (or the standard\n   `GOOGLE_APPLICATION_CREDENTIALS`) to the key path.\n3. Grant the service-account email access on each property:\n   - Search Console: add it as a user on the property.\n   - GA4: add it as a Viewer on the property.\n\nIf both OAuth and a service account are configured, OAuth is used.\n\n> **Coverage note.** The OAuth installed-app path is exercised in our\n> validation pass and in production-style smoke tests. The service-account\n> path is documented but not independently validated against a live Cloud\n> project. If you hit issues on the SA path, please open an issue.\n\n### Scopes (minimal vs full)\n\nThe default consent requests the scopes needed for every tool, including the two\nwrites:\n\n| Capability                          | Scope                          |\n|-------------------------------------|--------------------------------|\n| GSC read                            | `webmasters` (covers readonly) |\n| GSC sitemap submit                  | `webmasters`                   |\n| GSC indexing request                | `indexing`                     |\n| GA4 reporting                       | `analytics.readonly`           |\n\nIf you only want reads, you can consent to a narrower set\n(`webmasters.readonly` + `analytics.readonly`) and simply not call\n`gsc_submit_sitemap` / `gsc_request_indexing`; calling a write tool without its\nscope returns `SCOPE_INSUFFICIENT` with remediation, never a crash.\n\n### PageSpeed Insights\n\nPSI works without a key in principle, but in practice the **anonymous quota is\nshared across every caller without a key and is frequently exhausted**: a\nsingle `psi_analyze` call against the anonymous endpoint often returns\n`RATE_LIMITED`. **Treat the anonymous mode as a fallback, not the steady\nstate.**\n\nTo get reliable PSI access:\n\n1. In Cloud Console, enable the **PageSpeed Insights API**.\n2. Create an **API key** (Credentials > Create credentials > API key). It\n   takes a minute. The key is free.\n3. Set `PSI_API_KEY` (or use the field in the `.mcpb` configuration form).\n\nThe PSI API only accepts the key as a URL query parameter (not a header), so\ntreat PSI keys as low-sensitivity. Scope the key to the PageSpeed Insights\nAPI only and attach no other GCP roles.\n\n### Cloudflare\n\nCreate an API token at\n[dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens)\nand set `CF_API_TOKEN` (and optionally `CF_ZONE` for a default zone). Grant only\nthe permissions you need:\n\n| Permission              | Needed for                          |\n|-------------------------|-------------------------------------|\n| Zone: `Zone:Read`       | `cf_list_zones`, `cf_zone_info`     |\n| Zone: `DNS:Read`        | `cf_list_dns`                       |\n| Account: `Account Analytics:Read` | `cf_web_analytics`        |\n| Zone: `Cache Purge:Purge` | `cf_purge_cache`, `cf_purge_cache_all` (only if you enable destructive mode) |\n| Zone: `Single Redirect:Edit` | `cf_create_redirect`, `cf_delete_redirect` (only if you enable destructive mode); `cf_list_redirects` reads with it |\n| Zone: `Zone Settings:Edit` | `cf_settings_update` (only if you enable destructive mode); `cf_settings_audit` only needs Zone Settings Read |\n| Account: `Account Rulesets:Edit` + `Account Filter Lists:Edit` | `cf_bulk_redirect_upsert` (only if you enable destructive mode) |\n\n### IndexNow\n\nIndexNow notifies Bing, Yandex, Naver, Seznam, and Yep when a URL is created\nor updated. Google does not participate, so the IndexNow tools complement\nrather than replace `gsc_request_indexing`.\n\n#### One-time setup\n\n1. **Generate a key.** Any 8-128 character string of letters, digits, or\n   hyphens (`a-z`, `A-Z`, `0-9`, `-`) per the IndexNow spec. Common patterns:\n   a 32-char lowercase hex string (e.g. `python -c \"import secrets;\n   print(secrets.token_hex(16))\"`) or any random alphanumeric of similar\n   length. Treat it like an API key; do not commit it.\n2. **Configure SEOMonster** by setting `SEO_MCP_INDEXNOW_KEY` to that string\n   (or use the `.mcpb` configuration form; the field is marked sensitive and\n   lands in the OS keychain).\n3. **Host the verification file** at `https://<your-host>/<key>.txt`. The\n   file body MUST be **exactly** the key string with no trailing newline, no\n   BOM, no extra whitespace, no HTML wrapper. The Content-Type should be\n   `text/plain`. Confirm with `curl -i https://<your-host>/<key>.txt` before\n   moving on; the response body must be byte-identical to the key.\n4. **(Optional)** Set `SEO_MCP_INDEXNOW_KEY_LOCATION` if the verification\n   file lives at a non-standard URL (some CDNs rewrite `/key.txt` paths).\n   The default location is `https://<host>/<key>.txt` derived from the URLs\n   you submit, so you usually do not need this.\n\n#### Same-host constraint\n\nEvery URL submitted in one `indexnow_submit` or `indexnow_bulk_submit` call\nmust share the same host as the verification file. Mixed-host batches are\nrejected by IndexNow with HTTP 422; `indexnow_bulk_submit` enforces this\nclient-side and returns `INVALID_INPUT` before any network call when it\ndetects mixed hosts.\n\nIf you have multiple hosts, host a verification file per host and either\nmake separate calls per host or override `SEO_MCP_INDEXNOW_KEY_LOCATION`\nper call (the tool does not currently expose per-call override; set\ndistinct env values per session).\n\n#### Common errors\n\n| Symptom | Likely cause |\n|---|---|\n| `AUTH_INVALID` from `indexnow_submit` | Engines could not fetch `https://<host>/<key>.txt`. Confirm the file returns HTTP 200 with the exact key as the body |\n| `INVALID_INPUT` from `indexnow_bulk_submit` mentioning mixed hosts | URL list spans multiple hosts; split into per-host batches |\n| `RATE_LIMITED` | Hit IndexNow's per-host rate cap. Wait before retrying |\n\n### Verify your setup\n\nAfter configuring, call `system_status` to see what is detected. Call it with\n`{\"probe\": true}` to make one cheap live request per configured service and\nconfirm the credentials actually work (GSC lists properties, GA4 runs a 1-row\nreport against the default property, Cloudflare lists one zone, PSI pings the\nendpoint). With `probe` off (the default) it does a config-only check and makes\nno network calls.\n\n<a name=\"destructive-mode\"></a>\n## Destructive mode\n\nCache purges affect every visitor, so they are off by default. Set\n`SEO_MCP_ALLOW_DESTRUCTIVE=true` to enable `cf_purge_cache` and\n`cf_purge_cache_all`. While off, those tools return `DESTRUCTIVE_DISABLED` and\nmake no network call.\n\n`cf_purge_cache_all` (purge the whole zone) carries an extra safeguard: it\nrequires a `confirm` argument equal to the resolved zone hostname. A missing or\nmismatched `confirm` returns `CONFIRM_REQUIRED` and issues no purge.\n\nThe two GSC writes (`gsc_submit_sitemap`, `gsc_request_indexing`) are **not**\ngated; they are routine, low-blast-radius SEO tasks.\n\n<a name=\"configuration\"></a>\n## Configuration\n\nResolution is environment-first, with a TOML file fallback. Environment always\nwins. The config file is normally written for you by `seo-monster setup` (with\n`0600` permissions); you can also write it by hand or set the env vars below.\n\n| Env var                          | Service | Purpose                                          |\n|----------------------------------|---------|--------------------------------------------------|\n| `SEO_MCP_GOOGLE_OAUTH_CLIENT`    | Google  | OAuth client-secrets JSON path (recommended).    |\n| `SEO_MCP_GOOGLE_TOKEN`           | Google  | Writable cached-token path (OAuth).              |\n| `SEO_MCP_GOOGLE_CREDENTIALS`     | Google  | Service-account key path (alternative).          |\n| `GOOGLE_APPLICATION_CREDENTIALS` | Google  | Standard service-account fallback.               |\n| `SEO_MCP_GSC_DEFAULT_SITE`       | GSC     | Default property, e.g. `sc-domain:example.com`.  |\n| `SEO_MCP_GA4_PROPERTY_ID`        | GA4     | Default property, e.g. `properties/123456789`.   |\n| `SEO_MCP_DATA_STATE`             | GSC     | `all` (default) or `final`.                      |\n| `PSI_API_KEY`                    | PSI     | PageSpeed Insights API key (optional).           |\n| `CF_API_TOKEN`                   | CF      | Cloudflare API token.                            |\n| `CF_ZONE`                        | CF      | Default zone hostname.                           |\n| `SEO_MCP_INDEXNOW_KEY`           | IndexNow| Shared key for the IndexNow tools (sensitive).   |\n| `SEO_MCP_INDEXNOW_KEY_LOCATION`  | IndexNow| Override default key-file URL (optional).        |\n| `SEO_MCP_ALLOW_DESTRUCTIVE`      | all     | `true` enables cache-purge tools. Default off.   |\n| `SEO_MCP_CONFIG`                 | all     | Path to the TOML config file.                    |\n| `DATAFORSEO_LOGIN` / `DATAFORSEO_PASSWORD` | DataForSEO | Optional (v0.9): SERP/PAA, keyword volume/difficulty/intent, competitor gap, Google AIO. |\n| `OPENPAGERANK_API_KEY`           | Open PageRank | Optional (v0.9): free competitor domain authority. |\n| `PERPLEXITY_API_KEY` / `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `GEMINI_API_KEY` | AI engines | Optional (v0.9): engines for `ai_citation_track` (any subset). |\n| `GOOGLE_ADS_DEVELOPER_TOKEN` / `GOOGLE_ADS_CUSTOMER_ID` | Google Ads | Optional (v0.9): volume alt to DataForSEO (needs adwords-scope consent). |\n\nConfig file fallback at `~/.config/seo-mcp/config.toml` (or `SEO_MCP_CONFIG`):\n\n```toml\n[google]\noauth_client = \"/Users/me/.config/seo-mcp/client_secret.json\"\ntoken        = \"/Users/me/.config/seo-mcp/token.json\"\n# credentials = \"/Users/me/.config/seo-mcp/sa.json\"   # service-account alternative\n\n[gsc]\ndefault_site = \"sc-domain:example.com\"\ndata_state   = \"all\"\n\n[ga4]\nproperty_id  = \"properties/123456789\"\n\n[psi]\napi_key = \"AIza...\"\n\n[cloudflare]\napi_token = \"...\"\nzone      = \"example.com\"\n\n[server]\nallow_destructive = false\n\n# Optional v0.9 providers (discovery + AI/GEO). The free GSC/GA4/HTTP core\n# works without any of these; each tool degrades gracefully when unset.\n[dataforseo]\n# login    = \"...\"\n# password = \"...\"\n\n[openpagerank]\n# api_key = \"...\"\n\n[ai_engines]\n# perplexity = \"...\"\n# openai     = \"...\"\n# anthropic  = \"...\"\n# gemini     = \"...\"\n\n[google_ads]\n# developer_token = \"...\"   # volume alt to DataForSEO; needs adwords-scope OAuth\n# customer_id     = \"...\"\n```\n\n<a name=\"errors\"></a>\n## Result envelope\n\nEvery tool returns the same shape. On success:\n\n```json\n{ \"ok\": true, \"data\": { /* tool-specific */ }, \"error\": null }\n```\n\nOn failure:\n\n```json\n{\n  \"ok\": false,\n  \"data\": null,\n  \"error\": {\n    \"code\": \"AUTH_MISSING\",\n    \"service\": \"gsc\",\n    \"message\": \"No Google credentials found for Search Console.\",\n    \"remediation\": \"Configure OAuth ... or a service-account key. See README > Auth.\",\n    \"docs_url\": \"https://seomonster.avansaber.com#auth\",\n    \"details\": null\n  }\n}\n```\n\nError codes:\n\n| Code                   | Meaning                                                       |\n|------------------------|---------------------------------------------------------------|\n| `AUTH_MISSING`         | No credential configured for the service.                     |\n| `AUTH_INVALID`         | Credential present but rejected (401/403, bad key, expired).  |\n| `SCOPE_INSUFFICIENT`   | Token lacks the scope this tool needs.                        |\n| `DESTRUCTIVE_DISABLED` | A cache-purge tool was called with destructive mode off.      |\n| `CONFIRM_REQUIRED`     | `cf_purge_cache_all` called without a matching `confirm`.     |\n| `NOT_FOUND`            | Site / property / zone / record not found or not visible.     |\n| `INVALID_INPUT`        | Argument failed validation (bad date, missing required arg).  |\n| `RATE_LIMITED`         | Upstream 429.                                                 |\n| `SERVICE_DISABLED`     | A Google Cloud API is not enabled; `details` has the activation URL. |\n| `UPSTREAM_ERROR`       | Any other non-2xx from an upstream API.                       |\n\n## Development\n\n```sh\ngit clone https://github.com/avansaber/seo-monster\ncd seo-monster\nuv venv && uv pip install -e \".[dev]\"\nuv run pytest               # offline test suite\nuv run seo-monster          # run the server over stdio\nuv run seo-monster auth     # one-time OAuth consent (or `uv run seo-mcp auth`)\n```\n\nThe package exposes two console-script aliases: `seo-monster` (canonical,\nmatches the PyPI distribution) and `seo-mcp` (a v0.1.x dev alias kept for\nback-compat). Both invoke the same entry point. As of v0.2.0, invoking the\nserver via `seo-mcp` emits a one-line stderr deprecation notice; nothing on\nstdout, so the MCP protocol channel is unaffected. Production configs should\nuse `seo-monster`; the alias will be removed in a future major release.\n\nTests are fully offline: they mock at the client layer, so no network and no\ncredentials are needed to run them.\n\n> **Server identity note.** Some MCP host UIs display the server name as\n> `seo-mcp` and the version as the `mcp` SDK version (e.g. `1.27.1`). The\n> server-name string is the value we passed to `Server(\"seo-mcp\")` and is kept\n> stable for back-compat; the version readout is a quirk of the SDK\n> (`create_initialization_options()` does not propagate the package version).\n> The package's real version is in `pyproject.toml` and `seo_mcp.__version__`.\n\n## Changelog\n\nRelease-by-release notes, including the validation checks each version's\nexternal testing pass should cover, live in [CHANGELOG.md](CHANGELOG.md).\n\n## Privacy\n\nSEOMonster runs entirely on your machine and talks only to the upstream APIs\nyou configure. The maintainers do not see any of your data, credentials,\nqueries, or tool calls. See [PRIVACY.md](PRIVACY.md) for the full statement.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n\n<!-- mcp-name: io.github.avansaber/seo-monster -->\n\n",
  "bytes": 51928,
  "sha": "46dccc220426389b873076fd021bd59c57540f03582c40a54ef0300f6135e580",
  "repo_slug": "avansaber/seo-monster",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_avansaber_seo_monster_3ce4d39e/readme"
}