@channel47/google-ads-mcp 1.0.0
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/LICENSE +21 -0
- package/README.md +195 -0
- package/package.json +48 -0
- package/server/auth.js +74 -0
- package/server/index.js +199 -0
- package/server/prompts/templates.js +231 -0
- package/server/resources/index.js +67 -0
- package/server/tools/gaql-query.js +141 -0
- package/server/tools/list-accounts.js +61 -0
- package/server/tools/mutate.js +64 -0
- package/server/utils/gaql-templates.js +417 -0
- package/server/utils/mutations.js +436 -0
- package/server/utils/query-validator.js +74 -0
- package/server/utils/response-format.js +138 -0
- package/server/utils/validation.js +166 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { validateQuery, logValidation } from './query-validator.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GAQL query templates with placeholder substitution
|
|
5
|
+
*/
|
|
6
|
+
export const TEMPLATES = {
|
|
7
|
+
// List accounts query
|
|
8
|
+
LIST_ACCOUNTS: `
|
|
9
|
+
SELECT
|
|
10
|
+
customer_client.id,
|
|
11
|
+
customer_client.descriptive_name,
|
|
12
|
+
customer_client.manager,
|
|
13
|
+
customer_client.currency_code,
|
|
14
|
+
customer_client.time_zone,
|
|
15
|
+
customer_client.status
|
|
16
|
+
FROM customer_client
|
|
17
|
+
{{FILTERS}}
|
|
18
|
+
`,
|
|
19
|
+
|
|
20
|
+
// Campaign performance
|
|
21
|
+
CAMPAIGN_PERFORMANCE: `
|
|
22
|
+
SELECT
|
|
23
|
+
campaign.id,
|
|
24
|
+
campaign.name,
|
|
25
|
+
campaign.status,
|
|
26
|
+
campaign.advertising_channel_type,
|
|
27
|
+
{{METRICS}}
|
|
28
|
+
FROM campaign
|
|
29
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
30
|
+
{{FILTERS}}
|
|
31
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
32
|
+
LIMIT {{LIMIT}}
|
|
33
|
+
`,
|
|
34
|
+
|
|
35
|
+
// Ad group performance
|
|
36
|
+
AD_GROUP_PERFORMANCE: `
|
|
37
|
+
SELECT
|
|
38
|
+
ad_group.id,
|
|
39
|
+
ad_group.name,
|
|
40
|
+
ad_group.status,
|
|
41
|
+
campaign.id,
|
|
42
|
+
campaign.name,
|
|
43
|
+
{{METRICS}}
|
|
44
|
+
FROM ad_group
|
|
45
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
46
|
+
{{FILTERS}}
|
|
47
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
48
|
+
LIMIT {{LIMIT}}
|
|
49
|
+
`,
|
|
50
|
+
|
|
51
|
+
// Keyword performance
|
|
52
|
+
KEYWORD_PERFORMANCE: `
|
|
53
|
+
SELECT
|
|
54
|
+
ad_group_criterion.criterion_id,
|
|
55
|
+
ad_group_criterion.keyword.text,
|
|
56
|
+
ad_group_criterion.keyword.match_type,
|
|
57
|
+
ad_group_criterion.status,
|
|
58
|
+
ad_group.id,
|
|
59
|
+
ad_group.name,
|
|
60
|
+
campaign.id,
|
|
61
|
+
campaign.name,
|
|
62
|
+
{{METRICS}}
|
|
63
|
+
FROM keyword_view
|
|
64
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
65
|
+
{{FILTERS}}
|
|
66
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
67
|
+
LIMIT {{LIMIT}}
|
|
68
|
+
`,
|
|
69
|
+
|
|
70
|
+
// Product group performance
|
|
71
|
+
PRODUCT_GROUP_PERFORMANCE: `
|
|
72
|
+
SELECT
|
|
73
|
+
shopping_performance_view.product_item_id,
|
|
74
|
+
shopping_performance_view.product_title,
|
|
75
|
+
shopping_performance_view.product_brand,
|
|
76
|
+
campaign.id,
|
|
77
|
+
campaign.name,
|
|
78
|
+
{{METRICS}}
|
|
79
|
+
FROM shopping_performance_view
|
|
80
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
81
|
+
{{FILTERS}}
|
|
82
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
83
|
+
LIMIT {{LIMIT}}
|
|
84
|
+
`,
|
|
85
|
+
|
|
86
|
+
// Search terms report
|
|
87
|
+
SEARCH_TERMS: `
|
|
88
|
+
SELECT
|
|
89
|
+
search_term_view.search_term,
|
|
90
|
+
search_term_view.status,
|
|
91
|
+
ad_group.id,
|
|
92
|
+
ad_group.name,
|
|
93
|
+
campaign.id,
|
|
94
|
+
campaign.name,
|
|
95
|
+
{{METRICS}}
|
|
96
|
+
FROM search_term_view
|
|
97
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
98
|
+
AND metrics.impressions >= {{MIN_IMPRESSIONS}}
|
|
99
|
+
{{FILTERS}}
|
|
100
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
101
|
+
LIMIT {{LIMIT}}
|
|
102
|
+
`,
|
|
103
|
+
|
|
104
|
+
// Wasted spend - search terms
|
|
105
|
+
WASTED_SPEND_SEARCH_TERMS: `
|
|
106
|
+
SELECT
|
|
107
|
+
search_term_view.search_term,
|
|
108
|
+
ad_group.id,
|
|
109
|
+
ad_group.name,
|
|
110
|
+
campaign.id,
|
|
111
|
+
campaign.name,
|
|
112
|
+
metrics.cost_micros,
|
|
113
|
+
metrics.conversions,
|
|
114
|
+
metrics.clicks,
|
|
115
|
+
metrics.impressions
|
|
116
|
+
FROM search_term_view
|
|
117
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
118
|
+
AND metrics.cost_micros >= {{MIN_COST_MICROS}}
|
|
119
|
+
AND metrics.conversions = {{MAX_CONVERSIONS}}
|
|
120
|
+
{{FILTERS}}
|
|
121
|
+
ORDER BY metrics.cost_micros DESC
|
|
122
|
+
LIMIT {{LIMIT}}
|
|
123
|
+
`,
|
|
124
|
+
|
|
125
|
+
// Wasted spend - keywords
|
|
126
|
+
WASTED_SPEND_KEYWORDS: `
|
|
127
|
+
SELECT
|
|
128
|
+
ad_group_criterion.criterion_id,
|
|
129
|
+
ad_group_criterion.keyword.text,
|
|
130
|
+
ad_group_criterion.keyword.match_type,
|
|
131
|
+
ad_group.id,
|
|
132
|
+
ad_group.name,
|
|
133
|
+
campaign.id,
|
|
134
|
+
campaign.name,
|
|
135
|
+
metrics.cost_micros,
|
|
136
|
+
metrics.conversions,
|
|
137
|
+
metrics.clicks,
|
|
138
|
+
metrics.impressions
|
|
139
|
+
FROM keyword_view
|
|
140
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
141
|
+
AND metrics.cost_micros >= {{MIN_COST_MICROS}}
|
|
142
|
+
AND metrics.conversions = {{MAX_CONVERSIONS}}
|
|
143
|
+
{{FILTERS}}
|
|
144
|
+
ORDER BY metrics.cost_micros DESC
|
|
145
|
+
LIMIT {{LIMIT}}
|
|
146
|
+
`,
|
|
147
|
+
|
|
148
|
+
// Account average CPA (for smart threshold)
|
|
149
|
+
ACCOUNT_CPA: `
|
|
150
|
+
SELECT
|
|
151
|
+
metrics.cost_per_conversion
|
|
152
|
+
FROM customer
|
|
153
|
+
WHERE segments.date DURING LAST_30_DAYS
|
|
154
|
+
`,
|
|
155
|
+
|
|
156
|
+
// ============================================
|
|
157
|
+
// PHASE 2: Quality & Budget Analysis Templates
|
|
158
|
+
// ============================================
|
|
159
|
+
|
|
160
|
+
// Quality Score Analysis
|
|
161
|
+
QUALITY_SCORE_ANALYSIS: `
|
|
162
|
+
SELECT
|
|
163
|
+
ad_group_criterion.criterion_id,
|
|
164
|
+
ad_group_criterion.keyword.text,
|
|
165
|
+
ad_group_criterion.keyword.match_type,
|
|
166
|
+
ad_group_criterion.status,
|
|
167
|
+
ad_group_criterion.quality_info.quality_score,
|
|
168
|
+
ad_group_criterion.quality_info.creative_quality_score,
|
|
169
|
+
ad_group_criterion.quality_info.post_click_quality_score,
|
|
170
|
+
ad_group_criterion.quality_info.search_predicted_ctr,
|
|
171
|
+
ad_group.id,
|
|
172
|
+
ad_group.name,
|
|
173
|
+
campaign.id,
|
|
174
|
+
campaign.name,
|
|
175
|
+
metrics.impressions,
|
|
176
|
+
metrics.clicks,
|
|
177
|
+
metrics.cost_micros,
|
|
178
|
+
metrics.conversions
|
|
179
|
+
FROM keyword_view
|
|
180
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
181
|
+
AND ad_group_criterion.status = 'ENABLED'
|
|
182
|
+
AND metrics.impressions >= {{MIN_IMPRESSIONS}}
|
|
183
|
+
{{FILTERS}}
|
|
184
|
+
ORDER BY metrics.cost_micros DESC
|
|
185
|
+
LIMIT {{LIMIT}}
|
|
186
|
+
`,
|
|
187
|
+
|
|
188
|
+
// Budget Pacing - Campaign Budgets
|
|
189
|
+
CAMPAIGN_BUDGET: `
|
|
190
|
+
SELECT
|
|
191
|
+
campaign.id,
|
|
192
|
+
campaign.name,
|
|
193
|
+
campaign.status,
|
|
194
|
+
campaign.advertising_channel_type,
|
|
195
|
+
campaign_budget.id,
|
|
196
|
+
campaign_budget.name,
|
|
197
|
+
campaign_budget.amount_micros,
|
|
198
|
+
campaign_budget.period,
|
|
199
|
+
campaign_budget.delivery_method,
|
|
200
|
+
campaign_budget.explicitly_shared,
|
|
201
|
+
campaign_budget.has_recommended_budget,
|
|
202
|
+
campaign_budget.recommended_budget_amount_micros,
|
|
203
|
+
metrics.cost_micros,
|
|
204
|
+
metrics.impressions,
|
|
205
|
+
metrics.clicks,
|
|
206
|
+
metrics.conversions
|
|
207
|
+
FROM campaign
|
|
208
|
+
WHERE campaign.status = 'ENABLED'
|
|
209
|
+
AND segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
210
|
+
{{FILTERS}}
|
|
211
|
+
`,
|
|
212
|
+
|
|
213
|
+
// Budget Pacing - Yesterday's spend for daily rate
|
|
214
|
+
CAMPAIGN_DAILY_SPEND: `
|
|
215
|
+
SELECT
|
|
216
|
+
campaign.id,
|
|
217
|
+
campaign.name,
|
|
218
|
+
metrics.cost_micros
|
|
219
|
+
FROM campaign
|
|
220
|
+
WHERE campaign.status = 'ENABLED'
|
|
221
|
+
AND segments.date = '{{DATE}}'
|
|
222
|
+
{{FILTERS}}
|
|
223
|
+
`,
|
|
224
|
+
|
|
225
|
+
// ============================================
|
|
226
|
+
// PHASE 3: Shopping & Segmentation Templates
|
|
227
|
+
// ============================================
|
|
228
|
+
|
|
229
|
+
// Shopping Product Status
|
|
230
|
+
SHOPPING_PRODUCT_STATUS: `
|
|
231
|
+
SELECT
|
|
232
|
+
shopping_product.resource_name,
|
|
233
|
+
shopping_product.merchant_center_id,
|
|
234
|
+
shopping_product.channel,
|
|
235
|
+
shopping_product.language_code,
|
|
236
|
+
shopping_product.feed_label,
|
|
237
|
+
shopping_product.item_id,
|
|
238
|
+
shopping_product.title,
|
|
239
|
+
shopping_product.brand,
|
|
240
|
+
shopping_product.price_micros,
|
|
241
|
+
shopping_product.currency_code,
|
|
242
|
+
shopping_product.status,
|
|
243
|
+
shopping_product.issues
|
|
244
|
+
FROM shopping_product
|
|
245
|
+
WHERE 1=1
|
|
246
|
+
{{FILTERS}}
|
|
247
|
+
LIMIT {{LIMIT}}
|
|
248
|
+
`,
|
|
249
|
+
|
|
250
|
+
// Shopping Performance by Segment
|
|
251
|
+
SHOPPING_PERFORMANCE: `
|
|
252
|
+
SELECT
|
|
253
|
+
{{SEGMENT_FIELD}},
|
|
254
|
+
campaign.id,
|
|
255
|
+
campaign.name,
|
|
256
|
+
campaign.advertising_channel_type,
|
|
257
|
+
{{METRICS}}
|
|
258
|
+
FROM shopping_performance_view
|
|
259
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
260
|
+
AND metrics.impressions >= {{MIN_IMPRESSIONS}}
|
|
261
|
+
{{FILTERS}}
|
|
262
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
263
|
+
LIMIT {{LIMIT}}
|
|
264
|
+
`,
|
|
265
|
+
|
|
266
|
+
// Performance by Hour of Day
|
|
267
|
+
PERFORMANCE_BY_HOUR: `
|
|
268
|
+
SELECT
|
|
269
|
+
segments.hour,
|
|
270
|
+
{{METRICS}}
|
|
271
|
+
FROM campaign
|
|
272
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
273
|
+
{{FILTERS}}
|
|
274
|
+
`,
|
|
275
|
+
|
|
276
|
+
// Performance by Day of Week
|
|
277
|
+
PERFORMANCE_BY_DAY_OF_WEEK: `
|
|
278
|
+
SELECT
|
|
279
|
+
segments.day_of_week,
|
|
280
|
+
{{METRICS}}
|
|
281
|
+
FROM campaign
|
|
282
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
283
|
+
{{FILTERS}}
|
|
284
|
+
`,
|
|
285
|
+
|
|
286
|
+
// Performance by Device
|
|
287
|
+
PERFORMANCE_BY_DEVICE: `
|
|
288
|
+
SELECT
|
|
289
|
+
segments.device,
|
|
290
|
+
{{METRICS}}
|
|
291
|
+
FROM campaign
|
|
292
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
293
|
+
{{FILTERS}}
|
|
294
|
+
`,
|
|
295
|
+
|
|
296
|
+
// Performance by Geographic Location
|
|
297
|
+
PERFORMANCE_BY_GEO: `
|
|
298
|
+
SELECT
|
|
299
|
+
geographic_view.country_criterion_id,
|
|
300
|
+
geographic_view.location_type,
|
|
301
|
+
{{METRICS}}
|
|
302
|
+
FROM geographic_view
|
|
303
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
304
|
+
{{FILTERS}}
|
|
305
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
306
|
+
LIMIT {{LIMIT}}
|
|
307
|
+
`,
|
|
308
|
+
|
|
309
|
+
// Performance by Audience
|
|
310
|
+
PERFORMANCE_BY_AUDIENCE: `
|
|
311
|
+
SELECT
|
|
312
|
+
user_list.id,
|
|
313
|
+
user_list.name,
|
|
314
|
+
user_list.type,
|
|
315
|
+
{{METRICS}}
|
|
316
|
+
FROM user_list
|
|
317
|
+
WHERE segments.date BETWEEN '{{START_DATE}}' AND '{{END_DATE}}'
|
|
318
|
+
{{FILTERS}}
|
|
319
|
+
ORDER BY {{ORDER_BY}} DESC
|
|
320
|
+
LIMIT {{LIMIT}}
|
|
321
|
+
`
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Build GAQL query from template with parameter substitution
|
|
326
|
+
* @param {string} template - Template string with placeholders
|
|
327
|
+
* @param {Object} params - Substitution parameters
|
|
328
|
+
* @returns {string} Complete GAQL query
|
|
329
|
+
*/
|
|
330
|
+
export function buildQuery(template, params = {}) {
|
|
331
|
+
let query = template;
|
|
332
|
+
|
|
333
|
+
// Replace metrics (array → comma-separated string)
|
|
334
|
+
if (params.metrics) {
|
|
335
|
+
const metricsStr = params.metrics.map(m => {
|
|
336
|
+
// Add metrics. prefix if not present
|
|
337
|
+
return m.startsWith('metrics.') ? m : `metrics.${m}`;
|
|
338
|
+
}).join(', ');
|
|
339
|
+
query = query.replace('{{METRICS}}', metricsStr);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Replace dates
|
|
343
|
+
if (params.dates) {
|
|
344
|
+
query = query.replace('{{START_DATE}}', params.dates.start);
|
|
345
|
+
query = query.replace('{{END_DATE}}', params.dates.end);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Replace filters
|
|
349
|
+
const filters = params.filters || '';
|
|
350
|
+
query = query.replace('{{FILTERS}}', filters);
|
|
351
|
+
|
|
352
|
+
// Replace order by
|
|
353
|
+
const orderBy = params.orderBy || 'metrics.cost_micros';
|
|
354
|
+
query = query.replace('{{ORDER_BY}}', orderBy);
|
|
355
|
+
|
|
356
|
+
// Replace limit
|
|
357
|
+
const limit = params.limit || 50;
|
|
358
|
+
query = query.replace('{{LIMIT}}', limit);
|
|
359
|
+
|
|
360
|
+
// Replace min impressions
|
|
361
|
+
const minImpressions = params.minImpressions || 10;
|
|
362
|
+
query = query.replace('{{MIN_IMPRESSIONS}}', minImpressions);
|
|
363
|
+
|
|
364
|
+
// Replace min cost (convert dollars to micros)
|
|
365
|
+
if (params.minCost !== undefined) {
|
|
366
|
+
const minCostMicros = Math.round(params.minCost * 1000000);
|
|
367
|
+
query = query.replace('{{MIN_COST_MICROS}}', minCostMicros);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Replace max conversions
|
|
371
|
+
const maxConversions = params.maxConversions !== undefined ? params.maxConversions : 0;
|
|
372
|
+
query = query.replace('{{MAX_CONVERSIONS}}', maxConversions);
|
|
373
|
+
|
|
374
|
+
// Replace resource ID (for auction insights)
|
|
375
|
+
if (params.resourceId !== undefined) {
|
|
376
|
+
query = query.replace('{{RESOURCE_ID}}', params.resourceId);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Replace single date (for daily spend queries)
|
|
380
|
+
if (params.date) {
|
|
381
|
+
query = query.replace('{{DATE}}', params.date);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Replace max quality score filter
|
|
385
|
+
if (params.maxQualityScore !== undefined) {
|
|
386
|
+
query = query.replace('{{MAX_QUALITY_SCORE}}', params.maxQualityScore);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Replace segment field (for shopping performance)
|
|
390
|
+
if (params.segmentField) {
|
|
391
|
+
query = query.replace('{{SEGMENT_FIELD}}', params.segmentField);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Replace status filter (for shopping product status)
|
|
395
|
+
if (params.statusFilter) {
|
|
396
|
+
query = query.replace('{{STATUS_FILTER}}', `= '${params.statusFilter}'`);
|
|
397
|
+
} else {
|
|
398
|
+
// If no status filter, match all statuses
|
|
399
|
+
query = query.replace('{{STATUS_FILTER}}', 'IS NOT NULL');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Replace geo level (for geographic queries)
|
|
403
|
+
if (params.geoLevel) {
|
|
404
|
+
query = query.replace('{{GEO_LEVEL}}', params.geoLevel);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Clean up whitespace
|
|
408
|
+
const finalQuery = query.trim().replace(/\s+/g, ' ');
|
|
409
|
+
|
|
410
|
+
// Validate the query (logs warnings/errors if any)
|
|
411
|
+
const validation = validateQuery(finalQuery);
|
|
412
|
+
if (!validation.valid) {
|
|
413
|
+
logValidation(finalQuery, validation);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return finalQuery;
|
|
417
|
+
}
|