# SEO Console (npm · seo-console-mcp)

Search Console, Google Ads, App Store, Google Play, WordPress.org and Core Web Vitals over MCP

- Trust score: 70/100 (medium)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-09-25

## Components

- npm · `seo-console-mcp`: 70/100 (this document), [markdown](https://verifymcp.io/servers/ibrahimhajjaj-seo-console-mcp/seo-console-mcp.md), [page](https://verifymcp.io/servers/ibrahimhajjaj-seo-console-mcp/seo-console-mcp)

## Channel facts

- Registry: `npm`
- Package: `seo-console-mcp`
- Version: `0.18.2`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-09-25.

- **Supply Chain Security**: 98/100
  - No malware found by supply-chain analysis.
  - Known CVEs were checked across the 177 of 178 dependencies we could resolve, so this covers what we could see, not the whole tree.
  - No install/post-install scripts declared.
  - Dependency health was assessed across the 177 of 178 dependencies we could resolve, so this covers what we could see, not the whole tree.
- **Provenance & Transparency**: 45/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 0 days ago).
  - Disclosure check failed: no security disclosure policy was found in the source repository.
- **Schema Quality & AI Usability**: 83/100
  - 100% of prompts and resources have a non-trivial description (not blank, and not just the item's name).
  - AI-judged instruction clarity (excellent).
  - Context-footprint check failed: tool/resource definitions use about 6420 tokens (~145/item across 44 items; 43 tools + 1 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 0/100
  - Stability not yet verified: not enough scan history yet (needs a 30-day window).
- **Tool Coverage**: 100/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 100% of tool parameters carry a description.
  - Structured output schemas are declared (100% of tools); any adoption earns full credit.
- **Tool Safety**: 75/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 0 of 1 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "delete_sitemap" implies "delete" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 44 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a supported MCP spec version (2025-11-25); the latest is 2026-07-28.

**Unverified: 1 category.** A category scored 0 because we could not verify it: a data source with nothing on this package, evidence we could not reach, or a check we could not run. We only credit what we can confirm.

## Install

### How do I install the SEO Console MCP server?

SEO Console runs locally as an npm package, launched with npx -y seo-console-mcp. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add ibrahimhajjaj-seo-console-mcp -- npx -y seo-console-mcp
```

### Cursor

```json
{
  "mcpServers": {
    "ibrahimhajjaj-seo-console-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "seo-console-mcp"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "ibrahimhajjaj-seo-console-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "seo-console-mcp"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add ibrahimhajjaj-seo-console-mcp -- npx -y seo-console-mcp
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "ibrahimhajjaj-seo-console-mcp": {
      "type": "local",
      "command": [
        "npx",
        "-y",
        "seo-console-mcp"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add ibrahimhajjaj-seo-console-mcp --command npx --arg -y --arg seo-console-mcp
```

### Hermes

```yaml
mcp_servers:
  ibrahimhajjaj-seo-console-mcp:
    command: "npx"
    args: ["-y", "seo-console-mcp"]
```

### Netclaw

```json
{
  "McpServers": {
    "ibrahimhajjaj-seo-console-mcp": {
      "Transport": "stdio",
      "Command": "npx",
      "Arguments": [
        "-y",
        "seo-console-mcp"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add ibrahimhajjaj-seo-console-mcp -t stdio -c npx -a -y seo-console-mcp
```

### Other

```json
{
  "mcpServers": {
    "ibrahimhajjaj-seo-console-mcp": {
      "command": "npx",
      "args": [
        "-y",
        "seo-console-mcp"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-09-25 (score 70)

First indexed and scored.

## MCP tools (43)

### `search_analytics` (~265 tokens)

Query Google Search Console search analytics and return ranked clicks, impressions, CTR, and position

Input parameters:

- `aggregationType` (string): How Search Console aggregates rows
- `dataState` (string): full = finalized data (default, ~2-3 day lag); all = include recent partial data
- `dimensionFilterGroups` (array): Search Console dimension filters
- `dimensions` (array): Dimensions used to group results
- `endDate` (string): End date in YYYY-MM-DD; defaults to today
- `maxTableRows` (integer): Cap rows shown in the text table; structured rows are always complete. 0 = summary only.
- `rowLimit` (integer): Maximum rows to return
- `siteUrl` (string, required): Search Console property, such as https://example.com/ or sc-domain:example.com
- `startDate` (string): Start date in YYYY-MM-DD; defaults to 28 days ago
- `startRow` (integer): Zero-based row to start from, for paging through a large result
- `type` (string): Result type. discover is the Discover feed and googleNews is the Google News app and news.google.com, not the News tab in Search. Both support fewer dimensions than web: neither reports a query dimen…

Output parameters:

- `dimensions` (array)
- `endDate` (string)
- `firstIncompleteDate` (string)
- `rowCount` (number)
- `rows` (array)
- `siteUrl` (string)
- `startDate` (string)
- `startRow` (number)
- `truncated` (boolean): More rows follow this page. False does not mean the result is complete: Search Console returns top rows subject to its own internal limits

### `keyword_ideas` (~131 tokens)

Expand a seed with free Google Autocomplete suggestions and optionally cross-reference Search Console rankings; no extra API key needed

Input parameters:

- `country` (string): Autocomplete country passed as gl
- `days` (integer): Search Console lookback window in days
- `expansions` (array): Suggestion expansion families to run beyond the bare seed
- `language` (string): Autocomplete interface language passed as hl
- `limit` (integer): Maximum keyword ideas to return
- `seed` (string, required): Seed keyword to expand
- `siteUrl` (string): Optional Search Console property used to identify queries already ranking

Output parameters:

- `crossReferenced` (boolean)
- `gscMatched` (number)
- `ideas` (array)
- `requestFailures` (number)
- `returned` (number)
- `seed` (string)
- `totalFound` (number)

### `search_opportunities` (~152 tokens)

Find queries ranking just off page 1 with high impressions, the highest-ROI keywords to improve

Input parameters:

- `endDate` (string): End date in YYYY-MM-DD; defaults to today
- `limit` (integer): Maximum opportunities to return; defaults to 50
- `maxPosition` (number): Highest average position to include; defaults to 20
- `minImpressions` (number): Minimum impressions required; defaults to 10
- `minPosition` (number): Lowest average position to include; defaults to 5
- `siteUrl` (string, required): Search Console property to analyze
- `startDate` (string): Start date in YYYY-MM-DD; defaults to the latest 28-day window

Output parameters:

- `opportunities` (array): Highest-value striking-distance rows
- `siteUrl` (string): Search Console property analyzed
- `truncated` (boolean): Whether Search Console held more than 5000 rows for this window, so the list was computed from the top rows only
- `window` (object): Analysis window

### `compare_search_periods` (~113 tokens)

Compare an analysis window with the preceding equal period to identify search gainers and losers

Input parameters:

- `by` (string): Dimension used to compare performance
- `endDate` (string): End date in YYYY-MM-DD; defaults to today
- `limit` (integer): Maximum gainers and losers to return; defaults to 50 each
- `siteUrl` (string, required): Search Console property to analyze
- `startDate` (string): Start date in YYYY-MM-DD; defaults to the latest 28-day window

Output parameters:

- `currentTruncated` (boolean): Whether Search Console held more than 5000 rows for the current window
- `currentWindow` (object): Current comparison window
- `droppedAsUnknown` (number): Rows present on only one side whose other side was cut off, and so were excluded rather than counted as zero
- `gainers` (array): Rows with increased clicks
- `losers` (array): Rows with decreased clicks
- `previousTruncated` (boolean): Whether Search Console held more than 5000 rows for the previous window
- `previousWindow` (object): Immediately preceding equal-length window
- `siteUrl` (string): Search Console property analyzed

### `ctr_gaps` (~130 tokens)

Find high-impression queries or pages whose CTR trails peers at the same position for snippet rewrite prioritization

Input parameters:

- `by` (string): Dimension used to identify CTR gaps
- `endDate` (string): End date in YYYY-MM-DD; defaults to today
- `limit` (integer): Maximum gaps to return; defaults to 50
- `minImpressions` (number): Minimum impressions required; defaults to 100
- `siteUrl` (string, required): Search Console property to analyze
- `startDate` (string): Start date in YYYY-MM-DD; defaults to the latest 28-day window

Output parameters:

- `gaps` (array): Rows underperforming their position peers
- `siteUrl` (string): Search Console property analyzed
- `truncated` (boolean): Whether Search Console held more than 5000 rows for this window, so the list was computed from the top rows only
- `window` (object): Analysis window

### `query_cannibalization` (~100 tokens)

Find queries served by multiple pages to prioritize consolidation and internal-linking decisions

Input parameters:

- `endDate` (string): End date in YYYY-MM-DD; defaults to today
- `minImpressions` (number): Minimum impressions per query-page row; defaults to 10
- `siteUrl` (string, required): Search Console property to analyze
- `startDate` (string): Start date in YYYY-MM-DD; defaults to the latest 28-day window

Output parameters:

- `groups` (array): Queries with multiple ranking pages
- `siteUrl` (string): Search Console property analyzed
- `truncated` (boolean): Whether Search Console held more than 5000 rows for this window, so the list was computed from the top rows only
- `window` (object): Analysis window

### `list_sitemaps` (~33 tokens)

List sitemaps submitted for a Google Search Console property

Input parameters:

- `siteUrl` (string, required): Search Console property

Output parameters:

- `count` (number)
- `siteUrl` (string)
- `sitemaps` (array)

### `list_properties` (~23 tokens)

List Google Search Console properties the service account can access, with permission levels

Output parameters:

- `count` (number)
- `properties` (array)

### `submit_sitemap` (~69 tokens)

Submit a sitemap to Google Search Console and return its current state

Input parameters:

- `dryRun` (boolean): If true, report what would be submitted without writing to Search Console
- `feedpath` (string, required): Absolute URL of the sitemap to submit
- `siteUrl` (string, required): Search Console property

Output parameters:

- `dryRun` (boolean)
- `feedpath` (string)
- `siteUrl` (string)
- `sitemap`
- `stateRefreshError`
- `success` (boolean)

### `delete_sitemap` (~72 tokens)

Remove a submitted sitemap from a Search Console property (write; supports dryRun)

Input parameters:

- `dryRun` (boolean): If true, report what would be removed without writing to Search Console
- `feedpath` (string, required): Absolute URL of the sitemap to remove
- `siteUrl` (string, required): Search Console property

Output parameters:

- `dryRun` (boolean)
- `feedpath` (string)
- `siteUrl` (string)
- `success` (boolean)

### `inspect_url` (~54 tokens)

Inspect a URL's Google index status, canonical selection, mobile usability, and rich results

Input parameters:

- `inspectionUrl` (string, required): Fully qualified URL to inspect
- `siteUrl` (string, required): Search Console property containing the inspected URL

Output parameters:

- `indexStatus` (object)
- `inspectionUrl` (string)
- `mobileUsability`
- `richResults`
- `siteUrl` (string)

### `index_coverage` (~81 tokens)

Check how many of a sitemap's pages are indexed by Google (bounded; respects URL Inspection quota)

Input parameters:

- `concurrency` (integer): Concurrent URL Inspection requests
- `maxUrls` (integer): Maximum URLs to inspect
- `siteUrl` (string, required): Search Console property containing the sitemap URLs
- `sitemapUrl` (string, required): Fully qualified sitemap URL to inspect

Output parameters:

- `checked` (number)
- `childSitemapsSkipped` (number)
- `failed` (number)
- `indexed` (number)
- `notIndexed` (array)
- `results` (array)
- `siteUrl` (string)
- `sitemapUrl` (string)
- `totalDiscovered` (number)
- `truncated` (boolean)

### `request_recrawl` (~162 tokens)

Inspect URLs' Google index status and resubmit the covering sitemap for the ones not indexed, the supported bulk recrawl nudge (write; supports dryRun)

Input parameters:

- `concurrency` (integer): Concurrent URL Inspection requests
- `dryRun` (boolean): If true, inspect and report without resubmitting the sitemap
- `feedpath` (string): Sitemap to resubmit when unindexed URLs are found; defaults to sitemapUrl
- `maxUrls` (integer): Maximum sitemap URLs to inspect
- `siteUrl` (string, required): Search Console property containing the URLs
- `sitemapUrl` (string): Sitemap to read URLs from; also the default sitemap to resubmit
- `urls` (array): Explicit URLs to check; omit to read them from sitemapUrl

Output parameters:

- `checked` (number)
- `dryRun` (boolean)
- `failed` (number)
- `indexed` (number)
- `notIndexed` (array)
- `resubmit` (object)
- `results` (array)
- `siteUrl` (string)
- `totalDiscovered` (number)
- `truncated` (boolean)

### `indexnow_submit` (~192 tokens)

Submit changed URLs in bulk to IndexNow search engines: Bing, Yandex, Naver, Seznam, Yep; not Google. Needs an IndexNow key hosted on the site at https://<host>/<key>.txt (write; supports dryRun)

Input parameters:

- `dryRun` (boolean): If true, report what would be submitted without notifying the endpoint
- `endpoint` (string): IndexNow endpoint to notify; participating engines share submissions
- `key` (string): IndexNow key; defaults to SEO_MCP_INDEXNOW_KEY. The same key must be hosted on the site as a text file at https://<host>/<key>.txt (or at keyLocation) containing only the key
- `keyLocation` (string): URL of the hosted key file when it is not https://<host>/<key>.txt
- `urls` (array, required): Changed page URLs; one submission covers one host

Output parameters:

- `dryRun` (boolean)
- `endpoint` (string)
- `host` (string)
- `note` (string)
- `statusCode`
- `success` (boolean)
- `urlCount` (number)

### `pagespeed` (~83 tokens)

Run PageSpeed Insights for field Core Web Vitals, Lighthouse scores, and top opportunities

Input parameters:

- `apiKey` (string): Optional PageSpeed Insights API key; defaults to SEO_MCP_PAGESPEED_KEY
- `category` (array): Lighthouse categories to run
- `strategy` (string): Lighthouse device strategy
- `url` (string, required): Public page URL to analyze

Output parameters:

- `fieldData` (object)
- `opportunities` (array)
- `scores` (object)
- `strategy` (string)
- `url` (string)

### `seo_audit` (~35 tokens)

Fetch and audit a web page's on-page SEO without Google credentials

Input parameters:

- `url` (string, required): Public page URL to audit

Output parameters:

- `canonical`
- `h1` (object)
- `headingOutline` (array)
- `httpStatus` (number)
- `images` (object)
- `issues` (array)
- `lang`
- `links` (object)
- `metaDescription` (object)
- `metaRobots`
- `openGraph` (object)
- `schemaTypes` (array)
- `title` (object)
- `twitter` (object)
- `url` (string)
- `viewport`
- `wordCount` (number)

### `audit_site` (~87 tokens)

Audit the on-page SEO of up to N pages from a sitemap and roll up the most common issues across the site. Takes a sitemap URL rather than a Search Console property, and needs no Google credentials

Input parameters:

- `concurrency` (integer): Maximum page fetches in flight
- `maxPages` (integer): Maximum pages to audit
- `sitemapUrl` (string, required): Public sitemap URL to audit

Output parameters:

- `audited` (number)
- `childSitemapsFailed` (number)
- `childSitemapsSkipped` (number)
- `failed` (number)
- `pages` (array)
- `rollup` (object)
- `sitemapUrl` (string)
- `skipped` (number)
- `totalDiscovered` (number)
- `truncated` (boolean)

### `server_version` (~94 tokens)

Report which build of this server is answering, where it is running from, and whether it came out of an npx cache. Four values look like this one and are not: what npm calls latest, what the version range resolves to, what the plugin manifest declares, and what is actually running. Checking the command-line tool is not a substitute, since it is a separate process resolved separately. No credentials needed; read-only

Output parameters:

- `installPath` (string): Where this build is running from. An npx cache path is what distinguishes the release you expected from whatever npx already had
- `name` (string)
- `nodeVersion` (string)
- `npxCache` (boolean): Whether this build is being served out of an npx cache, which reuses a build without re-resolving the version range and without erroring
- `version` (string): The version of the build answering this call, which is not necessarily what npm calls latest, what the version range resolves to, or what the plugin manifest declares

### `wporg_plugin` (~98 tokens)

Look up a WordPress.org plugin's install base, downloads, ratings, and support stats by slug; public API, no credentials or API key needed

Input parameters:

- `downloadDays` (integer): Days of daily download history to fetch; 0 skips it
- `includeVersionDistribution` (boolean): Also fetch the share of active installs on each plugin version
- `slug` (string, required): WordPress.org plugin slug, e.g. akismet

Output parameters:

- `activeInstalls`
- `activeInstallsIsBucketed` (boolean)
- `added`
- `dailyDownloads`
- `downloadLink`
- `downloadSummary`
- `downloaded`
- `lastUpdated`
- `name`
- `notes` (array)
- `numRatings`
- `possiblyLagging` (boolean)
- `rating`
- `ratings`
- `requires`
- `requiresPhp`
- `slug` (string)
- `supportThreads`
- `supportThreadsResolved`
- `tags` (array)
- `tested`
- `version`
- `versionCount`
- `versionDistribution`

### `play_store_stats` (~359 tokens)

Read Google Play bulk reports for an app: active device installs and store-listing visitors and acquisitions by traffic source. installsDimension picks which installs breakdown is read (overview, country, language, device, os_version, carrier or app_version), include adds the ratings, crashes and reviews report families, and startDate with endDate reads every month the window touches instead of the single month in month. Reads the reporting bucket named by SEO_MCP_PLAY_BUCKET; read-only

Input parameters:

- `crashesDimension` (string): Dimension for the crashes report
- `endDate` (string): Window end in YYYY-MM-DD
- `include` (array): Extra report families to read. Missing files are normal: Google emits a report only when there is something to report
- `installsDimension` (string): Which installs report to read. overview is undocumented by Google but present in real buckets; the others are the documented breakdowns
- `month` (string): Report month as YYYYMM; defaults to the current UTC month. Ignored when startDate and endDate are given
- `packageName` (string, required): Android package name, e.g. app.getpsst
- `ratingsDimension` (string): Dimension for the ratings report
- `startDate` (string): Window start in YYYY-MM-DD. With endDate, reads every month the window touches and filters rows to it
- `storePerformanceDimension` (string): Which store performance breakdown to read
- `storePerformanceTotals` (boolean): Read the total_ variant instead. It is a different report, not a rollup of the same one: it carries acquisitions only, with no visitors and no conversion rate, and for some apps it covers far fewer d…

Output parameters:

- `activeDeviceInstalls`
- `crashes`
- `datesPresent` (array)
- `hasPlaySearchRows` (boolean)
- `installsDimension` (string)
- `installsLatest`
- `installsWindowTotals` (object)
- `installsZeroThroughout` (array)
- `lastDatePresent`
- `month` (string)
- `monthsRead` (array)
- `notes` (array)
- `packageName` (string)
- `ratings`
- `reviews`
- `storePerformanceDimension` (string)
- `storePerformanceTotals` (boolean)
- `trafficSources` (array)
- `window`

### `app_store_listing` (~189 tokens)

Read an App Store listing's indexed fields per locale (name, subtitle, keywords) against Apple's character limits, plus promotional text, version state and star ratings. The ratings come from the public storefront lookup because App Store Connect exposes no aggregate rating; each entry names its source. Needs SEO_MCP_ASC_KEY_PATH, SEO_MCP_ASC_KEY_ID and SEO_MCP_ASC_ISSUER_ID; read-only

Input parameters:

- `appId` (string): App Store Connect numeric app id; provide this or bundleId
- `bundleId` (string): Bundle id, resolved to an app id when appId is not given; provide this or appId
- `platform` (string): App Store platform whose version is read
- `state` (string): Read the live listing or the editable one being prepared for release
- `storefronts` (array): Storefront country codes for the public ratings lookup

Output parameters:

- `ageRating`
- `appId` (string)
- `appInfoState`
- `bundleId`
- `categories` (object)
- `fellBack` (boolean)
- `hasEditableRecord` (boolean)
- `hasLiveRecord` (boolean)
- `localeCount` (number)
- `locales` (array)
- `notes` (array)
- `overLimit` (array)
- `phasedRelease`
- `platform` (string)
- `ratings` (array)
- `requestedState` (string)
- `versionState`
- `versionString`

### `app_store_sales` (~200 tokens)

Read App Store Sales and Trends: units downloaded per day per territory per app, summarized by SKU. Needs SEO_MCP_ASC_VENDOR_NUMBER and a team key with Admin, Finance or Sales and Reports. A period with no sales is reported as an absence rather than an error. Read-only

Input parameters:

- `frequency` (string): Report period
- `includeRows` (boolean): Include every raw report row as well as the per-SKU summary
- `reportDate` (string): Report date. DAILY and WEEKLY take YYYY-MM-DD (WEEKLY means the week's ending date), MONTHLY takes YYYY-MM, YEARLY takes YYYY. Defaults to the most recent complete period for the frequency
- `reportSubType` (string): Report sub type
- `reportType` (string): Sales and Trends report type
- `version` (string): Report version, such as 1_0 or 1_3, when the default is not accepted

Output parameters:

- `apps` (array)
- `frequency` (string)
- `hasData` (boolean)
- `notes` (array)
- `reportDate` (string)
- `reportSubType` (string)
- `reportType` (string)
- `rowCount` (number)
- `rows` (array)
- `totalUnits` (number)
- `vendorNumber` (string)

### `play_vitals` (~186 tokens)

Read Android vitals from the Play Developer Reporting API: crash rate, ANR rate, error counts and startup metrics, daily or hourly, with optional breakdowns. Reports how fresh the data actually is. Carries no acquisition or conversion data; use play_store_stats for that. Read-only

Input parameters:

- `aggregationPeriod` (string): DAILY is reported in America/Los_Angeles, HOURLY in UTC
- `days` (integer): How many days back to query
- `dimensions` (array): Breakdown dimensions such as versionCode or countryCode
- `includeRows` (boolean): Include every raw row as well as the counts; off by default so a summary call stays small
- `metricSets` (array): Which Android vitals metric sets to query
- `packageName` (string, required): Android package name
- `pageSize` (integer): Rows per metric set

Output parameters:

- `aggregationPeriod` (string)
- `days` (number)
- `metricSets` (object)
- `notes` (array)
- `packageName` (string)

### `app_store_discovery` (~186 tokens)

Read the App Store surfaces beyond the listing text: search keywords, app tags, product page optimization experiments, custom product pages, in-app events, territory availability and review summarizations. A resource this key cannot read is reported as unavailable rather than as empty. Read-only

Input parameters:

- `appId` (string): App Store Connect numeric app id; provide this or bundleId
- `bundleId` (string): Bundle id; provide this or appId
- `include` (array): Which discovery surfaces to read; empty reads all of them
- `includeRows` (boolean): Include every raw row as well as the counts; off by default so a summary call stays small
- `limit` (integer): Rows per resource
- `locales` (array): Locales for per-locale resources such as searchKeywords
- `platform` (string): Platform for resources that require one

Output parameters:

- `appId` (string)
- `bundleId`
- `locales` (array)
- `notes` (array)
- `resources` (object)

### `app_store_reviews` (~155 tokens)

Read App Store customer reviews and your responses, filtered by rating or storefront. Reports the mean and star split of the reviews actually fetched, which is not the app's lifetime rating; App Store Connect exposes no aggregate rating resource. Read-only

Input parameters:

- `appId` (string): App Store Connect numeric app id; provide this or bundleId
- `bundleId` (string): Bundle id; provide this or appId
- `limit` (integer): Maximum reviews to return across pages
- `maxPages` (integer): Maximum pages to follow
- `rating` (array): Only these star ratings
- `sort` (string): Sort order; newest first by default
- `territory` (string): Only reviews from this storefront

Output parameters:

- `appId` (string)
- `bundleId`
- `filters` (object)
- `histogramOfFetched` (object)
- `meanOfFetched`
- `notes` (array)
- `pagesRead` (number)
- `returned` (number)
- `reviews` (array)
- `withoutResponse` (number)

### `crux_field_data` (~165 tokens)

Read real-user Core Web Vitals for an origin or URL from the Chrome UX Report: the current 28-day field record with p75s and histograms. Field data, not a lab test; PageSpeed's own field block is being discontinued. Needs SEO_MCP_CRUX_KEY or a PageSpeed key allowed to call the Chrome UX Report API; read-only

Input parameters:

- `formFactor` (string): Device class; omit for all form factors combined
- `metrics` (array): Metric names to request; omit for all available
- `origin` (string): Origin such as https://example.com; aggregates every page under it. Give origin or url, not both
- `url` (string): A single page URL. Give origin or url, not both

Output parameters:

- `collectionPeriod` (object)
- `formFactor`
- `hasData` (boolean)
- `metrics`
- `normalizedUrl`
- `notes` (array)
- `origin`
- `source` (string)
- `url`

### `crux_history` (~164 tokens)

Read the Chrome UX Report weekly history for an origin or URL, roughly six months of 28-day rolling windows, so a field metric can be seen trending rather than as one point. Read-only

Input parameters:

- `collectionPeriodCount` (integer): Weekly periods to return, 1 to 40. Documented history is about six months; the API decides what it actually has
- `formFactor` (string): Device class; omit for all form factors combined
- `metrics` (array): Metric names to request; omit for all available
- `origin` (string): Origin such as https://example.com; aggregates every page under it. Give origin or url, not both
- `url` (string): A single page URL. Give origin or url, not both

Output parameters:

- `collectionPeriods` (array)
- `formFactor`
- `hasData` (boolean)
- `metrics`
- `normalizedUrl`
- `notes` (array)
- `origin`
- `periodCount` (number)
- `source` (string)
- `url`

### `ads_campaigns` (~66 tokens)

Read Google Ads campaigns: status, daily budget, impressions, clicks, cost and conversions over a window. Needs GOOGLE_ADS_DEVELOPER_TOKEN, an OAuth client and a refresh token; read-only

Input parameters:

- `days` (integer): How many days back to report, ending today

Output parameters:

- `campaigns` (array)
- `days` (number)
- `notes` (array)
- `rowCount` (number)

### `ads_keywords` (~112 tokens)

Read every Google Ads keyword with its effective CPC bid, approval and serving status, and metrics. Returns every row rather than a first page, which is how a count taken from the console goes wrong; read-only

Input parameters:

- `days` (integer): How many days back to report, ending today
- `status` (string): Limit to one keyword state. Omitted, every keyword is returned with its state named, because dropping rows silently is how a count taken from this tool goes wrong the way a console count does

Output parameters:

- `days` (number)
- `keywords` (array)
- `notes` (array)
- `rowCount` (number)

### `ads_ads` (~44 tokens)

Read Google Ads ads with ad strength, policy approval status, serving status and metrics; read-only

Input parameters:

- `days` (integer): How many days back to report, ending today

Output parameters:

- `ads` (array)
- `days` (number)
- `notes` (array)
- `rowCount` (number)

### `ads_ad_copy` (~202 tokens)

Read what a Google Ads ad actually says: every headline and description with its pinning and Google's performance label, the display path, the final URLs, and the policy topics behind a limited or disapproved status rather than only the status word. Also reports headline text shared by more than one ad, since two ads in an ad group with the same headlines are not testing anything against each other. Assets such as sitelinks and promotions are not read here; read-only

Input parameters:

- `adGroup` (string): Limit to one ad group by name. Omitted, every ad in the account is read, which is what answers whether a headline is repeated across ad groups
- `adId` (string): Limit to one ad by its numeric id, for reading back the copy that was supposed to ship
- `includeRemoved` (boolean): Include removed ads. Off by default: a removed ad's copy is history, and it crowds out the ads that are serving

Output parameters:

- `ads` (array)
- `duplicateHeadlines` (array): Headline text appearing in more than one ad, with the ads carrying it. Two ads in one ad group sharing headlines test nothing against each other
- `notes` (array)
- `rowCount` (number)

### `ads_assets` (~216 tokens)

Read the sitelinks, callouts, structured snippets, promotions, prices, call and image assets attached to the account, its campaigns and its ad groups, with what each one actually says rather than only its type and id. An account-level asset applies to every campaign, so it is listed even when one campaign is named: an ad that looks bare in ads_ad_copy may be serving with these beside it. Attached is not shown, and a level that cannot be read is reported as an error in place rather than as nothing attached; read-only

Input parameters:

- `campaign` (string): Limit campaign and ad group assets to one campaign by name. Account-level assets are still listed, because they apply to every campaign including this one
- `includeRemoved` (boolean): Include links whose status is removed. Off by default: a removed asset is history and crowds out the ones that can serve
- `type` (string): Limit to one asset type. Omitted, every type is listed, including types this tool has no shaped reading for

Output parameters:

- `assets` (array)
- `byType` (object): How many of each type were found, so an absent type is visible as absent
- `levelErrors` (array): A level that could not be read is recorded here rather than omitted, so an empty list is never mistaken for nothing attached
- `notes` (array)
- `rowCount` (number)

### `ads_query` (~72 tokens)

Run an arbitrary GAQL SELECT against the Google Ads account for a question the shaped reads do not cover. GAQL has no statement other than SELECT, so this cannot change anything; read-only

Input parameters:

- `query` (string, required): A GAQL SELECT statement. GAQL has no other statement, so this cannot change anything

Output parameters:

- `query` (string)
- `rowCount` (number)
- `rows` (array)

### `ads_search_terms` (~137 tokens)

Read the queries that actually triggered an ad, with the keyword each one matched and its metrics. This is the paid equivalent of the Search Console query dimension. Google withholds terms too few people searched, so an absent term is unknown rather than absent; read-only

Input parameters:

- `days` (integer): How many days back to report, ending today
- `minCost` (number): Drop search terms that cost less than this over the window
- `minImpressions` (integer): Drop search terms below this many impressions
- `zeroConversionsOnly` (boolean): Keep only terms that converted nothing, which is the list that feeds negative keywords

Output parameters:

- `days` (number)
- `notes` (array)
- `rowCount` (number)
- `searchTerms` (array)

### `ads_negatives` (~103 tokens)

Read the negative keywords already in place, at campaign, ad group or shared-set level. A negative blocks traffic without leaving any record that it did, so this is what to check when a keyword stops serving and nothing looks wrong, and what to check before adding a term twice; read-only

Input parameters:

- `level` (string): Which negatives to read. A term blocked at campaign level is blocked everywhere in it; a shared set applies to every campaign it is attached to

Output parameters:

- `negatives` (array)
- `notes` (array)
- `rowCount` (number)

### `ads_changes` (~148 tokens)

Read the Google Ads change history: what changed, when, which fields, by whom, and whether it came from a tool or from someone in the browser. Google keeps 30 days and at most 10,000 rows, so an empty result over a longer window is a limit rather than a finding. Filter on resourceType rather than on changed field names: a budget change reports amountMicros and says neither budget nor status. This is the audit trail for anything ads_update writes; read-only

Input parameters:

- `days` (integer): How many days of change history to read, ending now. Google keeps 30 days and refuses more
- `limit` (integer): Most recent changes to return

Output parameters:

- `changes` (array)
- `days` (number)
- `notes` (array)
- `rowCount` (number)

### `ads_negatives_update` (~289 tokens)

Add or remove negative keywords in a batch, enumerated one by one with no pattern form. Before adding, every proposed negative is checked against the campaign's own live keywords and the batch is refused if one would block traffic, because a wrong negative leaves no evidence anywhere: the traffic just stops. Dry run unless dryRun is false, and the terms are read back afterwards

Input parameters:

- `action` (string, required): Add negative keywords or remove existing ones. Removal matters as much as adding: a wrong negative shows up as nothing at all
- `confirm` (boolean): Perform the batch even though a guard tripped. The dry run lists what tripped, so this confirms something already read
- `dryRun` (boolean): Report what would change, and which proposed negatives would block a live keyword, without changing anything
- `keywords` (array, required): The negative terms, enumerated one by one. There is no pattern or match-all form: a selector is one typo away from blocking a whole campaign
- `level` (string): Where the negatives live. A campaign-level negative blocks the term everywhere in that campaign
- `matchType` (string): How each term blocks. BROAD blocks any query containing all its words, which is the setting that can silently kill a campaign
- `target` (string, required): The campaign or ad group name. It must match exactly one or nothing is changed

Output parameters:

- `action` (string)
- `applied` (boolean)
- `changed` (array)
- `collisions` (array): Proposed negatives that would stop one of this campaign's own live keywords from serving. This is the error that otherwise produces no evidence at all
- `guards` (array)
- `level` (string)
- `matchType` (string)
- `notes` (array)
- `requested` (array)
- `skipped` (array): Terms not sent, because they are already present when adding or absent when removing
- `target` (string)

### `ads_update` (~337 tokens)

Change one Google Ads keyword bid, campaign daily budget, campaign status, ad status or keyword status. Pausing one keyword is its own kind because dropping a bid is not the same thing: the keyword stays eligible and goes on competing for the same budget. Spends money, so it is a dry run unless dryRun is false, it refuses a change that trips a guard unless confirm is true, and it re-reads the value after writing because an accepted request is not a stored value. Guards: more than three times the current amount, more than $25, or pausing something that is serving

Input parameters:

- `confirm` (boolean): Perform a change that trips a guard. Ignored on a dry run. The dry run lists the guard reasons, so this confirms something already read rather than something unseen
- `dryRun` (boolean): Report what would change and which guards it trips, without changing anything. On by default: this tool spends money, so performing a change has to be asked for
- `kind` (string, required): What to change: a keyword's max CPC bid, a campaign's daily budget, a campaign's status, an ad's status, or a keyword's status. Use keywordStatus to stop one keyword serving; dropping its bid is not…
- `target` (string, required): The keyword text, the campaign name, or the numeric ad id. It must match exactly one thing or the call is refused
- `value` (string, required): The new amount in dollars for a bid or budget, or pause or enable for a status

Output parameters:

- `after` (string)
- `applied` (boolean)
- `before` (string)
- `customerId` (string)
- `guards` (array)
- `kind` (string)
- `matches`: Whether the re-read value equals what was sent
- `noOp` (boolean)
- `readBack`: The value re-read from the account after the write. An accepted request is not proof of a stored value
- `target` (string)

### `ads_keyword_create` (~339 tokens)

Add one keyword to an ad group. This is the only tool here that creates rather than changes, and it is guarded differently for that reason: there is no current value to compare against, so it is a duplicate check instead. It refuses a keyword that already exists in the target ad group, including a removed one, since a removed criterion still holds the text and Google rejects the create with an error naming a resource the interface does not show. A copy elsewhere in the account trips a guard rather than refusing, because two copies compete for the same budget. EXACT by default; PHRASE and BROAD buy more than the text written and each trips a guard. Dry run unless dryRun is false, and the keyword is read back afterwards

Input parameters:

- `adGroup` (string, required): The ad group to add it to. It must match exactly one or nothing is added
- `bid` (number, required): The max CPC bid in dollars. There is no current bid to compare against on a create, so the only size check is the ceiling
- `confirm` (boolean): Add it even though a guard tripped. The dry run lists every reason, so this confirms something already read
- `dryRun` (boolean): Report what would be added and which guards it trips, without adding anything
- `keyword` (string, required): The keyword text to add. It is created as written; this tool does not guess at variants
- `matchType` (string): How the keyword matches. EXACT by default because it is the one that buys what it says; PHRASE and BROAD buy more than the text written here and each trips a guard

Output parameters:

- `adGroup` (string)
- `applied` (boolean)
- `bid` (number)
- `campaign` (string)
- `customerId` (string)
- `guards` (array): A create is guarded by a duplicate check rather than a before-and-after comparison, because there is no before
- `keyword` (string)
- `matchType` (string)
- `matches`
- `notes` (array)
- `readBack`: The keyword as the account holds it after the write. Null when it could not be read back, which is not the same as not created

### `ads_update_batch` (~238 tokens)

Change several Google Ads keyword bids, or several campaign daily budgets, in one call. It is a named list of pairs, not a rule applied to many things: each entry names one target and the value it should end at, and an entry that matches no row or more than one refuses the whole batch before anything is written. The sum is guarded as well as each entry, because separately reasonable raises are one large spend change together. Dry run unless dryRun is false, and every value is read back afterwards

Input parameters:

- `changes` (array, required): A named list of pairs, each with its own value. There is no selector form: enumeration cannot make the mistake that a pattern can
- `confirm` (boolean): Perform the batch even though a guard tripped. The dry run lists every reason, so this confirms something already read
- `dryRun` (boolean): Resolve and price every entry and report the total, without changing anything
- `kind` (string, required): One kind per call. A summed guard is only honest inside one kind: bids and budgets sum to dollars, statuses do not, and mixing them makes the total unreadable

Output parameters:

- `applied` (boolean)
- `customerId` (string)
- `entries` (array)
- `kind` (string)
- `notes` (array)
- `totalAfter` (number)
- `totalBefore`: Null when any entry has no current value to read, because a total that counts unknowns as zero is not a total
- `totalGuards` (array): Guards on the batch as a whole. Five individually reasonable raises are one large spend change, and doing them one at a time is how that gets missed
- `totalSummary` (string): The batch total in words, always present whether or not anything tripped. The sentence is what gets read; the guard is only what stops you when it is not

### `list_snapshots` (~99 tokens)

List the snapshot documents already in the snapshot directory, newest first: when each was taken, the window it covers, and how many properties, apps, packages and plugins it holds. This is what says whether there is an earlier snapshot to compare against and what to name as from and to; a file that does not parse is listed with its error rather than hidden; read-only

Input parameters:

- `limit` (integer): Maximum snapshots to return, newest first

Output parameters:

- `directory` (string)
- `snapshots` (array)
- `total` (number)
- `truncated` (boolean)

### `snapshot` (~292 tokens)

Capture four surfaces in one timestamped document: Search Console totals and top rows per property, App Store listings, Google Play installs and traffic, and WordPress.org stats. Core Web Vitals field data, Android vitals, App Store sales and App Store reviews are not captured. A surface that cannot be read is recorded as an error in place rather than omitted; list_snapshots names the documents already on disk to compare an earlier one against; read-only

Input parameters:

- `apps` (array): App Store apps, each a numeric app id or a bundle id
- `outPath` (string): File name or path inside the snapshot directory (SEO_MCP_SNAPSHOT_DIR, default ~/.config/seo-mcp/snapshots); must end in .json, or pass auto to name the file after the moment it was taken. An existin…
- `overwrite` (boolean): Replace an existing file at outPath; without it an existing file is left alone and reported
- `packages` (array): Google Play package names
- `platform` (string): App Store platform for the app surfaces
- `properties` (array): Search Console properties to capture
- `slugs` (array): WordPress.org plugin slugs
- `storefronts` (array): Storefront country codes for App Store ratings
- `windowDays` (integer): Search Console window in days, ending today

Output parameters:

- `apps` (array)
- `packages` (array)
- `properties` (array)
- `slugs` (array)
- `surfacesWithErrors` (array)
- `takenAt` (string)
- `window` (object)
- `windowDays` (number)
- `writtenTo` (string)

### `compare_snapshots` (~131 tokens)

Compare two snapshot documents and return the differences between them: clicks, impressions, positions, installs, ratings and locale counts. Reports arithmetic only, never whether a change was good or what caused it; read-only

Input parameters:

- `from` (string, required): Snapshot file name or path inside the snapshot directory; latest names the newest snapshot on disk and previous the one before it
- `minImpressions` (integer): Ignore page position moves below this many impressions on both sides
- `to` (string, required): Snapshot file name or path inside the snapshot directory; latest names the newest snapshot on disk and previous the one before it

Output parameters:

- `apps` (array)
- `argumentsReversed` (boolean)
- `elapsedHours`
- `from` (object)
- `minImpressions` (number)
- `notes` (array)
- `packages` (array)
- `properties` (array)
- `slugs` (array)
- `surfacesWithErrors` (array)
- `to` (object)

## Diagnostics

Captured diagnostic sections: Provenance, Dependencies. The full working is on the page: https://verifymcp.io/servers/ibrahimhajjaj-seo-console-mcp/seo-console-mcp#diagnostics

## Score history

- 2026-09-25: 70

## Common questions

### What is the SEO Console MCP server?

SEO Console is an MCP server listed in the public MCP registry as io.github.ibrahimhajjaj/seo-console-mcp. Search Console, Google Ads, App Store, Google Play, WordPress.org and Core Web Vitals over MCP. This page covers its npm package (seo-console-mcp).

### Is the SEO Console MCP server safe to use?

SEO Console scores 70 out of 100 on VerifyMCP. It declares no install or post-install scripts. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the SEO Console MCP server expose?

SEO Console exposes 43 tools: search_analytics, keyword_ideas, search_opportunities, compare_search_periods, ctr_gaps, and 38 more. Their descriptions and schemas cost roughly 6,403 tokens of context every time the server is loaded.

### Is the SEO Console MCP server still maintained?

SEO Console is still listed as active in the MCP registry. We last reached this channel on 25 September 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the SEO Console MCP server under?

SEO Console declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- npm package: https://www.npmjs.com/package/seo-console-mcp
- Socket report: https://socket.dev/npm/package/seo-console-mcp
- Repository: https://github.com/ibrahimhajjaj/seo-console-mcp
- Website: https://seo-console.verdelic.com/
- Changelog RSS feed: https://verifymcp.io/servers/ibrahimhajjaj-seo-console-mcp/seo-console-mcp.xml
- Changelog JSON feed: https://verifymcp.io/servers/ibrahimhajjaj-seo-console-mcp/seo-console-mcp.json
- HTML version of this page: https://verifymcp.io/servers/ibrahimhajjaj-seo-console-mcp/seo-console-mcp
