@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.
@@ -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
+ }