---
name: seobandwagon-mcp
description: Connect to the SEO Bandwagon remote MCP server with an API key, for Google Search Console, Google Analytics 4, rank-tracker and client data. 40 tools; 32 read data and 8 change it.
---

# SEO Bandwagon MCP Server

SEO Bandwagon exposes a remote MCP (Model Context Protocol) server with tools for
the authenticated user's Google Search Console and Google Analytics 4 data, tracked
keyword rankings, and client (lead) records with attached audit reports. It has
40 tools: 32 read data and 8 change it (gsc_submit_sitemap, gsc_delete_sitemap, clients_create, client_audits_add, client_audits_delete, client_share_link, billing_checkout, billing_portal).

- Endpoint: https://seobandwagon.com/api/mcp
- Transport: streamable HTTP (POST/GET/DELETE)
- Auth: `Authorization: Bearer sbw_<48 hex characters>` (opaque API key)

## Getting an API key

Keys are self-serve at https://seobandwagon.com/dashboard/api-keys (up to 5
per account, shown once at creation); admins can also mint one via
scripts/create-api-key.js. All tools return data scoped to the key's user.
How agents authenticate: https://seobandwagon.com/auth.md

## Connecting

Example (Claude Code):

```
claude mcp add --transport http seobandwagon https://seobandwagon.com/api/mcp \
  --header "Authorization: Bearer sbw_<your key>"
```

Unauthenticated requests receive 401.

## Tools

