Google Search Console MCP Server
# Google Search Console MCP Server
An MCP server covering the **entire Google Search Console API** — all 11
published methods, plus task-shaped tools on top for the questions people
actually ask of search performance data.
## Overview
This project provides a Model Context Protocol server that wraps the Google
Search Console API v1, letting LLMs work with search performance data, sitemaps,
property management and URL indexing through a standardised interface.
Search Console ships no official SDK in any language, so this is built on the
generic `google-api-python-client` with complete static types supplied by
`google-api-python-client-stubs`. See
[Why there is no Search Console SDK](#why-there-is-no-search-console-sdk).
## Features
- **Complete API Coverage**: all 11 published API methods, enforced by a test rather than claimed
- **Full Type Safety**: strict `pyright` with 0 errors
- **100% Test Coverage**: 144 tests, 100% statement and branch coverage, floor enforced in config
- **Task-Shaped Tools**: 11 convenience tools on top of the raw API
- **MCP Compliant**: FastMCP 4, stdio transport
- **Async Throughout**: the synchronous Google client is dispatched off the event loop
- **Read-Only Mode**: an opt-in switch that refuses every mutating call
## Installation
```bash
git clone https://github.com/rgellis/google-search-console-mcp.git
cd google-search-console-mcp
# Install dependencies using uv
uv sync
```
Requires Python 3.12+.
### 1. Enable the API
Search Console API must be enabled on the Google Cloud project that owns your
OAuth client:
```bash
gcloud services enable searchconsole.googleapis.com --project=<project>
```
### 2. Create an OAuth client
In the Google Cloud console, create an OAuth 2.0 client of type **Desktop app**
(or **Web application** with `http://localhost` as an authorised redirect URI).
Note the client ID and secret.
### 3. Mint a refresh token
Search Console needs the `https://www.googleapis.com/auth/webmasters` scope:
```bash
GOOGLE_CLIENT_ID="..." GOOGLE_CLIENT_SECRET="..." \
uv run scripts/get_refresh_token.py
# or, if this server will never manage properties:
GOOGLE_CLIENT_ID="..." GOOGLE_CLIENT_SECRET="..." \
uv run scripts/get_refresh_token.py --read-only
```
Sign in as an account with access to the Search Console properties you need. The
script prints the refresh token.
Scope the token to this server alone rather than reusing one minted for other
Google APIs — re-consenting a shared token to add `webmasters` rotates a secret
everything else using it depends on.
### 4. Set credentials
```bash
export GOOGLE_CLIENT_ID="your_client_id"
export GOOGLE_CLIENT_SECRET="your_client_secret"
export GOOGLE_SEARCH_CONSOLE_REFRESH_TOKEN="token_from_step_3"
# optional: request only the readonly scope and refuse all mutations
export SEARCH_CONSOLE_READ_ONLY=false
```
If those three are not all set, the client falls back to Application Default
Credentials, which covers a service account via `GOOGLE_APPLICATION_CREDENTIALS`
as well as local `gcloud` auth.
### 5. Verify
```bash
uv run main.py --groups sites
```
Then call `check_client_status`, which reports whether credentials resolve, the
scopes in use, and whether read-only mode is active — without spending API quota.
## Usage
```bash
uv run main.py # all tools, over stdio
uv run main.py --groups sites,analytics # a subset
```
Groups: `sites`, `sitemaps`, `analytics`, `inspection`, `testing-tools`.
### With an MCP client
```json
{
"mcpServers": {
"search-console": {
"command": "uv",
"args": ["run", "--directory", "/path/to/google-search-console-mcp", "main.py"],
"env": {
"GOOGLE_CLIENT_ID": "...",
"GOOGLE_CLIENT_SECRET": "...",
"GOOGLE_SEARCH_CONSOLE_REFRESH_TOKEN": "..."
}
}
}
}
```
### Property identifiers
Search Console has two property types and the API is strict about the exact
string for each. Tools normalise input, so all of these work:
| You pass | Sent to the API |
| --- | --- |
| `sc-domain:example.com` | `sc-domain:example.com` |
| `example.com` | `sc-domain:example.com` |
| `https://example.com` | `https://example.com/` |
| `https://example.com/` | `https://example.com/` |
A URL-prefix property passed without its trailing slash is the most common cause
of a spurious 403 from this API, which is why normalisation happens here rather
than being left to the caller. A 403 raised by this server appends a reminder of
exactly that.
## Feature Parity Table
Implementation status of Google Search Console API v1 methods (discovery
revision 20260909):
| Tool | Category | API Method | Implemented | Test Coverage | Notes |
| ----------------------------------- | ---------------- | ------------------------------------------ | ----------- | ------------- | ------------------------------------------ |
| **Property Management** | | | | | |
| `list_sites` | Sites | `sites.list` | ✅ Yes | ✅ Yes | All properties and permission levels |
| `get_site` | Sites | `sites.get` | ✅ Yes | ✅ Yes | One property, with permission level |
| `add_site` | Sites | `sites.add` | ✅ Yes | ✅ Yes | Mutation; needs verification afterwards |
| `delete_site` | Sites | `sites.delete` | ✅ Yes | ✅ Yes | Mutation; irreversible via the API |
| **Sitemaps** | | | | | |
| `list_sitemaps` | Sitemaps | `sitemaps.list` | ✅ Yes | ✅ Yes | Optional sitemap-index drill-down |
| `get_sitemap` | Sitemaps | `sitemaps.get` | ✅ Yes | ✅ Yes | Processing status and error counts |
| `submit_sitemap` | Sitemaps | `sitemaps.submit` | ✅ Yes | ✅ Yes | Mutation |
| `delete_sitemap` | Sitemaps | `sitemaps.delete` | ✅ Yes | ✅ Yes | Mutation |
| `submit_sitemaps` | Sitemaps | *(composes `sitemaps.submit`)* | ✅ Yes | ✅ Yes | Bulk; failure-tolerant per sitemap |
| **Search Analytics** | | | | | |
| `query_search_analytics` | Search Analytics | `searchanalytics.query` | ✅ Yes | ✅ Yes | Raw 1:1 wrapper, full request control |
| `query_all_rows` | Search Analytics | *(composes `searchanalytics.query`)* | ✅ Yes | ✅ Yes | Pages past the 25,000-row cap |
| `top_queries` | Search Analytics | *(grouped by `QUERY`)* | ✅ Yes | ✅ Yes | Sorted by clicks, named fields |
| `top_pages` | Search Analytics | *(grouped by `PAGE`)* | ✅ Yes | ✅ Yes | Sorted by clicks |
| `performance_by_date` | Search Analytics | *(grouped by `DATE`)* | ✅ Yes | ✅ Yes | Daily time series |
| `performance_by_country` | Search Analytics | *(grouped by `COUNTRY`)* | ✅ Yes | ✅ Yes | ISO-3166-1 alpha-3 |
| `performance_by_device` | Search Analytics | *(grouped by `DEVICE`)* | ✅ Yes | ✅ Yes | Desktop / mobile / tablet |
| `performance_by_search_appearance` | Search Analytics | *(grouped by `SEARCH_APPEARANCE`)* | ✅ Yes | ✅ Yes | Rich result types |
| `compare_periods` | Search Analytics | *(two queries, with deltas)* | ✅ Yes | ✅ Yes | Equal-length preceding period, flags new |
| **URL Inspection** | | | | | |
| `inspect_url` | URL Inspection | `urlInspection.index.inspect` | ✅ Yes | ✅ Yes | Index status, canonicals, crawl, AMP |
| `inspect_urls` | URL Inspection | *(composes `urlInspection.index.inspect`)* | ✅ Yes | ✅ Yes | Bulk, quota-aware, verdict summary |
| **URL Testing Tools** | | | | | |
| `run_mobile_friendly_test` | Testing Tools | `urlTestingTools.mobileFriendlyTest.run` | ✅ Yes | ✅ Yes | ⚠️ Shut down by Google 1 Dec 2023 |
| **Diagnostics** | | | | | |
| `check_client_status` | Diagnostics | *(no API call)* | ✅ Yes | ✅ Yes | Credential state; spends no quota |
### Summary Statistics
- **API Methods Implemented**: 11 out of 11 Search Console API v1 methods (**100%**)
- **Total Tools**: 22 (11 API methods + 11 convenience tools)
- **Tools with Tests**: 22 (**100% test coverage**)
- **Line & Branch Coverage**: **100%** across `src` (468 statements, 96 branches), enforced by `fail_under = 100`
- **Test Count**: 144
- **Type Safety**: `pyright` strict, 0 errors
### Implementation Highlights
1. ✅ **Complete API Surface**: every published method, including the one Google deprecated
2. ✅ **Coverage Enforced, Not Claimed**: `tests/test_api_coverage.py` diffs the implementation against the vendored discovery document in both directions
3. ✅ **End-to-End Tool Tests**: every tool is invoked through a real MCP client, not just unit-tested at the service layer
4. ✅ **Correct Metric Aggregation**: totals recompute CTR from summed clicks/impressions and weight position by impressions — averaging either across rows gives a wrong answer
5. ✅ **Quota Awareness**: bulk URL inspection refuses to exceed the documented 2,000/property/day limit
### Key Features
- **Typed Discovery Client**: complete static types from `google-api-python-client-stubs`
- **Off-Loop Execution**: the synchronous Google client is dispatched via `asyncio.to_thread`, so it never blocks the MCP event loop
- **Uniform Error Translation**: one `execute()` helper turns `HttpError` into a readable message, including a 403 hint about property-identifier format
- **Lazy Client Initialisation**: credentials resolve on first use, so the server starts even when misconfigured and `check_client_status` can say why
- **Read-Only Mode**: `SEARCH_CONSOLE_READ_ONLY=true` requests the readonly scope and refuses all four mutations
- **Property Normalisation**: domain vs URL-prefix properties handled at the boundary
### Deprecated API Methods
Nothing is unimplemented. One method is implemented but non-functional upstream:
- **`urlTestingTools.mobileFriendlyTest.run`** — Google shut this down on 1 December 2023. It is still published in the discovery document, so it is implemented here for complete coverage, and its docstring says plainly that it returns an error. For mobile usability signals use `inspect_url`, whose response carries a `mobileUsabilityResult`.
## Why there is no Search Console SDK
Search Console is a *discovery-document* API. Google ships no dedicated client
library for it in any language — no equivalent of `google-ads` or
`google-analytics-data`. It is one of ~300 APIs served by the generic
`google-api-python-client`, whose resources are built dynamically at runtime and
are entirely untyped.
Other languages fare better, because their generic clients generate code at build
time into per-API packages: Go has `google.golang.org/api/searchconsole/v1`, Java
has `google-api-services-searchconsole`, .NET has `Google.Apis.SearchConsole.v1`,
and Node's `googleapis` ships `Schema$*` TypeScript interfaces. Python is the
outlier — its client parses the discovery document at runtime, so there is
nothing to type-check.
That would normally rule out strict type checking. The way out is
`google-api-python-client-stubs`, which covers `searchconsole/v1` completely: all
30 schemas as TypedDicts with `Literal` enums, and a `build()` overload keyed on
the literal service name. It is a dev-only dependency — stubs never ship at
runtime, so if it goes stale the cost is type-checking, not functionality.
Two consequences to know before editing:
- `src/client.py` imports `SearchConsoleResource` under `if TYPE_CHECKING:`. The
`googleapiclient._apis` package **does not exist at runtime**; that guard must stay.
- FastMCP resolves tool annotations at runtime, so tool signatures use
`Dict[str, Any]` while the service layer beneath stays precisely typed.
Annotating a tool with a stub type raises `NameError` at registration.
## Testing
```bash
# Run tests
uv run pytest
# Run tests with coverage (must stay at 100%)
.venv/bin/python -m pytest --cov --cov-report=term-missing
# Run type checking
uv run pyright
# Run code formatting
uv run ruff format .
# Everything at once
./scripts/typecheck.sh
```
Coverage is invoked as `.venv/bin/python -m pytest` rather than `uv run pytest`
for one reason, documented in `pyproject.toml`: constructing a FastMCP instance
inside a coverage-instrumented module trips a type check in `cryptography`'s Rust
bindings, so `main.py` is excluded from the measured source set and covered
end-to-end by `tests/test_api_coverage.py` instead.
### Test layout
| File | Covers |
| --- | --- |
| `test_tools.py` | Every tool invoked through a real MCP client, end to end |
| `test_sites_service.py` | Sites service methods |
| `test_sitemaps_service.py` | Sitemaps service methods |
| `test_search_analytics_service.py` | Query building, pagination, aggregation maths, period comparison |
| `test_url_inspection_service.py` | Inspection, bulk summarising, quota guard |
| `test_url_testing_tools_service.py` | Deprecated Mobile-Friendly Test |
| `test_client.py` | Credential resolution, scopes, read-only gate, lifecycle |
| `test_base.py` | Error translation, 403 hint, off-loop dispatch |
| `test_utils.py` | Property normalisation, date maths, dotenv |
| `test_api_coverage.py` | The API coverage contract, both directions |
### When Google revises the API
```bash
uv run scripts/refresh_discovery.py
uv run pytest tests/test_api_coverage.py
```
A new method fails `test_every_api_method_is_declared` until it is implemented
and mapped in `src/coverage.py`. A removed one fails
`test_no_declared_method_has_been_removed`. That failure is the intended alarm —
do not weaken the test.
## Repository layout
```
src/client.py credentials, typed discovery client, read-only gate
src/coverage.py API method -> tool mapping; the coverage contract
src/services/ one module per API resource
src/services/base.py off-loop dispatch and error translation
refs/ vendored discovery document
scripts/ token minting, discovery refresh, checks
tests/ 144 tests, 100% coverage
main.py stdio MCP server
```
## Contributing
Contributions are welcome. Please ensure:
1. All code has proper type annotations
2. Tests are added for new functionality — coverage must stay at 100%
3. Code passes `uv run pyright` with no errors
4. Code is formatted with `uv run ruff format`
5. Any new API method is mapped in `src/coverage.py`
## License
MIT — see [LICENSE](LICENSE).
## Disclaimer
This is an unofficial integration and is not affiliated with or endorsed by
Google. Search Console API quotas apply; the URL Inspection API is limited to
2,000 queries per property per day and 600 per minute.
TDQS
Scored across 22 tools
Most tools target distinct resources or actions, and descriptions explicitly differentiate the raw analytics query from specialized ones. Minor overlap exists between singular/plural pairs (submit_sitemap/submit_sitemaps, inspect_url/inspect_urls), but the text clarifies the distinction.
Names are consistently snake_case and mostly follow a verb_noun pattern. A few analytics tools are noun phrases (top_queries, performance_by_date), which is a minor deviation but still readable and coherent.
At 22 tools the set feels heavy for the domain. Several analytics wrappers overlap with query_search_analytics, and run_mobile_friendly_test is explicitly deprecated and non-functional, so not every tool earns its place.
The surface covers full lifecycle operations for sites and sitemaps, offers comprehensive search analytics breakdowns, and includes both single and batch URL inspection. No obvious gaps remain for the Google Search Console API.