@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,166 @@
|
|
|
1
|
+
// Mutation keywords to block for read-only safety
|
|
2
|
+
const MUTATION_KEYWORDS = ['create', 'update', 'remove', 'mutate', 'delete'];
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validate that required parameters are present
|
|
6
|
+
* @param {Object} params - Parameters object
|
|
7
|
+
* @param {string[]} fields - Required field names
|
|
8
|
+
* @throws {Error} If any required field is missing
|
|
9
|
+
*/
|
|
10
|
+
export function validateRequired(params, fields) {
|
|
11
|
+
const missing = fields.filter(field => {
|
|
12
|
+
const value = params[field];
|
|
13
|
+
return value === undefined || value === null || value === '';
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (missing.length > 0) {
|
|
17
|
+
throw new Error(`Missing required parameter${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validate that a value is one of the allowed enum values
|
|
23
|
+
* @param {string} value - Value to check
|
|
24
|
+
* @param {string[]} allowed - Allowed values
|
|
25
|
+
* @param {string} paramName - Parameter name for error message
|
|
26
|
+
* @throws {Error} If value is not in allowed list
|
|
27
|
+
*/
|
|
28
|
+
export function validateEnum(value, allowed, paramName = 'value') {
|
|
29
|
+
if (!allowed.includes(value)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Invalid ${paramName}: ${value}. Allowed values: ${allowed.join(', ')}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse and validate date range
|
|
38
|
+
* @param {string} range - Predefined range or 'CUSTOM'
|
|
39
|
+
* @param {string} [startDate] - Start date for CUSTOM range (YYYY-MM-DD)
|
|
40
|
+
* @param {string} [endDate] - End date for CUSTOM range (YYYY-MM-DD)
|
|
41
|
+
* @returns {{ start: string, end: string }} Formatted dates (YYYY-MM-DD)
|
|
42
|
+
* @throws {Error} If range is invalid or CUSTOM dates are missing
|
|
43
|
+
*/
|
|
44
|
+
export function validateDateRange(range, startDate, endDate) {
|
|
45
|
+
const now = new Date();
|
|
46
|
+
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Format date as YYYY-MM-DD
|
|
50
|
+
*/
|
|
51
|
+
function formatDate(date) {
|
|
52
|
+
const year = date.getFullYear();
|
|
53
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
54
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
55
|
+
return `${year}-${month}-${day}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ranges = {
|
|
59
|
+
'TODAY': () => ({ start: today, end: today }),
|
|
60
|
+
'YESTERDAY': () => {
|
|
61
|
+
const yesterday = new Date(today);
|
|
62
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
63
|
+
return { start: yesterday, end: yesterday };
|
|
64
|
+
},
|
|
65
|
+
'LAST_7_DAYS': () => {
|
|
66
|
+
const start = new Date(today);
|
|
67
|
+
start.setDate(start.getDate() - 7);
|
|
68
|
+
return { start, end: today };
|
|
69
|
+
},
|
|
70
|
+
'LAST_14_DAYS': () => {
|
|
71
|
+
const start = new Date(today);
|
|
72
|
+
start.setDate(start.getDate() - 14);
|
|
73
|
+
return { start, end: today };
|
|
74
|
+
},
|
|
75
|
+
'LAST_30_DAYS': () => {
|
|
76
|
+
const start = new Date(today);
|
|
77
|
+
start.setDate(start.getDate() - 30);
|
|
78
|
+
return { start, end: today };
|
|
79
|
+
},
|
|
80
|
+
'LAST_90_DAYS': () => {
|
|
81
|
+
const start = new Date(today);
|
|
82
|
+
start.setDate(start.getDate() - 90);
|
|
83
|
+
return { start, end: today };
|
|
84
|
+
},
|
|
85
|
+
'THIS_MONTH': () => {
|
|
86
|
+
const start = new Date(today.getFullYear(), today.getMonth(), 1);
|
|
87
|
+
return { start, end: today };
|
|
88
|
+
},
|
|
89
|
+
'LAST_MONTH': () => {
|
|
90
|
+
const start = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
|
91
|
+
const end = new Date(today.getFullYear(), today.getMonth(), 0);
|
|
92
|
+
return { start, end };
|
|
93
|
+
},
|
|
94
|
+
'CUSTOM': () => {
|
|
95
|
+
if (!startDate || !endDate) {
|
|
96
|
+
throw new Error('start_date and end_date required for CUSTOM range');
|
|
97
|
+
}
|
|
98
|
+
// Parse dates as local time to avoid timezone conversion issues
|
|
99
|
+
const [startYear, startMonth, startDay] = startDate.split('-').map(Number);
|
|
100
|
+
const [endYear, endMonth, endDay] = endDate.split('-').map(Number);
|
|
101
|
+
return {
|
|
102
|
+
start: new Date(startYear, startMonth - 1, startDay),
|
|
103
|
+
end: new Date(endYear, endMonth - 1, endDay)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (!ranges[range]) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Invalid date_range: ${range}. Allowed values: ${Object.keys(ranges).join(', ')}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { start, end } = ranges[range]();
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
start: formatDate(start),
|
|
118
|
+
end: formatDate(end)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get customer ID from params or environment, with validation
|
|
124
|
+
* Consolidates the common pattern used across all tools
|
|
125
|
+
* @param {Object} params - Parameters object with optional customer_id
|
|
126
|
+
* @returns {string} Valid customer ID
|
|
127
|
+
* @throws {Error} If no customer ID is available
|
|
128
|
+
*/
|
|
129
|
+
export function getCustomerId(params = {}) {
|
|
130
|
+
const customerId = params.customer_id || process.env.GOOGLE_ADS_DEFAULT_CUSTOMER_ID;
|
|
131
|
+
if (!customerId) {
|
|
132
|
+
throw new Error('customer_id parameter or GOOGLE_ADS_DEFAULT_CUSTOMER_ID environment variable required');
|
|
133
|
+
}
|
|
134
|
+
return customerId;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build common campaign ID filter clause
|
|
139
|
+
* @param {string[]} campaignIds - Array of campaign IDs to filter
|
|
140
|
+
* @returns {string} GAQL filter clause or empty string
|
|
141
|
+
*/
|
|
142
|
+
export function buildCampaignFilter(campaignIds) {
|
|
143
|
+
if (!campaignIds || campaignIds.length === 0) {
|
|
144
|
+
return '';
|
|
145
|
+
}
|
|
146
|
+
const ids = campaignIds.join(', ');
|
|
147
|
+
return `AND campaign.id IN (${ids})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check query for mutation keywords and block if found
|
|
152
|
+
* @param {string} query - GAQL query string
|
|
153
|
+
* @throws {Error} If query contains mutation keywords
|
|
154
|
+
*/
|
|
155
|
+
export function blockMutations(query) {
|
|
156
|
+
const lowerQuery = query.toLowerCase();
|
|
157
|
+
|
|
158
|
+
for (const keyword of MUTATION_KEYWORDS) {
|
|
159
|
+
if (lowerQuery.includes(keyword)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Mutation operations not allowed in query tool. Query contains: "${keyword}". ` +
|
|
162
|
+
`Use the mutate tool for write operations.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|