- `gsc_connection_status` (no args): Check the authenticated user's Google connection. Returns per-capability grant state (gsc.read, gsc.write, ga4.read, ads.read), the granted scopes, and whether the user needs to reconnect Google (the Connect button at seobandwagon.com/dashboard/search-console) to pick up newly added capabilities — sign-in itself grants identity only.
- `gsc_list_sites` (no args): List the Google Search Console properties the authenticated user has access to.
- `gsc_search_analytics` (siteUrl, dimensions?, range?, startDate?, endDate?, rowLimit?, startRow?, searchType?, dataState?, aggregationType?, filters?): Query Google Search Console search analytics for a property. Returns rows of clicks, impressions, CTR, position, and sum_position grouped by the requested dimensions, plus aggregation_type, pagination meta (has_more/next_offset, one API request per call, max 25,000 rows), and data-loss warnings — read the warnings before summing anything: query grouping under-counts, and byPage impressions must never be compared to byProperty. Use gsc_list_sites first for valid siteUrl values (e.g. 'sc-domain:example.com').
- `gsc_verify_data_availability` (siteUrl, lookbackDays?, searchType?): Google's recommended preflight before any GSC reporting: which dates in the lookback window have data, which are still provisional (fresh, will change), and the latest date whose numbers are final. Build reports only up to latest_final_date; treat provisional_dates as an upsert window.
- `gsc_accurate_totals` (siteUrl, startDate, endDate, dimensions?, aggregationType?, dataState?, searchType?): Ground-truth totals for a window: clicks, impressions, CTR, impression-weighted position, and sum_position from a pull with NO lossy dimensions. Use this as the denominator any time you sum query-grained rows — the sums will not match, and the gap is real data loss, not an error. Optional lossless breakdown dimensions: country, device, date.
- `gsc_coverage_report` (siteUrl, startDate, endDate, entityType, searchType?): Measure what a query/page-grained analysis is blind to: pulls the detail rows and the matching accurate totals, and reports coverage_pct — the share of true impressions the chosen grain can see. State this number alongside any per-query conclusions. Coverage varies by window; measure it for the exact window you analyze.
- `gsc_compare_periods` (siteUrl, period1Start, period1End, period2Start, period2End, dimensions?, filters?, aggregationType?, searchType?): Compare two date windows: aggregate deltas for clicks/impressions/CTR/weighted position, and per-key rows when dimensions are given. Rows have status 'both' (real deltas), 'new' (only in period 2), or 'lost' (only in period 1) — new/lost rows carry null metrics for the missing side and null changes; never read them as zeros. Percentage deltas vs a zero base are null.
- `gsc_top_queries` (siteUrl, range?, startDate?, endDate?, limit?, metric?, includeTrend?, searchType?): Top queries ranked by a chosen metric (clicks, impressions, ctr, position) over the full retrievable row set, then cut to limit — has_more means the ranked list was cut by your limit (raise limit or narrow the window; there is no startRow here). Optional per-query daily click trend for the top 20. Query-grained and therefore lossy: check warnings and gsc_coverage_report.
- `gsc_top_pages` (siteUrl, range?, startDate?, endDate?, limit?, metric?, includeQueryBreakdown?, searchType?): Top pages ranked by a chosen metric over the full retrievable row set (byPage impression units — never compare to byProperty totals). Optional queryBreakdown: top 10 queries for each of the top 10 pages. Page rows are lossless; the per-page query lists are lossy and cut to 10 by Google.
- `gsc_query_page_pairs` (siteUrl, startDate, endDate, searchType?): Query→page attribution rows (the only way to get it) with byPage aggregation. The single most expensive request the GSC API offers — load scales with window length; keep windows short and avoid repeat pulls. Lossy at this grain: check warnings and gsc_coverage_report with entityType query_page.
- `gsc_search_appearance` (siteUrl, startDate, endDate, dimensions?, searchType?): Rich-result (search appearance) performance: enumerates the property's appearance types, then optionally breaks each down by your dimensions via Google's documented two-step. Everything here is byPage-forced — do not compare these numbers to byProperty results.
- `gsc_inspect_url` (siteUrl, inspectionUrl): Inspect one URL's Google index status: verdict, coverage state, robots.txt/indexing state, last crawl, canonical pair, mobile usability, and detected rich results. Unknown enum values pass through verbatim — 'UNKNOWN' means the API omitted the field, never a fabricated verdict.
- `gsc_bulk_inspect` (siteUrl, urls): Inspect up to 10 URLs in one call (per-URL results plus a success/failed summary). For larger sets, call repeatedly — the cap keeps one call inside the serverless time budget; URL Inspection also has a daily quota, so inspect samples, not whole sitemaps.
- `gsc_list_sitemaps` (siteUrl): List submitted sitemaps for a property with pending/index-file flags, last download time, file errors/warnings, and per-content-type submitted/indexed counts as reported by the Sitemaps API.
- `gsc_sitemap_indexation_summary` (siteUrl, sitemapUrl?): Submitted vs indexed URL counts across sitemaps, as reported by the Sitemaps API — NOT the Index Coverage report (the public API does not expose it). Counts are only as fresh as each sitemap's last_downloaded; never-fetched sitemaps, stale fetches, and index-file double counting are all called out in warnings.
- `gsc_submit_sitemap` (siteUrl, sitemapUrl): Submit (or resubmit) a sitemap to Google Search Console. Requires the Search Console manage grant (gsc.write), which the Connect flow does not currently request (write access is parked pending Google verification) — only connections made while it was offered can use this tool. Throws with the reason on failure; success is only reported when Google accepted the submission.
- `gsc_delete_sitemap` (siteUrl, sitemapUrl): Remove a sitemap from Google Search Console (does not delete the file from your server, only the submission). Requires gsc.write, which the Connect flow does not currently request (parked pending Google verification). Throws with the reason on failure.
- `gsc_analyze_opportunities` (siteUrl, analysisType?, focus?): Scan for SEO opportunities: low-CTR queries worth better titles/snippets, striking-distance keywords (positions 4-10), and indexation-coverage gaps. Returns prioritized opportunities, quick wins, and an impact estimate — with warnings whenever an underlying pull was truncated or grain-lossy, so counts are labeled floors when they are floors.
- `gsc_content_gaps` (siteUrl, startDate, endDate, minImpressions?): Cluster queries where the site ranks poorly but gets impressions — candidate topics for new or expanded content, with per-cluster impressions and weighted position. Query×page grained (lossy; warnings included).
- `gsc_cannibalization_check` (siteUrl, startDate, endDate, minPages?): Find queries where multiple pages of the site compete: per-query page lists with click/impression splits and a consolidation recommendation. Query×page grained (lossy; warnings included).
- `rank_tracker_keywords` (domain?): List the authenticated user's tracked keywords from the SEO Bandwagon rank tracker, with last known Google position, search volume, and GSC metrics. Optionally filter by domain.
- `rank_tracker_history` (domain, keyword?, limit?): Get historical Google position checks (serp_history) for a domain the authenticated user tracks, optionally filtered to one keyword. Note: positions are only recorded when a rank check runs.
- `clients_list` (status?): List clients (leads) visible to the authenticated user. Admins see the full roster; other users see only the client record linked to their account. Returns id, name, contact email, status, and notes — use the id with the client_audits_* tools.
- `clients_create` (name, contactEmail?, gscSiteUrl?, notes?, status?): Create a client (lead) record. Admin-only. Provide a name; contact email, GSC site URL, notes, and status (e.g. 'lead', 'active') are optional. Returns the created client including its id.
- `client_audits_add` (clientId, domain, title, kind?, contentMd, pdfBase64?, pdfFilename?, displayOrder?): Attach an audit report (markdown) to a client. Admin-only. Provide clientId (from clients_list/clients_create), the audited domain, a title, a kind tag (e.g. local-seo, gbp-listings, geo, summary), and the full markdown body. Clients see exactly ONE report: their newest kind='client-report' (or, if they have none, their top report). Every other report is working history, visible to admins only. A client-report must use the H2 sections Executive Summary → The Opportunity in Search → How We Capture It → Technical Fixes, in that order, or it is rejected; attach a new one to supersede the old. Optionally pass pdfBase64 (plus pdfFilename) to attach a branded PDF, offered as a download beside the HTML. Pass displayOrder to pin a report in the admin list (lowest first, 1 for the audit); omit it to leave the report in newest-first order.
- `client_audits_list` (clientId, includeContent?): List audit reports attached to a client (id, domain, title, kind, created date; bodies omitted unless includeContent is true). Admins can read any client; other users only their own.
- `client_audits_delete` (clientId, auditId): Permanently delete ONE audit report attached to a client — the uploads made by client_audits_add, nothing else. Admin-only. Requires both clientId and auditId, and the audit must belong to that client, so a report can only be removed through the client that owns it. Use it to clear a superseded report after uploading its replacement (client_audits_add appends rather than replaces, so the old row otherwise stays on the share page). Deletes the client_audits row and its stored PDF; it never touches the client record or any other table, and it cannot be undone.
- `client_share_link` (clientId, rotate?): Get the share URL for a client's gated audit page (seobandwagon.com/audit/<slug>, e.g. /audit/moli-windows). The page shows a teaser and requires Google sign-in; the first account to sign in claims the client record (sets its linked user + contact email). Admin-only. Generates the slug (and a legacy token) on first call; pass rotate=true to rotate the legacy token — the slug URL is unaffected.
- `account_plan` (no args): The authenticated account's plan and what it allows: the plan in force now (a paid plan past its grace date counts as free), grace_until while a failed payment is being retried, tracked keywords against the plan's keyword limit (with how many are over it, which rank checks skip), scans per day, and Pro's price read live from Stripe (price_source says when it is the site's price list instead), with how to buy it. Reads only; changes nothing.
- `billing_checkout` (plan?, idempotency_key?): Start buying Pro for the authenticated account. Returns a Stripe Checkout url for a person to open and pay; nothing is charged until they do, and the plan changes once Stripe confirms the payment. Needs an API key with the billing scope as well as mcp. Returns url, session_id, amount_total (in cents), currency, expires_at and reused (true when an open checkout started earlier is handed back instead of a second one). Refusals, as JSON with error, message, fix and docs_url: already_subscribed and payment_past_due (use billing_portal), not_self_serve for Agentic Local, Enterprise and the services, which are sold by talking to us, and insufficient_scope. Pass idempotency_key to make a retry safe: the same key with a different plan is idempotency_conflict.
- `billing_portal` (no args): Open Stripe's billing portal for the authenticated account and return its url, where a person can change the card, see invoices, or cancel; a cancelled plan runs to the end of the month already paid. Needs an API key with the billing scope as well as mcp. Refuses with no_billing_account when the account has never started a checkout.
- `ga4_list_properties` (no args): List the GA4 accounts and properties this Google account can access, with numeric property ids. Property ids ('123456789') are what every other ga4_* tool takes — never the G-XXXXXXX measurement id.
- `ga4_property_details` (propertyId): One property's settings (timezone, currency), its data streams (with G-XXXXXXX measurement ids and site URLs), and its configured key events — the metric behind keyEvents in reports.
- `ga4_run_report` (propertyId, startDate, endDate, dimensions, metrics, dimensionFilter?, metricFilter?, orderBys?, limit?, offset?): Run a GA4 Data API report: any dimensions/metrics (discover them with ga4_metadata), optional FilterExpression filters and orderBys, limit/offset paging. Responses ALWAYS disclose data-quality caveats as warnings — sampling (with the sampled %), '(other)'-row collapse, privacy thresholding, truncation — plus API-computed totals and property quota state. Read the warnings before trusting differences between numbers.
- `ga4_realtime_report` (propertyId, dimensions?, metrics?, limit?): Active users right now, by screen/page and other realtime dimensions. Totals come from the API aggregation — summing dimension rows double-counts users appearing in multiple rows.
- `ga4_landing_pages` (propertyId, startDate, endDate, limit?, offset?): Landing-page performance bundle: sessions, users, new users, bounce rate, engagement rate, session duration, and key events per landingPage — the GA4 counterpart of gsc_top_pages, with loss-disclosure warnings.
- `ga4_organic_search_performance` (propertyId, startDate, endDate, limit?, offset?): Google-organic landing-page performance: sessions/users/keyEvents per landingPage filtered to sessionSource=google AND sessionMedium=organic — the GA4 slice whose traffic scope corresponds to Search Console, and the input to GSC↔GA4 cross-verification.
- `ga4_metadata` (propertyId, customOnly?, checkDimensions?, checkMetrics?): Discover the dimensions and metrics (including custom definitions) a property supports, or — with checkDimensions/checkMetrics — pre-flight a combination for compatibility before running an expensive report.
- `gsc_ga4_page_join` (siteUrl, propertyId, startDate, endDate, limit?, ignoreQuery?): Cross-verify Search Console against GA4 per page: GSC clicks/impressions/position joined to GA4 google-organic sessions/users/keyEvents on normalized landing-page paths, with unjoinable rows in gsc_only/ga4_only buckets (never silently dropped), BOTH sources' loss warnings labeled by source, and scope_notes explaining why the two never match exactly (clicks≠sessions, timezone boundaries, anonymized queries vs untagged visitors). Use it to validate that sampled/thresholded GA4 numbers are consistent with GSC ground truth.
- `pricing_list` (no args): List SEO Bandwagon's public prices: every plan, retainer and service with its price in US dollars, what it includes, who it is not for, and how it is bought (sign-up, card checkout or talking to us), plus the terms (month-to-month, cancel anytime). The same list as seobandwagon.com/pricing.json. Reads no user data and changes nothing.

## More

- Documentation: https://seobandwagon.com/docs/api
- API catalog: https://seobandwagon.com/.well-known/api-catalog
- Server card: https://seobandwagon.com/.well-known/mcp/server-card.json
