@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 +141 -15
- package/package.json +5 -4
- package/server/tools/mutate.js +2 -2
- package/server/utils/operation-transform.js +95 -17
package/README.md
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@channel47/google-ads-mcp)
|
|
4
4
|
[](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
|
|
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 [
|
|
244
|
+
This server is designed to work with the [media-buyer plugin](https://github.com/channel47/channel47), which provides:
|
|
117
245
|
|
|
118
|
-
- **
|
|
119
|
-
|
|
120
|
-
|
|
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/
|
|
139
|
-
cd google-ads
|
|
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/
|
|
195
|
-
- Server-related: [Server Repository Issues](https://github.com/channel47/
|
|
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.
|
|
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/
|
|
34
|
+
"url": "https://github.com/channel47/mcps.git",
|
|
35
|
+
"directory": "google-ads"
|
|
35
36
|
},
|
|
36
37
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/channel47/
|
|
38
|
+
"url": "https://github.com/channel47/mcps/issues"
|
|
38
39
|
},
|
|
39
|
-
"homepage": "https://github.com/channel47/google-ads
|
|
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",
|
package/server/tools/mutate.js
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
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': '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
|