@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,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt templates for Google Ads MCP Server
|
|
3
|
+
* Each prompt defines a workflow that chains multiple tools together
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const PROMPT_TEMPLATES = {
|
|
7
|
+
// Phase 2 prompt
|
|
8
|
+
quick_health_check: {
|
|
9
|
+
name: 'quick_health_check',
|
|
10
|
+
description: 'Fast daily check on account status',
|
|
11
|
+
arguments: [
|
|
12
|
+
{
|
|
13
|
+
name: 'customer_id',
|
|
14
|
+
description: 'Google Ads account ID (without dashes)',
|
|
15
|
+
required: true
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
template: `Quick health check for Google Ads account {{customer_id}}:
|
|
19
|
+
|
|
20
|
+
1. Yesterday's spend vs daily average - any anomalies?
|
|
21
|
+
2. Any campaigns with $0 spend that should be running?
|
|
22
|
+
3. Budget pacing - anything hitting limits early?
|
|
23
|
+
4. Any new disapprovals or policy issues?
|
|
24
|
+
5. Conversion tracking - are conversions recording normally?
|
|
25
|
+
|
|
26
|
+
Just flag issues, don't deep-dive unless something's wrong.
|
|
27
|
+
|
|
28
|
+
To complete this check:
|
|
29
|
+
- Use get_performance with date_range YESTERDAY to see yesterday's metrics
|
|
30
|
+
- Use budget_pacing to check for campaigns limited by budget
|
|
31
|
+
- Compare yesterday's spend to the 7-day average
|
|
32
|
+
- Flag any enabled campaigns with zero impressions or spend`,
|
|
33
|
+
requiredTools: ['get_performance', 'budget_pacing']
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Phase 3 prompt - fully functional
|
|
37
|
+
weekly_account_review: {
|
|
38
|
+
name: 'weekly_account_review',
|
|
39
|
+
description: 'Comprehensive weekly performance review for search and shopping campaigns',
|
|
40
|
+
arguments: [
|
|
41
|
+
{
|
|
42
|
+
name: 'customer_id',
|
|
43
|
+
description: 'Google Ads account ID (without dashes)',
|
|
44
|
+
required: true
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
template: `Run a weekly account review for Google Ads account {{customer_id}}:
|
|
48
|
+
|
|
49
|
+
1. Start with campaign-level performance for the last 7 days vs prior 7 days
|
|
50
|
+
2. Identify the top 3 campaigns by spend and analyze their trend
|
|
51
|
+
3. Check search terms report for wasted spend (high cost, no conversions)
|
|
52
|
+
4. Review Quality Score distribution - flag any high-spend keywords with QS < 5
|
|
53
|
+
5. Check budget pacing - are any campaigns limited?
|
|
54
|
+
6. For shopping campaigns, check for product disapprovals
|
|
55
|
+
7. Analyze device performance - any major mobile vs desktop differences?
|
|
56
|
+
8. Summarize top 3 opportunities and top 3 concerns
|
|
57
|
+
|
|
58
|
+
Keep the analysis actionable and prioritized by impact.
|
|
59
|
+
|
|
60
|
+
To complete this review:
|
|
61
|
+
- Use get_performance with level=campaign for current and prior period
|
|
62
|
+
- Use search_terms_report and find_wasted_spend to identify waste
|
|
63
|
+
- Use quality_score_analysis with max_quality_score=5 to find QS issues
|
|
64
|
+
- Use budget_pacing to check for limited campaigns
|
|
65
|
+
- Use shopping_product_status to check feed health
|
|
66
|
+
- Use performance_by_dimension with dimension=device for device analysis`,
|
|
67
|
+
requiredTools: ['get_performance', 'search_terms_report', 'find_wasted_spend', 'quality_score_analysis', 'budget_pacing', 'shopping_product_status', 'performance_by_dimension']
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
negative_keyword_mining: {
|
|
71
|
+
name: 'negative_keyword_mining',
|
|
72
|
+
description: 'Find and add negative keywords from search terms data',
|
|
73
|
+
arguments: [
|
|
74
|
+
{
|
|
75
|
+
name: 'customer_id',
|
|
76
|
+
description: 'Google Ads account ID (without dashes)',
|
|
77
|
+
required: true
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'date_range',
|
|
81
|
+
description: 'Date range to analyze',
|
|
82
|
+
required: false,
|
|
83
|
+
default: 'LAST_30_DAYS'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'min_spend',
|
|
87
|
+
description: 'Minimum spend threshold in dollars',
|
|
88
|
+
required: false,
|
|
89
|
+
default: '20'
|
|
90
|
+
}
|
|
91
|
+
],
|
|
92
|
+
template: `Analyze search terms for {{customer_id}} over the last {{date_range}}:
|
|
93
|
+
|
|
94
|
+
1. Pull search terms with spend > \${{min_spend}} and 0 conversions
|
|
95
|
+
2. Group them into themes (irrelevant intent, competitor, informational, etc.)
|
|
96
|
+
3. Recommend specific negative keywords with appropriate match types
|
|
97
|
+
4. Flag any search terms that might be worth adding as keywords instead
|
|
98
|
+
5. After I approve, add the negatives at the appropriate level (campaign vs ad group)
|
|
99
|
+
|
|
100
|
+
Be aggressive on clear waste, conservative on ambiguous terms.`,
|
|
101
|
+
requiredTools: ['search_terms_report', 'find_wasted_spend', 'add_negative_keywords']
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
shopping_optimization: {
|
|
105
|
+
name: 'shopping_optimization',
|
|
106
|
+
description: 'Shopping campaign deep-dive and optimization recommendations',
|
|
107
|
+
arguments: [
|
|
108
|
+
{
|
|
109
|
+
name: 'customer_id',
|
|
110
|
+
description: 'Google Ads account ID (without dashes)',
|
|
111
|
+
required: true
|
|
112
|
+
}
|
|
113
|
+
],
|
|
114
|
+
template: `Deep-dive into Shopping performance for {{customer_id}}:
|
|
115
|
+
|
|
116
|
+
1. Product-level performance - find winners (high ROAS) and losers (high spend, low return)
|
|
117
|
+
2. Check product disapprovals and feed health
|
|
118
|
+
3. Analyze by brand and category - where should we increase/decrease investment?
|
|
119
|
+
4. Compare to last period - any products trending significantly up or down?
|
|
120
|
+
5. Recommend bid adjustments for top 10 products to optimize
|
|
121
|
+
6. Identify products with high impressions but low click-through (possible title/image issues)
|
|
122
|
+
|
|
123
|
+
Focus on actionable changes with clear expected impact.
|
|
124
|
+
|
|
125
|
+
To complete this analysis:
|
|
126
|
+
- Use shopping_performance with segment_by=product_item_id for product-level data
|
|
127
|
+
- Use shopping_performance with segment_by=brand for brand analysis
|
|
128
|
+
- Use shopping_performance with segment_by=category_l1 for category analysis
|
|
129
|
+
- Use shopping_product_status to check for disapprovals and feed issues
|
|
130
|
+
- Use get_performance with level=campaign and campaign_types=SHOPPING for overall Shopping performance`,
|
|
131
|
+
requiredTools: ['shopping_performance', 'shopping_product_status', 'get_performance']
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
competitive_analysis: {
|
|
135
|
+
name: 'competitive_analysis',
|
|
136
|
+
description: 'Analyze positioning and performance patterns to identify competitive opportunities',
|
|
137
|
+
arguments: [
|
|
138
|
+
{
|
|
139
|
+
name: 'customer_id',
|
|
140
|
+
description: 'Google Ads account ID (without dashes)',
|
|
141
|
+
required: true
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
template: `Run competitive positioning analysis for {{customer_id}}:
|
|
145
|
+
|
|
146
|
+
1. Identify top campaigns by spend and analyze impression share metrics
|
|
147
|
+
2. Analyze top-of-page and absolute top impression rates - where are we losing visibility?
|
|
148
|
+
3. Cross-reference with Quality Score - is positioning a bid issue or quality issue?
|
|
149
|
+
4. Check hour-of-day and day-of-week performance - are there time windows with better/worse performance?
|
|
150
|
+
5. Analyze search impression share loss due to budget vs rank
|
|
151
|
+
6. Recommend strategy adjustments based on performance gaps
|
|
152
|
+
|
|
153
|
+
To complete this analysis:
|
|
154
|
+
- Use get_performance with level=campaign including impression share metrics to identify positioning
|
|
155
|
+
- Use quality_score_analysis to determine if positioning issues are quality-related vs bid-related
|
|
156
|
+
- Use performance_by_dimension with dimension=hour_of_day for time-based patterns
|
|
157
|
+
- Use performance_by_dimension with dimension=day_of_week for day-based patterns`,
|
|
158
|
+
requiredTools: ['quality_score_analysis', 'performance_by_dimension', 'get_performance']
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get prompt definition by name
|
|
164
|
+
* @param {string} name - Prompt name
|
|
165
|
+
* @returns {Object|null} Prompt definition or null if not found
|
|
166
|
+
*/
|
|
167
|
+
export function getPromptDefinition(name) {
|
|
168
|
+
return PROMPT_TEMPLATES[name] || null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Render a prompt template with provided arguments
|
|
173
|
+
* @param {string} name - Prompt name
|
|
174
|
+
* @param {Object} args - Arguments to substitute
|
|
175
|
+
* @returns {Object} Rendered prompt with messages array
|
|
176
|
+
*/
|
|
177
|
+
export function renderPrompt(name, args = {}) {
|
|
178
|
+
const promptDef = PROMPT_TEMPLATES[name];
|
|
179
|
+
|
|
180
|
+
if (!promptDef) {
|
|
181
|
+
throw new Error(`Unknown prompt: ${name}. Available prompts: ${Object.keys(PROMPT_TEMPLATES).join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check required arguments
|
|
185
|
+
const missingArgs = promptDef.arguments
|
|
186
|
+
.filter(arg => arg.required && !args[arg.name])
|
|
187
|
+
.map(arg => arg.name);
|
|
188
|
+
|
|
189
|
+
if (missingArgs.length > 0) {
|
|
190
|
+
throw new Error(`Missing required arguments for prompt "${name}": ${missingArgs.join(', ')}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Start with template
|
|
194
|
+
let rendered = promptDef.template;
|
|
195
|
+
|
|
196
|
+
// Substitute provided arguments
|
|
197
|
+
for (const [key, value] of Object.entries(args)) {
|
|
198
|
+
rendered = rendered.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Apply defaults for remaining placeholders
|
|
202
|
+
promptDef.arguments.forEach(arg => {
|
|
203
|
+
if (arg.default !== undefined) {
|
|
204
|
+
rendered = rendered.replace(new RegExp(`\\{\\{${arg.name}\\}\\}`, 'g'), arg.default);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
messages: [
|
|
210
|
+
{
|
|
211
|
+
role: 'user',
|
|
212
|
+
content: {
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: rendered
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get all available prompts for listing
|
|
223
|
+
* @returns {Array} Array of prompt definitions for MCP
|
|
224
|
+
*/
|
|
225
|
+
export function getPromptsList() {
|
|
226
|
+
return Object.values(PROMPT_TEMPLATES).map(prompt => ({
|
|
227
|
+
name: prompt.name,
|
|
228
|
+
description: prompt.description,
|
|
229
|
+
arguments: prompt.arguments
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = dirname(__filename);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resource definitions for MCP
|
|
10
|
+
*/
|
|
11
|
+
export const RESOURCES = [
|
|
12
|
+
{
|
|
13
|
+
uri: 'gaql://reference',
|
|
14
|
+
name: 'GAQL Reference',
|
|
15
|
+
description: 'Google Ads Query Language syntax reference with examples',
|
|
16
|
+
mimeType: 'text/markdown'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
uri: 'metrics://definitions',
|
|
20
|
+
name: 'Metrics Glossary',
|
|
21
|
+
description: 'Google Ads metrics definitions and calculations',
|
|
22
|
+
mimeType: 'text/markdown'
|
|
23
|
+
}
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a resource by URI
|
|
28
|
+
* @param {string} uri - Resource URI
|
|
29
|
+
* @returns {Object} Resource content
|
|
30
|
+
*/
|
|
31
|
+
export function readResource(uri) {
|
|
32
|
+
const resourceMap = {
|
|
33
|
+
'gaql://reference': 'gaql-reference.md',
|
|
34
|
+
'metrics://definitions': 'metrics-glossary.md'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const filename = resourceMap[uri];
|
|
38
|
+
if (!filename) {
|
|
39
|
+
throw new Error(`Unknown resource: ${uri}. Available: ${Object.keys(resourceMap).join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filePath = join(__dirname, filename);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
contents: [
|
|
49
|
+
{
|
|
50
|
+
uri: uri,
|
|
51
|
+
mimeType: 'text/markdown',
|
|
52
|
+
text: content
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
throw new Error(`Failed to read resource ${uri}: ${error.message}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get list of available resources
|
|
63
|
+
* @returns {Array} Resource definitions
|
|
64
|
+
*/
|
|
65
|
+
export function getResourcesList() {
|
|
66
|
+
return RESOURCES;
|
|
67
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { getCustomerClient } from '../auth.js';
|
|
2
|
+
import { formatSuccess, formatError } from '../utils/response-format.js';
|
|
3
|
+
import { blockMutations } from '../utils/validation.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Execute raw GAQL queries for advanced users
|
|
7
|
+
* Provides escape hatch for custom queries with safety guards
|
|
8
|
+
*
|
|
9
|
+
* @param {Object} params
|
|
10
|
+
* @param {string} [params.customer_id] - Google Ads account ID
|
|
11
|
+
* @param {string} params.query - GAQL query string (SELECT only)
|
|
12
|
+
* @param {number} [params.limit=100] - Maximum rows to return (max: 10000)
|
|
13
|
+
* @returns {Object} MCP response with query results
|
|
14
|
+
*/
|
|
15
|
+
export async function runGaqlQuery(params = {}) {
|
|
16
|
+
try {
|
|
17
|
+
// Validate required parameters
|
|
18
|
+
if (!params.query || typeof params.query !== 'string') {
|
|
19
|
+
throw new Error('query parameter is required and must be a string');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const query = params.query.trim();
|
|
23
|
+
|
|
24
|
+
// Validate query is not empty
|
|
25
|
+
if (!query) {
|
|
26
|
+
throw new Error('query cannot be empty');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Block mutation operations for safety
|
|
30
|
+
blockMutations(query);
|
|
31
|
+
|
|
32
|
+
// Validate query starts with SELECT
|
|
33
|
+
if (!query.toUpperCase().startsWith('SELECT')) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
'Query must start with SELECT. This tool only supports read operations. ' +
|
|
36
|
+
'Use the mutate tool for write operations.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Get customer ID
|
|
41
|
+
const customerId = params.customer_id || process.env.GOOGLE_ADS_DEFAULT_CUSTOMER_ID;
|
|
42
|
+
if (!customerId) {
|
|
43
|
+
throw new Error('customer_id parameter or GOOGLE_ADS_DEFAULT_CUSTOMER_ID environment variable required');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Validate and enforce limit
|
|
47
|
+
const requestedLimit = params.limit || 100;
|
|
48
|
+
const maxLimit = 10000;
|
|
49
|
+
const limit = Math.min(Math.max(1, requestedLimit), maxLimit);
|
|
50
|
+
|
|
51
|
+
// Check if query already has LIMIT clause
|
|
52
|
+
const hasLimit = /\bLIMIT\s+\d+/i.test(query);
|
|
53
|
+
|
|
54
|
+
// Build final query with enforced limit
|
|
55
|
+
let finalQuery = query;
|
|
56
|
+
if (!hasLimit) {
|
|
57
|
+
finalQuery = `${query} LIMIT ${limit}`;
|
|
58
|
+
} else {
|
|
59
|
+
// Extract existing limit and enforce max
|
|
60
|
+
const existingLimit = parseInt(query.match(/\bLIMIT\s+(\d+)/i)[1], 10);
|
|
61
|
+
if (existingLimit > maxLimit) {
|
|
62
|
+
finalQuery = query.replace(/\bLIMIT\s+\d+/i, `LIMIT ${maxLimit}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const customer = getCustomerClient(customerId);
|
|
67
|
+
|
|
68
|
+
// Execute query
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
const results = await customer.query(finalQuery);
|
|
71
|
+
const executionTime = Date.now() - startTime;
|
|
72
|
+
|
|
73
|
+
// Process results
|
|
74
|
+
const rowCount = results.length;
|
|
75
|
+
|
|
76
|
+
// Flatten results for readability
|
|
77
|
+
const data = results.map(row => flattenRow(row));
|
|
78
|
+
|
|
79
|
+
// Extract field names from first row
|
|
80
|
+
const fields = data.length > 0 ? Object.keys(data[0]) : [];
|
|
81
|
+
|
|
82
|
+
return formatSuccess({
|
|
83
|
+
summary: `Query returned ${rowCount} row${rowCount !== 1 ? 's' : ''} in ${executionTime}ms`,
|
|
84
|
+
data: {
|
|
85
|
+
rows: data,
|
|
86
|
+
fields: fields
|
|
87
|
+
},
|
|
88
|
+
metadata: {
|
|
89
|
+
customer_id: customerId,
|
|
90
|
+
row_count: rowCount,
|
|
91
|
+
field_count: fields.length,
|
|
92
|
+
execution_time_ms: executionTime,
|
|
93
|
+
limit_applied: limit,
|
|
94
|
+
query_truncated: rowCount >= limit,
|
|
95
|
+
query_preview: finalQuery.length > 200
|
|
96
|
+
? finalQuery.substring(0, 200) + '...'
|
|
97
|
+
: finalQuery
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return formatError(error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Flatten a nested result row into a flat object
|
|
108
|
+
* Converts nested structures like { campaign: { id: '123' } } to { 'campaign.id': '123' }
|
|
109
|
+
*
|
|
110
|
+
* @param {Object} row - Query result row
|
|
111
|
+
* @param {string} [prefix=''] - Key prefix for nested objects
|
|
112
|
+
* @returns {Object} Flattened row
|
|
113
|
+
*/
|
|
114
|
+
function flattenRow(row, prefix = '') {
|
|
115
|
+
const result = {};
|
|
116
|
+
|
|
117
|
+
for (const [key, value] of Object.entries(row)) {
|
|
118
|
+
const newKey = prefix ? `${prefix}.${key}` : key;
|
|
119
|
+
|
|
120
|
+
if (value === null || value === undefined) {
|
|
121
|
+
result[newKey] = null;
|
|
122
|
+
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
|
123
|
+
// Recursively flatten nested objects
|
|
124
|
+
Object.assign(result, flattenRow(value, newKey));
|
|
125
|
+
} else if (Array.isArray(value)) {
|
|
126
|
+
// Keep arrays as-is but stringify for readability
|
|
127
|
+
result[newKey] = value;
|
|
128
|
+
} else {
|
|
129
|
+
// Handle micros conversion for common fields
|
|
130
|
+
if (key.endsWith('_micros') && typeof value === 'number') {
|
|
131
|
+
const baseKey = newKey.replace('_micros', '');
|
|
132
|
+
result[baseKey] = value / 1000000;
|
|
133
|
+
result[newKey] = value; // Keep original for precision
|
|
134
|
+
} else {
|
|
135
|
+
result[newKey] = value;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { getCustomerClient } from '../auth.js';
|
|
2
|
+
import { formatSuccess, formatError } from '../utils/response-format.js';
|
|
3
|
+
import { buildQuery, TEMPLATES } from '../utils/gaql-templates.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* List all accessible Google Ads accounts
|
|
7
|
+
* @param {Object} params
|
|
8
|
+
* @param {boolean} [params.include_manager_accounts=false] - Include MCC accounts
|
|
9
|
+
* @returns {Object} MCP response with account list
|
|
10
|
+
*/
|
|
11
|
+
export async function listAccounts(params = {}) {
|
|
12
|
+
try {
|
|
13
|
+
const includeManagers = params.include_manager_accounts || false;
|
|
14
|
+
|
|
15
|
+
// Use login customer ID or default customer ID
|
|
16
|
+
const customerId = process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID ||
|
|
17
|
+
process.env.GOOGLE_ADS_DEFAULT_CUSTOMER_ID;
|
|
18
|
+
|
|
19
|
+
if (!customerId) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Either GOOGLE_ADS_LOGIN_CUSTOMER_ID or GOOGLE_ADS_DEFAULT_CUSTOMER_ID must be set'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Get customer client
|
|
26
|
+
const customer = getCustomerClient(customerId);
|
|
27
|
+
|
|
28
|
+
// Build query with optional manager filter
|
|
29
|
+
const filters = includeManagers ? '' : "WHERE customer_client.manager = false";
|
|
30
|
+
const query = buildQuery(TEMPLATES.LIST_ACCOUNTS, { filters });
|
|
31
|
+
|
|
32
|
+
// Execute query
|
|
33
|
+
const results = await customer.query(query);
|
|
34
|
+
|
|
35
|
+
// Format results
|
|
36
|
+
const accounts = results.map(row => ({
|
|
37
|
+
id: row.customer_client.id,
|
|
38
|
+
name: row.customer_client.descriptive_name,
|
|
39
|
+
is_manager: row.customer_client.manager,
|
|
40
|
+
currency: row.customer_client.currency_code,
|
|
41
|
+
timezone: row.customer_client.time_zone,
|
|
42
|
+
status: row.customer_client.status
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const managerCount = accounts.filter(a => a.is_manager).length;
|
|
46
|
+
const clientCount = accounts.length - managerCount;
|
|
47
|
+
|
|
48
|
+
return formatSuccess({
|
|
49
|
+
summary: `Found ${accounts.length} accessible account${accounts.length !== 1 ? 's' : ''} (${clientCount} client, ${managerCount} manager)`,
|
|
50
|
+
data: accounts,
|
|
51
|
+
metadata: {
|
|
52
|
+
totalAccounts: accounts.length,
|
|
53
|
+
clientAccounts: clientCount,
|
|
54
|
+
managerAccounts: managerCount
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return formatError(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { getCustomerClient } from '../auth.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Execute mutation operations using GoogleAdsService.Mutate
|
|
6
|
+
* Supports any write operation with dry_run validation
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} params - Mutation parameters
|
|
9
|
+
* @param {string} params.customer_id - Google Ads customer ID (optional, uses env default)
|
|
10
|
+
* @param {Array} params.operations - Array of mutation operation objects
|
|
11
|
+
* @param {boolean} params.partial_failure - Enable partial failure mode (default: true)
|
|
12
|
+
* @param {boolean} params.dry_run - Validate only, don't execute (default: true)
|
|
13
|
+
* @returns {Promise<Object>} Mutation results
|
|
14
|
+
*/
|
|
15
|
+
export async function mutate(params) {
|
|
16
|
+
const {
|
|
17
|
+
customer_id = process.env.GOOGLE_ADS_CUSTOMER_ID,
|
|
18
|
+
operations,
|
|
19
|
+
partial_failure = true,
|
|
20
|
+
dry_run = true
|
|
21
|
+
} = params;
|
|
22
|
+
|
|
23
|
+
// Validate required parameters
|
|
24
|
+
if (!customer_id) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
error: 'customer_id is required (either as parameter or GOOGLE_ADS_CUSTOMER_ID env var)'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!operations || !Array.isArray(operations) || operations.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
success: false,
|
|
34
|
+
error: 'operations array is required and must contain at least one operation'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const customer = getCustomerClient(customer_id);
|
|
40
|
+
|
|
41
|
+
// Execute mutation with validation options
|
|
42
|
+
const response = await customer.mutateResources(operations, {
|
|
43
|
+
partialFailure: partial_failure,
|
|
44
|
+
validateOnly: dry_run
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
dry_run,
|
|
50
|
+
results: response,
|
|
51
|
+
message: dry_run
|
|
52
|
+
? 'Validation successful - no changes made'
|
|
53
|
+
: 'Mutations applied successfully',
|
|
54
|
+
operations_count: operations.length
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
dry_run,
|
|
60
|
+
error: error.message,
|
|
61
|
+
error_details: error.errors || null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|