# mcp-server-seo Rust MCP server for **SEO evidence** used by Grok Build and Claude Code: on-page audit, robots/sitemap, polite crawl, PageSpeed Insights, Google Search Console, optional paid SERP. stdio + JSON-RPC via `rmcp`. No Docker. Two binaries share the same engine: | Binary | Role | |--------|------| | `seo` | Headless CLI (`doctor`, `tool`, `audit daily\|weekly`, `mail`) | | `mcp-server-seo` | MCP stdio server for AI agents | ## Build ```bash cargo build --release install -Dm755 target/release/seo ~/.local/bin/seo install -Dm755 target/release/mcp-server-seo ~/.local/bin/mcp-servers/mcp-server-seo ``` Cron / SeoAuditBot uses `seo`. Agents keep using `mcp-server-seo`. ## Config Optional: `~/.config/mcp-server-seo/config.toml` (see `config.example.toml`). ```bash mkdir -p ~/.config/mcp-server-seo cp config.example.toml ~/.config/mcp-server-seo/config.toml chmod 600 ~/.config/mcp-server-seo/config.toml ``` Phase 1 tools work with **zero** keys. GSC needs a service-account JSON; PageSpeed needs a free API key; SERP needs Serper (optional). ## Tools (summary) | Group | Tools | |-------|--------| | Foundation | `status`, `sites_list`, `site_resolve` | | On-page | `page_audit` (`mode=quick\|full`, `target_keyword`), `page_text`, `redirect_trace`, `robots_check`, `sitemap_*`, `extract_structured_data`, `schema_validate`, `link_check`, `entity_coverage` | | Crawl / graph | `crawl_run` (**resumable**, `seed_sitemap`), `crawl_report`, `crawl_results`, `crawl_diff`, `sitemap_coverage`, `sitemap_build`, `internal_link_graph` (**true orphans**) | | Local / architecture | `url_architecture_plan`, `nap_consistency` (schema-first), `citation_prospects`, `gbp_audit`, **`gbp_reviews_snapshot`**, `local_audit` | | Content / KW | `content_brief` (SERP-grounded), `keyword_research` (**ranked**), **`keyword_volumes`**, `keyword_intent`, `keyword_suggest` | | Authority | `authority_snapshot` (free, Open PageRank), `backlink_overview` (paid, gated) | | Performance | `pagespeed_audit` | | GSC | `gsc_*`, `gsc_opportunities`, **`gsc_trends`**, **`log_event`** | | SERP / rank | `serp_fetch` (`num`, local pack), `rank_history` (series), **`rank_watch`** | | Composites | `page_snapshot`, `page_history`, `audit_page`, `audit_site`, `verify_live`, `rank_diagnosis` | | Fix loop | `findings_list`, `findings_diff` (default latest two runs), `url_to_source` | ### Recommended free-first agent playbook (v0.7) 1. `page_audit` `mode=full` (+ `target_keyword`) on home + key templates 2. `page_text` on any page whose copy you need to judge — the audit returns counts, this returns the actual sections 3. `crawl_run` with `seed_sitemap=true` → `internal_link_graph`; if `coverage.truncated`, re-call with `resume_crawl_id` until the frontier drains, then read `true_orphans` 4. `local_audit` for NAP/GBP/authority/reviews in one pass 5. `authority_snapshot` before concluding a clean page "just needs more on-page" — a deficit verdict means links, not polish 6. `serp_fetch` **once** per target query (~$0.001, `confirm=YES-PAY`) → `content_brief` reads the stored SERP for PAA questions and competitor-backed H2s 7. `rank_diagnosis` when a specific keyword underperforms — ranks indexation vs relevance vs authority vs technical 8. `verify_live` after deploy, then `log_event` so the movement has a cause attached 9. Next session: `findings_diff` (no run ids) — `fixed` vs `new`/`regressed`. `url_to_source` maps a URL to the file under `[[sites]].source_root` ### Weekly loop (schedule from your MCP client — this server runs no daemon) ``` gsc_trends site= refresh=true # stores today's snapshot, returns movers rank_watch action=run confirm=YES-PAY # re-checks tracked queries ``` Read `movers_up` / `movers_down` next to the `events` you logged. ### New site, no GSC history (~$0.10) ``` keyword_research seed= geo= mode=all keyword_volumes keywords=[...] confirm=YES-PAY # one call, cached 90 days serp_fetch confirm=YES-PAY # unlocks difficulty + SERP clustering keyword_research ... # now returns a ranked shortlist url_architecture_plan / content_brief # human approval before publish ``` Content publish stays in the CMS — this server is **evidence + briefs**, not a writer. ### Safety - **SSRF:** public http(s) only; private/reserved IPv4+IPv6 blocked; redirect hops re-checked - **`sitemap_build`:** writes only under `storage.data_dir` (relative `out_path`) - **GSC submit:** `confirm=YES-SUBMIT` (sitemaps API — not URL Inspection) - **Paid SERP:** `dry_run=true` default; `confirm=YES-PAY` + budget ledger - **Paid authority** (`backlink_overview`): same gate, separate `[authority]` budget ledger; dry run makes **zero** provider calls and the ledger records the provider's actual reported cost - **Static HTML:** `rendered: false` — use `agent-browser` for JS ### Optional keys (v0.7) | Key | Unlocks | Cost | |-----|---------|------| | `authority.open_pagerank_api_key` | `authority_snapshot`, authority step in `local_audit`, `rank_diagnosis` verdicts, keyword difficulty | free ([domcop](https://www.domcop.com/openpagerank/)) | | `authority.dataforseo_login` / `_password` | `backlink_overview` link gap, `keyword_volumes` | pay-as-you-go, budget-capped | | `serp.api_key` | `serp_fetch` / `rank_watch` → grounds `content_brief`, keyword difficulty, citation presence checks | ~$0.001/query | | `local.places_api_key` | `gbp_reviews_snapshot` rating, review velocity, competitor gap | free tier + monthly call cap | | `gsc.credentials_file` | `gsc_*` incl. `gsc_trends` history and `gsc_opportunities` | free | **Verify before trusting the numbers:** your SERP provider's credit cost for `num > 10` (`serp.num_100_multiplier`), the DataForSEO `keywords.location_code` for your country, and current Places API field pricing. All three are config values, not hardcoded. ### Do not reimplement Use sibling MCPs: `google-analytics`, `google-adwords`, `cloudflare`, `namecheap`, `agent-browser`. ## Clients Already registered as `seo` in Grok Build / Claude Code (restart clients after rebuild). ## Tests ```bash cargo test --release ./tests/test-stdio.sh ``` ## Plan See `PLAN.md` (merged Grok + Fable + Kimi + critique amendments). ## Module layout (v0.7) ``` src/ bin/seo.rs # CLI (doctor, tool, audit daily|weekly, mail) bin/mcp_server_seo.rs server/ # thin MCP tool-router only (no SEO logic) params.rs # all tool input schemas dispatch/ # named-tool router + one exec file per handler group playbooks.rs # daily/weekly audits; ok=false on step errors mod.rs # SeoServer, shared helpers, chained tool_router() handlers/ # #[tool] wrappers → run_named; one sub-router per file # (site, page, crawl, pagespeed, gsc, serp, audit, # findings, content, authority, local) tools/ # business logic per concern page/ # parse, og probe, structured data, text extraction, audit crawl/ # BFS crawl + report builder local/ # nap, citations, gbp, local_audit gsc/ # client (JWT) + ops (analytics/inspect/sitemaps) authority/ # open pagerank (free) + dataforseo (paid) adapters audit/ # composites: page, site, verify_live, rank_diagnosis source_map.rs # URL → repo file (convention presets, sandbox) serp_evidence.rs# stored-SERP parser shared by briefs/keywords html_dom.rs # shared HTML extract helpers incl. main-text extraction … rules/ # pure rules (checklist, schema, patterns, intent, relevance, issue) storage/ # SQLite split by domain (crawls, snapshots, spend, authority, # keywords, gsc, rank, places, events, # findings/{persist,extract,query,family}) over one shared conn http.rs / config.rs / site.rs / util.rs ``` **Rule:** no God files — logic lives under `tools/*` / `rules/*`; `server/` does not implement SEO algorithms. Shared issue builders live in `rules/issue.rs`.