google-seo-mcp
<div align="center">
# google-seo-mcp
**SEO & GEO MCP server for Claude, Codex, Cursor and any MCP client — Google Search Console, Google Analytics 4, PageSpeed Insights, structured data, llms.txt, WordPress and GitHub as 102 tools, so an assistant can diagnose and fix technical SEO, content and generative-engine-optimization issues in one conversation.**
[](https://github.com/Akxan/google-seo-mcp/stargazers)
[](LICENSE)
[](https://nodejs.org)
[](https://www.typescriptlang.org)
[](https://modelcontextprotocol.io)
[](#what-it-can-do)
[](https://github.com/Akxan/google-seo-mcp/releases)
[](https://github.com/Akxan/google-seo-mcp/actions/workflows/deploy.yml)
[](https://github.com/Akxan/google-seo-mcp/commits/main)
Google Search Console · Google Analytics 4 · PageSpeed & CrUX · on-page and GEO audits · WordPress over SSH · GitHub
[Quick start](#quick-start) · [Tools](#what-it-can-do) · [Any agent](#works-with-any-agent) · [Architecture](#architecture) · [Configuration](#configuration) · [Deploy 24/7](#running-as-a-247-http-server) · [中文文档](README.zh-CN.md)
</div>
---
## Why
**google-seo-mcp** is a [Model Context Protocol](https://modelcontextprotocol.io) server for **SEO automation with AI agents**. It connects **Google Search Console**, **Google Analytics 4 (GA4)**, **PageSpeed Insights / Core Web Vitals**, the **Chrome UX Report**, **Knowledge Graph**, **Wikidata**, **IndexNow**, **WordPress** (Yoast SEO, WP-CLI over SSH) and **GitHub**, and adds **GEO (generative engine optimization)** checks: AI crawler access for GPTBot, OAI-SearchBot, ClaudeBot and PerplexityBot, `llms.txt`, JSON-LD / schema.org structured data, E-E-A-T signals and AI citation tracking.
Most SEO MCP servers wrap one API. Real SEO work crosses several: you find a striking-distance keyword in Search Console, check the landing page's engagement in GA4, audit the page, rewrite its title and FAQ, publish the change to WordPress or a static-site repo, then watch the numbers. This server gives an assistant every step of that loop as tools, with the guard-rails a public-facing site needs: read-only mode, destructive-action annotations, and untrusted-content instructions.
## What it can do
| Area | Tools |
|---|---|
| **Search Console** (16) | `gsc_list_sites`, `gsc_search_analytics`, `gsc_site_snapshot`, `gsc_compare_periods`, `gsc_opportunities` (position 8–20 quick wins), `gsc_ctr_opportunities`, `gsc_cannibalization`, `gsc_question_queries`, `gsc_rich_results_report`, `gsc_inspect_url`, `gsc_index_coverage` (batch URL inspection with sitemap membership, referring URLs, rich-result issue severities and orphan detection), `gsc_list_sitemaps` (including the child sitemaps of an index) / `gsc_submit_sitemap` / `gsc_delete_sitemap`, `gsc_add_site` / `gsc_delete_site` |
| **Google Analytics 4** (13) | `ga_list_properties`, `ga_property_config` (streams, custom dimensions/metrics, key events, audiences with their defining clauses, Ads links, retention, attribution model and lookback windows, Google Signals state — read-only), `ga_create_custom_dimension` (registers event parameters as custom dimensions so GA4 keeps their values), `ga_create_key_event` (marks events as key events without waiting for them to show up in the UI; both GA4 writes are skipped in read-only mode), `ga_run_report` (up to 4 date ranges, in-list and OR filters, and a `dataQuality` note whenever rows were sampled or withheld by privacy thresholding), `ga_batch_run_reports`, `ga_run_pivot_report`, `ga_run_funnel_report`, `ga_run_realtime_report`, `ga_get_metadata`, `ga_check_compatibility`, `ga_compare_periods`, `ga_landing_page_seo` (organic landing pages merged with Search Console, and a warning when GA4 records far fewer organic sessions than Search Console clicks, the signature of consent mode or a missing tag) |
| **Page & site audits** (10) | `page_audit`, `site_crawl`, `pagespeed`, `sitemap_check`, `robots_check`, `canonical_host_check` (www/https/trailing-slash variants must all land on one address), `hreflang_check`, `social_preview_check`, `compare_pages`, `keyword_suggest` |
| **GEO** (14) | `ai_crawler_access`, `llms_txt_check`, `llms_txt_generate`, `structured_data_audit` (per-type validation, cross-page entity consistency, and what makes the entity resolvable: live sameAs profiles, schema phone against visible text, one @id for publisher/author to reference), `schema_generate`, `schema_validate`, `geo_page_score`, `geo_answer_coverage` (per question people actually search, does a passage on the page answer it in its opening sentences, or is the answer missing, unheaded or buried), `eeat_audit`, `knowledge_graph_check`, `indexnow_submit`, `ai_citation_check`, `ai_search_sources` (batch up to 10 questions against Perplexity's search endpoint and rank who gets cited), `brand_mentions` |
| **Analysis** (8) | `seo_digest` (what changed since the previous period: totals, the pages and queries that moved, what stopped bringing clicks - the weekly check the server can also send by itself), `migration_check` (pre-migration URL safety net), `cross_site_links`, `content_refresh_candidates`, `crux_history`, `crux_snapshot` (latest real-user record with the LCP sub-part breakdown, so you can see whether LCP is slow because of the server, resource discovery, transfer or render-blocking), `wikipedia_pageviews` (monthly views per language for an entity, with seasonality and year-on-year change), `reviews_snapshot` |
| **WordPress** (28, optional) | `wp_site_info`, `wp_list_posts`, `wp_get_post`, `wp_update_post`, `wp_seo_status`, `wp_update_seo`, `wp_bulk_update_seo` (Yoast fields), `wp_builder_check`, `wp_builder_list_items`, `wp_builder_update`, `wp_builder_restore` (BeTheme / Muffin Builder content, including the entries inside toggles, tabs and FAQ lists, which can also be appended to, with snapshot-based undo), `wp_list_media`, `wp_update_media` (alt text), `wp_upload_media` (fetch, convert and resize an image into the media library, optionally as the featured image), `wp_list_terms`, `wp_update_term`, `wp_internal_link_suggestions`, `wp_list_redirects`, `wp_add_redirect`, `wp_delete_redirect` (Yoast Premium), `wp_get_schema`, `wp_set_schema` (JSON-LD injection), `wp_get_seo_settings`, `wp_update_seo_settings` (site-wide Yoast: title templates, archive noindex, breadcrumbs, organization identity with its contact email, telephone and social profiles, sitemap), `wp_list_revisions`, `wp_update_post_from_revision` (rollback with a diff preview), `wp_delete_cache` (site-wide purge, critical-CSS regeneration, and clearing WP Rocket's used CSS after a header, footer or theme change), `wp_run` (raw WP-CLI) |
| **GitHub** (8, optional) | `github_get_file`, `github_list_dir`, `github_search_code`, `github_list_commits`, `github_commit_files` (atomic multi-file commits with full content, in-place find/replace edits for large files, or base64 binaries), `github_commit_image` (fetch an image URL, convert to webp, resize or crop, add variants, commit), `github_commit_attachment` (same, from a Gmail attachment), `github_build_status` (did the commit actually build and go live: check runs, deployments, or a direct fetch of the page for hosts that report nothing back) (so a static site can be edited from any client) |
| **Gmail** (2, optional) | `gmail_find_attachments` (read-only search of the authorized mailbox listing each message's attachments, so a photo someone emailed lands in the repo through `github_commit_attachment` without leaving the chat), `gmail_get_message` (read the body of an emailed draft article or a client's corrections, plain text or HTML, with paging) |
Plus `google_auth_status` for diagnostics, and `oauth_list_grants` / `oauth_revoke_grant` to see and cut off the clients that connected through the [OAuth layer](#connect-a-client) (operator token only: a client that arrived through OAuth cannot see them). Every tool carries MCP annotations (`readOnlyHint`, `destructiveHint`, `idempotentHint`), the server publishes `instructions` for the model, and there is a read-only mode and toolset filtering.
<details>
<summary><b>Example prompts</b></summary>
- *"Give me a snapshot of example.com for the last 28 days."*
- *"Which queries rank between 8 and 20 with the most impressions, and which posts are they on?"*
- *"Audit https://example.com/guide and score it for AI answer engines."*
- *"Find question-style queries we already get impressions for and tell me where a FAQ is missing."*
- *"Check whether GPTBot, PerplexityBot and ClaudeBot can reach the homepage."*
- *"Before we move to the new host, verify every URL with traffic still resolves on new.example.com."*
- *"Rewrite the SEO title and meta description of post 515 and publish it."*
- *"Take this photo URL, make a 1200×675 webp cover plus a 1000-wide card, commit both to public/assets/img/blog/, then register the cover in src/lib/blog.js."*
- *"Alba emailed three photos yesterday. Find them, turn the first one into the cover for the new post and commit it."*
</details>
## Tech stack
| Layer | Choice | Notes |
|---|---|---|
| Runtime | Node.js ≥ 22, TypeScript 5, ES modules | no build-time codegen, `tsc` only; `googleapis` requires Node 22 |
| Protocol | `@modelcontextprotocol/sdk` | stdio for local clients, stateless Streamable HTTP for servers |
| Google | `googleapis` (Search Console v1, Analytics Data v1beta, Analytics Admin v1beta, Gmail v1 read-only, optional) + `google-auth-library` | REST clients, no gRPC; service account or OAuth |
| Web audits | `cheerio`, `image-size`, `sharp`, native `fetch` | PageSpeed Insights, CrUX (history and latest record with the LCP sub-part breakdown), Knowledge Graph, Wikidata, Wikimedia pageviews, Internet Archive CDX, Google Autocomplete, IndexNow, Perplexity, Brave, Places APIs over HTTPS |
| WordPress | `ssh` + WP-CLI, two PHP helpers uploaded on first use | Yoast indexable rebuild, cache purge (WP Rocket / Super Cache / W3TC / LiteSpeed), mu-plugin for JSON-LD |
| GitHub | REST + Git Data API, `sharp` for images | token from `GITHUB_TOKEN` or `gh auth token`; edits are validated to match exactly once before anything is committed |
| Hosted mode | `node:sqlite`, `google-auth-library` OAuth2, GitHub App (RS256 JWT → installation tokens), server-rendered HTML | optional multi-tenant web UI: Google sign-in, encrypted refresh tokens, per-user bearer tokens, per-user GitHub access |
| Validation | `zod` schemas per tool | descriptions double as LLM documentation |
| Quality | smoke test with tool-list snapshot, secret-scan git hooks | `npm test`, `npm run check:secrets` |
## Architecture
```mermaid
flowchart LR
subgraph Clients
CC[Claude Code]
CD[Claude Desktop]
HTTP[Any MCP client<br/>over HTTPS]
end
subgraph Server["google-seo-mcp"]
direction TB
T1[stdio transport]
T2[Streamable HTTP<br/>Bearer auth · /healthz]
HM[Hosted mode<br/>Google sign-in · SQLite<br/>per-user tokens]
S["createServer()<br/>annotations · read-only · toolsets · instructions"]
subgraph Tools
GSC[gsc.ts]
GA[ga.ts]
WEB[web.ts · crawl.ts]
GEO[geo.ts]
AN[analysis.ts]
WP[wp.ts]
GH[github.ts]
GM[gmail.ts]
end
T1 --> S
T2 --> S
S --> Tools
end
subgraph External
G[(Google APIs<br/>Search Console · GA4<br/>PageSpeed · CrUX · KG)]
SITES[(Your websites)]
WPH[(WordPress host<br/>WP-CLI over SSH)]
GHA[(GitHub)]
GMA[(Gmail<br/>read-only)]
X[(Wikidata · Wikimedia<br/>Internet Archive · IndexNow<br/>Perplexity · Brave · Places)]
end
CC --> T1
CD --> T1
HTTP --> T2
T2 --> HM
HM --> S
GSC & GA --> G
WEB & GEO & AN --> SITES
GEO & AN --> X
AN --> G
WP --> WPH
GH --> GHA
GM --> GMA
GM --> GHA
```
**Request path.** A client calls a tool → `src/util.ts` `tool()` wraps the handler (JSON result or an actionable `isError`) → the handler talks to one or more upstreams → results are flattened into compact JSON (`{dimension: value, metric: number}` rows, totals first). Long-running tools (`pagespeed`, `site_crawl`, `migration_check`) send progress notifications.
**Cross-source analyses** (`ga_landing_page_seo`, `migration_check`, `cross_site_links`, `content_refresh_candidates`, `gsc_opportunities`) reuse the Search Console query function and a shared URL-path normaliser so pages line up across GA4, Search Console, sitemaps and WordPress post IDs.
**WordPress path.** Every call is `ssh host 'cd <wp> && wp …'` with POSIX-quoted arguments; large payloads go over stdin. Two PHP helpers are uploaded to `~/.google-seo-mcp/` on the host when their hash changes. Yoast meta writes trigger an indexable rebuild and a cache purge so changes are live immediately.
**Hosted mode.** With the `SEO_MCP_HOSTED_*` variables set, `src/hosted/` adds a landing page, Google OAuth sign-in and a token dashboard. A `seo_…` bearer token on `/mcp` resolves to that user's encrypted refresh token, and the request runs inside an `AsyncLocalStorage` scope so every Google client created by the tools uses that grant instead of the operator's credentials; the server instance for such requests is read-only and limited to own-data toolsets.
**Safety.** Write tools are recognised by name and receive `readOnlyHint:false` (`destructiveHint:true` for deletes, raw WP-CLI and commits). `--read-only` drops them at registration; `--toolsets=gsc,web` trims the tool list (102 definitions ≈ 37k tokens). Server instructions tell the model that fetched page text and CMS content are untrusted data.
## Quick start
Requirements: Node 22+, a Google Cloud project with the **Search Console API**, **Google Analytics Data API** and **Google Analytics Admin API** enabled.
```bash
git clone https://github.com/Akxan/google-seo-mcp.git
cd google-seo-mcp
npm install
npm run build
cp .env.example .env # fill in credentials (see below)
```
### Google credentials
**Service account (recommended, works unattended):** create a service account in the Cloud project, download its JSON key, set `GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json` in `.env`, then add the service-account email as a user on each Search Console property (permission *Full*) and each GA4 property (role *Viewer*, or *Editor* if it should write with `ga_create_custom_dimension` / `ga_create_key_event`).
**Your own Google account (OAuth):** create an OAuth client ID of type *Desktop app*, download `client_secret.json`, run
```bash
npm run auth -- --client-secret ./client_secret.json
```
and the resulting `~/.config/google-seo-mcp/credentials.json` is picked up automatically.
Lookup order: `GOOGLE_CREDENTIALS_JSON` (inline) → `GOOGLE_APPLICATION_CREDENTIALS` → `~/.config/google-seo-mcp/credentials.json` → Application Default Credentials.
**Gmail attachments (optional):** enable the Gmail API on the same Google Cloud project, create an OAuth client of type *Desktop app*, then run `npm run auth -- --gmail --client-secret ./client_secret.json` once. It writes `~/.config/google-seo-mcp/gmail.json` (read-only scope); point `GMAIL_CREDENTIALS` at it (on a server: `secrets/gmail.json`).
### Connect a client
The server speaks standard MCP over **stdio** (a local process the client starts) and **Streamable HTTP** (a remote server, see [Running as a 24/7 HTTP server](#running-as-a-247-http-server)), so any MCP client works, not only Claude. For a remote server the recipe is the same everywhere: the URL `https://mcp.example.com/mcp` plus the header `Authorization: Bearer <token>`. For a local server the client needs nothing but the command, because the server reads `.env` from its own directory at startup (environment variables passed by the client take precedence).
**Claude Code**
```bash
claude mcp add google-seo -- node /absolute/path/google-seo-mcp/dist/index.js # local
claude mcp add --transport http google-seo https://mcp.example.com/mcp --header "Authorization: Bearer <token>" # remote
```
**Claude Desktop, claude.ai and the mobile apps**: Settings → Connectors → *Add custom connector*, URL `https://mcp.example.com/mcp`, authentication *None*, and `Authorization: Bearer <token>` under *Request headers*. Local alternative for Claude Desktop (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"google-seo": { "command": "node", "args": ["/absolute/path/google-seo-mcp/dist/index.js"] }
}
}
```
**OpenAI Codex** (`~/.codex/config.toml`; the token is read from an environment variable, so `export GOOGLE_SEO_MCP_TOKEN=…` in your shell profile):
```toml
[mcp_servers.google-seo]
url = "https://mcp.example.com/mcp"
bearer_token_env_var = "GOOGLE_SEO_MCP_TOKEN"
tool_timeout_sec = 600 # pagespeed and site_crawl outlive Codex's 60 s default
# local alternative
# [mcp_servers.google-seo]
# command = "node"
# args = ["/absolute/path/google-seo-mcp/dist/index.js"]
```
**Cursor** (`~/.cursor/mcp.json`, or `.cursor/mcp.json` inside a project):
```json
{
"mcpServers": {
"google-seo": {
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
```
**VS Code** (Copilot agent mode; `.vscode/mcp.json` or the user-level file from *MCP: Open User Configuration*):
```json
{
"servers": {
"google-seo": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}
```
**Gemini CLI** (`~/.gemini/settings.json`; `httpUrl` selects Streamable HTTP, `timeout` is in milliseconds):
```json
{
"mcpServers": {
"google-seo": {
"httpUrl": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer <token>" },
"timeout": 600000
}
}
}
```
**Any other MCP client**: point it at `https://mcp.example.com/mcp` with that header (Streamable HTTP, stateless: every call is a `POST`, there is no session to keep), or launch `node dist/index.js` over stdio. A quick check from a shell:
```bash
curl -s https://mcp.example.com/mcp -H "Authorization: Bearer <token>" -H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```
**ChatGPT, and anything else that cannot send a header.** ChatGPT adds MCP servers at `chatgpt.com/plugins` (the *+* button, after switching on Developer mode under Settings → Account security & login). Its authentication menu offers *OAuth*, *No authentication* and *Hybrid* only - there is nowhere to paste a static token. Start the server with `SEO_MCP_OAUTH=1` and choose *OAuth*; the client then discovers everything it needs by itself.
```bash
SEO_MCP_OAUTH=1 # requires MCP_AUTH_TOKEN; adds the OAuth endpoints next to /mcp
```
The server becomes its own authorization server: the client registers itself (RFC 7591), sends you to an approval page, and that page asks for `MCP_AUTH_TOKEN` - the same secret, typed once in a browser instead of sent on every request. Approving mints an access token (24 h) and a rotating refresh token (90 days), stored only as hashes in `<SEO_MCP_DATA_DIR>/oauth.db`. A **read-only** checkbox on that page issues a token whose server registers no write tools, which is what you want for a third-party assistant; replaying a rotated refresh token revokes the whole grant. Endpoints: `/.well-known/oauth-protected-resource`, `/.well-known/oauth-authorization-server`, `/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`, with PKCE S256 required and `plain` refused. Your own `MCP_AUTH_TOKEN` keeps working as a plain bearer token throughout, and nothing changes for clients that never ask for the metadata.
Two things to know: `pagespeed`, `site_crawl` and `gsc_index_coverage` stream progress notifications but can run for minutes, so raise the client's per-tool timeout if it defaults to 60 s; and Developer mode really is required in ChatGPT - without it the server is treated as a Deep Research source and only `search` and `fetch` are looked for.
## Works with any agent
MCP is an open standard, so nothing here is tied to Claude. Whatever speaks MCP connects directly; whatever can call functions connects through a thin bridge. The one hard limit is a model without function calling: it cannot call tools at all, whichever vendor it comes from.
| You have | How it connects | Notes |
|---|---|---|
| An MCP client: Claude apps, Codex, Cursor, VS Code, Gemini CLI, Cline, Cherry Studio, n8n, Dify, … | URL + `Authorization: Bearer <token>` ([Connect a client](#connect-a-client)) | Raise the per-tool timeout for `pagespeed` and `site_crawl` |
| An agent you write: Claude Agent SDK, OpenAI Agents SDK, LangChain, Google ADK, Vercel AI SDK | The SDK's MCP client, same URL and header | Examples below |
| A third-party or local model: DeepSeek, Qwen, GLM, Kimi, Ollama | An MCP client that lets you choose the model (Cherry Studio, Cline), or an SDK pointed at the provider's OpenAI-compatible `base_url` | Needs function calling; give it a trimmed read-only instance (below) |
| A no-code platform that can send a URL but no headers | A read-only instance behind a reverse-proxy path that injects the header | Keeps the main instance's token out of any URL |
### From an agent SDK
Claude Agent SDK (TypeScript; Python has the same shape):
```ts
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const m of query({
prompt: "Snapshot example.com for the last 28 days and list the queries ranking 8-20 with the most impressions",
options: {
mcpServers: {
"google-seo": {
type: "http",
url: "https://mcp.example.com/mcp",
headers: { Authorization: `Bearer ${process.env.GOOGLE_SEO_MCP_TOKEN}` },
},
},
allowedTools: ["mcp__google-seo__*"], // without this the agent sees the tools but will not call them
},
})) {
if (m.type === "result" && m.subtype === "success") console.log(m.result);
}
```
OpenAI Agents SDK (Python). The same code drives any OpenAI-compatible provider; DeepSeek shown, drop the `model=` line for OpenAI itself:
```python
import os
from agents import Agent, Runner, AsyncOpenAI, OpenAIChatCompletionsModel, set_tracing_disabled
from agents.mcp import MCPServerStreamableHttp
async def main():
async with MCPServerStreamableHttp(
name="google-seo",
params={"url": "https://mcp.example.com/mcp",
"headers": {"Authorization": f"Bearer {os.environ['GOOGLE_SEO_MCP_TOKEN']}"}},
) as seo:
set_tracing_disabled(disabled=True)
deepseek = AsyncOpenAI(base_url="https://api.deepseek.com", api_key=os.environ["DEEPSEEK_API_KEY"])
agent = Agent(
name="seo",
instructions="Use the tools; quote numbers with their period and source.",
model=OpenAIChatCompletionsModel(model="deepseek-v4-flash", openai_client=deepseek),
mcp_servers=[seo],
)
result = await Runner.run(agent, "Can GPTBot and PerplexityBot fetch https://example.com/ ?")
print(result.final_output)
```
LangChain (`langchain-mcp-adapters`), Google ADK (`MCPToolset`) and the Vercel AI SDK (`experimental_createMCPClient`) take the same URL and header.
### Third-party and local models
- Desktop: Cherry Studio and Cline let you pick DeepSeek, Qwen, GLM, Kimi or a local Ollama model and add this server as a Streamable HTTP MCP server with the Authorization header.
- The tool catalogue is about 37k tokens and travels with every turn, and 102 tools are a lot for smaller models. Point them at a second, read-only instance with a trimmed toolset and its own token, so a confused model can neither write nor see what it does not need:
```yaml
# docker-compose.yml: a second service next to the main one
google-seo-mcp-lite:
build: .
restart: unless-stopped
ports: ["127.0.0.1:8788:8080"]
env_file: .env
environment:
MCP_TRANSPORT: http
MCP_HOST: 0.0.0.0
MCP_PORT: 8080
MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN_LITE} # its own token, defined in .env
SEO_MCP_READ_ONLY: "1"
SEO_MCP_TOOLSETS: gsc,ga4,web,analysis
GOOGLE_APPLICATION_CREDENTIALS: /secrets/service-account.json
volumes:
- ./secrets/service-account.json:/secrets/service-account.json:ro
```
- Whatever you connect, tool results (Search Console rows, GA4 numbers, WordPress content) are sent to that model's provider. Choose accordingly.
### Known limits
- ChatGPT: its plugin dialog (checked 2026-09-17) offers only *OAuth*, *No authentication* and *Hybrid*, so a static token cannot be entered there. Run the server with `SEO_MCP_OAUTH=1` and pick *OAuth* - see [Connect a client](#connect-a-client). The flow is covered end to end by `test/oauth-http.mjs`.
- Clients that only implement the legacy HTTP+SSE transport: this server speaks Streamable HTTP only (stateless, one `POST` per call). Open an issue if you need SSE.
- Codex custom model providers must implement the Responses API, so Codex cannot drive a Chat-Completions-only provider such as DeepSeek; use an SDK or Cherry Studio for those.
## Configuration
All settings live in `.env` (see [`.env.example`](.env.example), which documents every key).
| Variable | Purpose |
|---|---|
| `GOOGLE_APPLICATION_CREDENTIALS` / `GOOGLE_CREDENTIALS_JSON` | Google auth |
| `PAGESPEED_API_KEY` | PageSpeed Insights (free; without it you share a public quota that is usually exhausted) |
| `CRUX_API_KEY`, `GOOGLE_API_KEY` | Chrome UX Report API, Knowledge Graph Search API (free; fall back to `PAGESPEED_API_KEY`) |
| `BRAVE_API_KEY`, `PERPLEXITY_API_KEY`, `GOOGLE_PLACES_API_KEY` | optional: brand mentions, AI citation check, Google reviews |
| `INDEXNOW_KEY`, `INDEXNOW_KEY_LOCATION` | optional: IndexNow submissions |
| `GITHUB_TOKEN` | GitHub tools (falls back to `gh auth token`) |
| `GMAIL_CREDENTIALS` | optional: authorized_user JSON written by `npm run auth -- --gmail` (Gmail read-only), for `gmail_find_attachments` / `github_commit_attachment` |
| `WP_SITES` | JSON array of WordPress sites reachable over SSH; omit to disable `wp_*` tools |
| `SEO_MCP_READ_ONLY=1` or `--read-only` | register no write tools |
### Prompts (slash commands)
Six ready-made workflows are registered as MCP prompts, so a client shows them as slash commands and the model follows a written sequence instead of guessing which tool fits: `/traffic_drop` (what changed when clicks fell, and whether it is ranking, indexing or technical), `/quick_wins` (near-miss rankings plus pages that rank but are not clicked, merged and ranked), `/publish_check` (one page: on-page, structured data, sharing card, AI-answer readiness), `/site_health` (whole-site technical pass), `/monthly_report` (Search Console and GA4 written as a report), `/index_bloat` (thin archives that should not be indexed).
Prompts follow the toolset narrowing below: an instance limited to `gsc,ga4` offers only the workflows it can actually run.
Client support, as tested on 2026-09-17: the **Claude Code CLI** lists them as `/google-seo:monthly_report` and filters on a fragment; the **VS Code extension does not** expose MCP prompts as slash commands (upstream issue closed as not planned); Claude Desktop support is undocumented. Every argument is optional, because a client that lists a prompt without eliciting its arguments would otherwise fail the call outright - a missing value becomes an instruction to look it up or ask.
| `SEO_MCP_TOOLSETS` or `--toolsets=` | comma list of `gsc,ga4,web,geo,analysis,wordpress,github,gmail` (`google_auth_status` is always on) |
In HTTP mode a single instance can also be narrowed **per connection**, without changing the server's configuration or affecting other clients: append `?toolsets=gsc,ga4,web,geo,analysis` to the endpoint URL, and `?readOnly=1` to make that entry point unable to write. Both parameters only ever remove access — a request cannot reach a toolset the instance was not started with, and cannot turn a read-only tenant into a writing one. An unknown toolset name returns 400 rather than silently yielding an empty server.
The full set of tool definitions costs roughly 37k tokens in every conversation. Pointing a day-to-day client at `/mcp?toolsets=gsc,ga4,web,geo,analysis` cuts that to about 21k, and `?toolsets=gsc,ga4` to about 12k; keep the full URL for the connection you use to edit sites.
| `SEO_MCP_MAX_RESULT_CHARS` | cap on a single tool result (default 120000); oversized arrays are trimmed with a note on how to narrow the query |
| `MCP_TRANSPORT=http`, `MCP_HOST`, `MCP_PORT`, `MCP_PATH`, `MCP_AUTH_TOKEN` | HTTP mode |
| `SEO_MCP_OAUTH=1` | HTTP mode: also accept OAuth, for clients that cannot send an `Authorization` header (ChatGPT). Needs `MCP_AUTH_TOKEN`; grants live in `<SEO_MCP_DATA_DIR>/oauth.db` |
| `SEO_MCP_DIGEST_SITES` (+ `SEO_MCP_DIGEST_AT`, `SEO_MCP_DIGEST_REPO`) | HTTP mode: run `seo_digest` for those Search Console properties once a week (default `mon:08` UTC), keep a Markdown copy under `<SEO_MCP_DATA_DIR>/digests/` and, with a repo, file it as a GitHub issue |
| `SEO_MCP_HOSTED_CLIENT_ID`, `SEO_MCP_HOSTED_CLIENT_SECRET`, `SEO_MCP_HOSTED_SECRET`, `SEO_MCP_PUBLIC_URL` (+ optional `SEO_MCP_DATA_DIR`, `SEO_MCP_HOSTED_CONTACT`, `SEO_MCP_HOSTED_VERIFIED`) | [Hosted mode](#hosted-mode-let-other-people-sign-in-with-google): Google sign-in for other users, per-user read-only tokens |
| `SEO_MCP_GITHUB_APP_ID`, `SEO_MCP_GITHUB_APP_SLUG`, `SEO_MCP_GITHUB_APP_CLIENT_ID`, `SEO_MCP_GITHUB_APP_CLIENT_SECRET`, `SEO_MCP_GITHUB_APP_PRIVATE_KEY_FILE` | hosted mode: a GitHub App users install on their repositories to get the `github_*` tools (installation tokens, one hour) |
| `GOOGLE_OAUTH_CLIENT_SECRET_FILE` (or `--client-secret`), `GOOGLE_OAUTH_CLIENT_ID` + `GOOGLE_OAUTH_CLIENT_SECRET`, `GOOGLE_OAUTH_PORT` | `npm run auth` only: the OAuth client for the user-account flow (callback port defaults to 53682) |
Tools that need an optional key return an error explaining how to obtain it instead of silently disappearing. Empty values count as unset, including a `KEY=` that Docker passes through from an env file.
### WordPress over SSH
```
WP_SITES=[{"name":"mysite","host":"1.2.3.4","port":22,"user":"ssh_user","path":"domains/example.com/public_html"}]
```
Needs WP-CLI on the host and passwordless SSH from the machine running the server. Posts built with BeTheme's Muffin Builder (empty `post_content`) are handled by the `wp_builder_*` tools. `wp_set_schema` installs a 5-line mu-plugin that prints stored JSON-LD in `<head>`. Every write tool (and `github_commit_files`) accepts `dryRun: true` to return the current values and the intended changes without touching anything.
## Running as a 24/7 HTTP server
```bash
MCP_TRANSPORT=http MCP_AUTH_TOKEN=$(openssl rand -hex 32) node dist/index.js --http
curl http://127.0.0.1:8080/healthz
```
Stateless Streamable HTTP: a fresh server instance per request, Bearer-token auth, loopback bind by default. Every write-tool call leaves one audit line on stderr (tool, outcome, duration, client, identifiers such as post id or file paths; never content), so `docker logs` shows who changed what. With `SEO_MCP_DATA_DIR` set the same line also goes to `<dir>/audit.log`, which keeps 30 days (what the hosted privacy page promises) and drops older lines on its own. `/healthz` answers `{"ok":true}` without a token and adds the version and credential source when the request carries the Bearer token. `deploy/vps-self-update.sh` updates a Docker deployment in place, and `.github/workflows/deploy.yml` runs it on every push to `main` through a forced-command SSH deploy key stored in repository secrets (`VPS_HOST`, `VPS_USER`, `VPS_SSH_KEY`, `VPS_KNOWN_HOSTS`). [`deploy/`](deploy/) contains a systemd unit, an env-file example and Caddy/Nginx reverse-proxy samples (Nginx needs `proxy_buffering off`). `deploy/backup-data.sh <data-dir>` backs up `hosted.db` and `oauth.db` for cron (14 days, integrity-checked); use it rather than `cp`, because both databases run in WAL mode and a copied `.db` can be missing everything written since the last checkpoint. `Dockerfile` and `docker-compose.yml` are provided. Connect remote clients with
Client-side setup for the remote server (Claude apps, Codex, Cursor, VS Code, Gemini CLI, anything else that speaks MCP) is under [Connect a client](#connect-a-client).
## Hosted mode: let other people sign in with Google
The same binary can run as a small multi-tenant service: a landing page, *Sign in with Google*, and a dashboard where each user creates personal bearer tokens for `/mcp`. Users grant **read-only** Search Console and GA4 scopes; their refresh tokens are stored encrypted (AES-256-GCM) in a SQLite file (`node:sqlite`, no extra dependency) and every `/mcp` request carrying a `seo_…` token runs against that user's Google account, with a read-only server limited to the `gsc`, `ga4`, `web`, `geo` and `analysis` toolsets (tools that write, that need SSH/GitHub/Gmail credentials, or that spend paid third-party quotas are not registered). Your own `MCP_AUTH_TOKEN` keeps working unchanged with the full tool set.
1. In Google Cloud create an OAuth client of type **Web application** with the authorised redirect URI `https://mcp.example.com/oauth/callback`, enable the Search Console and Analytics Data/Admin APIs, and add the `webmasters.readonly` and `analytics.readonly` scopes on the consent screen. While the consent screen is unverified, Google shows a warning and caps sign-ins at 100 users; publishing to everyone requires [Google's OAuth verification](https://support.google.com/cloud/answer/13463073).
2. Set the four variables together (`.env`): `SEO_MCP_HOSTED_CLIENT_ID`, `SEO_MCP_HOSTED_CLIENT_SECRET`, `SEO_MCP_HOSTED_SECRET` (`openssl rand -hex 32`), `SEO_MCP_PUBLIC_URL`. Optional: `SEO_MCP_DATA_DIR` (database location; the Docker image uses `/data`, mounted from `./data`), `SEO_MCP_HOSTED_CONTACT` (shown on the privacy page), `SEO_MCP_HOSTED_VERIFIED=1` once Google has verified the app.
3. Restart. `/` serves the landing page (English and Chinese), `/login` starts the Google flow, `/dashboard` manages tokens (up to 10 per user, shown once, revocable), `/privacy` and `/terms` are the legal pages Google's verification asks for, and *Disconnect* revokes the Google grant and deletes the user's record and tokens.
**GitHub for hosted users.** With a GitHub App configured (`SEO_MCP_GITHUB_APP_ID`, `_SLUG`, `_CLIENT_ID`, `_CLIENT_SECRET`, `_PRIVATE_KEY_FILE`; the app needs *Contents: read & write*, *Request user authorization (OAuth) during installation* enabled, and `https://mcp.example.com/connect/github/callback` as callback URL), the dashboard shows a *Connect GitHub* button. The user installs the app on the repositories of their choice; the server verifies the installation belongs to the signed-in GitHub user, stores only the installation id, and mints one-hour installation tokens when a tool needs one. Their MCP tokens then include the `github_*` tools, including `github_commit_files` and `github_commit_image` (each with `dryRun`), so a static site on Cloudflare, Vercel or Netlify can be edited through their own agent. *Disconnect* uninstalls the app, which revokes every token minted from it. The operator's `GITHUB_TOKEN` is never used for hosted requests.
Cookies are `HttpOnly`, `SameSite=Lax` and `Secure` behind HTTPS; forms carry a CSRF token; the OAuth `state` is signed. Sign-ins and disconnects leave one JSON line on stderr (user id only). Leave all four variables empty and nothing of this exists: the server stays a private single-user instance.
## Development
```bash
npm run dev # tsx src/index.ts (stdio, no build)
npm run build # tsc -> dist/
npm test # unit tests (node:test), smoke test (annotations, instructions, tool-list snapshot), README count check
npm run inspector # MCP Inspector against dist/
npm run check:secrets # scan tracked files for keys / personal data (also pre-commit and pre-push hooks)
```
```
src/
├── index.ts entry: stdio or --http
├── server.ts createServer(): registration wrapper, annotations, read-only, toolsets, instructions
├── http.ts Streamable HTTP transport with Bearer auth
├── google.ts GoogleAuth + googleapis clients
├── env.ts .env loader
├── gmail.ts Gmail read-only client (optional)
├── util.ts tool() wrapper, error formatting, date helpers, progress heartbeat
└── tools/ gsc · ga · web · crawl · geo · analysis · wp · github · gmail
scripts/ wp-helper.php · mfn-builder.php (uploaded to the WordPress host) · check-secrets.sh
deploy/ systemd · Caddy · Nginx samples
test/ unit tests · smoke test · tool snapshot
```
## Notes and limits
- Search Console data lags 2–3 days; end date ranges at `3daysAgo`. URL Inspection has a ~2,000 calls/day quota per property.
- PageSpeed runs take 15–60 s; the tool retries once and sends progress notifications. Pages that never become idle cannot be audited by Lighthouse.
- CrUX only has data for origins with enough Chrome traffic.
- All fetched page text and CMS content is untrusted third-party data; the server instructions tell the model not to follow instructions found in it.
## Contributing
Issues and pull requests are welcome. CI runs build, tests and the secret scan on every push and pull request; `main` deploys only after they pass. Add new tools to the matching `src/tools/*.ts` module, give every parameter a `.describe()`, run `npm run docs:sync`, and add a line to `CHANGELOG.md`.
## Keywords
MCP server · Model Context Protocol · SEO MCP · GEO · generative engine optimization · AI SEO agent · Claude MCP · Claude Code · OpenAI Codex MCP · Cursor MCP · Gemini CLI MCP · Claude Agent SDK · OpenAI Agents SDK · LangChain MCP · n8n · Dify · DeepSeek · Google Search Console API · Google Analytics 4 API · GA4 Data API · PageSpeed Insights API · Core Web Vitals · CrUX · technical SEO audit · site crawler · static site publishing · Astro · Cloudflare Pages · webp image pipeline · Gmail attachments · structured data · schema.org · JSON-LD · FAQPage · llms.txt · AI crawlers · GPTBot · ClaudeBot · PerplexityBot · robots.txt · sitemap · hreflang · keyword cannibalization · striking distance keywords · content decay · E-E-A-T · Knowledge Graph · Wikipedia pageviews · Wayback Machine · IndexNow · LCP breakdown · canonical host check · redirect chain · index bloat · WordPress SEO automation · Yoast SEO · WP-CLI · post scheduling · TypeScript
## Star history
[](https://star-history.com/#Akxan/google-seo-mcp&Date)
## License
[MIT](LICENSE)
## Community
This project takes part in and acknowledges the [LINUX DO](https://linux.do/) community. 本项目积极参与并认可 [linux.do 社区](https://linux.do/)。
TDQS
Scored across 72 tools
The 72 tools have detailed descriptions and many explicitly say when to use one over another, but several adjacent SEO/GSC/audit tools overlap in practice (e.g. gsc_opportunities vs gsc_ctr_opportunities vs content_refresh_candidates vs gsc_question_queries, and page_audit vs site_crawl vs compare_pages). An agent often needs to read full descriptions to pick correctly, which indicates real boundary friction across the set.
There are recognizable prefix groups such as ga_, gsc_, github_, gmail_, schema_, ai_, and crux_, but many tools are un-prefixed or inconsistently grouped (page_audit, pagespeed, sitemap_check, compare_pages, brand_mentions, reviews_snapshot). The overall naming is mixed but still readable, fitting the middle of the scale.
72 tools is an extreme mismatch for a well-scoped MCP server, substantially above the 50+ threshold. Many tools are adjacent variants or niche sub-checks, making the surface difficult to discover and operate as a coherent set.
The server covers a very broad SEO/GEO lifecycle: Search Console and GA4 analytics, crawling, audits, schema, AI-search visibility, Core Web Vitals, local reviews, Wikipedia/KG entities, and GitHub/Gmail content operations. There are still minor gaps, such as no direct WordPress CRUD tool despite schema_generate referencing wp_set_schema, and no true backlink/rank-tracking API, but most workflows can be worked around.