google-ads-write-mcp
A write-side MCP server for Google Ads that builds/edits Search campaigns and assets (dry-run by default, audited), plus a read-only Keyword Planner tool.
Create campaigns/ad groups:
create_search_campaignbuilds a full Search campaign atomically (budget, geo/language criteria, negatives, ad groups, keywords, RSAs, managed label, created PAUSED);add_ad_groupadds one to an existing campaign.Ads & keywords:
create_responsive_search_ad,update_responsive_search_ad(edit in place),add_keywords,add_negative_keywords(campaign or ad-group level).Extensions/assets:
add_sitelinks,add_callouts,add_structured_snippet,add_image_assets,add_business_assets(name/logo), attached to campaigns or ad groups with local size/format validation.Bidding & budget:
set_campaign_target_cpa,set_campaign_cpc_ceiling,set_campaign_daily_budget(refuses shared budgets).Targeting:
set_device_bid_modifiers,set_location_bid_modifiers,set_campaign_final_url_suffix,set_final_urls.Status:
set_statusto ENABLED/PAUSED campaigns, ad groups, ads, keywords.Guarded deletion:
remove_entitypermanently removes campaigns/ad groups/ads/keywords/criteria/asset links, only under a campaign with the managed label.Read-only research:
keyword_ideas(Keyword Planner volumes/bids, 1 req/sec) andaudit_log_tail(local JSONL audit log).Safety: every mutating call is one atomic mutate, dry-run unless
confirm=true; all calls are audit-logged with no secrets.
Provides write-side management for Google Ads, including creating search campaigns, ad groups, responsive search ads, keywords, assets, sitelinks, and callouts; setting budgets, target CPA, CPC ceilings, final URLs, and bid modifiers; updating or removing entities under a managed label; and retrieving Keyword Planner ideas.
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-ads-write-mcp Create a paused search campaign named 'CLAUDE - Spring Sale' with a $50 daily budget"
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-ads-write-mcp
A small, allow-listed write-side MCP server for Google Ads, plus a read-only
Keyword Planner tool. It is the companion to Google's official
google-ads-mcp, which is
read-only by design: use that one to look things up, and this one to change them.
Works with Claude Code (and any MCP client over stdio).
Safety model
Dry run by default. Every mutating tool sends its request with
validate_only=true— Google checks auth and payload and changes nothing. Passconfirm=trueto apply.Atomic. Each call is one
MutateGoogleAdsRequestwith partial failure off: either everything in it is created, or nothing is.Additive. Tools add ads, keywords, assets, ad groups and campaigns, or toggle ENABLED/PAUSED, or tune budget and target CPA. Nothing edits existing ad text. New campaigns are created PAUSED.
One guarded delete.
remove_entityrefuses anything that does not sit under a campaign carrying the managed label (defaultclaude-managed, whichcreate_search_campaignattaches to every campaign it creates), and verifies that with a read query first. Removal in Google Ads is permanent; there is no restore.Audited. Every call — dry run or applied, success or rejection — is appended to a local JSONL audit log with timestamp, tool, customer, payload and API result. No secrets are written.
Local limits. RSA headline/description counts and lengths, sitelink, callout and snippet lengths, image size and aspect ratio are checked before anything is sent.
Related MCP server: Google Ads MCP Admin
Tools
Tool | Purpose |
| Build a complete Search campaign in one atomic mutate: budget, campaign, location and language criteria, campaign negatives, the managed label, every ad group with its keywords, optional ad-group negatives, optional final URL suffix (UTMs) and one responsive search ad. Created PAUSED; Google Search only; Maximize conversions (optional target CPA) or Maximize clicks |
| Add one ad group with keywords, optional negatives, optional final URL suffix and one RSA to an existing campaign |
| Add a new RSA to an ad group (existing ads untouched) |
| Add positive keywords to an ad group |
| Add negatives at campaign or ad-group level |
| Create the assets and attach them to a campaign; |
| Put one image into the asset library (no link): |
| Campaign-specific conversion goals: toggle |
| Point a campaign at one custom conversion goal by id (action-level goals: the campaign bids on exactly the listed actions, primary/secondary flags ignored), or |
| Create an ENABLED custom conversion goal from a list of conversion action ids |
| Flip a conversion action between primary (biddable under account-default / category goals) and secondary (observation only); custom goals are unaffected |
| Upload PNG/JPEG files and attach them to a Search campaign ( |
| Attach a business name (TEXT asset, |
| ENABLED / PAUSED on a campaign, ad group, ad or keyword |
| Change a campaign's non-shared daily budget |
| Set tCPA on a Maximize-conversions campaign |
| Create a portfolio (shared) Maximize Conversions bid strategy, optionally with a target CPA, and attach campaigns to it in the same atomic request — pools Smart Bidding's learning across campaigns |
| Attach campaigns to an existing portfolio bid strategy, or (no |
| Change (or remove with 0) the target CPA of a portfolio strategy — Maximize Conversions or legacy Target CPA |
| Create an explicitly shared daily budget and move campaigns onto it atomically, so spend flows between them; old individual budgets are left unused |
| Change the daily amount of an explicitly shared budget ( |
| Set (or remove with 0) the max-CPC ceiling on a Maximize-Clicks campaign ( |
| Update final URLs (ads, assets) and final URL suffixes (ad groups, ads) in one atomic mutate — e.g. move UTMs into the final URL itself |
| Edit an existing RSA in place (same ad id and history; re-review, asset labels reset): replace headlines/descriptions, paths, final URL. Any argument left out is unchanged |
| Campaign-level final URL suffix (ValueTrack, e.g. |
| Campaign-level device bid adjustments in percent; −100 excludes a device (mobile-only = desktop −100, tablet −100). Call it after |
| Campaign-level location bid adjustments in percent (−90 to +900) on locations the campaign already targets, keyed by geo target constant id ( |
| Change an existing campaign's location option ( |
| Edit an existing campaign's location targeting atomically: |
| Attach in-market / affinity segments ( |
| Permanently remove a campaign, ad group, ad, keyword, campaign criterion (e.g. a campaign negative) or an asset link (campaign/ad-group asset) — one resource name or a list of the same kind, atomically — only under a campaign carrying the managed label |
| Keyword Planner ideas with monthly volume and bid ranges (read-only, 1 request/second) |
| Last N audit-log entries |
| SEARCH_CUSTOM A/B experiment on an existing Search campaign: experiment shell plus control/treatment arms with a traffic split; returns the treatment's draft campaign to edit with the other tools. Dry run validates the shell only — arms need the real experiment, so they are created on confirm |
| Start a SETUP experiment serving (materializes the treatment draft; asynchronous on Google's side) |
| End a running experiment; the base campaign resumes full traffic |
| Apply the treatment to the base campaign — the winner becomes the live campaign (asynchronous) |
Customer ids may contain dashes. Use the read-only MCP first to look up campaign and ad-group ids and resource names.
Prerequisites
You need the same three things the official read-only server needs — this package creates none of them:
A Google Ads API developer token (from a manager account's API Center).
An OAuth client in a Google Cloud project, and an Ads ADC file produced by
gcloud auth application-default login --scopes=https://www.googleapis.com/auth/adwords(anauthorized_userJSON with client id, client secret and refresh token).Edit access on the Google Ads accounts you intend to change. Read-only users can query but every mutate is rejected.
Keep the token and the ADC file out of any repository.
Install
pipx install "git+https://github.com/Luxand/google-ads-write-mcp@v0.5.2"
google-ads-write-mcp --check # shows which credential files it found; no secrets printed
google-ads-write-mcp --list-toolsThe install is pinned to a tag, so pipx upgrade will not move it; upgrade
with pipx install --force "git+https://github.com/Luxand/google-ads-write-mcp@<new tag>".
Configuration
Everything is optional if the defaults fit:
Variable | Meaning | Default |
| the token itself | — |
| file holding the token |
|
| Ads ADC JSON |
|
| manager (MCC) id when the target account sits under one; | none |
| audit log path |
|
| Google Ads label that marks campaigns |
|
A login customer id applies to the whole client, so register one server entry per manager context you need.
Register in Claude Code
# direct accounts
claude mcp add --scope user google-ads-write -- "$(command -v google-ads-write-mcp)"
# accounts under a manager
claude mcp add --scope user google-ads-write-luxand \
--env GOOGLE_ADS_LOGIN_CUSTOMER_ID=1234567890 \
-- "$(command -v google-ads-write-mcp)"Then in Claude Code, /mcp should list the server and its tools. A safe first
call is audit_log_tail, then any tool without confirm (a dry run).
Scripted use
--call TOOL ARGS.json runs one tool from a JSON file through the same code
path, same dry-run default and same audit log:
google-ads-write-mcp --call create_search_campaign /abs/path/campaign.jsonUseful for pushing a large campaign generated from a spreadsheet, where passing
hundreds of keywords as chat tool arguments is impractical. Add "confirm": true
to the JSON to apply.
Things the API taught us
Search campaigns accept image assets only as
AD_IMAGE;MARKETING_IMAGE/SQUARE_MARKETING_IMAGEare Performance Max and Display field types and are rejected withFIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE.contains_eu_political_advertisingis required on campaign creation in current API versions; this server sets it to "does not contain".Geo and language criteria are plain resource names (
geoTargetConstants/2840,languageConstants/1000).GenerateKeywordIdeasis limited to 1 request per second per customer id and returnsRESOURCE_EXHAUSTEDabove that;keyword_ideasspaces and retries.A mutate request may hold at most 10,000 operations; a 20-ad-group Search campaign with ~250 keywords is about 370.
Device criteria (desktop 30000, mobile 30001, tablet 30002) exist implicitly on every campaign; set a bid modifier by updating
campaignCriteria/<campaign>~<id>. Namebid_modifierin the update mask explicitly: a field mask built by comparing against defaults drops a 0.0 (−100%) value and the request succeeds while changing nothing.Setting a campaign back to campaign-level Maximize Conversions means assigning an empty
MaximizeConversionsmessage to thecampaign_bidding_strategyoneof and namingmaximize_conversionsin the update mask explicitly; a generated mask sees no set fields and sends nothing.Custom conversion goals live on
conversionGoalCampaignConfigs/<campaign>:goal_config_level = CAMPAIGNplus the goal resource name, orCUSTOMERto fall back to account defaults. Toggling a category goal (campaignConversionGoals/<campaign>~<category>~<origin>) also flips the campaign to campaign-specific goals, but categories are shared across products (e.g.SUBMIT_LEAD_FORM), so per-product isolation needs a custom goal.primary_for_goal = falsemust be named in the update mask explicitly.
License
MIT.
Available Tools
22 toolsadd_ad_groupA
Add one ad group - with its keywords, optional ad-group negatives, optional final URL suffix and one responsive search ad - to an EXISTING campaign, atomically. Same argument shapes as create_search_campaign's ad_groups entries. Dry run unless confirm=true.
Inputs: name (required), keywords (required), final_url (required), headlines (required), +9 more. Returns: result
add_business_assetsA
Attach a business name and/or business logo to a Search campaign.
business_name (<=25 chars, Google's limit) is created as a TEXT asset and linked as BUSINESS_NAME. For the logo pass logo_path (absolute PNG/JPEG, square within 2%,
=128x128, <=5 MB; 1200x1200 recommended) to upload a new asset, or logo_asset_resource_name to re-link an image asset the account already holds; either is linked as BUSINESS_LOGO. At least one input is required. Dry run unless confirm=true.
Inputs: campaign_id (required), customer_id (required), confirm, logo_path, +2 more. Returns: result
add_calloutsA
Create callout assets (each <=25 chars) and attach them to a campaign. Dry run unless confirm=true.
Inputs: callouts (required), campaign_id (required), customer_id (required), confirm. Returns: result
add_image_assetsA
Upload image files as image assets and attach them to a Search campaign as AD_IMAGE.
images: [{"path": "/absolute/file.png", "field_type": "SQUARE_MARKETING_IMAGE"|"MARKETING_IMAGE", "name": optional asset name}, ...] field_type here declares the SHAPE (1:1 or 1.91:1) and drives local validation; the link itself is always AD_IMAGE, which is the only image field type Search campaigns accept (MARKETING_IMAGE / SQUARE_MARKETING_IMAGE are Performance Max and Display field types). or, to attach an image already in the account: {"asset_resource_name": "customers/../assets/..", "field_type": ...}. PNG or JPEG, <=5 MB, 1:1 >=300x300 or 1.91:1 >=600x314; the file's aspect ratio must match the field_type. Google rejects an image identical to an existing asset (DUPLICATE_ASSET) - pass its asset_resource_name instead. Dry run unless confirm=true.
Inputs: images (required), campaign_id (required), customer_id (required), confirm. Returns: result
add_keywordsA
Add positive keywords to an ad group. keywords: [{"text": "photo to pdf", "match_type": "PHRASE|EXACT|BROAD"}]. Dry run unless confirm=true.
Inputs: keywords (required), ad_group_id (required), customer_id (required), status, +1 more. Returns: result
add_negative_keywordsA
Add NEGATIVE keywords at campaign level (campaign_id) or ad-group level (ad_group_id). match_type: PHRASE (default) | EXACT | BROAD. Dry run unless confirm=true.
Inputs: keywords (required), customer_id (required), confirm, match_type, +2 more. Returns: result
add_sitelinksA
Create sitelink assets and attach them to a campaign, or to one ad group when ad_group_id is given (ad-group sitelinks override the campaign's for that ad group). sitelinks: [{"link_text": <=25 chars, "final_url": "https://...", "description1": <=35, "description2": <=35}, ...] Link text must be unique and URLs should differ (Google will not serve two sitelinks with the same URL together). Dry run unless confirm=true.
Inputs: sitelinks (required), campaign_id (required), customer_id (required), confirm, +1 more. Returns: result
add_structured_snippetA
Create one structured-snippet asset (header e.g. "Types", "Services"; 3-10 values, each <=25 chars) and attach it to a campaign. Dry run unless confirm=true.
Inputs: header (required), values (required), campaign_id (required), customer_id (required), +1 more. Returns: result
audit_log_tailA
Return the last n entries of this server's local audit log (dry runs and applied changes).
Inputs: n. Returns: result
create_responsive_search_adA
Create a NEW responsive search ad in an ad group (existing ads are untouched).
headlines: 3-15 items, each <=30 chars; descriptions: 2-4 items, each <=90 chars. An item may be a string or {"text": "...", "pin": "HEADLINE_1|HEADLINE_2|HEADLINE_3|DESCRIPTION_1|DESCRIPTION_2"}. status: ENABLED or PAUSED. Dry run unless confirm=true.
Inputs: final_url (required), headlines (required), ad_group_id (required), customer_id (required), +5 more. Returns: result
create_search_campaignA
Create a complete Search campaign in ONE atomic mutate: budget, campaign, location and language criteria, campaign negatives, and every ad group with its keywords and one RSA.
The campaign gets the managed label (default "claude-managed"; created in the account on first use) so remove_entity can later act on it. It is created PAUSED; enable it with set_status. locations: geo target constant ids (United States = 2840); location targeting is created presence-only (positive and negative geo_target_type = PRESENCE). languages: language constant ids (English = 1000); an empty list means "all languages" (no language criterion is created). bidding: MAXIMIZE_CONVERSIONS (optional target_cpa) or MAXIMIZE_CLICKS (optional cpc_bid_ceiling in account currency; omitted = uncapped). Headlines/descriptions accept plain strings or {"text", "pin": "H1".."H3" | "D1" | "D2"}. Network: Google Search only (no partners, no Display). ad_groups: [{"name", "final_url", "keywords": [{"text","match_type"}], "headlines": [...], "descriptions": [...], "path1", "path2", "status", "final_url_suffix", "negative_keywords": [...]}, ...] negative_keywords: [{"text", "match_type"}] at campaign level. Device bid adjustments (e.g. mobile-only) are a follow-up call to set_device_bid_modifiers with the returned campaign id: the device criteria only exist once the campaign does. Extensions (sitelinks, callouts, snippets, images) are separate tools - call them with the returned campaign id. Dry run unless confirm=true.
Inputs: name (required), ad_groups (required), languages (required), locations (required), +7 more. Returns: result
keyword_ideasA
Keyword Planner ideas with US-style monthly search volume and bid ranges. READ-ONLY: creates nothing, so there is no confirm flag. Costs one API operation per call, rate-limited to 1/second.
seeds: up to 20 seed keywords; url: a landing page to seed from (either or both).
geo_target_ids: geo target constant ids (default [2840] = United States); language_id: language
constant id (default 1000 = English). Google Search network only (no partners).
Returns ideas sorted by avg_monthly_searches desc, up to limit. Volumes are rounded 12-month
averages and close variants share one aggregated number.
Inputs: customer_id (required), url, limit, seeds, +3 more. Returns: result
remove_entityA
PERMANENTLY remove a campaign, ad group, ad (adGroupAds/...), keyword (adGroupCriteria/...), campaign criterion such as a campaign negative keyword (campaignCriteria/...), or an asset LINK (campaignAssets/..., adGroupAssets/... - the asset itself stays in the account's asset library).
resource_name may be one resource name or a list of names of the SAME kind; a list is removed in one atomic mutate. Refuses unless every entity sits under a campaign carrying the managed label (default "claude-managed") - i.e. one created by this server. Google Ads has no restore for removed entities; historical data stays in reports but the entity cannot be re-enabled. Dry run unless confirm=true.
Inputs: customer_id (required), resource_name (required), confirm. Returns: result
set_campaign_cpc_ceilingA
Set the max CPC bid ceiling (account currency, e.g. 5.0) of a campaign that uses Maximize Clicks (Campaign.target_spend). Pass 0 to remove the ceiling. Dry run unless confirm=true.
Inputs: campaign_id (required), customer_id (required), cpc_bid_ceiling (required), confirm. Returns: result
set_campaign_daily_budgetA
Set a campaign's daily budget (account currency). Refuses if the budget is shared with other campaigns. Dry run unless confirm=true.
Inputs: campaign_id (required), customer_id (required), daily_budget (required), confirm. Returns: result
set_campaign_final_url_suffixA
Set the CAMPAIGN-level final URL suffix, or clear it with "". Google appends the suffix to every final URL in the campaign at click time and substitutes ValueTrack tokens, e.g. "utm_term={keyword}&kw_match={matchtype}&device={device}". An ad-group or ad suffix overrides the campaign's, so keep the suffix at one level. Ads, their text and history are untouched. Dry run unless confirm=true.
Inputs: campaign_id (required), customer_id (required), final_url_suffix (required), confirm. Returns: result
set_campaign_target_cpaA
Set the target CPA (in account currency, e.g. 9.5) of a campaign that uses Maximize Conversions. Dry run unless confirm=true.
Inputs: target_cpa (required), campaign_id (required), customer_id (required), confirm. Returns: result
set_device_bid_modifiersA
Set campaign-level device bid adjustments in percent (-100 to +900). -100 excludes the device: a mobile-only Search campaign is desktop=-100, tablet=-100. Smart Bidding strategies honour only the -100 case; other values are advisory under Maximize conversions / tCPA. Leave a device None to keep it unchanged. Dry run unless confirm=true.
Inputs: campaign_id (required), customer_id (required), mobile, tablet, +2 more. Returns: result
set_final_urlsA
Update final URLs and/or final URL suffixes in one atomic mutate.
targets: [{"resource_name": <ad (adGroupAds/..~..) | ad group (adGroups/..) | asset (assets/..)>, "final_url": "https://..." (ads and assets), "final_url_suffix": "utm_..." or "" to clear (ad groups and ads)}, ...] Ads keep their text and history: Google allows changing an ad's final URLs in place. Typical use: move UTMs from an ad-group suffix into the final URL itself, or retag a campaign. Dry run unless confirm=true.
Inputs: targets (required), customer_id (required), confirm. Returns: result
set_location_bid_modifiersA
Set campaign-level LOCATION bid adjustments in percent (-90 to +900) on locations the campaign already targets. modifiers maps a geo target constant id (2840 = US, 2344 = Hong Kong; also accepts "geoTargetConstants/2840") to the adjustment: {"2840": 30, "2344": -30}. 0 removes an adjustment. Honoured by Manual CPC and Maximize clicks; Smart Bidding (tCPA / Maximize conversions) ignores location adjustments. Targeting is never changed: a geo the campaign does not target is rejected. One atomic mutate; dry run unless confirm=true.
Inputs: modifiers (required), campaign_id (required), customer_id (required), confirm. Returns: result
set_statusA
Set ENABLED or PAUSED on a campaign, ad group, ad (adGroupAds/......) or keyword (adGroupCriteria/......).
resource_name must be the full resource name from the read MCP, e.g. customers/123/campaigns/456.
Dry run unless confirm=true. (REMOVED is intentionally not supported.)
Inputs: status (required), customer_id (required), resource_name (required), confirm. Returns: result
update_responsive_search_adA
Edit an EXISTING responsive search ad in place (AdService update): the ad keeps its id and reporting history, goes back through policy review, and its asset performance labels / Ad Strength reset. Every argument left None is unchanged. headlines / descriptions, when given, REPLACE the whole list (3-15 headlines <=30 chars, 2-4 descriptions <=90 chars; items are strings or {"text", "pin": "HEADLINE_1|HEADLINE_2|HEADLINE_3|DESCRIPTION_1|DESCRIPTION_2"}). path1 / path2: "" clears. ad_id: the numeric ad id or an adGroupAds/~ resource name. Dry run unless confirm=true.
Inputs: ad_id (required), customer_id (required), path1, path2, +4 more. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 22 tools
Each tool clearly targets a distinct Google Ads entity or lifecycle action: campaign creation, ad group addition, keyword/negative keyword management, RSA create/update, asset extensions, bid/budget settings, and removal. The only near-overlap, set_campaign_final_url_suffix versus set_final_urls, is clearly separated by campaign-level vs ad/ad-group/asset-level scope.
The majority of tools follow a clear snake_case verb_noun pattern using create_, add_, set_, update_, and remove_. The exceptions are keyword_ideas and audit_log_tail, which are noun-phrase read/utility tools, and the create_/ add_ distinction is subtle, so the set is mostly consistent but not perfectly uniform.
At 22 tools, the server sits in the heavy range and feels larger than the typical well-scoped toolset. However, the count is understandable given the breadth of Google Ads write operations, and most tools cover distinct, non-redundant actions.
The surface covers the core Search campaign lifecycle: create campaign/ad group/RSA, manage keywords and negatives, attach sitelinks/callouts/snippets/image/business assets, update budget/bids/status/final URLs, and remove entities. Minor gaps remain, such as keyword bid updates and in-place extension edits, but they can be worked around with remove/re-add.
Maintenance
Related MCP Connectors
Google Ads MCP server — manage campaigns, keywords, and metrics.
Google Ads MCP: reports, search terms, negatives, budgets, campaigns. Approval on every write.
Google Ads MCP server: run Search, Display, Demand Gen and PMax campaigns from AI.
1Google Ads MCP with 20,000+ account peer context and staged approve-then-execute writes.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceMCP server that exposes write operations on Google Ads, enabling management of campaigns, ad groups, keywords, RSA ads, sitelinks, images, Customer Match audiences, and recommendations.MIT
- AlicenseAqualityCmaintenanceAn MCP server that extends the official Google Ads MCP with safe, confirmed campaign administration across manager accounts, featuring immutable mutation plans, multi-step approvals, and post-write verification.8Apache 2.0
- AlicenseNot gradedqualityDmaintenanceMCP server for managing Google Ads campaigns through the official Google Ads API, covering accounts, campaigns, budgets, keywords, search terms, and keyword ideas. It provides tools for both reading and mutating live ads data, such as pausing campaigns, updating budgets, and adding keywords.MIT
- AlicenseAqualityCmaintenanceAn MCP server that provides read and write access to Google Ads, allowing natural language management of campaigns, budgets, ad groups, bids, and keywords, with dry-run validation for safety.18MIT