@channel47/google-ads-mcp 1.0.6 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,7 +3,9 @@
3
3
  [![npm version](https://badge.fury.io/js/@channel47%2Fgoogle-ads-mcp.svg)](https://www.npmjs.com/package/@channel47/google-ads-mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- MCP server for Google Ads API access via GAQL (Google Ads Query Language).
6
+ MCP server for Google Ads API access via GAQL (Google Ads Query Language). Built by a practitioner managing 25+ ad accounts daily — not a demo.
7
+
8
+ Part of [Channel 47](https://channel47.dev), the open-source ecosystem of profession plugins for Claude Code. [Get the newsletter](https://channel47.dev/subscribe) for weekly skill breakdowns from production use.
7
9
 
8
10
  ## Overview
9
11
 
@@ -13,7 +15,7 @@ This is a Model Context Protocol (MCP) server that provides tools for querying a
13
15
 
14
16
  ### For Claude Code Plugin Users
15
17
 
16
- This package is automatically installed when using the [google-ads-specialist Claude Code plugin](https://marketplace.claude.com/plugins/google-ads-specialist). No manual installation required!
18
+ This package is bundled with the [media-buyer Claude Code plugin](https://channel47.dev). No manual installation required.
17
19
 
18
20
  ### Standalone Use
19
21
 
@@ -105,6 +107,132 @@ Execute write operations using GoogleAdsService.Mutate.
105
107
  }
106
108
  ```
107
109
 
110
+ #### Working with Responsive Search Ads (RSAs)
111
+
112
+ RSAs have two different resource types with different mutability:
113
+
114
+ | Resource | Entity | Use Case |
115
+ |----------|--------|----------|
116
+ | `customers/{id}/ads/{ad_id}` | `ad` | Update ad **content** (headlines, descriptions, URLs) |
117
+ | `customers/{id}/adGroupAds/{ad_group_id}~{ad_id}` | `ad_group_ad` | Change ad **status** (pause, enable, remove) |
118
+
119
+ **Update RSA headlines/descriptions** (use `entity: 'ad'`):
120
+ ```javascript
121
+ {
122
+ "operations": [{
123
+ "entity": "ad",
124
+ "operation": "update",
125
+ "resource": {
126
+ "resource_name": "customers/1234567890/ads/9876543210",
127
+ "responsive_search_ad": {
128
+ "headlines": [
129
+ {"text": "New Headline 1"},
130
+ {"text": "New Headline 2"},
131
+ {"text": "New Headline 3"}
132
+ ],
133
+ "descriptions": [
134
+ {"text": "New Description 1"},
135
+ {"text": "New Description 2"}
136
+ ]
137
+ },
138
+ "final_urls": ["https://example.com"]
139
+ }
140
+ }],
141
+ "dry_run": false
142
+ }
143
+ ```
144
+
145
+ **Change RSA status** (use `entity: 'ad_group_ad'`):
146
+ ```javascript
147
+ {
148
+ "operations": [{
149
+ "entity": "ad_group_ad",
150
+ "operation": "update",
151
+ "resource": {
152
+ "resource_name": "customers/1234567890/adGroupAds/111222333~9876543210",
153
+ "status": "PAUSED"
154
+ }
155
+ }],
156
+ "dry_run": false
157
+ }
158
+ ```
159
+
160
+ #### Creating Image Assets with File Paths
161
+
162
+ When creating image assets, you can provide a local file path instead of base64 data:
163
+
164
+ ```javascript
165
+ {
166
+ "operations": [{
167
+ "entity": "asset",
168
+ "operation": "create",
169
+ "resource": {
170
+ "name": "My Image Asset",
171
+ "image_file_path": "/path/to/image.png"
172
+ }
173
+ }]
174
+ }
175
+ ```
176
+
177
+ The server will automatically read the file and convert it to the required base64 format.
178
+
179
+ #### Creating Campaigns
180
+
181
+ Campaign creation requires specific fields since Google Ads API v19.2 (September 2025):
182
+
183
+ | Field | Required | Description |
184
+ |-------|----------|-------------|
185
+ | `name` | Yes | Campaign name |
186
+ | `advertising_channel_type` | Yes | Campaign type (SEARCH, DISPLAY, etc.) |
187
+ | `campaign_budget` | Yes | Reference to budget resource |
188
+ | `bidding strategy` | Yes | One of: `manual_cpc`, `maximize_conversions`, `target_cpa`, etc. |
189
+ | `contains_eu_political_advertising` | Auto | Auto-defaults to `DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING` |
190
+
191
+ **Complete campaign creation example:**
192
+ ```javascript
193
+ {
194
+ "operations": [
195
+ // 1. Create budget first (with temp ID for atomic creation)
196
+ {
197
+ "entity": "campaign_budget",
198
+ "operation": "create",
199
+ "resource": {
200
+ "resource_name": "customers/1234567890/campaignBudgets/-1",
201
+ "name": "My Budget",
202
+ "amount_micros": 10000000,
203
+ "delivery_method": "STANDARD"
204
+ }
205
+ },
206
+ // 2. Create campaign referencing the budget
207
+ {
208
+ "entity": "campaign",
209
+ "operation": "create",
210
+ "resource": {
211
+ "name": "My Search Campaign",
212
+ "advertising_channel_type": "SEARCH",
213
+ "status": "PAUSED",
214
+ "campaign_budget": "customers/1234567890/campaignBudgets/-1",
215
+ "manual_cpc": { "enhanced_cpc_enabled": false },
216
+ "network_settings": {
217
+ "target_google_search": true,
218
+ "target_search_network": true
219
+ }
220
+ }
221
+ }
222
+ ],
223
+ "dry_run": false
224
+ }
225
+ ```
226
+
227
+ **Supported bidding strategies:**
228
+ - `manual_cpc` - Manual cost-per-click
229
+ - `maximize_conversions` - Maximize conversions
230
+ - `maximize_conversion_value` - Maximize conversion value (with optional `target_roas`)
231
+ - `target_cpa` - Target cost-per-acquisition
232
+ - `target_spend` - Maximize clicks (target spend)
233
+ - `target_impression_share` - Target impression share
234
+ - `bidding_strategy` - Reference to portfolio bidding strategy
235
+
108
236
  ## Resources & Prompts
109
237
 
110
238
  The server provides:
@@ -113,14 +241,11 @@ The server provides:
113
241
 
114
242
  ## Usage with Claude Code
115
243
 
116
- This server is designed to work with the [google-ads-specialist plugin](https://marketplace.claude.com/plugins/google-ads-specialist), which provides:
244
+ This server is designed to work with the [media-buyer plugin](https://github.com/channel47/channel47), which provides:
117
245
 
118
- - **9 Skill Files**: Progressive disclosure of GAQL patterns and best practices
119
- - Atomic skills for focused tasks (campaign performance, search terms, wasted spend, etc.)
120
- - Playbooks for comprehensive workflows (account health audit)
121
- - Troubleshooting guides for common errors
122
- - **PreToolUse Hook**: Validates skill references before query/mutate operations
123
- - **Comprehensive Documentation**: Setup guides and OAuth configuration help
246
+ - **Skills** for campaign building, creative testing, and account audits
247
+ - **PreToolUse Hook** that validates mutations before execution
248
+ - **Reference docs** with GAQL patterns, ad copy formulas, and performance benchmarks
124
249
 
125
250
  The plugin ensures Claude consults domain knowledge before executing queries, preventing hallucinated GAQL.
126
251
 
@@ -135,8 +260,8 @@ The plugin ensures Claude consults domain knowledge before executing queries, pr
135
260
  ### Setup
136
261
 
137
262
  ```bash
138
- git clone https://github.com/channel47/google-ads-mcp-server.git
139
- cd google-ads-mcp-server
263
+ git clone https://github.com/channel47/mcps.git
264
+ cd mcps/google-ads
140
265
  npm install
141
266
  ```
142
267
 
@@ -177,9 +302,10 @@ Contributions welcome! Please:
177
302
 
178
303
  ## Links
179
304
 
305
+ - [Channel 47](https://channel47.dev) — open-source profession plugins for Claude Code
306
+ - [Build Notes Newsletter](https://channel47.dev/subscribe) — weekly skill breakdowns from production use
307
+ - [Media Buyer Plugin](https://github.com/channel47/channel47) — the full paid-search toolkit this MCP powers
180
308
  - [NPM Package](https://www.npmjs.com/package/@channel47/google-ads-mcp)
181
- - [GitHub Repository](https://github.com/channel47/google-ads-mcp-server)
182
- - [Claude Code Plugin](https://marketplace.claude.com/plugins/google-ads-specialist)
183
309
  - [Google Ads API Documentation](https://developers.google.com/google-ads/api/docs/start)
184
310
  - [GAQL Reference](https://developers.google.com/google-ads/api/docs/query/overview)
185
311
  - [Model Context Protocol](https://modelcontextprotocol.io)
@@ -191,5 +317,5 @@ MIT - See [LICENSE](LICENSE) file for details.
191
317
  ## Support
192
318
 
193
319
  For issues or questions:
194
- - Plugin-related: [Plugin Repository Issues](https://github.com/ctrlswing/channel47-marketplace/issues)
195
- - Server-related: [Server Repository Issues](https://github.com/channel47/google-ads-mcp-server/issues)
320
+ - Plugin-related: [Plugin Repository Issues](https://github.com/channel47/channel47/issues)
321
+ - Server-related: [Server Repository Issues](https://github.com/channel47/mcps/issues)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@channel47/google-ads-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Google Ads MCP Server - Query and mutate Google Ads data using GAQL",
5
5
  "main": "server/index.js",
6
6
  "bin": {
@@ -31,12 +31,13 @@
31
31
  "license": "MIT",
32
32
  "repository": {
33
33
  "type": "git",
34
- "url": "https://github.com/channel47/google-ads-mcp-server.git"
34
+ "url": "https://github.com/channel47/mcps.git",
35
+ "directory": "google-ads"
35
36
  },
36
37
  "bugs": {
37
- "url": "https://github.com/channel47/google-ads-mcp-server/issues"
38
+ "url": "https://github.com/channel47/mcps/issues"
38
39
  },
39
- "homepage": "https://github.com/channel47/google-ads-mcp-server#readme",
40
+ "homepage": "https://github.com/channel47/mcps/tree/main/google-ads#readme",
40
41
  "dependencies": {
41
42
  "@modelcontextprotocol/sdk": "^1.0.0",
42
43
  "google-ads-api": "^21.0.1",
@@ -51,8 +51,8 @@ export async function mutate(params) {
51
51
  try {
52
52
  // Execute mutation with validation options
53
53
  response = await customer.mutateResources(normalizedOps, {
54
- partialFailure: partial_failure,
55
- validateOnly: dry_run
54
+ partial_failure: partial_failure,
55
+ validate_only: dry_run
56
56
  });
57
57
  } catch (error) {
58
58
  // The Opteo library throws exceptions with error.errors array for partial failures
@@ -11,9 +11,36 @@ import { transformImageAssetResource } from './image-asset.js';
11
11
  * See: https://developers.google.com/google-ads/api/docs/mutating/overview
12
12
  */
13
13
  const ENTITIES_REQUIRING_RESOURCE_NAME_IN_CREATE = new Set([
14
- 'campaign_budget' // Used for temp IDs when creating budget + campaign atomically
14
+ 'campaign_budget', // Used for temp IDs when creating budget + campaign atomically
15
+ 'campaign' // Used for temp IDs when creating campaign + asset group atomically (PMax)
15
16
  ]);
16
17
 
18
+ /**
19
+ * Valid bidding strategy fields for campaigns.
20
+ * Exactly ONE must be present for campaign creation.
21
+ * See: https://developers.google.com/google-ads/api/docs/campaigns/bidding/assign-strategies
22
+ */
23
+ const CAMPAIGN_BIDDING_STRATEGIES = [
24
+ 'manual_cpc',
25
+ 'manual_cpm',
26
+ 'manual_cpv',
27
+ 'maximize_conversions',
28
+ 'maximize_conversion_value',
29
+ 'target_cpa',
30
+ 'target_roas',
31
+ 'target_spend',
32
+ 'target_impression_share',
33
+ 'percent_cpc',
34
+ 'commission',
35
+ 'bidding_strategy' // Portfolio bidding strategy reference
36
+ ];
37
+
38
+ /**
39
+ * Default value for EU political advertising field.
40
+ * Required for all campaign creation since API v19.2 (September 2025).
41
+ */
42
+ const DEFAULT_EU_POLITICAL_ADVERTISING = 'DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING';
43
+
17
44
  /**
18
45
  * Resource name URL path segments to entity type mapping
19
46
  * Based on Google Ads API resource name patterns
@@ -28,8 +55,8 @@ const RESOURCE_PATH_TO_ENTITY = {
28
55
  'sharedCriteria': 'shared_criterion',
29
56
  'campaignBudgets': 'campaign_budget',
30
57
  'biddingStrategies': 'bidding_strategy',
31
- 'ads': 'ad_group_ad',
32
- 'adGroupAds': 'ad_group_ad',
58
+ 'ads': 'ad', // For ad content updates (RSA headlines, descriptions, URLs)
59
+ 'adGroupAds': 'ad_group_ad', // For ad group membership and status changes
33
60
  'assets': 'asset',
34
61
  'conversionActions': 'conversion_action',
35
62
  'customerNegativeCriteria': 'customer_negative_criterion',
@@ -121,6 +148,42 @@ function inferEntityFromCreateResource(resource) {
121
148
  return null;
122
149
  }
123
150
 
151
+ /**
152
+ * Process campaign resource for CREATE operations.
153
+ * - Adds required contains_eu_political_advertising field if missing
154
+ * - Validates bidding strategy is present
155
+ * @param {Object} resource - Campaign resource object
156
+ * @param {number} index - Operation index for error messages
157
+ * @returns {{ resource: Object, warnings: string[] }} - Processed resource and warnings
158
+ * @throws {Error} - If validation fails
159
+ */
160
+ function processCampaignCreate(resource, index) {
161
+ const warnings = [];
162
+ let processedResource = { ...resource };
163
+
164
+ // Add EU political advertising field if missing (required since API v19.2)
165
+ if (!processedResource.contains_eu_political_advertising) {
166
+ processedResource.contains_eu_political_advertising = DEFAULT_EU_POLITICAL_ADVERTISING;
167
+ warnings.push(
168
+ `Operation ${index}: Auto-added contains_eu_political_advertising='${DEFAULT_EU_POLITICAL_ADVERTISING}' (required since API v19.2)`
169
+ );
170
+ }
171
+
172
+ // Validate bidding strategy is present
173
+ const hasBiddingStrategy = CAMPAIGN_BIDDING_STRATEGIES.some(
174
+ field => processedResource[field] !== undefined
175
+ );
176
+
177
+ if (!hasBiddingStrategy) {
178
+ throw new Error(
179
+ `Operation ${index}: Campaign CREATE requires a bidding strategy. ` +
180
+ `Set one of: ${CAMPAIGN_BIDDING_STRATEGIES.join(', ')}`
181
+ );
182
+ }
183
+
184
+ return { resource: processedResource, warnings };
185
+ }
186
+
124
187
  /**
125
188
  * Check if operation is already in Opteo format
126
189
  * @param {Object} operation
@@ -226,10 +289,19 @@ function transformToOpteoFormat(operation, index) {
226
289
  }
227
290
  }
228
291
 
292
+ // Process campaign CREATE operations (EU political advertising, bidding strategy validation)
293
+ let campaignWarnings = [];
294
+ if (opType === 'create' && entity === 'campaign') {
295
+ const campaignResult = processCampaignCreate(resource, index);
296
+ resource = campaignResult.resource;
297
+ campaignWarnings = campaignResult.warnings;
298
+ }
299
+
229
300
  return {
230
301
  entity,
231
302
  operation: opType,
232
- resource
303
+ resource,
304
+ _campaignWarnings: campaignWarnings // Pass warnings up for logging
233
305
  };
234
306
  }
235
307
 
@@ -248,6 +320,7 @@ export function normalizeOperations(operations) {
248
320
 
249
321
  if (isOpteoFormat(op)) {
250
322
  // Already in Opteo format - pass through, but normalize special cases
323
+ let normalizedOp = { ...op };
251
324
 
252
325
  // Handle image_file_path in Opteo format asset operations
253
326
  if (op.entity === 'asset' && op.operation === 'create' && op.resource?.image_file_path) {
@@ -256,28 +329,33 @@ export function normalizeOperations(operations) {
256
329
  throw new Error(`Operation ${i}: ${imageResult.error}`);
257
330
  }
258
331
  if (imageResult.processed) {
259
- normalizedOps.push({
260
- ...op,
261
- resource: imageResult.resource
262
- });
263
- continue;
332
+ normalizedOp.resource = imageResult.resource;
264
333
  }
265
334
  }
266
335
 
336
+ // Handle campaign CREATE operations (EU political advertising, bidding strategy)
337
+ if (op.entity === 'campaign' && op.operation === 'create' && op.resource) {
338
+ const campaignResult = processCampaignCreate(op.resource, i);
339
+ normalizedOp.resource = campaignResult.resource;
340
+ warnings.push(...campaignResult.warnings);
341
+ }
342
+
267
343
  // For remove ops, ensure resource is the string, not an object
268
344
  if (op.operation === 'remove' && op.resource && typeof op.resource === 'object') {
269
- normalizedOps.push({
270
- ...op,
271
- resource: op.resource.resource_name || op.resource
272
- });
273
- } else {
274
- normalizedOps.push(op);
345
+ normalizedOp.resource = op.resource.resource_name || op.resource;
275
346
  }
347
+
348
+ normalizedOps.push(normalizedOp);
276
349
  } else {
277
350
  // Transform from standard format
278
351
  const transformed = transformToOpteoFormat(op, i);
279
- normalizedOps.push(transformed);
280
- warnings.push(`Operation ${i}: Transformed from standard format (entity: ${transformed.entity})`);
352
+ // Collect campaign warnings and strip internal property
353
+ if (transformed._campaignWarnings?.length > 0) {
354
+ warnings.push(...transformed._campaignWarnings);
355
+ }
356
+ const { _campaignWarnings, ...cleanTransformed } = transformed;
357
+ normalizedOps.push(cleanTransformed);
358
+ warnings.push(`Operation ${i}: Transformed from standard format (entity: ${cleanTransformed.entity})`);
281
359
  }
282
360
  }
283
361