Skip to main content
Glama
Google Marketing MCP

Google Marketing MCP

by konkomaji

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.py auto-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 status

If .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

ads_*

Google Ads (22)

Accounts, run_gaql + performance reports, monitor_account, keyword research (Keyword Planner), campaign/ad-group/ad/keyword creation, bids/budgets, list_recommendations/apply_recommendation, search_terms_report

gtm_*

Tag Manager (29)

Accounts/containers/workspaces, generic entity CRUD (list_entities/get_entity/create_entity/update_entity/delete_entity across tags/triggers/variables/folders/templates/...), typed helpers (create_ga4_event_tag, create_ads_conversion_tag, create_trigger), find_references, versions + publish_version, audit_container, tag_coverage

gsc_*

Search Console (~20)

list_sites, flexible search-analytics queries, trend comparison, opportunity analysis (CTR gaps, striking-distance keywords, cannibalization, content decay), URL inspection, sitemaps

ga4_*

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.

  1. Google Cloud project → enable the Google Ads API, Tag Manager API, Search Console API, and Analytics Admin + Data API.

  2. Cloud Console → Credentials → OAuth client ID → type Desktop app.

  3. python launch.py --setup then run:

    .venv/Scripts/python -m google_marketing_mcp.auth_unified   # Windows
    .venv/bin/python -m google_marketing_mcp.auth_unified       # macOS/Linux

    One 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.

  4. Still add GOOGLE_ADS_DEVELOPER_TOKEN separately (Ads UI → Tools & Settings → API Center) — OAuth doesn't cover it, nothing does.

  5. 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_*)

  1. Google Cloud project → enable the Google Ads API.

  2. Google Ads UI → Tools & Settings → API Center → apply for a developer token (starts test-access; apply for Basic for production data).

  3. Cloud Console → Credentials → OAuth client ID → type Desktop app.

  4. python launch.py --setup then .venv/Scripts/python -m google_marketing_mcp.ads.auth (Windows; .venv/bin/python elsewhere) → browser consent → copy the printed GOOGLE_ADS_REFRESH_TOKEN.

  5. Fill the GOOGLE_ADS_* block in .env. Set GOOGLE_ADS_LOGIN_CUSTOMER_ID to 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:

  1. Cloud Console → IAM & Admin → Service Accounts → create one → download the JSON key. Point GOOGLE_APPLICATION_CREDENTIALS at it.

  2. GTM: add the service account email under Admin → Account User Management on each account, with Edit (and Publish if you'll publish).

  3. Search Console: add it under Settings → Users and permissions on each property — Full, not Restricted, or URL Inspection is blocked.

  4. 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; see ga4/client.py.)

Verify everything

python launch.py --check

Prints 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.py

Claude 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=true blocks 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=true explicitly, 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 note stating what happened (DRY-RUN vs APPLIED) and why.

6. Error handling

  • Google Ads: the official google-ads gRPC SDK retries transient UNAVAILABLE/DEADLINE_EXCEEDED errors internally. Application errors (GoogleAdsException) are flattened into a readable code: message @ field string — 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 (honoring Retry-After on 429) before surfacing an error. Non-transient errors (400/401/403/404/409) are never retried — each domain's client.py has 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

did not find executable .../python.exe

.venv was built elsewhere — run python launch.py again; it rebuilds automatically.

No Python >= 3.10 found

Install Python 3.10+ and ensure it's on PATH.

MISSING credentials: ... for one domain

Fill that domain's block in .env (§3). Other domains still work.

Mutation "did nothing"

Dry-run gate — re-call with confirm=true.

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_sync_workspace.

GTM publish refused

GTM_ALLOW_PUBLISH=true needed in addition to confirm=true.


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 tools
ads_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_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_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_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

    A3.5/5.0

    Scored across 94 tools

    Disambiguation5/5

    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.

    Naming Consistency4/5

    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.

    Tool Count2/5

    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.

    Completeness4/5

    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

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Unified 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
    • A
      license
      Not graded
      quality
      C
      maintenance
      A 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
    • A
      license
      Not graded
      quality
      C
      maintenance
      MCP server for Google Tag Manager and Google Ads, enabling AI agents to audit, fix, and manage GTM containers and Google Ads campaigns programmatically.
      106 npm
      MIT
    • F
      license
      Not graded
      quality
      C
      maintenance
      Enables 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.
      -