Google Marketing MCP
Integrates with the Google Ads API, providing tools for account monitoring, GAQL queries and performance reports, keyword research via Keyword Planner, campaign/ad group/ad/keyword creation, bid and budget management, recommendations, and search terms reports.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Google Marketing MCP show GA4 sessions for last 7 days by country"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Google Marketing MCP
A full-capability Model Context Protocol server that unifies Google Ads + Tag Manager + Search Console + Analytics (GA4) behind one process — reporting, monitoring, keyword research, campaign/tag/property creation, and live mutations, all behind a dry-run safety gate. Built by merging three already-proven standalone servers (Ads, GTM, GSC) plus a new GA4 module, same skeleton throughout.
Plain MCP over stdio — no Claude-specific behavior. Works with any MCP-compatible client: Claude Code, Claude Desktop, OpenAI Codex CLI, and Google Gemini CLI (wiring for all four below).
Built to run anywhere.
launch.pyauto-detects Python, builds its own virtualenv, installs dependencies, and self-heals if the repo is moved or copied to another machine. You do not pre-install anything except Python.
1. Quick start (any OS, any machine)
You need only Python ≥ 3.10 on PATH.
python launch.py --setup # builds .venv + installs everything
python launch.py --check # per-domain tool counts + credential statusIf .venv is missing, broken, or was built on a different computer,
launch.py rebuilds it automatically on the next run.
Related MCP server: Marketing MCP
2. Tools — ~110, four domain-prefixed groups
Every tool name is prefixed so the four APIs never collide (ads_list_accounts
and gtm_list_accounts both exist — the prefix is what disambiguates them).
Prefix | Domain | Tools |
| Google Ads (22) | Accounts, |
| Tag Manager (29) | Accounts/containers/workspaces, generic entity CRUD ( |
| Search Console (~20) |
|
| Analytics / GA4 (25) | Accounts/properties (create/update), data streams, conversion (Key) events, custom dimensions/metrics, audiences, GA4↔Ads links, Data API reporting + realtime |
Full per-tool docstrings are visible to any connected MCP client — this table is the map, not the manual.
3. Credentials — fill in only what you need
Each domain is independent. Leave a whole block blank in .env and that
domain's tools still register (so --check shows the full count) but error
with an actionable message when actually called.
Recommended: one unified token for all four domains
Google Cloud project → enable the Google Ads API, Tag Manager API, Search Console API, and Analytics Admin + Data API.
Cloud Console → Credentials → OAuth client ID → type Desktop app.
python launch.py --setupthen run:.venv/Scripts/python -m google_marketing_mcp.auth_unified # Windows .venv/bin/python -m google_marketing_mcp.auth_unified # macOS/LinuxOne browser consent covering every scope this project uses → prints
GOOGLE_OAUTH_CLIENT_ID/GOOGLE_OAUTH_CLIENT_SECRET/GOOGLE_OAUTH_REFRESH_TOKEN— paste all three into.env. Every domain's client (ads,gtm,gsc,ga4) falls back to these automatically when its own domain-specific vars are unset.Still add
GOOGLE_ADS_DEVELOPER_TOKENseparately (Ads UI → Tools & Settings → API Center) — OAuth doesn't cover it, nothing does.This token only reaches products/accounts where that Google account is already a user. Add it under GTM Account User Management, Search Console Users and permissions, and GA4 Property Access Management, same as any human editor would need — this script gets you a token, not access.
This is the fastest path for a single owner/operator. For a multi-person
agency, or headless automation where a personal login is undesirable, use
the per-domain service-account setup below for GTM/GSC/GA4 instead (Google
Ads has no service-account path — it always needs a token from either
auth_unified above or ads/auth.py below).
Google Ads (ads_*)
Google Cloud project → enable the Google Ads API.
Google Ads UI → Tools & Settings → API Center → apply for a developer token (starts test-access; apply for Basic for production data).
Cloud Console → Credentials → OAuth client ID → type Desktop app.
python launch.py --setupthen.venv/Scripts/python -m google_marketing_mcp.ads.auth(Windows;.venv/bin/pythonelsewhere) → browser consent → copy the printedGOOGLE_ADS_REFRESH_TOKEN.Fill the
GOOGLE_ADS_*block in.env. SetGOOGLE_ADS_LOGIN_CUSTOMER_IDto your MCC id if you reach client accounts through a manager.
Tag Manager / Search Console / GA4 — service-account alternative
Skip this if you used the unified token above. Otherwise these three share
one service account (GOOGLE_APPLICATION_CREDENTIALS in .env) —
create it once, add its email under each product's own permissions page:
Cloud Console → IAM & Admin → Service Accounts → create one → download the JSON key. Point
GOOGLE_APPLICATION_CREDENTIALSat it.GTM: add the service account email under Admin → Account User Management on each account, with Edit (and Publish if you'll publish).
Search Console: add it under Settings → Users and permissions on each property — Full, not Restricted, or URL Inspection is blocked.
GA4: add it under Admin → Property Access Management on each property — Editor, not Viewer, or every
ga4_*write 403s. (This exact mistake — Viewer-only, write rejected — is why this project has a dedicated error message for it; seega4/client.py.)
Verify everything
python launch.py --checkPrints tool count per domain and OK / MISSING <vars> per domain — not
all-or-nothing.
4. Connect a client
launch.py is the universal entry point — every client below points at the
same file with any system Python.
Claude Code
claude mcp add google-marketing -- python /ABSOLUTE/PATH/TO/google-marketing-mcp/launch.pyClaude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"google-marketing": {
"command": "python",
"args": ["C:\\ABSOLUTE\\PATH\\TO\\google-marketing-mcp\\launch.py"]
}
}
}(Credentials come from .env in the repo; omit env here unless you want
to pass them inline instead.)
OpenAI Codex CLI
Codex reads MCP servers from ~/.codex/config.toml (or .codex/config.toml
in the project):
[mcp_servers.google-marketing]
command = "python"
args = ["/ABSOLUTE/PATH/TO/google-marketing-mcp/launch.py"]Or via the CLI: codex mcp add google-marketing -- python /ABSOLUTE/PATH/TO/google-marketing-mcp/launch.py
(flag name may differ by Codex CLI version — check codex mcp --help).
Google Gemini CLI
Gemini reads MCP servers from ~/.gemini/settings.json (or
.gemini/settings.json in the project) under mcpServers, same shape as
Claude Desktop:
{
"mcpServers": {
"google-marketing": {
"command": "python",
"args": ["/ABSOLUTE/PATH/TO/google-marketing-mcp/launch.py"]
}
}
}Or via the CLI: gemini mcp add google-marketing python /ABSOLUTE/PATH/TO/google-marketing-mcp/launch.py.
On Windows use the full path with python.exe; on macOS/Linux use python3
if python isn't aliased.
5. Safety model
Dry-run by default, per domain. Every mutation runs as a validate/preview call unless the tool is called with
confirm=true.Read-only kill switch, per domain:
{ADS,GTM,GSC,GA4}_READ_ONLY=trueblocks all mutations in that domain.Publish is gated separately in GTM (
GTM_ALLOW_PUBLISH) — it pushes a container to every live visitor instantly, with no staging step.GA4 deletes/archives always require
confirm=trueexplicitly, on top of the dry-run gate — there's no preview for an archive and GA4 has no undo for one.PAUSED by default. New Ads campaigns and ads are created PAUSED.
Every mutation response includes a
notestating what happened (DRY-RUN vs APPLIED) and why.
6. Error handling
Google Ads: the official
google-adsgRPC SDK retries transient UNAVAILABLE/DEADLINE_EXCEEDED errors internally. Application errors (GoogleAdsException) are flattened into a readablecode: message @ fieldstring — never a raw proto dump.GTM / GSC / GA4 (plain REST): every call routes through a shared retry helper (
_retry.py) that retries network exceptions and HTTP 429/5xx with exponential backoff + jitter (honoringRetry-Afteron 429) before surfacing an error. Non-transient errors (400/401/403/404/409) are never retried — each domain'sclient.pyhas a_explain()that turns the raw status into the specific, actionable fix (which permission to add, which id format is expected, which page to check), not a generic HTTP message.
7. Layout
google-marketing-mcp/
├── launch.py # universal self-bootstrapping entry point
├── pyproject.toml
├── .env.example # all 4 domains' vars, prefixed
└── google_marketing_mcp/
├── __main__.py # `--check`: per-domain tool count + creds
├── app.py # ONE shared MCP instance, combined instructions
├── server.py # imports app + all 4 domains' tools
├── _retry.py # shared backoff for the REST domains
├── ads/ config.py client.py helpers.py tools/*.py (Google Ads SDK)
├── gtm/ config.py client.py helpers.py tools/*.py (REST)
├── gsc/ config.py client.py helpers.py tools/*.py (REST)
└── ga4/ config.py client.py helpers.py tools/*.py (REST)Each domain keeps its own config.py/client.py/safety flags — merging
only shares the MCP instance and the assembly point, not the auth or error
logic, so a credential problem in one domain never affects another.
8. Troubleshooting
Symptom | Fix |
|
|
| Install Python 3.10+ and ensure it's on PATH. |
| Fill that domain's block in |
Mutation "did nothing" | Dry-run gate — re-call with |
GA4 write 403s | Service account has Viewer, not Editor, on that property — Admin → Property Access Management. |
GTM 409 Conflict | Workspace is behind the live container — call |
GTM publish refused |
|
9. Related
This project also ships a Google Ads optimization skill (account
structure, bidding-strategy selection, conversion-tracking audit checklist,
scaling rules) — see .claude/skills/ in the parent workspace. The skill
and this server are independent; the skill's playbook cross-references this
server's tool names.
Built by Konko Maji (@konkomaji).
Available Tools
94 toolsads_add_keywordsA
Add positive keywords to an ad group.
Args: customer_id: 10-digit account id. ad_group_id: target ad group id. keywords: keyword text list. match_type: EXACT | PHRASE | BROAD (default PHRASE). cpc_bid: optional per-keyword max CPC (currency). confirm: must be true to apply (else dry-run).
Inputs: confirm, cpc_bid, keywords, match_type, +2 more. Returns: result
ads_add_negative_keywordsA
Add negative keywords to an ad group (block wasteful queries).
Args: customer_id: 10-digit account id. ad_group_id: target ad group id. keywords: negative keyword text list. match_type: EXACT | PHRASE | BROAD. confirm: must be true to apply (else dry-run).
Inputs: confirm, keywords, match_type, ad_group_id, +1 more. Returns: result
ads_apply_recommendationA
Apply a specific Google Ads Recommendation by its resource_name (from list_recommendations). This can change bids, budgets, keywords, or ads.
Args: customer_id: 10-digit account id. resource_name: recommendation resource name to apply. confirm: must be true to apply (else dry-run; recommendations support validate_only).
Inputs: confirm, customer_id, resource_name. Returns: result
ads_campaign_performanceA
Campaign performance report: impressions, clicks, CTR, cost, conversions, CPA, ROAS, conversion value. Sorted by cost descending.
Args: customer_id: 10-digit account id. date_range: a GAQL date constant (e.g. LAST_7_DAYS, LAST_30_DAYS, THIS_MONTH). limit: max campaigns. only_active: restrict to ENABLED campaigns.
Inputs: limit, date_range, customer_id, only_active. Returns: result
ads_create_ad_groupA
Create an ad group inside a campaign.
Args: customer_id: 10-digit account id. campaign_id: parent campaign id. name: ad group name. default_cpc_bid: optional default max CPC (currency) for manual bidding. status: ENABLED | PAUSED. confirm: must be true to apply (else dry-run).
Inputs: name, status, confirm, campaign_id, +2 more. Returns: result
ads_create_campaignA
Create a new campaign with its own daily budget. Created PAUSED by default so it never spends until you explicitly enable it.
Args: customer_id: 10-digit account id. name: campaign name (must be unique). daily_budget: daily budget in account currency (e.g. 25.00). channel_type: SEARCH | DISPLAY | SHOPPING | VIDEO | PERFORMANCE_MAX. NOTE: Google blocks VIDEO campaign creation via the API (MUTATE_NOT_ALLOWED, trigger=VIDEO) — the campaign shell must be made in the Google Ads UI. The video tools (create_video_ad_group / create_video_ad) still work to populate an existing video campaign. channel_sub_type: optional VIDEO sub type. Leave None for a plain video views / TrueView (skippable in-stream, CPV) campaign. Set VIDEO_ACTION for conversion video, VIDEO_NON_SKIPPABLE for CPM reach, VIDEO_SEQUENCE, or VIDEO_REACH_TARGET_FREQUENCY. bidding_strategy: MAXIMIZE_CONVERSIONS | MAXIMIZE_CONVERSION_VALUE | TARGET_SPEND | MANUAL_CPC | TARGET_CPM | TARGET_CPV. Skippable in-stream CPV = TARGET_CPV (per-view bid set on the ad group via create_video_ad_group; manual CPV is retired by Google). target_cpa: optional target CPA (currency) for MAXIMIZE_CONVERSIONS. target_roas: optional target ROAS (e.g. 4.0 = 400%) for value strategies. status: ENABLED | PAUSED (default PAUSED). start_paused: force PAUSED regardless of status (safety, default True). confirm: must be true to actually apply (else dry-run validate only).
Inputs: name, status, confirm, target_cpa, +7 more. Returns: result
ads_create_responsive_search_adA
Create a Responsive Search Ad (RSA) in an ad group.
Google policy/limits:
3 to 15 headlines (<=30 chars each). Minimum 3 required.
2 to 4 descriptions (<=90 chars each). Minimum 2 required.
final_url must be a valid landing page URL. Created PAUSED by default.
Args: customer_id: 10-digit account id. ad_group_id: target ad group id. final_url: landing page URL (https://...). headlines: 3-15 headline strings (<=30 chars). descriptions: 2-4 description strings (<=90 chars). path1: optional display URL path segment (<=15 chars). path2: optional second display URL path segment (<=15 chars). status: ENABLED | PAUSED (default PAUSED). confirm: must be true to apply (else dry-run).
Inputs: path1, path2, status, confirm, +5 more. Returns: result
ads_create_video_adA
Create a video ad in a video ad group, linking an existing YouTube video asset.
The YouTube video must already exist as an asset in the account (type YOUTUBE_VIDEO). Find its id via a GAQL asset query. The video_format must match the ad group type: IN_STREAM = skippable TrueView (VIDEO_TRUE_VIEW_IN_STREAM), BUMPER = 6s bumper, NON_SKIPPABLE = non-skippable in-stream. Created PAUSED.
Args: customer_id: 10-digit account id. ad_group_id: target video ad group id. video_asset_id: numeric id of an existing YOUTUBE_VIDEO asset. final_url: landing page URL (https://...). video_format: IN_STREAM (default) | BUMPER | NON_SKIPPABLE | OUT_STREAM. name: optional ad name. action_headline: optional CTA headline overlay (in_stream only). action_button_label: optional CTA button label (in_stream only). status: ENABLED | PAUSED (default PAUSED). confirm: must be true to apply (else dry-run).
Inputs: name, status, confirm, final_url, +6 more. Returns: result
ads_create_video_ad_groupA
Create a video ad group inside a VIDEO campaign.
Use this (not create_ad_group) for video campaigns. For skippable in-stream TrueView with CPV bidding, keep the default ad_group_type and pass cpv_bid (the max cost-per-view, e.g. 1.00). The parent campaign must be channel_type VIDEO with MANUAL_CPV bidding.
Args: customer_id: 10-digit account id. campaign_id: parent VIDEO campaign id. name: ad group name. cpv_bid: max cost-per-view in account currency (skippable in-stream / CPV). ad_group_type: VIDEO_TRUE_VIEW_IN_STREAM (default, skippable in-stream) | VIDEO_BUMPER | VIDEO_NON_SKIPPABLE_IN_STREAM | VIDEO_TRUE_VIEW_IN_DISPLAY | VIDEO_RESPONSIVE | VIDEO_EFFICIENT_REACH. status: ENABLED | PAUSED. confirm: must be true to apply (else dry-run).
Inputs: name, status, confirm, cpv_bid, +3 more. Returns: result
ads_get_account_detailsA
Get details for a single account: name, currency, time zone, status, auto-tagging, conversion tracking, and whether it is a manager account.
Args: customer_id: 10-digit account id (dashes ok). Defaults to env default.
Inputs: customer_id. Returns: result
ads_historical_search_volumeA
Get historical monthly search volume + competition for a specific list of keywords (no idea expansion). Use to size demand for exact terms you already have.
Args: customer_id: 10-digit account id. keywords: exact keyword list to look up. location_ids: geo target ids (e.g. [2840] = US). language_id: language constant id (1000 = English).
Inputs: keywords, customer_id, language_id, location_ids. Returns: result
ads_keyword_ideasA
Keyword research: generate keyword ideas with avg monthly searches, competition level, and top-of-page bid range (low/high). Seed from keywords and/or a landing page URL. This is the Keyword Planner research tool.
Args: customer_id: 10-digit account id (any account with Planner access works). seed_keywords: list of seed terms, e.g. ["running shoes","trail shoes"]. page_url: optional landing page URL to seed ideas from. location_ids: Google geo target ids (e.g. [2840] = United States, [2826] = United Kingdom, [2356] = India). Omit for no geo constraint. language_id: language constant id (1000 = English, 1019 = Spanish, 1010 = French, 1001 = German). limit: max ideas to return. include_adult: include adult keywords (default False).
Inputs: limit, page_url, customer_id, language_id, +3 more. Returns: result
ads_keyword_performanceA
Keyword-level performance: text, match type, quality score, clicks, cost, conversions, CPA. Useful to find wasteful or high-performing keywords.
Args: customer_id: 10-digit account id. date_range: GAQL date constant. campaign_id: optional filter to one campaign. limit: max keywords.
Inputs: limit, date_range, campaign_id, customer_id. Returns: result
ads_list_accountsA
List Google Ads accounts (customer ids + names) available to use.
When GOOGLE_ADS_LOGIN_CUSTOMER_ID is set to a manager (MCC) account, this enumerates the whole tree under that manager -- which is what you want for an agency setup. Otherwise it falls back to the directly accessible customers.
Start here to discover the 10-digit customer_id for other tools.
Inputs: no named parameters. Returns: result
ads_list_recommendationsA
List Google's optimization Recommendations for the account (the same suggestions shown in the Ads UI 'Recommendations' tab): add keywords, fix budgets, improve bids, ad strength, etc. Each has a resource_name + type you can pass to apply_recommendation.
Args: customer_id: 10-digit account id. limit: max recommendations.
Inputs: limit, customer_id. Returns: result
ads_monitor_accountA
Health/status monitor. Surfaces issues that need attention:
Disapproved or limited ads (policy problems)
Budget-limited campaigns (losing impressions to budget)
Paused campaigns that recently spent
Account optimization score Run this for a quick 'what's wrong / what to fix' overview.
Args: customer_id: 10-digit account id.
Inputs: customer_id. Returns: result
ads_run_gaqlA
Run an arbitrary GAQL (Google Ads Query Language) query and return rows.
This is the most flexible read tool. Use it for any custom report. Example query: SELECT campaign.id, campaign.name, metrics.clicks, metrics.cost_micros FROM campaign WHERE segments.date DURING LAST_7_DAYS ORDER BY metrics.clicks DESC
Args: customer_id: 10-digit account id (dashes ok). query: full GAQL query string. limit: max rows to return (client-side cap; default 200).
Inputs: limit, query, customer_id. Returns: result
ads_search_terms_reportA
Search-terms report: the actual queries that triggered ads, with cost and conversions. Use to mine new keywords (high converters) and negatives (spend, zero conversions).
Args: customer_id: 10-digit account id. date_range: GAQL date constant (e.g. LAST_30_DAYS). campaign_id: optional campaign filter. limit: max rows.
Inputs: limit, date_range, campaign_id, customer_id. Returns: result
ads_set_ad_group_bidA
Set an ad group's default max CPC bid (manual bidding only). Money change.
Args: customer_id: 10-digit account id. ad_group_id: ad group id. cpc_bid: new max CPC in account currency (e.g. 1.50). confirm: must be true to apply (else dry-run).
Inputs: confirm, cpc_bid, ad_group_id, customer_id. Returns: result
ads_set_ad_group_statusB
Enable, pause, or remove an ad group.
Args: customer_id: 10-digit account id. ad_group_id: ad group id. status: ENABLED | PAUSED | REMOVED. confirm: must be true to apply (else dry-run).
Inputs: status, confirm, ad_group_id, customer_id. Returns: result
ads_set_ad_statusA
Enable, pause, or remove a specific ad.
Args: customer_id: 10-digit account id. ad_group_id: ad group id the ad belongs to. ad_id: ad id. status: ENABLED | PAUSED | REMOVED. confirm: must be true to apply (else dry-run).
Inputs: ad_id, status, confirm, ad_group_id, +1 more. Returns: result
ads_set_campaign_bidding_strategyA
Change an existing campaign's standard (non-portfolio) bidding strategy.
Sets the new strategy oneof on the campaign and updates only that field. Switching strategy resets the previous one. Bid-affecting change.
Args: customer_id: 10-digit account id. campaign_id: campaign id. bidding_strategy: MAXIMIZE_CONVERSIONS | MAXIMIZE_CONVERSION_VALUE | TARGET_SPEND | MANUAL_CPC. target_cpa: optional target CPA (currency) for MAXIMIZE_CONVERSIONS. target_roas: optional target ROAS (e.g. 4.0 = 400%) for value strategy. cpc_ceiling: max CPC ceiling (currency) for TARGET_SPEND/Maximize Clicks. Required for TARGET_SPEND (Google rejects a zero/unset ceiling). confirm: must be true to apply (else dry-run validate only).
Inputs: confirm, target_cpa, campaign_id, cpc_ceiling, +3 more. Returns: result
ads_set_campaign_statusA
Enable, pause, or remove a campaign. Core status-monitoring action.
Args: customer_id: 10-digit account id. campaign_id: campaign id. status: ENABLED | PAUSED | REMOVED. confirm: must be true to apply (else dry-run).
Inputs: status, confirm, campaign_id, customer_id. Returns: result
ads_update_campaign_budgetA
Change a campaign's daily budget (in account currency). Looks up the campaign's budget resource and updates the amount. Money-affecting change.
Args: customer_id: 10-digit account id. campaign_id: campaign id. new_daily_budget: new daily budget (e.g. 40.00). confirm: must be true to apply (else dry-run).
Inputs: confirm, campaign_id, customer_id, new_daily_budget. Returns: result
ga4_archive_custom_dimensionA
Archive a custom dimension. GA4 has no delete for these -- archive is permanent and the parameter name cannot be reused. Always needs confirm=true.
Inputs: dimension_id (required), confirm, property_id. Returns: result
ga4_create_audienceA
Create a simple event-based audience (e.g. 'viewed a property page, no generate_lead'). For anything beyond a single event condition, build it in the GA4 UI -- the filterClauses shape for sequences and multi-step funnels is deep enough that hand-building the request body here would be more error-prone than the UI's audience builder.
Args: membership_duration_days: how long a user stays in the audience once they qualify, 1-540. RLSA on Search needs 1,000+ members before it can serve. event_name_filter: if set, membership = fired this event at least once. Leave unset and build the audience by hand in the UI for anything more complex. confirm: must be true to actually create.
Inputs: description (required), display_name (required), confirm, property_id, +2 more. Returns: result
ga4_create_conversion_eventA
Mark an event as a conversion (Key event) so it feeds Ads bidding and the Conversions column, once GA4↔Ads is linked (see ga4_create_google_ads_link).
Args: event_name: the exact dataLayer/gtag event name, e.g. generate_lead, purchase, phone_call_click. The event must already be firing at least once, or GA4 rejects it. confirm: must be true to actually create.
Inputs: event_name (required), confirm, property_id. Returns: result
ga4_create_custom_dimensionA
Register a custom dimension so an event parameter becomes reportable.
Check first whether GA4's predefined dimensions already cover it (Link URL / Link text / Link classes / Link ID, Page path, etc.) -- a custom copy of an already-predefined parameter just duplicates it.
Args: parameter_name: the event/user parameter key, e.g. 'value', 'listing_id'. scope: EVENT | USER | ITEM. confirm: must be true to actually create.
Inputs: display_name (required), parameter_name (required), scope, confirm, +2 more. Returns: result
ga4_create_custom_metricB
Register a custom metric (a numeric event parameter, e.g. a lead value).
Inputs: display_name (required), parameter_name (required), confirm, description, +2 more. Returns: result
ga4_create_data_streamA
Create a data stream. For WEB_DATA_STREAM this mints the measurement id (G-XXXXXXX) a GTM Google tag or site gtag.js points at.
Args: display_name: stream name shown in the GA4 UI. default_uri: the site's root URL, required for WEB_DATA_STREAM. stream_type: WEB_DATA_STREAM | ANDROID_APP_DATA_STREAM | IOS_APP_DATA_STREAM. confirm: must be true to actually create.
Inputs: display_name (required), confirm, default_uri, property_id, +1 more. Returns: result
ga4_create_google_ads_linkA
Link a Google Ads account to a GA4 property. Creates the auto-audiences ('All Users', 'Purchasers') and lets GA4 conversion events (Key events) import into Ads as conversions once each is also marked biddable in Ads.
Args: customer_id: 10-digit Ads customer id, dashes ok. confirm: must be true to actually create.
Inputs: customer_id (required), confirm, property_id, ads_personalization_enabled. Returns: result
ga4_create_propertyA
Create a new GA4 property under an account.
Args: account_id: numeric account id from ga4_list_accounts. display_name: property name shown in the GA4 UI. time_zone: IANA time zone, e.g. Asia/Calcutta. currency_code: ISO currency code, e.g. INR, USD. industry_category: optional, e.g. REAL_ESTATE, EDUCATION, RETAIL. confirm: must be true to actually create.
Inputs: account_id (required), display_name (required), confirm, time_zone, +2 more. Returns: result
ga4_delete_conversion_eventA
Un-mark an event as a conversion. Always needs confirm=true.
Args: event_id: numeric id from ga4_list_conversion_events (not the event name -- the id is the last path segment of conversionEvents/).
Inputs: event_id (required), confirm, property_id. Returns: result
ga4_delete_data_streamA
Delete a data stream. Always needs confirm=true -- deleting the wrong stream orphans every GTM tag or gtag.js call still pointed at its measurement id, and it stops silently rather than erroring.
Inputs: stream_id (required), confirm, property_id. Returns: result
ga4_delete_google_ads_linkA
Remove a GA4<->Ads link, e.g. one pointed at a deactivated Ads account.
Args: link_id: from ga4_list_google_ads_links (not the customer_id). confirm: must be true to actually delete. If this 403s, the credential has Viewer, not Editor, on the property -- fix under Admin > Property Access Management, or delete it by hand in the GA4 UI as a fallback.
Inputs: link_id (required), confirm, property_id. Returns: result
ga4_get_propertyB
Full detail for one GA4 property.
Inputs: property_id. Returns: result
ga4_list_accountsA
List every GA4 account this credential can reach.
Call this first. If it returns nothing, the service account has not been added under Admin > Account Access Management on any account.
Inputs: no named parameters. Returns: result
ga4_list_audiencesB
List audiences on a property, including the ones GA4 auto-creates when a Google Ads link is made ('All Users', 'Purchasers').
Inputs: property_id. Returns: result
ga4_list_conversion_eventsA
List conversion events (Key events) on a property, with whether each is custom or a default GA4 event, and whether it counts (deleted events can appear briefly with countingMethod unset during propagation).
Inputs: property_id. Returns: result
ga4_list_custom_dimensionsB
List custom dimensions on a property.
Inputs: property_id. Returns: result
ga4_list_custom_metricsB
List custom metrics on a property.
Inputs: property_id. Returns: result
ga4_list_data_streamsA
List data streams on a property, with the measurement id (G-XXXXXXX) used to wire up GTM tags or a site's gtag.js.
Inputs: property_id. Returns: result
ga4_list_google_ads_linksA
List Google Ads accounts linked to a GA4 property.
Inputs: property_id. Returns: result
ga4_list_propertiesA
List GA4 properties, optionally filtered to one account.
Args: account_id: numeric account id from ga4_list_accounts. Omit to list every property the credential can reach across all accounts.
Inputs: account_id. Returns: result
ga4_run_realtime_reportC
Realtime report: activity in roughly the last 30 minutes.
Args: dimensions: e.g. ["unifiedScreenName"], ["deviceCategory"], ["country"]. metrics: e.g. ["activeUsers"], ["screenPageViews"], ["eventCount"].
Inputs: metrics (required), dimensions (required), limit, property_id. Returns: result
ga4_run_reportA
Run a GA4 report: any dimension x metric combination over a date range.
Args: dimensions: e.g. ["sessionSourceMedium", "date"], ["eventName"], ["landingPagePlusQueryString"], ["city"], ["deviceCategory"]. metrics: e.g. ["sessions", "activeUsers", "engagementRate", "averageSessionDuration", "eventCount", "conversions", "totalRevenue"]. start_date / end_date: 'YYYY-MM-DD', or relative like 'today', 'yesterday', 'NdaysAgo'. dimension_filter_field / dimension_filter_value: optional single exact-match filter, e.g. field='eventName', value='generate_lead'. order_by_metric: metric name to sort by. Defaults to the first metric. desc: sort descending (default) or ascending.
Inputs: metrics (required), dimensions (required), desc, limit, +6 more. Returns: result
ga4_top_conversion_pathsB
Convenience report: sessions by source/medium alongside conversions and conversion rate, sorted by conversions descending. Answers 'which channels actually produce leads', the question this project's client reports keep needing (see kolkataproperties-ads-report §4.1).
Inputs: limit, end_date, start_date, property_id. Returns: result
ga4_update_data_streamB
Update a data stream's display name or (web streams only) default URI.
Inputs: stream_id (required), confirm, default_uri, property_id, +1 more. Returns: result
ga4_update_propertyB
Update a GA4 property's display name, time zone, currency, or industry.
Only the fields you pass are changed.
Inputs: property_id (required), confirm, time_zone, display_name, +2 more. Returns: result
gsc_cannibalizationC
Queries where several of your own URLs compete for the same term.
Google usually picks one and splits authority across the rest. Consolidating, or differentiating the pages by intent, normally lifts the winner.
Args: min_pages: how many competing URLs before it counts (default 2). min_impressions: total impressions across the competing pages.
Inputs: days, limit, end_date, site_url, +3 more. Returns: result
gsc_compare_periodsA
Compare a window against the equal-length window immediately before it, and return the biggest gainers and losers.
This is the tool for "what changed". The API cannot diff periods itself, so this runs both windows and joins them.
Args: dimension: query, page, country, or device. days: length of each window (default 28, so 28 vs the prior 28). limit: how many gainers and how many losers to return.
Inputs: days, limit, end_date, site_url, +2 more. Returns: result
gsc_content_decayB
Pages that lost the most traffic against the previous equal-length period.
Catches decaying content early, while it still has enough authority that a refresh works. Only pages that had real traffic before are considered, so a page going from 1 click to 0 does not crowd out a page that went from 200 to 90.
Args: min_prev_clicks: ignore pages below this in the earlier window.
Inputs: days, limit, site_url, min_prev_clicks. Returns: result
gsc_ctr_opportunitiesB
Queries that already rank well but earn far fewer clicks than their position implies. These are title and meta-description problems, not ranking problems, which makes them the cheapest wins available.
Args: min_impressions: ignore low-volume noise (default 100). max_position: only consider queries ranking this well or better. limit: how many opportunities to return.
Returns rows sorted by estimated clicks left on the table.
Inputs: days, limit, end_date, site_url, +3 more. Returns: result
gsc_delete_sitemapA
Remove a sitemap from Search Console.
This only stops Google tracking that sitemap; it does not deindex the URLs inside it. It is a dry run unless confirm=true.
Inputs: feedpath (required), confirm, site_url. Returns: result
gsc_get_sitemapA
Detail for one sitemap, including per-content-type submitted and indexed counts.
Args: feedpath: full URL of the sitemap, e.g. https://example.com/sitemap.xml
Inputs: feedpath (required), site_url. Returns: result
gsc_inspect_urlA
Inspect one URL: is it indexed, which canonical Google chose, when it was last crawled, whether robots.txt allows it, mobile usability, and any rich results detected.
Requires Full or Owner permission on the property -- Restricted users get a 403 here even though performance data works for them.
Args: page_url: the exact URL to inspect. Must sit inside site_url. site_url: the property that owns the URL. language_code: BCP-47 code for the returned messages.
Quota note: the API allows roughly 2000 inspections per property per day and 600 per minute, so avoid looping it over a whole sitemap.
Inputs: page_url (required), site_url, language_code. Returns: result
gsc_inspect_urlsA
Inspect several URLs and return a compact per-URL verdict table.
Use this to check a batch after a deploy or a migration. Keep the list under about 20 -- each URL is a separate API call and the per-minute quota is real. A failure on one URL does not stop the rest; it is reported inline.
Inputs: page_urls (required), site_url, language_code. Returns: result
gsc_list_sitemapsA
List every sitemap submitted for a property, with error and warning counts and how many URLs each one carries.
A sitemap that has never been downloaded, or that shows errors, is the first thing to check when new pages are not getting indexed.
Inputs: site_url. Returns: result
gsc_list_sitesA
List every Search Console property this credential can read, with the permission level held on each.
Call this first. The returned site_url strings are the exact values every other tool expects -- domain properties look like 'sc-domain:example.com', URL-prefix properties keep their scheme and trailing slash.
permission_level meanings: siteOwner / siteFullUser -- full access, URL inspection works. siteRestrictedUser -- performance data only, URL inspection blocked. siteUnverifiedUser -- verification incomplete; most calls will fail.
Inputs: no named parameters. Returns: result
gsc_queries_for_pageA
Every query a specific URL ranks for. Use this before rewriting a page -- it shows what the page actually earns impressions on, which is often not what it was written to target.
Args: page_url: exact URL, or a substring to match loosely.
Inputs: page_url (required), days, limit, end_date, +2 more. Returns: result
gsc_search_analyticsA
Run any Search Console performance query. The most flexible read tool -- use it for anything the shortcut tools do not cover.
Args:
site_url: property, exactly as list_sites reports it.
dimensions: any mix of query, page, country, device, date,
searchAppearance. Empty list returns a single totals row.
start_date / end_date: YYYY-MM-DD. Omit both to use the last days.
days: window length when dates are omitted (default 28).
row_limit: max rows, up to 25000 (default 100).
search_type: web, image, video, news, discover, or googleNews.
filters: list of {dimension, operator, expression}. Operators are
equals, notEquals, contains, notContains, includingRegex,
excludingRegex.
start_row: offset for paging past row_limit.
Inputs: days, filters, end_date, site_url, +5 more. Returns: result
gsc_striking_distanceA
Queries ranking just off page one, where a small gain converts to real traffic. Position 8-20 by default.
Each row carries the page currently ranking, so the work has an obvious target. Sorted by impressions, since that is the traffic on offer.
Inputs: days, limit, end_date, site_url, +4 more. Returns: result
gsc_submit_sitemapB
Submit a sitemap to Search Console.
This is a write. It is a dry run unless confirm=true.
Args: feedpath: full URL of the sitemap, e.g. https://example.com/sitemap.xml confirm: set true to actually submit.
Inputs: feedpath (required), confirm, site_url. Returns: result
gsc_top_pagesC
Top landing pages by clicks, with impressions, CTR and average position.
Inputs: days, limit, end_date, site_url, +1 more. Returns: result
gsc_top_queriesA
Top search queries by clicks.
Args: site_url: property. start_date / end_date / days: date window. limit: how many queries. page_filter: optional substring; restricts to URLs containing it, so you can ask which queries drive a particular section of the site.
Inputs: days, limit, end_date, site_url, +2 more. Returns: result
gtm_audit_containerB
Full container health check in one call.
Reports: tags that can never fire, paused tags, triggers and variables nothing uses, references to variables that do not exist, and inconsistent GA4 or Ads ids. Findings are ordered most severe first.
Inputs: account_id, container_id, workspace_id. Returns: result
gtm_container_snippetC
The install snippet for a web container: the script and the
Inputs: account_id, container_id. Returns: result
gtm_create_ads_conversion_tagC
Create a Google Ads conversion tracking tag (type 'awct').
Args: conversion_id: the AW-XXXXXXXXX id, digits only or with the AW- prefix. conversion_label: the label from the Google Ads conversion action. conversion_value: a fixed number, or a variable reference like {{DL - value}}. Leave unset for a valueless conversion. currency_code: ISO code, or a variable reference.
Inputs: name (required), conversion_id (required), conversion_label (required), firing_trigger_ids (required), +6 more. Returns: result
gtm_create_constant_variableB
Create a Constant variable (type 'c').
Worth doing for any id used in more than one tag -- a GA4 measurement id or an Ads conversion id. Changing it then means editing one variable rather than hunting every tag that hardcoded it.
Inputs: name (required), value (required), confirm, account_id, +2 more. Returns: result
gtm_create_datalayer_variableB
Create a Data Layer Variable (type 'v').
Args: datalayer_name: the dataLayer key to read, e.g. 'ecommerce.value'. default_value: returned when the key is absent. version: dataLayer version, 2 unless you have a specific reason.
Inputs: name (required), datalayer_name (required), confirm, version, +4 more. Returns: result
gtm_create_entityA
Create an entity of any type.
Args: entity_type: tags, triggers, variables, folders, templates, clients, transformations, zones, gtag_config. name: display name in the GTM UI. type: the GTM type string, e.g. 'gaawe' for a GA4 event tag, 'googtag' for a Google tag, 'awct' for an Ads conversion, 'linkClick' for a trigger, 'jsm' for a custom JavaScript variable. Not needed for folders. parameters: plain dict, converted to the API's typed shape. Reference other variables with double braces, e.g. {"pageUrl": "{{Page URL}}"}. firing_trigger_ids: trigger ids that fire a tag. blocking_trigger_ids: trigger ids that block a tag. extra: any additional top-level fields to merge into the request body, for fields this signature does not name (monitoringMetadata, consentSettings, scheduleStartMs, and so on). confirm: set true to actually create.
Inputs: name (required), entity_type (required), type, extra, +7 more. Returns: result
gtm_create_ga4_event_tagA
Create a GA4 event tag (type 'gaawe').
Args: measurement_id: the G-XXXXXXX id, or a variable reference such as {{GA4 Measurement ID}} if you keep it in a constant. event_name: the GA4 event, e.g. generate_lead, purchase, phone_call_click. firing_trigger_ids: trigger ids that fire this tag. Get them from list_entities with entity_type='triggers'. event_parameters: extra event parameters as a plain dict, e.g. {"value": "{{DL - value}}", "currency": "INR"}.
Inputs: name (required), event_name (required), measurement_id (required), firing_trigger_ids (required), +5 more. Returns: result
gtm_create_triggerB
Create a trigger.
Args: trigger_type: pageview, domReady, windowLoaded, click, linkClick, formSubmission, customEvent, historyChange, scrollDepth, elementVisibility, timer, youTubeVideo, jsError. conditions: list of {variable, operator, value}. Operator is one of equals, contains, startsWith, endsWith, matchRegex, greater, less. Variable is a GTM variable reference, e.g. '{{Page URL}}'. Example: [{"variable": "{{Page URL}}", "operator": "contains", "value": "/thank-you"}] event_name: required for customEvent triggers -- the dataLayer event name.
A click or form trigger only captures useful conditions when the matching built-in variables are enabled; call enable_built_in_variables first.
Inputs: name (required), trigger_type (required), confirm, account_id, +4 more. Returns: result
gtm_create_versionA
Snapshot a workspace into a container version.
This does not publish. It freezes the current workspace so it can be reviewed, published, or rolled back to later. Write real notes -- they are the only record of why a change was made.
Inputs: name (required), notes, confirm, account_id, +2 more. Returns: result
gtm_create_workspaceA
Create a workspace to hold a batch of changes.
Do this before any set of edits you might want to abandon. Deleting a workspace discards everything in it, which is far cleaner than unpicking edits made directly in the Default Workspace.
GTM allows a limited number of concurrent workspaces per container (3 on the free tier), so delete them once merged.
Inputs: name (required), confirm, account_id, description, +1 more. Returns: result
gtm_delete_entityA
Delete an entity from a workspace.
Deleting a trigger or variable that a tag still references leaves that tag broken, and the API will not warn you. Run find_references first.
Inputs: entity_id (required), entity_type (required), confirm, account_id, +2 more. Returns: result
gtm_delete_workspaceA
Delete a workspace and discard every uncommitted change inside it.
This cannot be undone. Run workspace_status first to see what is about to be thrown away.
Inputs: workspace_id (required), confirm, account_id, container_id. Returns: result
gtm_enable_built_in_variablesA
Turn on GTM built-in variables.
Built-in variables are enabled by type, not created. Common types: pageUrl, pageHostname, pagePath, referrer, event, clickElement, clickClasses, clickId, clickTarget, clickUrl, clickText, formElement, formClasses, formId, formTarget, formUrl, formText, scrollDepthThreshold, videoStatus, containerId, environmentName.
Click and form triggers silently fail to capture anything useful when the matching built-in variables are off, so this is usually step one.
Inputs: variable_types (required), confirm, account_id, container_id, +1 more. Returns: result
gtm_find_referencesA
Find everything that references a variable or trigger, by name or id.
Run this before deleting or renaming. The API has no referential integrity: deleting a trigger a tag still fires on, or renaming a variable another entity interpolates as {{Name}}, breaks silently at runtime.
Inputs: entity_name (required), account_id, container_id, workspace_id. Returns: result
gtm_get_containerA
Full detail for one container, including its usage context (web, server, ios, android, amp) and the domains it is declared on.
Inputs: account_id, container_id. Returns: result
gtm_get_entityA
Fetch one entity in full, including its parameter tree.
Read a working entity of the same type before creating a new one -- tag and trigger parameter shapes are strict and mirroring a known-good example is far more reliable than guessing.
Inputs: entity_id (required), entity_type (required), account_id, container_id, +1 more. Returns: result
gtm_get_live_versionB
The version currently serving live traffic.
Compare this against your workspace before publishing, and note the version_id so you have a rollback target.
Inputs: account_id, container_id. Returns: result
gtm_get_versionB
Full detail of one container version, including every tag it contains.
Inputs: version_id (required), account_id, container_id. Returns: result
gtm_list_accountsA
List every Tag Manager account this credential can reach.
Call this first. If it returns nothing, the service account has not been added under GTM > Admin > Account User Management on any account.
Inputs: no named parameters. Returns: result
gtm_list_containersA
List containers in an account.
Note the two different ids: container_id is numeric and is what every other tool wants; public_id is the GTM-XXXXXXX string you paste into a site.
Inputs: account_id. Returns: result
gtm_list_entitiesA
List entities of any type in a workspace.
Args: entity_type: one of tags, triggers, variables, folders, templates, clients, transformations, zones, built_in_variables, gtag_config. name_contains: case-insensitive substring filter on the entity name. full: return the complete API object instead of a compact summary. Use this when you are about to copy an entity, since update_entity needs the full body.
Inputs: entity_type (required), full, account_id, container_id, +2 more. Returns: result
gtm_list_user_permissionsA
Who has access to this GTM account, and at what level.
Useful for confirming the service account itself is wired correctly, and for auditing who can publish.
Inputs: account_id. Returns: result
gtm_list_versionsA
List container version headers, newest first.
Use this to find a version to roll back to. The live version is reported by get_live_version.
Inputs: account_id, container_id, include_deleted. Returns: result
gtm_list_workspacesB
List workspaces in a container.
A workspace is an isolated set of pending changes. The Default Workspace is usually id 1. Prefer creating your own workspace for a batch of edits so the change set stays reviewable and easy to abandon.
Inputs: account_id, container_id. Returns: result
gtm_publish_versionA
Publish a container version to live traffic.
This takes effect immediately for every visitor. There is no staging step and no undo beyond publishing an earlier version, so note the current live version id first via get_live_version.
Requires GTM_ALLOW_PUBLISH=true in addition to confirm=true, and Publish permission on the container.
Inputs: version_id (required), confirm, account_id, container_id. Returns: result
gtm_sync_workspaceA
Pull the latest live container state into a workspace.
Run this when workspace_status reports merge conflicts. Changes that cannot be merged automatically are returned so you can resolve them by hand.
Inputs: confirm, account_id, container_id, workspace_id. Returns: result
gtm_tag_coverageA
What this container actually measures: every tag with the trigger names that fire it, resolved from ids to readable names.
This is the map to read before adding anything, so you do not duplicate a conversion that already fires.
Inputs: account_id, container_id, workspace_id. Returns: result
gtm_update_entityA
Update an entity by merging updates into its current body.
The API replaces the whole object on update, so this reads the entity first
and merges, rather than making you resend every field. Pass parameters as a
plain dict inside updates to replace the parameter list; anything else is set
as a top-level field.
Example updates: {"name": "New name", "paused": true}
Inputs: updates (required), entity_id (required), entity_type (required), confirm, +3 more. Returns: result
gtm_workspace_statusA
Every uncommitted change in a workspace, and any conflict with the live container.
This is the diff to review before creating a version. A non-empty merge_conflicts list means the live container moved underneath you; call sync_workspace to resolve.
Inputs: account_id, container_id, workspace_id. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 94 tools
Tools are grouped by clear product prefixes (ads_, gtm_, gsc_, ga4_) with a consistent resource+action structure, making selection unambiguous. Even where convenience wrappers overlap generic tools (e.g., gtm_create_entity vs specialized GTM creators), the descriptions explicitly delineate when to use which.
The dominant <prefix>_<verb>_<noun> pattern is very consistent, with clear get/list/create/update/delete/run verbs. Minor deviations exist: several read-only/report tools use noun-only names (e.g., ads_campaign_performance, gtm_workspace_status, gsc_top_queries), and GA4 reports use run_ while Ads/GSC search analytics do not.
94 tools is far beyond the 25+ threshold, even for a server spanning four Google products. Many convenience wrappers (specialized GTM tag creators, GSC insight shortcuts) duplicate generic tools like gtm_create_entity or gsc_search_analytics, and the sheer surface area creates a heavy context burden for agents.
The set covers the full end-to-end marketing lifecycle: GA4 property/data stream setup, GTM container and tag publishing, Google Ads campaign/ad group/ad creation, Search Console insights, and cross-product linking. Minor gaps exist (no keyword removal/update in Ads, no GA4 data stream getter or audience update/delete, no GSC property creation), but generic query tools partially compensate.
Related MCP Connectors
Google Ads MCP server — manage campaigns, keywords, and metrics.
Google Ads MCP server: run Search, Display, Demand Gen and PMax campaigns from AI.
Hosted MCP server for GA4, Google Ads and Search Console. Google OAuth, nothing to install.
Hosted MCP server for marketing agencies: connect Google Ads, GA4, Search Console, Meta, LinkedIn and more to Claude or ChatGPT. Pull reports and make changes via OAuth login, no setup.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceUnified MCP server for managing Meta Ads, LinkedIn Ads, Google Ads, GA4, and Search Console with 89 read/write tools, multi-account support, OAuth setup, and safe dry-run mutations.MIT
- AlicenseNot gradedqualityCmaintenanceA unified MCP server for marketing analytics and management across Google Ads, Meta Ads, GA4, and keyword research, supporting multi-client rollups and platform-conditional credential requirements.MIT
- AlicenseNot gradedqualityCmaintenanceMCP server for Google Tag Manager and Google Ads, enabling AI agents to audit, fix, and manage GTM containers and Google Ads campaigns programmatically.106 npmMIT
- FlicenseNot gradedqualityCmaintenanceEnables management of Google Ads accounts via MCP, providing read and write tools for campaigns, ad groups, keywords, assets, and more, with support for reporting and mutations.-