@getmcpads/google-ads-mcp-server 1.0.1 → 2.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/CHANGELOG.md +20 -0
- package/README.md +139 -106
- package/dist/cli.js +1649 -423
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1658 -432
- package/dist/index.js.map +1 -1
- package/package.json +15 -8
- package/server-card.json +4461 -0
- package/server.json +61 -0
package/dist/cli.js
CHANGED
|
@@ -51,24 +51,838 @@ function loadConfig() {
|
|
|
51
51
|
enableWrites: isTruthy(process.env["GOOGLE_ADS_ENABLE_WRITES"]),
|
|
52
52
|
logLevel: process.env["LOG_LEVEL"] ?? "info"
|
|
53
53
|
};
|
|
54
|
-
const
|
|
55
|
-
if (!
|
|
56
|
-
const missing =
|
|
54
|
+
const result2 = configSchema.safeParse(raw);
|
|
55
|
+
if (!result2.success) {
|
|
56
|
+
const missing = result2.error.issues.map((i) => i.message).join(", ");
|
|
57
57
|
logger.error("config", `Missing credentials: ${missing}`);
|
|
58
58
|
throw new Error(`Missing Google Ads credentials: ${missing}`);
|
|
59
59
|
}
|
|
60
|
-
return
|
|
60
|
+
return result2.data;
|
|
61
61
|
}
|
|
62
62
|
function isTruthy(value) {
|
|
63
63
|
if (!value) return false;
|
|
64
64
|
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// src/parameter-descriptions.ts
|
|
68
|
+
var PARAMETER_DESCRIPTIONS = {
|
|
69
|
+
"filters": "Filters combined according to this tool's query schema. Use provider field names and the declared operators.",
|
|
70
|
+
"searchType": "Search surface to report, such as web, image, video, news or Discover. Availability depends on the property.",
|
|
71
|
+
"searchTypes": "Search surfaces to compare. Each surface is queried separately; do not sum overlapping surfaces.",
|
|
72
|
+
"datePreset": "Named reporting period. Use the custom date range when you need exact start and end dates.",
|
|
73
|
+
"dateRange": "Explicit reporting start and end dates in YYYY-MM-DD format.",
|
|
74
|
+
"timeRange": "Explicit provider reporting period with since and until dates.",
|
|
75
|
+
"startDate": "First date of the reporting period, in YYYY-MM-DD format.",
|
|
76
|
+
"endDate": "Last date of the reporting period, in YYYY-MM-DD format, on or after startDate.",
|
|
77
|
+
"since": "Beginning of the requested reporting period; use the format accepted by the provider.",
|
|
78
|
+
"until": "End of the requested reporting period; use the format accepted by the provider.",
|
|
79
|
+
"orderBy": "Field used to order report rows.",
|
|
80
|
+
"orderField": "Provider field used to order report rows.",
|
|
81
|
+
"orderDirection": "Sort direction for the selected ordering field.",
|
|
82
|
+
"orderType": "Sort direction for the selected ordering field.",
|
|
83
|
+
"sortBy": "Metric or dimension used to rank the returned results.",
|
|
84
|
+
"sort": "Native provider ordering expressions.",
|
|
85
|
+
"limit": "Maximum number of returned items or rows. The declared bounds and default apply; use pagination for additional results.",
|
|
86
|
+
"topN": "Maximum number of highest-ranked results to return.",
|
|
87
|
+
"pageSize": "Maximum items requested per page. Follow the returned pagination information for remaining results.",
|
|
88
|
+
"page": "Page number to request, starting at 1.",
|
|
89
|
+
"offset": "Zero-based number of rows to skip before returning results.",
|
|
90
|
+
"bookmark": "Opaque Pinterest pagination bookmark returned by the previous response. Omit on the first request.",
|
|
91
|
+
"after": "Opaque cursor for the next page returned by Meta.",
|
|
92
|
+
"before": "Opaque cursor for the previous page returned by Meta.",
|
|
93
|
+
"includeUnchanged": "Include entries with no observed change in the comparison.",
|
|
94
|
+
"minImpressions": "Minimum reported impressions required for a row to enter the analysis.",
|
|
95
|
+
"objectives": "Analysis goals that determine the recommended large-site sampling strategy.",
|
|
96
|
+
"pageLimit": "Maximum number of pages considered for the sampling plan.",
|
|
97
|
+
"maxInspectionUrls": "Maximum URL Inspection candidates included in the plan; inspection calls consume provider quota.",
|
|
98
|
+
"minImpressionsForLowCtr": "Minimum impressions before a page is considered for low-CTR analysis.",
|
|
99
|
+
"staleSitemapDays": "Number of days after which a sitemap's last download is considered stale.",
|
|
100
|
+
"continueOnError": "Continue with other URLs when one inspection fails, returning individual failures.",
|
|
101
|
+
"dimension": "Reporting dimension used to group and compare results.",
|
|
102
|
+
"maxClusters": "Maximum query clusters to return.",
|
|
103
|
+
"topQueriesPerCluster": "Maximum example queries returned for each cluster.",
|
|
104
|
+
"minPages": "Minimum distinct pages ranking for a query before it is considered a cannibalization candidate.",
|
|
105
|
+
"adAccountId": "Owning advertising account ID. Use the exact ID returned by account discovery; do not substitute a campaign or business ID.",
|
|
106
|
+
"advertiserId": "TikTok advertiser account ID as a string.",
|
|
107
|
+
"customerId": "Google Ads customer ID, without hyphens.",
|
|
108
|
+
"propertyId": "Google Analytics property ID; use the property, not the account or measurement ID.",
|
|
109
|
+
"level": "Entity or aggregation level for the requested report.",
|
|
110
|
+
"dataLevel": "TikTok reporting aggregation level. Metrics and dimensions must be compatible with this level.",
|
|
111
|
+
"granularity": "Time bucket used to aggregate reporting rows.",
|
|
112
|
+
"columns": "Native Pinterest reporting column names. Discover supported columns before selecting metrics.",
|
|
113
|
+
"executionMode": "Choose the supported synchronous, asynchronous or automatic report execution strategy.",
|
|
114
|
+
"entityIds": "Exact IDs of the entities to include in the selected reporting level.",
|
|
115
|
+
"targetingTypes": "Native targeting categories used to break down delivery.",
|
|
116
|
+
"clickWindowDays": "Click-through attribution window, in days.",
|
|
117
|
+
"engagementWindowDays": "Engagement-through attribution window, in days.",
|
|
118
|
+
"viewWindowDays": "View-through attribution window, in days.",
|
|
119
|
+
"conversionReportTime": "Whether conversions are assigned to the ad action date or the conversion date.",
|
|
120
|
+
"attributionTypes": "Provider attribution categories included in the report.",
|
|
121
|
+
"reportingTimezone": "Provider reporting timezone selector. Keep it consistent when comparing periods.",
|
|
122
|
+
"campaignIds": "Restrict results to these campaign IDs.",
|
|
123
|
+
"adGroupIds": "Restrict results to these ad group IDs.",
|
|
124
|
+
"adgroupIds": "Restrict TikTok results to these ad group IDs.",
|
|
125
|
+
"adIds": "Restrict results to these ad IDs.",
|
|
126
|
+
"campaignId": "Exact campaign ID in the selected advertising account.",
|
|
127
|
+
"adGroupId": "Exact ad group ID in the selected advertising account.",
|
|
128
|
+
"adSetId": "Exact Meta ad set ID in the selected advertising account.",
|
|
129
|
+
"adId": "Exact ad ID in the selected advertising account.",
|
|
130
|
+
"entityId": "Exact ID of the entity selected by this operation. It must belong to the selected account.",
|
|
131
|
+
"onlyWithPeriodDelivery": "Keep only creatives with observed delivery in the requested date range.",
|
|
132
|
+
"breakdown": "Native reporting breakdown used to group results.",
|
|
133
|
+
"breakdowns": "Native reporting breakdowns. Check compatibility before combining them.",
|
|
134
|
+
"productGroupIds": "Restrict reporting to these product group IDs.",
|
|
135
|
+
"productItemIds": "Restrict reporting to these product item IDs.",
|
|
136
|
+
"includeProductSamples": "Include a limited sample of product records alongside inventory diagnostics.",
|
|
137
|
+
"sampleProductGroups": "Maximum product groups to sample for inventory diagnostics.",
|
|
138
|
+
"reportName": "Provider report name identifying the requested report recipe.",
|
|
139
|
+
"conversionProductAttributionType": "Pinterest product-conversion attribution category.",
|
|
140
|
+
"conversionProductBreakdown": "Pinterest product-conversion grouping level.",
|
|
141
|
+
"productSkuIds": "Restrict product-conversion reporting to these SKU IDs.",
|
|
142
|
+
"request": "Exact request schema name to inspect before composing a write payload.",
|
|
143
|
+
"entity": "Entity collection or level to inspect.",
|
|
144
|
+
"entityType": "Provider entity type to retrieve.",
|
|
145
|
+
"entityStatuses": "Native status values used to filter the entity collection.",
|
|
146
|
+
"statusFilter": "Native status values used to filter the returned entities.",
|
|
147
|
+
"targetingType": "Targeting dictionary to inspect, such as interests, locations or demographics.",
|
|
148
|
+
"interestId": "Native interest ID for the targeting lookup.",
|
|
149
|
+
"mode": "Operation or analysis mode. Only the values declared in this schema are supported.",
|
|
150
|
+
"matchTypes": "Keyword match types included in the lookup.",
|
|
151
|
+
"countryCode": "Country code used to localize keyword or targeting results.",
|
|
152
|
+
"keywords": "Keyword phrases to analyze; use the language and market of the intended audience.",
|
|
153
|
+
"term": "Search term used for the keyword lookup.",
|
|
154
|
+
"terms": "Search terms used for the keyword lookup.",
|
|
155
|
+
"audienceId": "Exact ID of the audience to inspect or modify.",
|
|
156
|
+
"customerListId": "Exact customer-list ID. Reading metadata does not upload customer data.",
|
|
157
|
+
"businessId": "Exact business account ID used for the lookup.",
|
|
158
|
+
"accountType": "Provider account category used to filter results.",
|
|
159
|
+
"ownershipType": "Filter audiences by provider ownership category.",
|
|
160
|
+
"excludeNca": "Whether to exclude Pinterest new-customer-acquisition audiences from this lookup.",
|
|
161
|
+
"audienceInsightType": "Pinterest audience-insights category to request.",
|
|
162
|
+
"surfaces": "Conversion-setup surfaces to inspect, according to the supported values.",
|
|
163
|
+
"includeDeletedTags": "Include deleted conversion tags in diagnostics.",
|
|
164
|
+
"lookbackPeriod": "Provider lookback period used for conversion diagnostics.",
|
|
165
|
+
"sourcePlatform": "Filter conversion data by source platform.",
|
|
166
|
+
"ingestionSource": "Filter conversion events by ingestion source.",
|
|
167
|
+
"catalogId": "Exact product catalog ID accessible to the selected account.",
|
|
168
|
+
"feedId": "Exact catalog feed ID.",
|
|
169
|
+
"processingResultId": "Exact catalog feed processing result ID to inspect.",
|
|
170
|
+
"productGroupId": "Exact product group ID.",
|
|
171
|
+
"productSetId": "Exact Meta product set ID belonging to the selected catalog.",
|
|
172
|
+
"exportType": "Provider export category to request.",
|
|
173
|
+
"action": "Supported operation within this tool's scoped provider surface.",
|
|
174
|
+
"includeDetails": "Include detailed provider records in addition to summary information.",
|
|
175
|
+
"leadFormId": "Exact lead form ID to inspect.",
|
|
176
|
+
"subscriptionId": "Exact subscription ID to inspect.",
|
|
177
|
+
"assetId": "Exact provider asset ID.",
|
|
178
|
+
"memberId": "Exact business member ID used for the read lookup.",
|
|
179
|
+
"partnerId": "Exact business partner ID used for the read lookup.",
|
|
180
|
+
"pinId": "Exact Pinterest pin ID.",
|
|
181
|
+
"pinIds": "Exact Pinterest pin IDs to include.",
|
|
182
|
+
"metrics": "Native or documented calculated metric names to request. Check compatibility with the selected dimensions.",
|
|
183
|
+
"metric": "Metric used for the analysis; use a name from the platform metric catalogue.",
|
|
184
|
+
"boardId": "Exact Pinterest board ID.",
|
|
185
|
+
"searchTerm": "Text used to filter or search the selected collection.",
|
|
186
|
+
"region": "Provider region selector used to localize trends.",
|
|
187
|
+
"productRegion": "Region used for product trend analysis.",
|
|
188
|
+
"trendType": "Provider trend category to return.",
|
|
189
|
+
"interests": "Native interest categories used to filter trends.",
|
|
190
|
+
"genders": "Native gender categories used to filter aggregate trends.",
|
|
191
|
+
"ageBuckets": "Native age groups used to filter aggregate trends.",
|
|
192
|
+
"includeKeywords": "Include related keyword details in the trend response.",
|
|
193
|
+
"includeDemographics": "Include available aggregate demographic breakdowns.",
|
|
194
|
+
"productCategories": "Native product category IDs or values used to filter trends.",
|
|
195
|
+
"verticals": "Native business verticals used to filter trends.",
|
|
196
|
+
"productLookbackDays": "Product-trend lookback window, in days.",
|
|
197
|
+
"productEngagementType": "Engagement signal used for product-trend analysis.",
|
|
198
|
+
"featuredInterest": "Featured interest category for the trends lookup.",
|
|
199
|
+
"reportType": "Native provider report type; determines supported metrics and dimensions.",
|
|
200
|
+
"promotionId": "Exact Pinterest product-group promotion ID.",
|
|
201
|
+
"type": "Provider object or operation type selected from this schema's allowed values.",
|
|
202
|
+
"productBreakdown": "Native product grouping used to join catalog items with reported insights.",
|
|
203
|
+
"catalogProductLimit": "Maximum catalog product records read for the join.",
|
|
204
|
+
"includeAdsets": "Include ad set configuration when checking brand safety.",
|
|
205
|
+
"includeBlockLists": "Include accessible brand-safety block-list metadata.",
|
|
206
|
+
"includeRawTargeting": "Include the native targeting specification for diagnostics.",
|
|
207
|
+
"cellEntityType": "Entity type represented by experiment cells.",
|
|
208
|
+
"includePagePosts": "Read related Facebook Page posts when the token has permission.",
|
|
209
|
+
"includeInstagramMedia": "Read related Instagram media when the token has permission.",
|
|
210
|
+
"includeInsights": "Include available insight metrics; additional provider permissions may be required.",
|
|
211
|
+
"edge": "Allowlisted Meta Graph edge to read on the selected node.",
|
|
212
|
+
"fields": "Native provider fields to return. Select only fields supported by the chosen object or report.",
|
|
213
|
+
"filtering": "Native provider filter expressions; credentials and account overrides are not allowed.",
|
|
214
|
+
"includeSummary": "Request the provider's available summary or total-count metadata.",
|
|
215
|
+
"actionBreakdowns": "Meta action-level breakdowns, subject to Insights compatibility rules.",
|
|
216
|
+
"actionAttributionWindows": "Meta attribution windows used to attribute action metrics.",
|
|
217
|
+
"timeIncrement": "Provider time-bucket size or supported aggregate value.",
|
|
218
|
+
"locationTypes": "Location dictionary categories to include in targeting search.",
|
|
219
|
+
"adFormat": "Native Meta ad preview format; must be compatible with this ad's creative.",
|
|
220
|
+
"handle": "Asynchronous catalog batch handle returned by the submission to inspect.",
|
|
221
|
+
"keepEmptyRows": "Keep provider rows whose selected metrics are all zero, where supported.",
|
|
222
|
+
"sampleLimit": "Maximum records included in the diagnostic sample.",
|
|
223
|
+
"isOpenFunnel": "Allow users to enter at any funnel step instead of requiring the first step.",
|
|
224
|
+
"visualizationType": "Supported funnel visualization mode.",
|
|
225
|
+
"breakdownLimit": "Maximum breakdown values to include in the funnel response.",
|
|
226
|
+
"nextActionLimit": "Maximum next actions returned for each funnel step.",
|
|
227
|
+
"fallbackToStepCounts": "If the advanced funnel is unavailable, return independent step counts with the limitation made explicit.",
|
|
228
|
+
"report": "Native GA4 report request object for this endpoint.",
|
|
229
|
+
"reports": "Native GA4 report request objects to execute in one batch.",
|
|
230
|
+
"dimensions": "Native reporting dimensions. Validate compatibility with the selected metrics.",
|
|
231
|
+
"compatibilityFilter": "Limit compatibility results to the requested GA4 compatibility category.",
|
|
232
|
+
"collection": "Allowlisted administrative resource collection to read.",
|
|
233
|
+
"settings": "Property configuration surfaces to inspect.",
|
|
234
|
+
"includeRecurring": "Include recurring audience-export configurations.",
|
|
235
|
+
"audienceExportName": "Full GA4 audience-export resource name returned by list_audience_exports.",
|
|
236
|
+
"queryLifetime": "Request lifetime reporting instead of the explicit reporting period where TikTok supports it.",
|
|
237
|
+
"declineThresholdPct": "Percentage decline used as a candidate signal for creative fatigue, requiring further evidence.",
|
|
238
|
+
"includeSavedAudiences": "Include accessible saved-audience metadata in the overlap analysis.",
|
|
239
|
+
"overlapThreshold": "Overlap threshold used to flag audience pairs, in the units declared by this schema.",
|
|
240
|
+
"serviceType": "TikTok report service category.",
|
|
241
|
+
"catalog": "Allowlisted targeting dictionary or catalog to inspect.",
|
|
242
|
+
"query": "Search text used to find matching entries in the selected catalogue.",
|
|
243
|
+
"parameters": "Native query parameters for this allowlisted read endpoint. Do not provide credentials or account overrides.",
|
|
244
|
+
"account_id": "Exact account ID selected for this platform in the GetMCPAds workspace.",
|
|
245
|
+
"platform": "Platform key identifying the connected advertising or analytics source.",
|
|
246
|
+
"id": "Exact saved object ID returned by the corresponding create or list tool.",
|
|
247
|
+
"revision": "Last known object revision, used to reject conflicting updates. Read the object before editing.",
|
|
248
|
+
"name": "Human-readable name for this object.",
|
|
249
|
+
"title": "Human-readable title for the resulting object or review.",
|
|
250
|
+
"clientLabel": "Display label identifying the client in the saved view.",
|
|
251
|
+
"config": "Complete saved-view configuration, including source, selected account, period and display options.",
|
|
252
|
+
"subject": "Entity or creative subject whose dated performance will be analyzed.",
|
|
253
|
+
"compare_previous": "Include the immediately preceding period of equal duration for comparison.",
|
|
254
|
+
"profile": "Creative brief profile containing the client's business context and analysis preferences.",
|
|
255
|
+
"primaryMetric": "Metric used to rank or evaluate the review; it must be available for the selected platform.",
|
|
256
|
+
"direction": "Whether higher or lower values of the primary metric represent improvement.",
|
|
257
|
+
"creative_ids": "Exact creative IDs to include in this review.",
|
|
258
|
+
"request_id": "Caller-generated request identifier. Reuse it for a retry of the same operation to avoid duplicate work.",
|
|
259
|
+
"expiresInDays": "Number of days before the public review sharing link expires.",
|
|
260
|
+
"client_id": "Exact saved business-client workspace ID.",
|
|
261
|
+
"review_id": "Exact review ID returned by the review creation or listing tool.",
|
|
262
|
+
"page_id": "Exact page identifier within the landing-page review.",
|
|
263
|
+
"entity_id": "Exact entity identifier in the selected review or platform context.",
|
|
264
|
+
"source_key": "Source identifier returned by the business review; keep the platform/account association intact.",
|
|
265
|
+
"metric_key": "Exact metric key in the review evidence.",
|
|
266
|
+
"follow_up_on": "Follow-up date for the decision, using the format declared in this schema.",
|
|
267
|
+
"hypothesis": "Specific hypothesis to record, supported by the review evidence.",
|
|
268
|
+
"successCriterion": "Measurable condition used to decide whether the recorded action succeeded.",
|
|
269
|
+
"outcome": "Observed outcome to record; distinguish evidence from an untested expectation.",
|
|
270
|
+
"status": "Status selected from the allowed values for this object or operation.",
|
|
271
|
+
"retry_source": "Source identifier to retry after reviewing its reported error.",
|
|
272
|
+
"site_origin": "Website origin to analyze, including https:// and hostname, without credentials or a path.",
|
|
273
|
+
"matchType": "Google Ads keyword match type for the forecast.",
|
|
274
|
+
"negativeKeywords": "Keyword phrases excluded from the keyword forecast.",
|
|
275
|
+
"negativeMatchType": "Match type applied to the negative keywords.",
|
|
276
|
+
"geoTargetIds": "Google Ads geo-target constant IDs for the intended locations.",
|
|
277
|
+
"languageId": "Google Ads language constant ID for the keyword request.",
|
|
278
|
+
"languageIds": "Google Ads language constant IDs for the intended audience.",
|
|
279
|
+
"network": "Google Ads search network selector used for planning.",
|
|
280
|
+
"biddingStrategy": "Supported bidding strategy for this temporary forecast. The request does not create a live campaign.",
|
|
281
|
+
"includeAdultKeywords": "Allow adult keyword ideas where supported by the provider and the selected market.",
|
|
282
|
+
"seedKeywords": "Seed keyword phrases from which Google generates keyword ideas.",
|
|
283
|
+
"includeAverageCpc": "Request average cost-per-click metrics when available.",
|
|
284
|
+
"includeDeviceBreakdown": "Include device-specific planning metrics when supported.",
|
|
285
|
+
"includeKeywordConcepts": "Include Google's keyword concept grouping metadata.",
|
|
286
|
+
"startYearMonth": "First year and month of the historical metrics period.",
|
|
287
|
+
"endYearMonth": "Last year and month of the historical metrics period.",
|
|
288
|
+
"includeDismissed": "Include recommendations already dismissed in the account.",
|
|
289
|
+
"operation": "Exact allowlisted operation to execute. Arbitrary provider operations are not accepted.",
|
|
290
|
+
"category": "Category selected from the provider dictionary or field catalogue.",
|
|
291
|
+
"selectable": "Filter the field catalogue by whether the field can appear in SELECT.",
|
|
292
|
+
"filterable": "Filter the field catalogue by whether the field can appear in WHERE.",
|
|
293
|
+
"sortable": "Filter the field catalogue by whether the field can appear in ORDER BY.",
|
|
294
|
+
"pageToken": "Opaque continuation token returned by the preceding provider response.",
|
|
295
|
+
"locationNames": "Human-readable location names for Google to resolve into geo-target constants.",
|
|
296
|
+
"locale": "Locale used for human-readable lookup results.",
|
|
297
|
+
"resource": "Native Google Ads resource from which the GAQL query selects rows.",
|
|
298
|
+
"cursor": "Opaque pagination cursor returned by the preceding response; omit for the first page.",
|
|
299
|
+
"kind": "Entity or request kind whose schema or data is being requested.",
|
|
300
|
+
"startTime": "Beginning of the reporting period as an ISO 8601 timestamp with an explicit timezone.",
|
|
301
|
+
"endTime": "End of the reporting period as an ISO 8601 timestamp with an explicit timezone, after startTime.",
|
|
302
|
+
"reportDimension": "Native dimension used to break down the Snapchat report."
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
// src/tool-quality.ts
|
|
306
|
+
import { z as z2 } from "zod";
|
|
307
|
+
var RESULT_SCHEMA = {
|
|
308
|
+
type: "object",
|
|
309
|
+
properties: {
|
|
310
|
+
result: {
|
|
311
|
+
description: "Original tool result: parsed JSON when the text is JSON, otherwise the text or multiple MCP content blocks. Provider fields depend on the selected query.",
|
|
312
|
+
type: ["object", "array", "string", "number", "boolean", "null"]
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
required: ["result"],
|
|
316
|
+
additionalProperties: true
|
|
317
|
+
};
|
|
318
|
+
function toolAnnotations(write) {
|
|
319
|
+
return { readOnlyHint: !write, destructiveHint: write, idempotentHint: !write, openWorldHint: true };
|
|
320
|
+
}
|
|
321
|
+
function structuredResult(value) {
|
|
322
|
+
if (value.isError) return value;
|
|
323
|
+
let result2 = value.content;
|
|
324
|
+
const first = value.content[0];
|
|
325
|
+
if (value.content.length === 1 && first?.type === "text" && typeof first.text === "string") {
|
|
326
|
+
result2 = first.text;
|
|
327
|
+
try {
|
|
328
|
+
result2 = JSON.parse(first.text);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return { ...value, structuredContent: { ...value.structuredContent, result: result2 } };
|
|
333
|
+
}
|
|
334
|
+
var resultShape = { result: z2.union([z2.object({}).passthrough(), z2.array(z2.unknown()), z2.string(), z2.number(), z2.boolean(), z2.null()]).describe(RESULT_SCHEMA.properties.result.description) };
|
|
335
|
+
function installToolQuality(server) {
|
|
336
|
+
server.tool = ((name, description, inputSchema, handler) => {
|
|
337
|
+
const write = Object.hasOwn(inputSchema, "confirm");
|
|
338
|
+
inputSchema = Object.fromEntries(Object.entries(inputSchema).map(([key, schema]) => [key, schema.description || PARAMETER_DESCRIPTIONS[key] ? schema.describe(schema.description || PARAMETER_DESCRIPTIONS[key]) : schema]));
|
|
339
|
+
return server.registerTool(name, {
|
|
340
|
+
title: name.replace(/_/g, " "),
|
|
341
|
+
description,
|
|
342
|
+
inputSchema,
|
|
343
|
+
outputSchema: resultShape,
|
|
344
|
+
annotations: toolAnnotations(write)
|
|
345
|
+
}, async (...args) => structuredResult(await handler(...args)));
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
67
349
|
// src/server.ts
|
|
68
350
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
69
351
|
|
|
352
|
+
// src/platforms/google-ads/extended-writes.ts
|
|
353
|
+
import { webcrypto } from "crypto";
|
|
354
|
+
import { z as z3 } from "zod";
|
|
355
|
+
var BASE = "https://googleads.googleapis.com/v25";
|
|
356
|
+
var resources = {
|
|
357
|
+
conversionActionOperation: "conversionActions",
|
|
358
|
+
campaignBudgetOperation: "campaignBudgets",
|
|
359
|
+
campaignOperation: "campaigns",
|
|
360
|
+
adGroupOperation: "adGroups",
|
|
361
|
+
adGroupAdOperation: "adGroupAds",
|
|
362
|
+
adOperation: "ads",
|
|
363
|
+
assetOperation: "assets",
|
|
364
|
+
assetGroupOperation: "assetGroups",
|
|
365
|
+
assetGroupAssetOperation: "assetGroupAssets",
|
|
366
|
+
campaignAssetOperation: "campaignAssets",
|
|
367
|
+
campaignCriterionOperation: "campaignCriteria",
|
|
368
|
+
adGroupCriterionOperation: "adGroupCriteria",
|
|
369
|
+
assetGroupListingGroupFilterOperation: "assetGroupListingGroupFilters"
|
|
370
|
+
};
|
|
371
|
+
var json = z3.record(z3.unknown());
|
|
372
|
+
var operation = z3.object({
|
|
373
|
+
kind: z3.enum(
|
|
374
|
+
Object.keys(resources)
|
|
375
|
+
),
|
|
376
|
+
create: json.optional(),
|
|
377
|
+
update: json.optional(),
|
|
378
|
+
remove: z3.string().optional(),
|
|
379
|
+
updateMask: z3.string().min(1).optional()
|
|
380
|
+
}).strict();
|
|
381
|
+
var result = (v) => ({
|
|
382
|
+
content: [{ type: "text", text: JSON.stringify(v, null, 2) }]
|
|
383
|
+
});
|
|
384
|
+
function canonical(v) {
|
|
385
|
+
if (Array.isArray(v)) return `[${v.map(canonical).join(",")}]`;
|
|
386
|
+
if (v && typeof v === "object")
|
|
387
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`;
|
|
388
|
+
return JSON.stringify(v);
|
|
389
|
+
}
|
|
390
|
+
async function digest(v) {
|
|
391
|
+
return Array.from(
|
|
392
|
+
new Uint8Array(
|
|
393
|
+
await webcrypto.subtle.digest(
|
|
394
|
+
"SHA-256",
|
|
395
|
+
new TextEncoder().encode(canonical(v))
|
|
396
|
+
)
|
|
397
|
+
)
|
|
398
|
+
).map((n) => n.toString(16).padStart(2, "0")).join("");
|
|
399
|
+
}
|
|
400
|
+
function previewData(v) {
|
|
401
|
+
if (Array.isArray(v)) return v.map(previewData);
|
|
402
|
+
if (v && typeof v === "object")
|
|
403
|
+
return Object.fromEntries(
|
|
404
|
+
Object.entries(v).map(([k, x]) => [
|
|
405
|
+
k,
|
|
406
|
+
k === "data" && typeof x === "string" ? `[base64 media: ${x.length} characters]` : previewData(x)
|
|
407
|
+
])
|
|
408
|
+
);
|
|
409
|
+
return v;
|
|
410
|
+
}
|
|
411
|
+
function normalizeGooglePlan(customerId, input) {
|
|
412
|
+
if (!/^\d{10}$/.test(customerId))
|
|
413
|
+
throw new Error("Use a ten-digit customer ID.");
|
|
414
|
+
const prefix = `customers/${customerId}/`;
|
|
415
|
+
function check(v) {
|
|
416
|
+
if (typeof v === "string" && v.startsWith("customers/") && !v.startsWith(prefix))
|
|
417
|
+
throw new Error(
|
|
418
|
+
"Every customer resource must belong to the selected customerId."
|
|
419
|
+
);
|
|
420
|
+
if (Array.isArray(v)) v.forEach(check);
|
|
421
|
+
else if (v && typeof v === "object")
|
|
422
|
+
for (const [k, x] of Object.entries(v)) {
|
|
423
|
+
if (/access.?token|refresh.?token|developer.?token|authorization|client.?secret/i.test(
|
|
424
|
+
k
|
|
425
|
+
))
|
|
426
|
+
throw new Error("Do not put credentials in a plan.");
|
|
427
|
+
if (k === "data" && typeof x === "string") {
|
|
428
|
+
let valid = x.length > 0 && x.length <= 6990508 && /^[A-Za-z0-9+/]*={0,2}$/.test(x);
|
|
429
|
+
try {
|
|
430
|
+
valid = valid && atob(x).length <= 5 * 1024 * 1024;
|
|
431
|
+
} catch {
|
|
432
|
+
valid = false;
|
|
433
|
+
}
|
|
434
|
+
if (!valid) throw new Error("Image data must be base64, at most 5 MiB decoded.");
|
|
435
|
+
}
|
|
436
|
+
check(x);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return input.map((raw) => {
|
|
440
|
+
const op = operation.parse(raw);
|
|
441
|
+
if ([op.create, op.update, op.remove].filter((x) => x !== void 0).length !== 1)
|
|
442
|
+
throw new Error(
|
|
443
|
+
"Each operation needs exactly one of create, update or remove."
|
|
444
|
+
);
|
|
445
|
+
if (op.update && !op.updateMask)
|
|
446
|
+
throw new Error("Updates require an explicit updateMask.");
|
|
447
|
+
if (op.kind === "adOperation" && op.update && ("name" in op.update || op.updateMask?.split(",").some((field) => field.trim() === "name")))
|
|
448
|
+
throw new Error("Google ad.name is immutable. Edit supported creative fields, or explicitly create a new paused ad with the desired name; never silently replace the original.");
|
|
449
|
+
if (!op.update && op.updateMask)
|
|
450
|
+
throw new Error("updateMask is only valid on updates.");
|
|
451
|
+
if (op.updateMask && /(^|,)\s*(\*|resource_name)\s*(,|$)/.test(op.updateMask))
|
|
452
|
+
throw new Error(
|
|
453
|
+
"Use a precise updateMask; never wildcard or resource_name."
|
|
454
|
+
);
|
|
455
|
+
const body = structuredClone(op.create ?? op.update);
|
|
456
|
+
const expected = prefix + resources[op.kind] + "/";
|
|
457
|
+
const resource = op.remove ?? body?.resourceName;
|
|
458
|
+
if ((op.update || op.remove) && !resource)
|
|
459
|
+
throw new Error("Update/remove requires a resource name.");
|
|
460
|
+
if (resource && (!resource.startsWith(expected) || !/^-?\d+(~[A-Za-z0-9_-]+)*$/.test(resource.slice(expected.length))))
|
|
461
|
+
throw new Error(`Expected resource in ${expected}.`);
|
|
462
|
+
if (op.create && [
|
|
463
|
+
"campaignOperation",
|
|
464
|
+
"adGroupOperation",
|
|
465
|
+
"adGroupAdOperation",
|
|
466
|
+
"assetGroupOperation"
|
|
467
|
+
].includes(op.kind)) {
|
|
468
|
+
if (body.status && body.status !== "PAUSED")
|
|
469
|
+
throw new Error("New campaigns, groups and ads must be PAUSED.");
|
|
470
|
+
body.status = "PAUSED";
|
|
471
|
+
}
|
|
472
|
+
if (op.create && op.kind === "campaignOperation") {
|
|
473
|
+
if (body.advertisingChannelType === "VIDEO")
|
|
474
|
+
throw new Error(
|
|
475
|
+
"Google Ads API cannot create VIDEO campaigns. Use an explicitly requested DEMAND_GEN video campaign instead."
|
|
476
|
+
);
|
|
477
|
+
if (![
|
|
478
|
+
"CONTAINS_EU_POLITICAL_ADVERTISING",
|
|
479
|
+
"DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING"
|
|
480
|
+
].includes(body.containsEuPoliticalAdvertising))
|
|
481
|
+
throw new Error(
|
|
482
|
+
"Explicit EU political advertising declaration required."
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
const value = op.remove ? { remove: op.remove } : op.create ? { create: body } : { update: body, updateMask: op.updateMask };
|
|
486
|
+
check(value);
|
|
487
|
+
return { [op.kind]: value };
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function registerGoogleVideoReads(c, config) {
|
|
491
|
+
c.tool("google_ads_get_video_upload", "Read the processing state and YouTube video ID of one Google Ads upload in the selected customer. Use only the returned videoId after state=PROCESSED. This is a read; it does not upload again.", {
|
|
492
|
+
customerId: z3.string().regex(/^[\d-]+$/),
|
|
493
|
+
currency: z3.string().regex(/^[A-Z]{3}$/).describe("Actual customer currency, verified before reading the upload."),
|
|
494
|
+
uploadId: z3.string().regex(/^\d+$/).describe("Numeric upload ID returned by google_ads_upload_video."),
|
|
495
|
+
loginCustomerId: z3.string().regex(/^[\d-]+$/).optional().describe("Optional MCC manager ID; defaults to the configured manager."),
|
|
496
|
+
testAccountOnly: z3.boolean().optional().describe("Require customer.test_account=true; enable for sandbox checks.")
|
|
497
|
+
}, async (raw) => {
|
|
498
|
+
const a = z3.object({ customerId: z3.string().regex(/^[\d-]+$/), currency: z3.string().regex(/^[A-Z]{3}$/), uploadId: z3.string().regex(/^\d+$/), loginCustomerId: z3.string().regex(/^[\d-]+$/).optional(), testAccountOnly: z3.boolean().optional() }).strict().parse(raw);
|
|
499
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
500
|
+
const { api } = await googleSession(config, cid, a.currency, a.testAccountOnly === true, (a.loginCustomerId ?? config.loginCustomerId)?.replace(/-/g, ""));
|
|
501
|
+
const resourceName = `customers/${cid}/youTubeVideoUploads/${a.uploadId}`;
|
|
502
|
+
const response = await api("googleAds:search", { query: `SELECT you_tube_video_upload.resource_name, you_tube_video_upload.state, you_tube_video_upload.video_id FROM you_tube_video_upload WHERE you_tube_video_upload.resource_name = '${resourceName}' LIMIT 1` });
|
|
503
|
+
const video = response.results?.[0]?.youTubeVideoUpload;
|
|
504
|
+
return result({ uploadId: a.uploadId, state: video?.state ?? "NOT_FOUND", ...video?.state === "PROCESSED" && video.videoId ? { videoId: video.videoId } : {} });
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function registerGoogleExtendedWrites(c, config) {
|
|
508
|
+
const original = c;
|
|
509
|
+
const descriptions = {
|
|
510
|
+
customerId: "Selected Google Ads customer ID, not the manager account.",
|
|
511
|
+
currency: "Actual account currency as a three-letter ISO code; verified natively before writes.",
|
|
512
|
+
loginCustomerId: "Optional MCC ID; defaults to the selected account's inferred manager.",
|
|
513
|
+
testAccountOnly: "Require customer.test_account=true before writing. Always use true for sandbox acceptance.",
|
|
514
|
+
confirm: "Apply only when true and expectedPlanHash matches the exact preview.",
|
|
515
|
+
validateOnly: "Ask Google to validate without applying. Takes priority over confirm.",
|
|
516
|
+
expectedPlanHash: "SHA-256 planHash from the preview of this exact payload, required for application.",
|
|
517
|
+
uploadId: "Numeric ID of a video uploaded through Google Ads in this customer.",
|
|
518
|
+
title: "Nonempty title for this new video upload.",
|
|
519
|
+
mimeType: "Actual MIME type of the supplied video bytes."
|
|
520
|
+
};
|
|
521
|
+
c = {
|
|
522
|
+
tool: (n, d, shape, h) => original.tool(
|
|
523
|
+
n,
|
|
524
|
+
d,
|
|
525
|
+
Object.fromEntries(
|
|
526
|
+
Object.entries(shape).map(([k, v]) => [
|
|
527
|
+
k,
|
|
528
|
+
v.description ? v : v.describe(descriptions[k] ?? k)
|
|
529
|
+
])
|
|
530
|
+
),
|
|
531
|
+
h
|
|
532
|
+
)
|
|
533
|
+
};
|
|
534
|
+
registerGoogleVideoWrites(c, config);
|
|
535
|
+
c.tool(
|
|
536
|
+
"google_ads_apply_plan",
|
|
537
|
+
"Preview, validate and atomically apply a Google Ads v25 build/change plan: campaigns, budgets, ad groups, RSA/Display/Demand Gen ads, image/text/YouTube assets, PMax asset groups and links, keywords, geographic/language targeting listing groups and conversion action configuration (no conversion event uploads). Native REST camelCase fields; amounts are integer micros of the explicitly stated currency. Supports temporary negative resource IDs for ordered creation in one transaction. No VIDEO campaign creation (Google API restriction), billing, account access changes, customer lists or conversion uploads. New campaigns/groups/ads are always PAUSED. Call first without confirm to inspect the plan and get planHash; validateOnly asks Google without applying; execution requires confirm and the matching expectedPlanHash. Read back results with google_ads_run_gaql. Never retry a creation after an uncertain network result before checking its name/IDs.",
|
|
538
|
+
{
|
|
539
|
+
customerId: z3.string().regex(/^[\d-]+$/),
|
|
540
|
+
currency: z3.string().regex(/^[A-Z]{3}$/).describe(
|
|
541
|
+
"Actual account currency, mandatory even when only linking assets."
|
|
542
|
+
),
|
|
543
|
+
operations: z3.array(operation).min(1).max(100).describe(
|
|
544
|
+
"Ordered operations. kind is a native MutateOperation field; create/update contains native REST fields; remove is the full owned resource name. Images use imageAsset.data base64 bytes; videos use youtubeVideoAsset.youtubeVideoId (already uploaded on YouTube)."
|
|
545
|
+
),
|
|
546
|
+
loginCustomerId: z3.string().regex(/^[\d-]+$/).optional(),
|
|
547
|
+
testAccountOnly: z3.boolean().optional().describe(
|
|
548
|
+
"Require Google's customer.test_account=true before any mutation. Use true for acceptance tests."
|
|
549
|
+
),
|
|
550
|
+
validateOnly: z3.boolean().optional(),
|
|
551
|
+
confirm: z3.boolean().optional(),
|
|
552
|
+
expectedPlanHash: z3.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
553
|
+
},
|
|
554
|
+
async (a) => {
|
|
555
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
556
|
+
const operations = normalizeGooglePlan(cid, a.operations);
|
|
557
|
+
if (JSON.stringify(operations).length > 12e6)
|
|
558
|
+
throw new Error(
|
|
559
|
+
"Plan too large; upload assets in separate confirmed plans and reference their resource names."
|
|
560
|
+
);
|
|
561
|
+
const manager = (a.loginCustomerId ?? config.loginCustomerId)?.replace(
|
|
562
|
+
/-/g,
|
|
563
|
+
""
|
|
564
|
+
);
|
|
565
|
+
const planHash = await digest({
|
|
566
|
+
customerId: cid,
|
|
567
|
+
currency: a.currency,
|
|
568
|
+
manager: manager ?? null,
|
|
569
|
+
testAccountOnly: a.testAccountOnly === true,
|
|
570
|
+
operations
|
|
571
|
+
});
|
|
572
|
+
if (!a.confirm && !a.validateOnly)
|
|
573
|
+
return result({
|
|
574
|
+
applied: false,
|
|
575
|
+
planHash,
|
|
576
|
+
customerId: cid,
|
|
577
|
+
currency: a.currency,
|
|
578
|
+
operationCount: operations.length,
|
|
579
|
+
operations: previewData(operations)
|
|
580
|
+
});
|
|
581
|
+
if (a.confirm && !a.validateOnly && a.expectedPlanHash !== planHash)
|
|
582
|
+
throw new Error(
|
|
583
|
+
"Preview this exact plan first and provide its matching expectedPlanHash."
|
|
584
|
+
);
|
|
585
|
+
const { api, customer } = await googleSession(
|
|
586
|
+
config,
|
|
587
|
+
cid,
|
|
588
|
+
a.currency,
|
|
589
|
+
a.testAccountOnly,
|
|
590
|
+
manager
|
|
591
|
+
);
|
|
592
|
+
const response = await api("googleAds:mutate", {
|
|
593
|
+
mutateOperations: operations,
|
|
594
|
+
partialFailure: false,
|
|
595
|
+
validateOnly: a.validateOnly === true
|
|
596
|
+
});
|
|
597
|
+
return result({
|
|
598
|
+
applied: a.validateOnly !== true,
|
|
599
|
+
validatedByGoogle: true,
|
|
600
|
+
planHash,
|
|
601
|
+
operationCount: operations.length,
|
|
602
|
+
customerId: cid,
|
|
603
|
+
currency: a.currency,
|
|
604
|
+
testAccount: customer.testAccount === true,
|
|
605
|
+
result: response
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
async function googleSession(config, cid, currency, testAccountOnly, manager) {
|
|
611
|
+
const auth = await fetch("https://oauth2.googleapis.com/token", {
|
|
612
|
+
method: "POST",
|
|
613
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
614
|
+
body: new URLSearchParams({
|
|
615
|
+
grant_type: "refresh_token",
|
|
616
|
+
client_id: config.clientId,
|
|
617
|
+
client_secret: config.clientSecret,
|
|
618
|
+
refresh_token: config.refreshToken
|
|
619
|
+
}),
|
|
620
|
+
redirect: "error"
|
|
621
|
+
});
|
|
622
|
+
const token = await auth.json();
|
|
623
|
+
if (!auth.ok || !token.access_token)
|
|
624
|
+
throw new Error(
|
|
625
|
+
"Google authorization could not be refreshed. Reconnect the Google Ads source."
|
|
626
|
+
);
|
|
627
|
+
const headers = {
|
|
628
|
+
authorization: `Bearer ${token.access_token}`,
|
|
629
|
+
"developer-token": config.developerToken,
|
|
630
|
+
"content-type": "application/json"
|
|
631
|
+
};
|
|
632
|
+
if (manager) headers["login-customer-id"] = manager;
|
|
633
|
+
async function api(path, body) {
|
|
634
|
+
const r = await fetch(`${BASE}/customers/${cid}/${path}`, {
|
|
635
|
+
method: "POST",
|
|
636
|
+
headers,
|
|
637
|
+
body: JSON.stringify(body),
|
|
638
|
+
redirect: "error"
|
|
639
|
+
});
|
|
640
|
+
const x = await r.json();
|
|
641
|
+
if (!r.ok) {
|
|
642
|
+
const errors = x.error?.details?.flatMap((d) => d.errors ?? []).map((e) => ({
|
|
643
|
+
code: e.errorCode,
|
|
644
|
+
message: e.message,
|
|
645
|
+
location: e.location
|
|
646
|
+
}));
|
|
647
|
+
throw new Error(
|
|
648
|
+
`Google Ads: ${JSON.stringify(errors?.length ? errors : x.error?.message ?? r.status).slice(0, 4e3)}`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
return x;
|
|
652
|
+
}
|
|
653
|
+
const read = await api("googleAds:search", {
|
|
654
|
+
query: "SELECT customer.id, customer.currency_code, customer.test_account FROM customer"
|
|
655
|
+
});
|
|
656
|
+
const customer = read.results?.[0]?.customer;
|
|
657
|
+
if (customer?.id !== cid || customer?.currencyCode !== currency)
|
|
658
|
+
throw new Error("Customer identity or currency does not match the plan.");
|
|
659
|
+
if (testAccountOnly && customer.testAccount !== true)
|
|
660
|
+
throw new Error("This plan requires a Google test account.");
|
|
661
|
+
return { headers, api, customer };
|
|
662
|
+
}
|
|
663
|
+
function registerGoogleVideoWrites(c, config) {
|
|
664
|
+
const common = {
|
|
665
|
+
customerId: z3.string().regex(/^[\d-]+$/),
|
|
666
|
+
currency: z3.string().regex(/^[A-Z]{3}$/),
|
|
667
|
+
loginCustomerId: z3.string().regex(/^[\d-]+$/).optional(),
|
|
668
|
+
testAccountOnly: z3.boolean().optional(),
|
|
669
|
+
confirm: z3.boolean().optional(),
|
|
670
|
+
expectedPlanHash: z3.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
671
|
+
};
|
|
672
|
+
c.tool(
|
|
673
|
+
"google_ads_upload_video",
|
|
674
|
+
"Upload MP4/WebM/QuickTime bytes (max 16 MiB) to the Google-managed YouTube channel for the selected ad account. Visibility is UNLISTED: viewable by anyone with the link, not private. Default preview omits bytes and returns planHash; confirm requires expectedPlanHash. This API has no validateOnly. After upload, query you_tube_video_upload.state and video_id with google_ads_run_gaql; only use the video ID after PROCESSED, via google_ads_apply_plan assetOperation.youtubeVideoAsset. No automatic retry after an uncertain upload.",
|
|
675
|
+
{
|
|
676
|
+
...common,
|
|
677
|
+
bytesBase64: z3.string().min(4).max(22369624).describe(
|
|
678
|
+
"Raw base64 video bytes, no data URL. Prefer an existing YouTube asset for large videos."
|
|
679
|
+
),
|
|
680
|
+
mimeType: z3.enum(["video/mp4", "video/webm", "video/quicktime"]),
|
|
681
|
+
title: z3.string().trim().min(1).max(100),
|
|
682
|
+
description: z3.string().trim().min(1).max(5e3).describe(
|
|
683
|
+
"Nonempty video description; Google rejects an empty value during upload."
|
|
684
|
+
),
|
|
685
|
+
visibility: z3.literal("UNLISTED").describe(
|
|
686
|
+
"Explicit acknowledgment of the visibility of this YouTube upload."
|
|
687
|
+
)
|
|
688
|
+
},
|
|
689
|
+
async (a) => {
|
|
690
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
691
|
+
if (!/^\d{10}$/.test(cid))
|
|
692
|
+
throw new Error("Use a ten-digit customer ID.");
|
|
693
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(a.bytesBase64))
|
|
694
|
+
throw new Error("Invalid base64 video.");
|
|
695
|
+
let binary;
|
|
696
|
+
try {
|
|
697
|
+
binary = atob(a.bytesBase64);
|
|
698
|
+
} catch {
|
|
699
|
+
throw new Error("Invalid base64 video.");
|
|
700
|
+
}
|
|
701
|
+
if (binary.length > 16 * 1024 * 1024)
|
|
702
|
+
throw new Error("Video exceeds 16 MiB.");
|
|
703
|
+
const manager = (a.loginCustomerId ?? config.loginCustomerId)?.replace(
|
|
704
|
+
/-/g,
|
|
705
|
+
""
|
|
706
|
+
);
|
|
707
|
+
const planHash = await digest({
|
|
708
|
+
customerId: cid,
|
|
709
|
+
currency: a.currency,
|
|
710
|
+
manager: manager ?? null,
|
|
711
|
+
testAccountOnly: a.testAccountOnly === true,
|
|
712
|
+
title: a.title,
|
|
713
|
+
description: a.description ?? "",
|
|
714
|
+
mimeType: a.mimeType,
|
|
715
|
+
visibility: a.visibility,
|
|
716
|
+
bytes: a.bytesBase64
|
|
717
|
+
});
|
|
718
|
+
if (!a.confirm)
|
|
719
|
+
return result({
|
|
720
|
+
applied: false,
|
|
721
|
+
planHash,
|
|
722
|
+
customerId: cid,
|
|
723
|
+
title: a.title,
|
|
724
|
+
description: a.description,
|
|
725
|
+
mimeType: a.mimeType,
|
|
726
|
+
visibility: a.visibility,
|
|
727
|
+
byteLength: binary.length
|
|
728
|
+
});
|
|
729
|
+
if (a.expectedPlanHash !== planHash)
|
|
730
|
+
throw new Error(
|
|
731
|
+
"Preview this exact upload first and supply expectedPlanHash."
|
|
732
|
+
);
|
|
733
|
+
const { headers } = await googleSession(
|
|
734
|
+
config,
|
|
735
|
+
cid,
|
|
736
|
+
a.currency,
|
|
737
|
+
a.testAccountOnly,
|
|
738
|
+
manager
|
|
739
|
+
);
|
|
740
|
+
const start = await fetch(
|
|
741
|
+
`https://googleads.googleapis.com/resumable/upload/v25/customers/${cid}/youTubeVideoUploads:create`,
|
|
742
|
+
{
|
|
743
|
+
method: "POST",
|
|
744
|
+
headers: {
|
|
745
|
+
...headers,
|
|
746
|
+
"X-Goog-Upload-Protocol": "resumable",
|
|
747
|
+
"X-Goog-Upload-Command": "start",
|
|
748
|
+
"X-Goog-Upload-Header-Content-Length": String(binary.length)
|
|
749
|
+
},
|
|
750
|
+
body: JSON.stringify({
|
|
751
|
+
customer_id: cid,
|
|
752
|
+
you_tube_video_upload: {
|
|
753
|
+
video_title: a.title,
|
|
754
|
+
video_description: a.description ?? "",
|
|
755
|
+
video_privacy: "UNLISTED"
|
|
756
|
+
}
|
|
757
|
+
}),
|
|
758
|
+
redirect: "error"
|
|
759
|
+
}
|
|
760
|
+
);
|
|
761
|
+
if (!start.ok)
|
|
762
|
+
throw new Error(
|
|
763
|
+
`Google video upload initiation failed (${start.status}). No automatic retry.`
|
|
764
|
+
);
|
|
765
|
+
const target = start.headers.get("x-goog-upload-url");
|
|
766
|
+
if (!target || new URL(target).origin !== "https://googleads.googleapis.com")
|
|
767
|
+
throw new Error(
|
|
768
|
+
"Unexpected Google upload URL; no bytes or credentials sent."
|
|
769
|
+
);
|
|
770
|
+
const uploaded = await fetch(target, {
|
|
771
|
+
method: "PUT",
|
|
772
|
+
headers: {
|
|
773
|
+
...headers,
|
|
774
|
+
"content-type": a.mimeType,
|
|
775
|
+
"X-Goog-Upload-Offset": "0",
|
|
776
|
+
"X-Goog-Upload-Command": "upload, finalize"
|
|
777
|
+
},
|
|
778
|
+
body: Uint8Array.from(binary, (c2) => c2.charCodeAt(0)),
|
|
779
|
+
redirect: "error"
|
|
780
|
+
});
|
|
781
|
+
if (!uploaded.ok) {
|
|
782
|
+
const error = await uploaded.json().catch(() => ({}));
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Google video upload failed (${uploaded.status}): ${String(error.error?.message ?? "unknown error").slice(0, 700)}. Inspect you_tube_video_upload before retrying.`
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const response = await uploaded.json();
|
|
788
|
+
if (!String(response.resourceName).startsWith(
|
|
789
|
+
`customers/${cid}/youTubeVideoUploads/`
|
|
790
|
+
))
|
|
791
|
+
throw new Error(
|
|
792
|
+
"Google did not return an owned video upload resource. Inspect uploads before retrying."
|
|
793
|
+
);
|
|
794
|
+
return result({
|
|
795
|
+
applied: true,
|
|
796
|
+
planHash,
|
|
797
|
+
resourceName: response.resourceName,
|
|
798
|
+
uploadId: String(response.resourceName).split("/").pop(),
|
|
799
|
+
visibility: "UNLISTED",
|
|
800
|
+
nextAction: "Query you_tube_video_upload.state and video_id; wait for PROCESSED before creating the video asset."
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
);
|
|
804
|
+
c.tool(
|
|
805
|
+
"google_ads_remove_video_upload",
|
|
806
|
+
"Remove a video upload owned by the selected ad account. Deletes the uploaded YouTube video and can break ads referencing it. Preview and matching expectedPlanHash required. Google v25 marks title/description immutable; this tool does not pretend to edit them.",
|
|
807
|
+
{ ...common, uploadId: z3.string().regex(/^\d+$/) },
|
|
808
|
+
async (a) => {
|
|
809
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
810
|
+
if (!/^\d{10}$/.test(cid))
|
|
811
|
+
throw new Error("Use a ten-digit customer ID.");
|
|
812
|
+
const manager = (a.loginCustomerId ?? config.loginCustomerId)?.replace(
|
|
813
|
+
/-/g,
|
|
814
|
+
""
|
|
815
|
+
);
|
|
816
|
+
const resourceName = `customers/${cid}/youTubeVideoUploads/${a.uploadId}`;
|
|
817
|
+
const planHash = await digest({
|
|
818
|
+
customerId: cid,
|
|
819
|
+
currency: a.currency,
|
|
820
|
+
manager: manager ?? null,
|
|
821
|
+
testAccountOnly: a.testAccountOnly === true,
|
|
822
|
+
remove: resourceName
|
|
823
|
+
});
|
|
824
|
+
if (!a.confirm)
|
|
825
|
+
return result({ applied: false, planHash, remove: resourceName });
|
|
826
|
+
if (a.expectedPlanHash !== planHash)
|
|
827
|
+
throw new Error(
|
|
828
|
+
"Preview this exact removal first and supply expectedPlanHash."
|
|
829
|
+
);
|
|
830
|
+
const { api } = await googleSession(
|
|
831
|
+
config,
|
|
832
|
+
cid,
|
|
833
|
+
a.currency,
|
|
834
|
+
a.testAccountOnly,
|
|
835
|
+
manager
|
|
836
|
+
);
|
|
837
|
+
return result({
|
|
838
|
+
applied: true,
|
|
839
|
+
planHash,
|
|
840
|
+
result: await api("youTubeVideoUploads:remove", {
|
|
841
|
+
resourceNames: [resourceName]
|
|
842
|
+
})
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/platforms/google-ads/pmax-metrics.ts
|
|
849
|
+
var PMAX_METRIC_FIELDS = [
|
|
850
|
+
"metrics.impressions",
|
|
851
|
+
"metrics.clicks",
|
|
852
|
+
"metrics.cost_micros",
|
|
853
|
+
"metrics.conversions",
|
|
854
|
+
"metrics.conversions_value",
|
|
855
|
+
"metrics.all_conversions",
|
|
856
|
+
"metrics.all_conversions_from_interactions_rate",
|
|
857
|
+
"metrics.all_conversions_from_interactions_value_per_interaction",
|
|
858
|
+
"metrics.all_conversions_value",
|
|
859
|
+
"metrics.all_conversions_value_per_cost",
|
|
860
|
+
"metrics.average_cost",
|
|
861
|
+
"metrics.average_cpc",
|
|
862
|
+
"metrics.average_cpe",
|
|
863
|
+
"metrics.average_cpm",
|
|
864
|
+
"metrics.conversions_from_interactions_rate",
|
|
865
|
+
"metrics.conversions_from_interactions_value_per_interaction",
|
|
866
|
+
"metrics.conversions_value_per_cost",
|
|
867
|
+
"metrics.cost_per_all_conversions",
|
|
868
|
+
"metrics.cost_per_conversion",
|
|
869
|
+
"metrics.cross_device_conversions",
|
|
870
|
+
"metrics.cross_device_conversions_value",
|
|
871
|
+
"metrics.ctr",
|
|
872
|
+
"metrics.engagement_rate",
|
|
873
|
+
"metrics.engagements",
|
|
874
|
+
"metrics.interaction_rate",
|
|
875
|
+
"metrics.interactions",
|
|
876
|
+
"metrics.trueview_average_cpv",
|
|
877
|
+
"metrics.value_per_all_conversions",
|
|
878
|
+
"metrics.value_per_conversion",
|
|
879
|
+
"metrics.video_trueview_view_rate",
|
|
880
|
+
"metrics.video_trueview_views",
|
|
881
|
+
"metrics.view_through_conversions"
|
|
882
|
+
];
|
|
883
|
+
|
|
70
884
|
// src/platforms/google-ads/tools.ts
|
|
71
|
-
import { z as
|
|
885
|
+
import { z as z7 } from "zod";
|
|
72
886
|
|
|
73
887
|
// src/platforms/google-ads/types.ts
|
|
74
888
|
var GoogleAdsApiException = class extends Error {
|
|
@@ -3706,9 +4520,9 @@ var BIDDING_TARGET_DIMENSIONS = [
|
|
|
3706
4520
|
{
|
|
3707
4521
|
key: "campaignStartDate",
|
|
3708
4522
|
name: "Campaign Start Date",
|
|
3709
|
-
description: "The start date of the campaign in YYYY-MM-DD
|
|
4523
|
+
description: "The start date and time of the campaign in the account time zone (YYYY-MM-DD HH:mm:ss).",
|
|
3710
4524
|
category: "entity",
|
|
3711
|
-
apiField: "campaign.
|
|
4525
|
+
apiField: "campaign.start_date_time",
|
|
3712
4526
|
isSegment: false,
|
|
3713
4527
|
isResourceAttribute: true,
|
|
3714
4528
|
compatibleResources: ["campaign"]
|
|
@@ -3716,9 +4530,9 @@ var BIDDING_TARGET_DIMENSIONS = [
|
|
|
3716
4530
|
{
|
|
3717
4531
|
key: "campaignEndDate",
|
|
3718
4532
|
name: "Campaign End Date",
|
|
3719
|
-
description: "The end date of the campaign in YYYY-MM-DD
|
|
4533
|
+
description: "The end date and time of the campaign in the account time zone (YYYY-MM-DD HH:mm:ss), if set.",
|
|
3720
4534
|
category: "entity",
|
|
3721
|
-
apiField: "campaign.
|
|
4535
|
+
apiField: "campaign.end_date_time",
|
|
3722
4536
|
isSegment: false,
|
|
3723
4537
|
isResourceAttribute: true,
|
|
3724
4538
|
compatibleResources: ["campaign"]
|
|
@@ -4151,7 +4965,7 @@ function buildDateWhereClause(startDate, endDate, datePreset) {
|
|
|
4151
4965
|
}
|
|
4152
4966
|
return { clause: null, warnings: [] };
|
|
4153
4967
|
}
|
|
4154
|
-
function planQuery(
|
|
4968
|
+
function planQuery(request2) {
|
|
4155
4969
|
const {
|
|
4156
4970
|
resource,
|
|
4157
4971
|
metrics,
|
|
@@ -4163,7 +4977,7 @@ function planQuery(request) {
|
|
|
4163
4977
|
orderBy,
|
|
4164
4978
|
orderDirection,
|
|
4165
4979
|
limit
|
|
4166
|
-
} =
|
|
4980
|
+
} = request2;
|
|
4167
4981
|
const errors = [];
|
|
4168
4982
|
const warnings = [];
|
|
4169
4983
|
const calculatedMetricKeys = metrics.filter((k) => isCalculatedMetric(k));
|
|
@@ -4516,13 +5330,6 @@ var PlatformApiError = class extends Error {
|
|
|
4516
5330
|
this.retryAfter = retryAfter;
|
|
4517
5331
|
this.name = "PlatformApiError";
|
|
4518
5332
|
}
|
|
4519
|
-
platform;
|
|
4520
|
-
code;
|
|
4521
|
-
isRateLimit;
|
|
4522
|
-
isAuth;
|
|
4523
|
-
isPermission;
|
|
4524
|
-
suggestion;
|
|
4525
|
-
retryAfter;
|
|
4526
5333
|
toMcpError() {
|
|
4527
5334
|
return {
|
|
4528
5335
|
error: this.message,
|
|
@@ -4657,7 +5464,7 @@ var KeywordPlannerRateLimiter = class {
|
|
|
4657
5464
|
};
|
|
4658
5465
|
|
|
4659
5466
|
// src/platforms/google-ads/read-rpc.ts
|
|
4660
|
-
import { z as
|
|
5467
|
+
import { z as z4 } from "zod";
|
|
4661
5468
|
var READ_ONLY_OPERATION_NAMES = [
|
|
4662
5469
|
"generateAudienceCompositionInsights",
|
|
4663
5470
|
"generateAudienceDefinition",
|
|
@@ -4773,13 +5580,13 @@ function validateGoogleAdsReadOnlyRequest(value, path = "request", depth = 0, st
|
|
|
4773
5580
|
}
|
|
4774
5581
|
throw new Error(`${path} must contain JSON-compatible values.`);
|
|
4775
5582
|
}
|
|
4776
|
-
function buildGoogleAdsReadOnlyPath(
|
|
4777
|
-
const config = GOOGLE_ADS_READ_ONLY_OPERATIONS[
|
|
4778
|
-
if (!config) throw new Error(`Unsupported read-only operation: ${
|
|
5583
|
+
function buildGoogleAdsReadOnlyPath(operation2, customerId) {
|
|
5584
|
+
const config = GOOGLE_ADS_READ_ONLY_OPERATIONS[operation2];
|
|
5585
|
+
if (!config) throw new Error(`Unsupported read-only operation: ${operation2}.`);
|
|
4779
5586
|
if (config.scope === "customer") {
|
|
4780
5587
|
const cleanCustomerId = customerId?.replace(/-/g, "");
|
|
4781
5588
|
if (!cleanCustomerId || !/^\d+$/.test(cleanCustomerId)) {
|
|
4782
|
-
throw new Error(`${
|
|
5589
|
+
throw new Error(`${operation2} requires a numeric customerId.`);
|
|
4783
5590
|
}
|
|
4784
5591
|
return { path: `customers/${cleanCustomerId}${config.path}`, config };
|
|
4785
5592
|
}
|
|
@@ -4790,20 +5597,20 @@ function registerGoogleAdsReadOnlyRpcTool(server, client, ok3) {
|
|
|
4790
5597
|
"google_ads_run_readonly_rpc",
|
|
4791
5598
|
"Advanced read-only escape hatch for allowlisted Google Ads services outside GAQL: Audience Insights, Reach Planner, benchmarks, creator/trending insights, targeting suggestions, Smart Campaign suggestions, identity verification, invoices, and payments accounts. The operation is an enum; arbitrary paths and all mutations/uploads are impossible.",
|
|
4792
5599
|
{
|
|
4793
|
-
operation:
|
|
4794
|
-
customerId:
|
|
4795
|
-
request:
|
|
5600
|
+
operation: z4.enum(READ_ONLY_OPERATION_NAMES),
|
|
5601
|
+
customerId: z4.string().regex(/^\d[\d-]*\d$|^\d$/).optional().describe("Required for customer-scoped operations; omit for global planning catalogs"),
|
|
5602
|
+
request: z4.record(z4.unknown()).optional().default({}).describe("Official REST JSON request body for POST operations, or query parameters for GET operations")
|
|
4796
5603
|
},
|
|
4797
|
-
async ({ operation, customerId, request }) => {
|
|
5604
|
+
async ({ operation: operation2, customerId, request: request2 }) => {
|
|
4798
5605
|
try {
|
|
4799
|
-
validateGoogleAdsReadOnlyRequest(
|
|
4800
|
-
const encodedLength = JSON.stringify(
|
|
5606
|
+
validateGoogleAdsReadOnlyRequest(request2);
|
|
5607
|
+
const encodedLength = JSON.stringify(request2).length;
|
|
4801
5608
|
if (encodedLength > 1e5) throw new Error("Encoded request exceeds 100,000 characters.");
|
|
4802
|
-
const { path, config } = buildGoogleAdsReadOnlyPath(
|
|
5609
|
+
const { path, config } = buildGoogleAdsReadOnlyPath(operation2, customerId);
|
|
4803
5610
|
let response;
|
|
4804
5611
|
if (config.method === "GET") {
|
|
4805
5612
|
const query = {};
|
|
4806
|
-
for (const [key, value] of Object.entries(
|
|
5613
|
+
for (const [key, value] of Object.entries(request2)) {
|
|
4807
5614
|
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
4808
5615
|
throw new Error(`GET query parameter ${key} must be a string, number, or boolean.`);
|
|
4809
5616
|
}
|
|
@@ -4811,17 +5618,17 @@ function registerGoogleAdsReadOnlyRpcTool(server, client, ok3) {
|
|
|
4811
5618
|
}
|
|
4812
5619
|
response = await client.runReadOnlyService(path, "GET", void 0, query);
|
|
4813
5620
|
} else {
|
|
4814
|
-
response = await client.runReadOnlyService(path, "POST",
|
|
5621
|
+
response = await client.runReadOnlyService(path, "POST", request2);
|
|
4815
5622
|
}
|
|
4816
5623
|
return ok3({
|
|
4817
5624
|
dataKind: "google_ads_readonly_rpc",
|
|
4818
|
-
operation,
|
|
5625
|
+
operation: operation2,
|
|
4819
5626
|
purpose: config.purpose,
|
|
4820
5627
|
scope: config.scope,
|
|
4821
5628
|
method: config.method,
|
|
4822
5629
|
response,
|
|
4823
5630
|
readOnly: true,
|
|
4824
|
-
warnings:
|
|
5631
|
+
warnings: operation2 === "generateShareablePreviews" ? ["The returned preview URL is shareable and can expose ad creative until it expires; disclose it only to intended recipients."] : [],
|
|
4825
5632
|
limitations: [
|
|
4826
5633
|
"The request body follows Google's native REST schema and is intentionally not translated into this server's metric aliases.",
|
|
4827
5634
|
"Availability depends on developer-token access level, account eligibility, OAuth permissions, and operation-specific quotas."
|
|
@@ -4836,8 +5643,20 @@ function registerGoogleAdsReadOnlyRpcTool(server, client, ok3) {
|
|
|
4836
5643
|
);
|
|
4837
5644
|
}
|
|
4838
5645
|
|
|
5646
|
+
// src/core/redirects.ts
|
|
5647
|
+
function refuseRedirect(response, api) {
|
|
5648
|
+
if (response.status < 300 || response.status >= 400) return;
|
|
5649
|
+
throw new Error(`${api} returned an unexpected HTTP ${response.status} redirect. Redirects are never followed. Check the configured endpoint and authorization.`);
|
|
5650
|
+
}
|
|
5651
|
+
|
|
4839
5652
|
// src/platforms/google-ads/client.ts
|
|
4840
5653
|
var DEFAULT_GOOGLE_ADS_REQUEST_TIMEOUT_MS = 3e4;
|
|
5654
|
+
function withManagerHint(message, url, announcedManager) {
|
|
5655
|
+
if (announcedManager) return message;
|
|
5656
|
+
if (!/permission|PERMISSION_DENIED/i.test(message)) return message;
|
|
5657
|
+
const customer = url.match(/customers\/(\d+)/)?.[1];
|
|
5658
|
+
return `${message} This account may sit under a manager (MCC). Google refuses such requests unless the call names the manager in login-customer-id. Set GOOGLE_ADS_LOGIN_CUSTOMER_ID to the manager's ID` + (customer ? `, or check with google_ads_get_account_hierarchy which manager holds ${customer}` : "") + `.`;
|
|
5659
|
+
}
|
|
4841
5660
|
function googleAdsRequestTimeoutMs() {
|
|
4842
5661
|
const parsed = Number(process.env["GOOGLE_ADS_REQUEST_TIMEOUT_MS"]);
|
|
4843
5662
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GOOGLE_ADS_REQUEST_TIMEOUT_MS;
|
|
@@ -4850,6 +5669,14 @@ var GoogleAdsClient = class {
|
|
|
4850
5669
|
loginCustomerId;
|
|
4851
5670
|
accessToken = "";
|
|
4852
5671
|
tokenExpiresAt = 0;
|
|
5672
|
+
/**
|
|
5673
|
+
* Le gestionnaire résolu pour un compte, ou `undefined` s'il n'en a pas.
|
|
5674
|
+
*
|
|
5675
|
+
* La distinction compte : `undefined` mémorisé veut dire « on a cherché et il
|
|
5676
|
+
* n'y en a pas », ce qui évite de refaire la découverte à chaque appel sur un
|
|
5677
|
+
* compte accessible en direct.
|
|
5678
|
+
*/
|
|
5679
|
+
managerCache = /* @__PURE__ */ new Map();
|
|
4853
5680
|
rateLimiter = new RateLimiter();
|
|
4854
5681
|
keywordPlannerRateLimiter = new KeywordPlannerRateLimiter();
|
|
4855
5682
|
constructor(config) {
|
|
@@ -4906,18 +5733,66 @@ var GoogleAdsClient = class {
|
|
|
4906
5733
|
// ============================================
|
|
4907
5734
|
// PRIVATE METHODS
|
|
4908
5735
|
// ============================================
|
|
4909
|
-
getHeaders() {
|
|
5736
|
+
getHeaders(loginCustomerId) {
|
|
4910
5737
|
const headers = {
|
|
4911
5738
|
Authorization: `Bearer ${this.accessToken}`,
|
|
4912
5739
|
"developer-token": this.developerToken,
|
|
4913
5740
|
"Content-Type": "application/json"
|
|
4914
5741
|
};
|
|
4915
|
-
|
|
4916
|
-
|
|
5742
|
+
const manager = loginCustomerId ?? this.loginCustomerId;
|
|
5743
|
+
if (manager) {
|
|
5744
|
+
headers["login-customer-id"] = stripCustomerId(manager);
|
|
4917
5745
|
}
|
|
4918
5746
|
return headers;
|
|
4919
5747
|
}
|
|
4920
|
-
|
|
5748
|
+
/**
|
|
5749
|
+
* The manager account to announce when querying a given customer.
|
|
5750
|
+
*
|
|
5751
|
+
* Google rejects any request against an account managed by an MCC unless the
|
|
5752
|
+
* call names that MCC in `login-customer-id`. The rejection is a bare
|
|
5753
|
+
* `USER_PERMISSION_DENIED` that names neither the account nor the manager, so
|
|
5754
|
+
* it reads as missing access rather than as a missing header, and that is
|
|
5755
|
+
* where people lose an afternoon.
|
|
5756
|
+
*
|
|
5757
|
+
* `GOOGLE_ADS_LOGIN_CUSTOMER_ID` solves it when you know the answer in
|
|
5758
|
+
* advance. This resolves it when you do not: if the customer is not directly
|
|
5759
|
+
* accessible, the accessible managers are asked which accounts they hold, and
|
|
5760
|
+
* the one holding this customer is used.
|
|
5761
|
+
*
|
|
5762
|
+
* Resolved once and cached for the life of the client. The hierarchy of an
|
|
5763
|
+
* advertising account does not change between two tool calls, and paying two
|
|
5764
|
+
* extra requests on every query to discover that it has not would be worse
|
|
5765
|
+
* than the problem.
|
|
5766
|
+
*/
|
|
5767
|
+
async resolveLoginCustomerId(customerId) {
|
|
5768
|
+
if (this.loginCustomerId) return this.loginCustomerId;
|
|
5769
|
+
const wanted = stripCustomerId(customerId);
|
|
5770
|
+
if (this.managerCache.has(wanted)) return this.managerCache.get(wanted);
|
|
5771
|
+
try {
|
|
5772
|
+
const accessible = (await this.listAccessibleCustomers()).map((name) => stripCustomerId(name.replace(/^customers\//, "")));
|
|
5773
|
+
if (accessible.includes(wanted)) {
|
|
5774
|
+
this.managerCache.set(wanted, void 0);
|
|
5775
|
+
return void 0;
|
|
5776
|
+
}
|
|
5777
|
+
for (const candidate of accessible) {
|
|
5778
|
+
const children = await this.getClientAccounts(candidate);
|
|
5779
|
+
for (const child of children) {
|
|
5780
|
+
if (stripCustomerId(child.id) === wanted) {
|
|
5781
|
+
this.managerCache.set(wanted, candidate);
|
|
5782
|
+
return candidate;
|
|
5783
|
+
}
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
} catch (error) {
|
|
5787
|
+
logger.debug(
|
|
5788
|
+
"google-ads",
|
|
5789
|
+
`Could not resolve a manager for ${wanted}: ${error instanceof Error ? error.message : error}`
|
|
5790
|
+
);
|
|
5791
|
+
}
|
|
5792
|
+
this.managerCache.set(wanted, void 0);
|
|
5793
|
+
return void 0;
|
|
5794
|
+
}
|
|
5795
|
+
async request(url, options = {}, beforeAttempt, deadlineAtMs, loginCustomerId) {
|
|
4921
5796
|
await this.ensureValidToken();
|
|
4922
5797
|
return this.rateLimiter.execute(async () => {
|
|
4923
5798
|
await beforeAttempt?.();
|
|
@@ -4942,10 +5817,11 @@ var GoogleAdsClient = class {
|
|
|
4942
5817
|
// whatever host the redirect names.
|
|
4943
5818
|
redirect: "error",
|
|
4944
5819
|
headers: {
|
|
4945
|
-
...this.getHeaders(),
|
|
5820
|
+
...this.getHeaders(loginCustomerId),
|
|
4946
5821
|
...options.headers
|
|
4947
5822
|
}
|
|
4948
5823
|
});
|
|
5824
|
+
refuseRedirect(response, "Google Ads API");
|
|
4949
5825
|
if (!response.ok) {
|
|
4950
5826
|
const errorBody = await response.json().catch(() => ({}));
|
|
4951
5827
|
const errorDetails = errorBody?.error;
|
|
@@ -4954,7 +5830,7 @@ var GoogleAdsClient = class {
|
|
|
4954
5830
|
const requestId = errorDetails?.details?.[0]?.requestId ?? errorBody?.[0]?.error?.details?.[0]?.requestId;
|
|
4955
5831
|
const detailedMessage = gaErrors?.[0]?.message || errorDetails?.message || `Request failed: ${response.statusText}`;
|
|
4956
5832
|
throw new GoogleAdsApiException(
|
|
4957
|
-
detailedMessage,
|
|
5833
|
+
withManagerHint(detailedMessage, url, loginCustomerId),
|
|
4958
5834
|
response.status,
|
|
4959
5835
|
errorDetails?.status,
|
|
4960
5836
|
requestId,
|
|
@@ -5015,12 +5891,16 @@ var GoogleAdsClient = class {
|
|
|
5015
5891
|
async searchStream(customerId, gaqlQuery) {
|
|
5016
5892
|
const cleanCustomerId = stripCustomerId(customerId);
|
|
5017
5893
|
const url = `${GOOGLE_ADS_API_BASE_URL}/customers/${cleanCustomerId}/googleAds:searchStream`;
|
|
5894
|
+
const manager = await this.resolveLoginCustomerId(cleanCustomerId);
|
|
5018
5895
|
const response = await this.request(
|
|
5019
5896
|
url,
|
|
5020
5897
|
{
|
|
5021
5898
|
method: "POST",
|
|
5022
5899
|
body: JSON.stringify({ query: gaqlQuery })
|
|
5023
|
-
}
|
|
5900
|
+
},
|
|
5901
|
+
void 0,
|
|
5902
|
+
void 0,
|
|
5903
|
+
manager
|
|
5024
5904
|
);
|
|
5025
5905
|
const allRows = [];
|
|
5026
5906
|
for (const batch of response) {
|
|
@@ -5037,12 +5917,19 @@ var GoogleAdsClient = class {
|
|
|
5037
5917
|
async search(customerId, gaqlQuery, pageToken) {
|
|
5038
5918
|
const cleanCustomerId = stripCustomerId(customerId);
|
|
5039
5919
|
const url = `${GOOGLE_ADS_API_BASE_URL}/customers/${cleanCustomerId}/googleAds:search`;
|
|
5920
|
+
const manager = await this.resolveLoginCustomerId(cleanCustomerId);
|
|
5040
5921
|
const body = { query: gaqlQuery };
|
|
5041
5922
|
if (pageToken) body.pageToken = pageToken;
|
|
5042
|
-
return this.request(
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5923
|
+
return this.request(
|
|
5924
|
+
url,
|
|
5925
|
+
{
|
|
5926
|
+
method: "POST",
|
|
5927
|
+
body: JSON.stringify(body)
|
|
5928
|
+
},
|
|
5929
|
+
void 0,
|
|
5930
|
+
void 0,
|
|
5931
|
+
manager
|
|
5932
|
+
);
|
|
5046
5933
|
}
|
|
5047
5934
|
// ============================================
|
|
5048
5935
|
// KEYWORD PLANNER (PLANLESS, READ-ONLY RPCS)
|
|
@@ -5195,8 +6082,8 @@ var GoogleAdsClient = class {
|
|
|
5195
6082
|
return {
|
|
5196
6083
|
id: String(row.customer?.id || cleanCustomerId),
|
|
5197
6084
|
descriptiveName: row.customer?.descriptiveName || "Unknown Account",
|
|
5198
|
-
currencyCode: row.customer?.currencyCode || "
|
|
5199
|
-
timeZone: row.customer?.timeZone || "
|
|
6085
|
+
currencyCode: row.customer?.currencyCode || "",
|
|
6086
|
+
timeZone: row.customer?.timeZone || "",
|
|
5200
6087
|
manager: row.customer?.manager || false,
|
|
5201
6088
|
testAccount: row.customer?.testAccount || false,
|
|
5202
6089
|
resourceName: row.customer?.resourceName || `customers/${cleanCustomerId}`
|
|
@@ -5264,9 +6151,9 @@ var GoogleAdsClient = class {
|
|
|
5264
6151
|
/**
|
|
5265
6152
|
* Execute a query request with automatic query planning
|
|
5266
6153
|
*/
|
|
5267
|
-
async executeQuery(
|
|
6154
|
+
async executeQuery(request2) {
|
|
5268
6155
|
const startTime = Date.now();
|
|
5269
|
-
const plan = planQuery(
|
|
6156
|
+
const plan = planQuery(request2);
|
|
5270
6157
|
const errors = [...plan.errors];
|
|
5271
6158
|
const warnings = [...plan.warnings];
|
|
5272
6159
|
const gaqlQueries = [];
|
|
@@ -5294,7 +6181,7 @@ var GoogleAdsClient = class {
|
|
|
5294
6181
|
gaqlQueries.push(query.gaql);
|
|
5295
6182
|
rawRequests.push({ gaql: query.gaql, resource: query.resource });
|
|
5296
6183
|
try {
|
|
5297
|
-
const apiRows = (await this.search(
|
|
6184
|
+
const apiRows = (await this.search(request2.customerId, query.gaql)).results ?? [];
|
|
5298
6185
|
rawResponses.push(apiRows.slice(0, 5));
|
|
5299
6186
|
const flatRows = apiRows.map((row) => flattenGoogleAdsRow(row));
|
|
5300
6187
|
queryResults.push(flatRows);
|
|
@@ -5505,7 +6392,7 @@ function camelToSnakeResource(key) {
|
|
|
5505
6392
|
}
|
|
5506
6393
|
|
|
5507
6394
|
// src/platforms/google-ads/keyword-planner.ts
|
|
5508
|
-
import { z as
|
|
6395
|
+
import { z as z5 } from "zod";
|
|
5509
6396
|
var FORECAST_WITH_BREAKDOWN_TIME_BUDGET_MS = 45e3;
|
|
5510
6397
|
var MONTHS = [
|
|
5511
6398
|
"JANUARY",
|
|
@@ -5524,17 +6411,17 @@ var MONTHS = [
|
|
|
5524
6411
|
var MONTH_NUMBER = new Map(
|
|
5525
6412
|
MONTHS.map((month, index) => [month, index + 1])
|
|
5526
6413
|
);
|
|
5527
|
-
var customerIdSchema =
|
|
5528
|
-
var numericIdSchema =
|
|
5529
|
-
var keywordTextSchema =
|
|
6414
|
+
var customerIdSchema = z5.string().regex(/^\d[\d-]*\d$|^\d$/, "Expected a numeric Google Ads customer ID, with or without dashes");
|
|
6415
|
+
var numericIdSchema = z5.string().regex(/^\d+$/, "Expected a numeric Google Ads criterion ID");
|
|
6416
|
+
var keywordTextSchema = z5.string().trim().min(1).max(80).refine(
|
|
5530
6417
|
(value) => value.split(/\s+/).length <= 10,
|
|
5531
6418
|
"Google Ads keywords can contain at most 10 words"
|
|
5532
6419
|
);
|
|
5533
|
-
var yearMonthSchema =
|
|
5534
|
-
var isoDateSchema =
|
|
5535
|
-
var networkSchema =
|
|
5536
|
-
var matchTypeSchema =
|
|
5537
|
-
var currencyAmountSchema =
|
|
6420
|
+
var yearMonthSchema = z5.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Expected YYYY-MM");
|
|
6421
|
+
var isoDateSchema = z5.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD");
|
|
6422
|
+
var networkSchema = z5.enum(["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"]);
|
|
6423
|
+
var matchTypeSchema = z5.enum(["EXACT", "PHRASE", "BROAD"]);
|
|
6424
|
+
var currencyAmountSchema = z5.number().finite().min(1e-6).max(1e9);
|
|
5538
6425
|
function compactObject(value) {
|
|
5539
6426
|
return Object.fromEntries(
|
|
5540
6427
|
Object.entries(value).filter(([, fieldValue]) => fieldValue !== void 0)
|
|
@@ -5806,22 +6693,22 @@ function normalizeKeywordHistoricalMetrics(metrics, includeMonthlySearchVolumes
|
|
|
5806
6693
|
trends: computeKeywordPlannerTrends(metrics?.monthlySearchVolumes)
|
|
5807
6694
|
};
|
|
5808
6695
|
}
|
|
5809
|
-
function normalizeHistoricalResult(
|
|
6696
|
+
function normalizeHistoricalResult(result2, includeMonthlySearchVolumes) {
|
|
5810
6697
|
return {
|
|
5811
|
-
keyword:
|
|
5812
|
-
closeVariants:
|
|
6698
|
+
keyword: result2.text ?? null,
|
|
6699
|
+
closeVariants: result2.closeVariants ?? [],
|
|
5813
6700
|
metrics: normalizeKeywordHistoricalMetrics(
|
|
5814
|
-
|
|
6701
|
+
result2.keywordMetrics,
|
|
5815
6702
|
includeMonthlySearchVolumes
|
|
5816
6703
|
)
|
|
5817
6704
|
};
|
|
5818
6705
|
}
|
|
5819
|
-
function normalizeIdeaResult(
|
|
6706
|
+
function normalizeIdeaResult(result2) {
|
|
5820
6707
|
return {
|
|
5821
|
-
keyword:
|
|
5822
|
-
closeVariants:
|
|
5823
|
-
metrics: normalizeKeywordHistoricalMetrics(
|
|
5824
|
-
concepts:
|
|
6708
|
+
keyword: result2.text ?? null,
|
|
6709
|
+
closeVariants: result2.closeVariants ?? [],
|
|
6710
|
+
metrics: normalizeKeywordHistoricalMetrics(result2.keywordIdeaMetrics),
|
|
6711
|
+
concepts: result2.keywordAnnotations?.concepts ?? []
|
|
5825
6712
|
};
|
|
5826
6713
|
}
|
|
5827
6714
|
function normalizeAggregateMetrics(results) {
|
|
@@ -5998,15 +6885,15 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
5998
6885
|
"Get Keyword Planner search-volume history for supplied keywords. Returns average monthly searches, monthly volumes (up to 48 months), latest volume, computed 3-month and YoY changes, competition, CPC/bid ranges, close variants, and optional device totals. This is a read-only planless RPC.",
|
|
5999
6886
|
{
|
|
6000
6887
|
customerId: customerIdSchema.describe("Google Ads serving customer ID; use the client account, not its MCC manager"),
|
|
6001
|
-
keywords:
|
|
6002
|
-
geoTargetIds:
|
|
6888
|
+
keywords: z5.array(keywordTextSchema).min(1).max(1e4).describe("Keywords to analyze; Google may combine near-exact close variants"),
|
|
6889
|
+
geoTargetIds: z5.array(numericIdSchema).max(10).optional().default([]).describe("Geo target criterion IDs, e.g. 2250 for France; empty means all geographies"),
|
|
6003
6890
|
languageId: numericIdSchema.optional().describe("Optional language criterion ID, e.g. 1002 for French; omit for all languages"),
|
|
6004
6891
|
network: networkSchema.optional().default("GOOGLE_SEARCH"),
|
|
6005
|
-
includeAdultKeywords:
|
|
6006
|
-
includeAverageCpc:
|
|
6007
|
-
includeDeviceBreakdown:
|
|
6008
|
-
includeMonthlySearchVolumes:
|
|
6009
|
-
historyMonths:
|
|
6892
|
+
includeAdultKeywords: z5.boolean().optional().default(false),
|
|
6893
|
+
includeAverageCpc: z5.boolean().optional().default(true).describe("Request legacy average CPC in addition to top-of-page bid ranges"),
|
|
6894
|
+
includeDeviceBreakdown: z5.boolean().optional().default(false).describe("Return aggregate searches by device across all requested keywords"),
|
|
6895
|
+
includeMonthlySearchVolumes: z5.boolean().optional().default(true).describe("Include every monthly point in the MCP output. Set false to trim an allowed response; Google's upstream series and the 50,000-point guard are unchanged"),
|
|
6896
|
+
historyMonths: z5.number().int().min(3).max(48).optional().default(24).describe("History length when no explicit YYYY-MM range is supplied; 24 enables YoY calculation"),
|
|
6010
6897
|
startYearMonth: yearMonthSchema.optional().describe("Optional inclusive historical range start YYYY-MM"),
|
|
6011
6898
|
endYearMonth: yearMonthSchema.optional().describe("Optional inclusive historical range end YYYY-MM")
|
|
6012
6899
|
},
|
|
@@ -6026,7 +6913,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6026
6913
|
}) => {
|
|
6027
6914
|
try {
|
|
6028
6915
|
const startedAt = Date.now();
|
|
6029
|
-
const { request, historyRange } = buildKeywordHistoricalMetricsRequest({
|
|
6916
|
+
const { request: request2, historyRange } = buildKeywordHistoricalMetricsRequest({
|
|
6030
6917
|
keywords,
|
|
6031
6918
|
geoTargetIds,
|
|
6032
6919
|
languageId,
|
|
@@ -6042,9 +6929,9 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6042
6929
|
keywords.length,
|
|
6043
6930
|
historyRange.monthCount
|
|
6044
6931
|
);
|
|
6045
|
-
const response = await client.generateKeywordHistoricalMetrics(customerId,
|
|
6046
|
-
const results = (response.results ?? []).map((
|
|
6047
|
-
|
|
6932
|
+
const response = await client.generateKeywordHistoricalMetrics(customerId, request2);
|
|
6933
|
+
const results = (response.results ?? []).map((result2) => normalizeHistoricalResult(
|
|
6934
|
+
result2,
|
|
6048
6935
|
includeMonthlySearchVolumes
|
|
6049
6936
|
));
|
|
6050
6937
|
const warnings = [];
|
|
@@ -6098,21 +6985,21 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6098
6985
|
"Discover Keyword Planner ideas from up to 20 seed keywords, a URL, keywords plus URL, or a whole site. Returns historical volume, monthly trends, computed 3-month/YoY changes, competition, bids, close variants, optional concepts, and pagination. Read-only; no plan is saved.",
|
|
6099
6986
|
{
|
|
6100
6987
|
customerId: customerIdSchema.describe("Google Ads serving customer ID; use the client account, not its MCC manager"),
|
|
6101
|
-
seedKeywords:
|
|
6102
|
-
url:
|
|
6103
|
-
site:
|
|
6104
|
-
geoTargetIds:
|
|
6988
|
+
seedKeywords: z5.array(keywordTextSchema).min(1).max(20).optional(),
|
|
6989
|
+
url: z5.string().trim().min(1).optional().describe("Specific page URL to crawl; combine with seedKeywords if desired"),
|
|
6990
|
+
site: z5.string().trim().min(1).optional().describe("Whole-domain seed; exclusive with seedKeywords/url"),
|
|
6991
|
+
geoTargetIds: z5.array(numericIdSchema).max(10).optional().default([]),
|
|
6105
6992
|
languageId: numericIdSchema.optional(),
|
|
6106
6993
|
network: networkSchema.optional().default("GOOGLE_SEARCH"),
|
|
6107
|
-
includeAdultKeywords:
|
|
6108
|
-
includeAverageCpc:
|
|
6109
|
-
includeDeviceBreakdown:
|
|
6110
|
-
includeKeywordConcepts:
|
|
6111
|
-
historyMonths:
|
|
6994
|
+
includeAdultKeywords: z5.boolean().optional().default(false),
|
|
6995
|
+
includeAverageCpc: z5.boolean().optional().default(true),
|
|
6996
|
+
includeDeviceBreakdown: z5.boolean().optional().default(false),
|
|
6997
|
+
includeKeywordConcepts: z5.boolean().optional().default(true),
|
|
6998
|
+
historyMonths: z5.number().int().min(3).max(48).optional().default(13).describe("13 months is enough to compute latest-month YoY while limiting response size"),
|
|
6112
6999
|
startYearMonth: yearMonthSchema.optional(),
|
|
6113
7000
|
endYearMonth: yearMonthSchema.optional(),
|
|
6114
|
-
pageSize:
|
|
6115
|
-
pageToken:
|
|
7001
|
+
pageSize: z5.number().int().min(1).max(1e4).optional().default(100).describe("Google supports up to 10,000; keep pages small for interactive MCP use and use larger pages only for controlled exports"),
|
|
7002
|
+
pageToken: z5.string().optional().describe("nextPageToken from a prior identical request")
|
|
6116
7003
|
},
|
|
6117
7004
|
async ({
|
|
6118
7005
|
customerId,
|
|
@@ -6134,7 +7021,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6134
7021
|
}) => {
|
|
6135
7022
|
try {
|
|
6136
7023
|
const startedAt = Date.now();
|
|
6137
|
-
const { request, historyRange, seedType } = buildKeywordIdeasRequest({
|
|
7024
|
+
const { request: request2, historyRange, seedType } = buildKeywordIdeasRequest({
|
|
6138
7025
|
seedKeywords,
|
|
6139
7026
|
url,
|
|
6140
7027
|
site,
|
|
@@ -6155,7 +7042,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6155
7042
|
pageSize,
|
|
6156
7043
|
historyRange.monthCount
|
|
6157
7044
|
);
|
|
6158
|
-
const response = await client.generateKeywordIdeas(customerId,
|
|
7045
|
+
const response = await client.generateKeywordIdeas(customerId, request2);
|
|
6159
7046
|
const ideas = (response.results ?? []).map(normalizeIdeaResult);
|
|
6160
7047
|
const nextPageToken = response.nextPageToken ?? null;
|
|
6161
7048
|
return ok3({
|
|
@@ -6204,23 +7091,23 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6204
7091
|
"Forecast impressions, clicks, CTR, CPC, cost, conversions, and CPA for a temporary keyword campaign. Supports targeting, negatives, match type, three bidding strategies, explicit future dates, and an optional independent per-keyword breakdown. This read-only planless RPC does not create a campaign or saved plan.",
|
|
6205
7092
|
{
|
|
6206
7093
|
customerId: customerIdSchema.describe("Google Ads serving customer ID; use a relevant client account for better estimates"),
|
|
6207
|
-
keywords:
|
|
7094
|
+
keywords: z5.array(keywordTextSchema).min(1).max(1e3),
|
|
6208
7095
|
matchType: matchTypeSchema.optional().default("BROAD"),
|
|
6209
|
-
negativeKeywords:
|
|
7096
|
+
negativeKeywords: z5.array(keywordTextSchema).max(1e3).optional().default([]),
|
|
6210
7097
|
negativeMatchType: matchTypeSchema.optional().default("BROAD"),
|
|
6211
|
-
geoTargetIds:
|
|
6212
|
-
languageIds:
|
|
7098
|
+
geoTargetIds: z5.array(numericIdSchema).max(20).optional().default([]),
|
|
7099
|
+
languageIds: z5.array(numericIdSchema).max(10).optional().default([]),
|
|
6213
7100
|
network: networkSchema.optional().default("GOOGLE_SEARCH"),
|
|
6214
|
-
biddingStrategy:
|
|
7101
|
+
biddingStrategy: z5.enum(["MANUAL_CPC", "MAXIMIZE_CLICKS", "MAXIMIZE_CONVERSIONS"]).optional().default("MANUAL_CPC"),
|
|
6215
7102
|
maxCpcBid: currencyAmountSchema.optional().describe("Bid in standard account-currency units; required for MANUAL_CPC"),
|
|
6216
7103
|
dailyBudget: currencyAmountSchema.optional().describe("Daily amount in standard currency units; required for maximize strategies and optional for MANUAL_CPC"),
|
|
6217
7104
|
maxCpcBidCeiling: currencyAmountSchema.optional().describe("Optional standard-currency CPC ceiling for MAXIMIZE_CLICKS"),
|
|
6218
|
-
conversionRate:
|
|
6219
|
-
currencyCode:
|
|
7105
|
+
conversionRate: z5.number().finite().min(0).max(1).optional().describe("Expected conversion rate as a decimal, e.g. 0.02 for 2%"),
|
|
7106
|
+
currencyCode: z5.string().regex(/^[A-Z]{3}$/).optional().describe("Optional ISO 4217 conversion currency; account currency is used by default"),
|
|
6220
7107
|
startDate: isoDateSchema.optional().describe("Optional inclusive future forecast start YYYY-MM-DD; supply with endDate"),
|
|
6221
7108
|
endDate: isoDateSchema.optional().describe("Optional inclusive forecast end YYYY-MM-DD, no more than one year ahead; supply with startDate"),
|
|
6222
|
-
includeKeywordBreakdown:
|
|
6223
|
-
keywordBreakdownLimit:
|
|
7109
|
+
includeKeywordBreakdown: z5.boolean().optional().default(false).describe("Make independent one-keyword forecast calls in addition to the combined campaign forecast"),
|
|
7110
|
+
keywordBreakdownLimit: z5.number().int().min(1).max(20).optional().default(10).describe("Safety cap for additional rate-limited Keyword Planner requests")
|
|
6224
7111
|
},
|
|
6225
7112
|
async ({
|
|
6226
7113
|
customerId,
|
|
@@ -6261,11 +7148,11 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6261
7148
|
startDate,
|
|
6262
7149
|
endDate
|
|
6263
7150
|
};
|
|
6264
|
-
const { request, periodDays } = buildKeywordForecastRequest(baseInput);
|
|
7151
|
+
const { request: request2, periodDays } = buildKeywordForecastRequest(baseInput);
|
|
6265
7152
|
const deadlineAtMs = includeKeywordBreakdown ? startedAt + FORECAST_WITH_BREAKDOWN_TIME_BUDGET_MS : void 0;
|
|
6266
7153
|
const response = await client.generateKeywordForecastMetrics(
|
|
6267
7154
|
customerId,
|
|
6268
|
-
|
|
7155
|
+
request2,
|
|
6269
7156
|
deadlineAtMs
|
|
6270
7157
|
);
|
|
6271
7158
|
const campaignForecast = normalizeKeywordForecastMetrics(
|
|
@@ -6355,7 +7242,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6355
7242
|
}
|
|
6356
7243
|
|
|
6357
7244
|
// src/platforms/google-ads/discovery-tools.ts
|
|
6358
|
-
import { z as
|
|
7245
|
+
import { z as z6 } from "zod";
|
|
6359
7246
|
var FIELD_COLUMNS = [
|
|
6360
7247
|
"name",
|
|
6361
7248
|
"category",
|
|
@@ -6371,12 +7258,12 @@ var FIELD_COLUMNS = [
|
|
|
6371
7258
|
"metrics",
|
|
6372
7259
|
"segments"
|
|
6373
7260
|
].join(", ");
|
|
6374
|
-
var customerIdSchema2 =
|
|
7261
|
+
var customerIdSchema2 = z6.string().regex(
|
|
6375
7262
|
/^\d[\d-]*\d$|^\d$/,
|
|
6376
7263
|
"Expected a numeric Google Ads customer ID, with or without dashes"
|
|
6377
7264
|
);
|
|
6378
|
-
var numericIdSchema2 =
|
|
6379
|
-
var fieldCategorySchema =
|
|
7265
|
+
var numericIdSchema2 = z6.string().regex(/^\d+$/, "Expected a numeric Google Ads ID");
|
|
7266
|
+
var fieldCategorySchema = z6.enum([
|
|
6380
7267
|
"RESOURCE",
|
|
6381
7268
|
"ATTRIBUTE",
|
|
6382
7269
|
"SEGMENT",
|
|
@@ -6431,14 +7318,14 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6431
7318
|
"google_ads_search_fields",
|
|
6432
7319
|
"Search Google's live GoogleAdsField catalog. Discovers every queryable GAQL resource, attribute, segment, metric, enum value, and selectable-with compatibility relationship. Read-only and useful before a raw GAQL query.",
|
|
6433
7320
|
{
|
|
6434
|
-
query:
|
|
6435
|
-
nameContains:
|
|
7321
|
+
query: z6.string().trim().min(6).max(5e4).optional().describe("Optional raw GoogleAdsField SELECT query. When provided it replaces the structured filters."),
|
|
7322
|
+
nameContains: z6.string().trim().min(1).max(250).optional().describe("Case-sensitive field-name substring, e.g. conversion or asset_group"),
|
|
6436
7323
|
category: fieldCategorySchema.optional(),
|
|
6437
|
-
selectable:
|
|
6438
|
-
filterable:
|
|
6439
|
-
sortable:
|
|
6440
|
-
pageSize:
|
|
6441
|
-
pageToken:
|
|
7324
|
+
selectable: z6.boolean().optional(),
|
|
7325
|
+
filterable: z6.boolean().optional(),
|
|
7326
|
+
sortable: z6.boolean().optional(),
|
|
7327
|
+
pageSize: z6.number().int().min(1).max(1e4).optional().default(500),
|
|
7328
|
+
pageToken: z6.string().trim().min(1).max(1e4).optional()
|
|
6442
7329
|
},
|
|
6443
7330
|
async ({ query, nameContains, category, selectable, filterable, sortable, pageSize, pageToken }) => {
|
|
6444
7331
|
try {
|
|
@@ -6481,22 +7368,22 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6481
7368
|
"google_ads_suggest_geo_targets",
|
|
6482
7369
|
"Resolve up to 25 location names or geo target IDs to Google Ads geoTargetConstants. Returns criterion IDs, canonical names, target types, status, parents, locale, and approximate reach. Read-only.",
|
|
6483
7370
|
{
|
|
6484
|
-
locationNames:
|
|
6485
|
-
geoTargetIds:
|
|
6486
|
-
locale:
|
|
6487
|
-
countryCode:
|
|
7371
|
+
locationNames: z6.array(z6.string().trim().min(1).max(250)).min(1).max(25).optional(),
|
|
7372
|
+
geoTargetIds: z6.array(numericIdSchema2).min(1).max(25).optional(),
|
|
7373
|
+
locale: z6.string().trim().regex(/^[A-Za-z]{2,3}(?:[-_][A-Za-z]{2,4})?$/, "Expected a locale such as en, fr, or pt-BR").optional().default("en"),
|
|
7374
|
+
countryCode: z6.string().trim().regex(/^[A-Za-z]{2}$/, "Expected an ISO-3166 alpha-2 country code").optional().transform((value) => value?.toUpperCase())
|
|
6488
7375
|
},
|
|
6489
7376
|
async ({ locationNames, geoTargetIds, locale, countryCode }) => {
|
|
6490
7377
|
try {
|
|
6491
7378
|
if (Boolean(locationNames?.length) === Boolean(geoTargetIds?.length)) {
|
|
6492
7379
|
throw new Error("Provide exactly one of locationNames or geoTargetIds.");
|
|
6493
7380
|
}
|
|
6494
|
-
const
|
|
7381
|
+
const request2 = {
|
|
6495
7382
|
locale,
|
|
6496
7383
|
...countryCode ? { countryCode } : {},
|
|
6497
7384
|
...locationNames ? { locationNames: { names: [...new Set(locationNames)] } } : { geoTargets: { geoTargetConstants: [...new Set(geoTargetIds)].map((id) => `geoTargetConstants/${id}`) } }
|
|
6498
7385
|
};
|
|
6499
|
-
const response = await client.suggestGeoTargetConstants(
|
|
7386
|
+
const response = await client.suggestGeoTargetConstants(request2);
|
|
6500
7387
|
const suggestions = (response.geoTargetConstantSuggestions ?? []).map((suggestion) => ({
|
|
6501
7388
|
searchTerm: suggestion.searchTerm ?? null,
|
|
6502
7389
|
locale: suggestion.locale ?? locale,
|
|
@@ -6510,7 +7397,7 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6510
7397
|
dataKind: "geo_target_suggestions",
|
|
6511
7398
|
suggestions,
|
|
6512
7399
|
count: suggestions.length,
|
|
6513
|
-
request,
|
|
7400
|
+
request: request2,
|
|
6514
7401
|
readOnly: true,
|
|
6515
7402
|
warnings: [],
|
|
6516
7403
|
limitations: ["Reach is approximate and rounded by Google."],
|
|
@@ -6526,19 +7413,19 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6526
7413
|
"Organize supplied keywords into existing Google Ads ad groups. Returns suggested ad group/campaign pairings, normalized keyword text, and suggested match type without creating or editing keywords. Read-only Keyword Planner RPC.",
|
|
6527
7414
|
{
|
|
6528
7415
|
customerId: customerIdSchema2.describe("Serving customer ID containing the existing ad groups"),
|
|
6529
|
-
keywords:
|
|
6530
|
-
adGroupIds:
|
|
7416
|
+
keywords: z6.array(z6.string().trim().min(1).max(80)).min(1).max(1e3),
|
|
7417
|
+
adGroupIds: z6.array(numericIdSchema2).min(1).max(200).describe("Existing ad group IDs in the same customer account")
|
|
6531
7418
|
},
|
|
6532
7419
|
async ({ customerId, keywords, adGroupIds }) => {
|
|
6533
7420
|
try {
|
|
6534
7421
|
const cleanCustomerId = customerId.replace(/-/g, "");
|
|
6535
|
-
const
|
|
7422
|
+
const request2 = {
|
|
6536
7423
|
keywords: [...new Set(keywords)],
|
|
6537
7424
|
adGroups: [...new Set(adGroupIds)].map(
|
|
6538
7425
|
(id) => `customers/${cleanCustomerId}/adGroups/${id}`
|
|
6539
7426
|
)
|
|
6540
7427
|
};
|
|
6541
|
-
const response = await client.generateAdGroupThemes(cleanCustomerId,
|
|
7428
|
+
const response = await client.generateAdGroupThemes(cleanCustomerId, request2);
|
|
6542
7429
|
const suggestions = response.adGroupKeywordSuggestions ?? [];
|
|
6543
7430
|
const unusableAdGroups = response.unusableAdGroups ?? [];
|
|
6544
7431
|
return ok3({
|
|
@@ -6547,8 +7434,8 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6547
7434
|
suggestionCount: suggestions.length,
|
|
6548
7435
|
unusableAdGroups,
|
|
6549
7436
|
unusableAdGroupCount: unusableAdGroups.length,
|
|
6550
|
-
requestedKeywordCount:
|
|
6551
|
-
requestedAdGroupCount:
|
|
7437
|
+
requestedKeywordCount: request2.keywords.length,
|
|
7438
|
+
requestedAdGroupCount: request2.adGroups.length,
|
|
6552
7439
|
readOnly: true,
|
|
6553
7440
|
warnings: unusableAdGroups.length ? ["Google could not use one or more supplied ad groups; inspect unusableAdGroups for their campaign context."] : [],
|
|
6554
7441
|
limitations: ["This RPC suggests organization into existing ad groups; it does not create themes, ad groups, or keywords."],
|
|
@@ -6562,11 +7449,11 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6562
7449
|
}
|
|
6563
7450
|
|
|
6564
7451
|
// src/platforms/google-ads/tools.ts
|
|
6565
|
-
var customerIdSchema3 =
|
|
6566
|
-
var loginCustomerIdSchema =
|
|
6567
|
-
var isoDateSchema2 =
|
|
6568
|
-
var numericIdSchema3 =
|
|
6569
|
-
var gaqlEnumSchema =
|
|
7452
|
+
var customerIdSchema3 = z7.string().describe("Google Ads customer ID (without dashes, e.g., 1234567890)");
|
|
7453
|
+
var loginCustomerIdSchema = z7.string().optional().describe("MCC Manager account ID (required for sub-accounts managed by an MCC)");
|
|
7454
|
+
var isoDateSchema2 = z7.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected date format YYYY-MM-DD");
|
|
7455
|
+
var numericIdSchema3 = z7.string().regex(/^\d+$/, "Expected a numeric Google Ads ID");
|
|
7456
|
+
var gaqlEnumSchema = z7.string().regex(/^[A-Z0-9_]+$/, "Expected an uppercase Google Ads enum value");
|
|
6570
7457
|
var ZERO_METRIC_LIMIT_WARNING = "All returned rows have zero metric values and rowCount equals the requested limit. The response may be truncated before active entities; retry with a higher limit or pass orderBy on a non-zero metric.";
|
|
6571
7458
|
function isAgentResponseRecord(value) {
|
|
6572
7459
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -6729,6 +7616,35 @@ function normalizeChangeEventRange(startDate, endDate) {
|
|
|
6729
7616
|
warnings
|
|
6730
7617
|
};
|
|
6731
7618
|
}
|
|
7619
|
+
function recordOf(value) {
|
|
7620
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
7621
|
+
}
|
|
7622
|
+
function assetIdFromResourceName(value) {
|
|
7623
|
+
if (typeof value !== "string") return void 0;
|
|
7624
|
+
return value.match(/\/assets\/(\d+)$/)?.[1];
|
|
7625
|
+
}
|
|
7626
|
+
function youtubeDerivedUrls(videoId) {
|
|
7627
|
+
if (typeof videoId !== "string" || !/^[A-Za-z0-9_-]{5,20}$/.test(videoId)) return void 0;
|
|
7628
|
+
return {
|
|
7629
|
+
watch_url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
7630
|
+
embed_url: `https://www.youtube.com/embed/${videoId}`,
|
|
7631
|
+
thumbnail_url: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`,
|
|
7632
|
+
thumbnail_fallback_url: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
|
|
7633
|
+
};
|
|
7634
|
+
}
|
|
7635
|
+
function collectAssetIds(value, into) {
|
|
7636
|
+
const id = assetIdFromResourceName(value);
|
|
7637
|
+
if (id) {
|
|
7638
|
+
into.add(id);
|
|
7639
|
+
return;
|
|
7640
|
+
}
|
|
7641
|
+
if (Array.isArray(value)) {
|
|
7642
|
+
for (const item of value) collectAssetIds(item, into);
|
|
7643
|
+
return;
|
|
7644
|
+
}
|
|
7645
|
+
const record = recordOf(value);
|
|
7646
|
+
if (record) for (const item of Object.values(record)) collectAssetIds(item, into);
|
|
7647
|
+
}
|
|
6732
7648
|
async function runGaqlWithFallback(client, customerId, attempts) {
|
|
6733
7649
|
const warnings = [];
|
|
6734
7650
|
let lastError;
|
|
@@ -6816,7 +7732,7 @@ function registerGoogleAdsTools(server, config) {
|
|
|
6816
7732
|
Example: SELECT campaign.name, metrics.impressions FROM campaign WHERE campaign.status = 'ENABLED' AND segments.date DURING LAST_30_DAYS ORDER BY metrics.impressions DESC LIMIT 100`,
|
|
6817
7733
|
{
|
|
6818
7734
|
customerId: customerIdSchema3,
|
|
6819
|
-
query:
|
|
7735
|
+
query: z7.string().min(10).describe("GAQL query string (SELECT ... FROM ... WHERE ...)")
|
|
6820
7736
|
},
|
|
6821
7737
|
async ({ customerId, query }) => {
|
|
6822
7738
|
try {
|
|
@@ -6833,12 +7749,12 @@ Example: SELECT campaign.name, metrics.impressions FROM campaign WHERE campaign.
|
|
|
6833
7749
|
Use google-ads://metrics for available metrics, google-ads://dimensions for dimensions.`,
|
|
6834
7750
|
{
|
|
6835
7751
|
customerId: customerIdSchema3,
|
|
6836
|
-
resource:
|
|
6837
|
-
metrics:
|
|
6838
|
-
dimensions:
|
|
6839
|
-
startDate:
|
|
6840
|
-
endDate:
|
|
6841
|
-
datePreset:
|
|
7752
|
+
resource: z7.string().optional().default("campaign").describe("GAQL FROM clause resource type (campaign, ad_group, ad_group_ad, keyword_view, shopping_performance_view, asset_group, geographic_view, video, search_term_view, landing_page_view, etc.)"),
|
|
7753
|
+
metrics: z7.array(z7.string()).min(1).describe("Metric keys (e.g., impressions, clicks, cost_micros, conversions)"),
|
|
7754
|
+
dimensions: z7.array(z7.string()).optional().describe("Dimension keys (e.g., date, campaignName, device)"),
|
|
7755
|
+
startDate: z7.string().optional().describe("Start date YYYY-MM-DD"),
|
|
7756
|
+
endDate: z7.string().optional().describe("End date YYYY-MM-DD"),
|
|
7757
|
+
datePreset: z7.enum([
|
|
6842
7758
|
"TODAY",
|
|
6843
7759
|
"YESTERDAY",
|
|
6844
7760
|
"LAST_7_DAYS",
|
|
@@ -6850,9 +7766,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6850
7766
|
"THIS_QUARTER",
|
|
6851
7767
|
"LAST_QUARTER"
|
|
6852
7768
|
]).optional().describe("Predefined date range"),
|
|
6853
|
-
orderBy:
|
|
6854
|
-
orderDirection:
|
|
6855
|
-
limit:
|
|
7769
|
+
orderBy: z7.string().optional().describe("Optional GAQL field to order by, e.g. metrics.impressions or campaign.name"),
|
|
7770
|
+
orderDirection: z7.enum(["ASC", "DESC"]).optional().default("DESC"),
|
|
7771
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(500)
|
|
6856
7772
|
},
|
|
6857
7773
|
async ({ customerId, resource, metrics, dimensions, startDate, endDate, datePreset, orderBy, orderDirection, limit }) => {
|
|
6858
7774
|
try {
|
|
@@ -6879,7 +7795,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6879
7795
|
gaqlPreview: generateQueryPreview(plan)
|
|
6880
7796
|
});
|
|
6881
7797
|
}
|
|
6882
|
-
const
|
|
7798
|
+
const result2 = await client.executeQuery({
|
|
6883
7799
|
customerId,
|
|
6884
7800
|
resource,
|
|
6885
7801
|
metrics,
|
|
@@ -6892,18 +7808,18 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6892
7808
|
orderDirection,
|
|
6893
7809
|
limit
|
|
6894
7810
|
});
|
|
6895
|
-
const debugWarnings = Array.isArray(
|
|
6896
|
-
const debugErrors = Array.isArray(
|
|
6897
|
-
const zeroMetricLimitWarnings =
|
|
7811
|
+
const debugWarnings = Array.isArray(result2.debug.warnings) ? result2.debug.warnings : [];
|
|
7812
|
+
const debugErrors = Array.isArray(result2.debug.errors) ? result2.debug.errors : [];
|
|
7813
|
+
const zeroMetricLimitWarnings = result2.data.length === limit && allReturnedMetricValuesAreZero(result2.data, metrics) ? [ZERO_METRIC_LIMIT_WARNING] : [];
|
|
6898
7814
|
const warnings = [...dateInput.warnings, ...debugWarnings, ...zeroMetricLimitWarnings];
|
|
6899
7815
|
return ok({
|
|
6900
7816
|
status: debugErrors.length > 0 ? "error" : "ok",
|
|
6901
|
-
data:
|
|
6902
|
-
rowCount:
|
|
7817
|
+
data: result2.data,
|
|
7818
|
+
rowCount: result2.data.length,
|
|
6903
7819
|
errors: debugErrors,
|
|
6904
7820
|
warnings,
|
|
6905
7821
|
debug: {
|
|
6906
|
-
...
|
|
7822
|
+
...result2.debug,
|
|
6907
7823
|
warnings,
|
|
6908
7824
|
executionTimeMs: Date.now() - startTime,
|
|
6909
7825
|
gaqlPreview: generateQueryPreview(plan)
|
|
@@ -6919,8 +7835,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6919
7835
|
"List campaigns for a Google Ads account with status, budget, channel type, and bidding strategy.",
|
|
6920
7836
|
{
|
|
6921
7837
|
customerId: customerIdSchema3,
|
|
6922
|
-
statusFilter:
|
|
6923
|
-
limit:
|
|
7838
|
+
statusFilter: z7.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
7839
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(100)
|
|
6924
7840
|
},
|
|
6925
7841
|
async ({ customerId, statusFilter, limit }) => {
|
|
6926
7842
|
try {
|
|
@@ -6939,9 +7855,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6939
7855
|
"List ad groups for a Google Ads account, optionally filtered by campaign.",
|
|
6940
7856
|
{
|
|
6941
7857
|
customerId: customerIdSchema3,
|
|
6942
|
-
campaignId:
|
|
6943
|
-
statusFilter:
|
|
6944
|
-
limit:
|
|
7858
|
+
campaignId: z7.string().optional().describe("Filter by campaign ID"),
|
|
7859
|
+
statusFilter: z7.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
7860
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(100)
|
|
6945
7861
|
},
|
|
6946
7862
|
async ({ customerId, campaignId, statusFilter, limit }) => {
|
|
6947
7863
|
try {
|
|
@@ -6963,9 +7879,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6963
7879
|
"Get keyword-level performance data from keyword_view resource. Shows quality score, impressions, clicks, cost.",
|
|
6964
7880
|
{
|
|
6965
7881
|
customerId: customerIdSchema3,
|
|
6966
|
-
startDate:
|
|
6967
|
-
endDate:
|
|
6968
|
-
limit:
|
|
7882
|
+
startDate: z7.string().describe("Start date YYYY-MM-DD"),
|
|
7883
|
+
endDate: z7.string().describe("End date YYYY-MM-DD"),
|
|
7884
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(100)
|
|
6969
7885
|
},
|
|
6970
7886
|
async ({ customerId, startDate, endDate, limit }) => {
|
|
6971
7887
|
try {
|
|
@@ -6981,14 +7897,14 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6981
7897
|
"google_ads_validate_query",
|
|
6982
7898
|
"Validate metric/dimension/resource compatibility BEFORE executing a query. Checks segment restrictions and resource availability.",
|
|
6983
7899
|
{
|
|
6984
|
-
metrics:
|
|
6985
|
-
dimensions:
|
|
6986
|
-
resource:
|
|
7900
|
+
metrics: z7.array(z7.string()).min(1).describe("Metric keys to validate"),
|
|
7901
|
+
dimensions: z7.array(z7.string()).optional().describe("Dimension keys to validate"),
|
|
7902
|
+
resource: z7.string().optional().default("campaign")
|
|
6987
7903
|
},
|
|
6988
7904
|
async ({ metrics, dimensions, resource }) => {
|
|
6989
7905
|
try {
|
|
6990
|
-
const
|
|
6991
|
-
return ok(
|
|
7906
|
+
const result2 = validateQuerySelection(metrics, dimensions ?? [], resource, dimensions ?? []);
|
|
7907
|
+
return ok(result2);
|
|
6992
7908
|
} catch (e) {
|
|
6993
7909
|
return formatMcpToolError(e);
|
|
6994
7910
|
}
|
|
@@ -7018,7 +7934,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7018
7934
|
warnings.push("GOOGLE_ADS_LOGIN_CUSTOMER_ID is configured but was not returned by listAccessibleCustomers; verify MCC access if child account queries fail.");
|
|
7019
7935
|
}
|
|
7020
7936
|
if (!loginCustomerId && accessibleCustomers.some((customer) => customer.manager)) {
|
|
7021
|
-
warnings.push(
|
|
7937
|
+
warnings.push(
|
|
7938
|
+
"Manager accounts are accessible and GOOGLE_ADS_LOGIN_CUSTOMER_ID is not set. The server resolves the manager of each account it queries, so this is not a fault on its own. Setting the variable skips that discovery and is worth it when every account sits under the same MCC."
|
|
7939
|
+
);
|
|
7022
7940
|
}
|
|
7023
7941
|
return ok({
|
|
7024
7942
|
status: "ok",
|
|
@@ -7049,7 +7967,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7049
7967
|
"List accessible customers and, where possible, manager/client relationships from GAQL customer_client. Falls back to accessible customers if hierarchy queries are unavailable.",
|
|
7050
7968
|
{
|
|
7051
7969
|
managerCustomerId: loginCustomerIdSchema,
|
|
7052
|
-
includeInactive:
|
|
7970
|
+
includeInactive: z7.boolean().optional().default(false).describe("Include non-ENABLED customer_client links")
|
|
7053
7971
|
},
|
|
7054
7972
|
async ({ managerCustomerId, includeInactive }) => {
|
|
7055
7973
|
try {
|
|
@@ -7124,8 +8042,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7124
8042
|
"List conversion actions with status, type, category, primary/include-in-conversions flags, owner customer, and last activity dates when supported.",
|
|
7125
8043
|
{
|
|
7126
8044
|
customerId: customerIdSchema3,
|
|
7127
|
-
statusFilter:
|
|
7128
|
-
limit:
|
|
8045
|
+
statusFilter: z7.enum(["ENABLED", "HIDDEN", "REMOVED"]).optional(),
|
|
8046
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7129
8047
|
},
|
|
7130
8048
|
async ({ customerId, statusFilter, limit }) => {
|
|
7131
8049
|
try {
|
|
@@ -7164,11 +8082,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7164
8082
|
ORDER BY conversion_action.name ASC
|
|
7165
8083
|
LIMIT ${limit}
|
|
7166
8084
|
`);
|
|
7167
|
-
const
|
|
8085
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7168
8086
|
{ label: "conversion_action_with_last_activity", gaql: richGaql, failureWarning: "Conversion action query with last activity metrics failed" },
|
|
7169
8087
|
{ label: "conversion_action_minimal", gaql: fallbackGaql }
|
|
7170
8088
|
]);
|
|
7171
|
-
return ok({ conversionActions:
|
|
8089
|
+
return ok({ conversionActions: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7172
8090
|
} catch (e) {
|
|
7173
8091
|
return formatMcpToolError(e);
|
|
7174
8092
|
}
|
|
@@ -7183,10 +8101,10 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7183
8101
|
endDate: isoDateSchema2.optional().describe("End date YYYY-MM-DD. Defaults to today."),
|
|
7184
8102
|
resourceType: gaqlEnumSchema.optional().describe("Optional ChangeEventResourceType filter, e.g. CAMPAIGN or AD_GROUP_AD"),
|
|
7185
8103
|
operation: gaqlEnumSchema.optional().describe("Optional ResourceChangeOperation filter, e.g. CREATE, UPDATE, REMOVE"),
|
|
7186
|
-
userEmail:
|
|
7187
|
-
limit:
|
|
8104
|
+
userEmail: z7.string().email().optional().describe("Optional user email filter"),
|
|
8105
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7188
8106
|
},
|
|
7189
|
-
async ({ customerId, startDate, endDate, resourceType, operation, userEmail, limit }) => {
|
|
8107
|
+
async ({ customerId, startDate, endDate, resourceType, operation: operation2, userEmail, limit }) => {
|
|
7190
8108
|
try {
|
|
7191
8109
|
const range = normalizeChangeEventRange(startDate, endDate);
|
|
7192
8110
|
const where = [
|
|
@@ -7194,7 +8112,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7194
8112
|
`change_event.change_date_time < '${range.endExclusiveDate}'`
|
|
7195
8113
|
];
|
|
7196
8114
|
if (resourceType) where.push(`change_event.change_resource_type = '${resourceType}'`);
|
|
7197
|
-
if (
|
|
8115
|
+
if (operation2) where.push(`change_event.resource_change_operation = '${operation2}'`);
|
|
7198
8116
|
if (userEmail) where.push(`change_event.user_email = '${quoteGaqlString(userEmail)}'`);
|
|
7199
8117
|
const gaql = oneLineGaql(`
|
|
7200
8118
|
SELECT
|
|
@@ -7232,8 +8150,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7232
8150
|
{
|
|
7233
8151
|
customerId: customerIdSchema3,
|
|
7234
8152
|
typeFilter: gaqlEnumSchema.optional().describe("Optional RecommendationType enum filter"),
|
|
7235
|
-
includeDismissed:
|
|
7236
|
-
limit:
|
|
8153
|
+
includeDismissed: z7.boolean().optional().default(false),
|
|
8154
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7237
8155
|
},
|
|
7238
8156
|
async ({ customerId, typeFilter, includeDismissed, limit }) => {
|
|
7239
8157
|
try {
|
|
@@ -7273,11 +8191,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7273
8191
|
ORDER BY recommendation.type ASC
|
|
7274
8192
|
LIMIT ${limit}
|
|
7275
8193
|
`);
|
|
7276
|
-
const
|
|
8194
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7277
8195
|
{ label: "recommendations_with_impact", gaql: richGaql, failureWarning: "Recommendation query with attributed resources/impact failed" },
|
|
7278
8196
|
{ label: "recommendations_minimal", gaql: fallbackGaql }
|
|
7279
8197
|
]);
|
|
7280
|
-
return ok({ recommendations:
|
|
8198
|
+
return ok({ recommendations: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7281
8199
|
} catch (e) {
|
|
7282
8200
|
return formatMcpToolError(e);
|
|
7283
8201
|
}
|
|
@@ -7288,8 +8206,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7288
8206
|
"List campaign budgets with amount, status, delivery method, and recommended budget fields when supported.",
|
|
7289
8207
|
{
|
|
7290
8208
|
customerId: customerIdSchema3,
|
|
7291
|
-
statusFilter:
|
|
7292
|
-
limit:
|
|
8209
|
+
statusFilter: z7.enum(["ENABLED", "REMOVED", "UNKNOWN", "UNSPECIFIED"]).optional(),
|
|
8210
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7293
8211
|
},
|
|
7294
8212
|
async ({ customerId, statusFilter, limit }) => {
|
|
7295
8213
|
try {
|
|
@@ -7333,11 +8251,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7333
8251
|
ORDER BY campaign_budget.name ASC
|
|
7334
8252
|
LIMIT ${limit}
|
|
7335
8253
|
`);
|
|
7336
|
-
const
|
|
8254
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7337
8255
|
{ label: "campaign_budget_with_recommendations", gaql: richGaql, failureWarning: "Campaign budget query with recommended budget fields failed" },
|
|
7338
8256
|
{ label: "campaign_budget_minimal", gaql: fallbackGaql }
|
|
7339
8257
|
]);
|
|
7340
|
-
return ok({ budgets:
|
|
8258
|
+
return ok({ budgets: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7341
8259
|
} catch (e) {
|
|
7342
8260
|
return formatMcpToolError(e);
|
|
7343
8261
|
}
|
|
@@ -7351,7 +8269,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7351
8269
|
startDate: isoDateSchema2.optional().describe("Optional start date YYYY-MM-DD for metrics"),
|
|
7352
8270
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD for metrics"),
|
|
7353
8271
|
typeFilter: gaqlEnumSchema.optional().describe("Optional BiddingStrategyType enum filter"),
|
|
7354
|
-
limit:
|
|
8272
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7355
8273
|
},
|
|
7356
8274
|
async ({ customerId, startDate, endDate, typeFilter, limit }) => {
|
|
7357
8275
|
try {
|
|
@@ -7398,11 +8316,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7398
8316
|
ORDER BY bidding_strategy.name ASC
|
|
7399
8317
|
LIMIT ${limit}
|
|
7400
8318
|
`);
|
|
7401
|
-
const
|
|
8319
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7402
8320
|
{ label: "bidding_strategy_requested", gaql, failureWarning: "Bidding strategy query with requested fields failed" },
|
|
7403
8321
|
{ label: "bidding_strategy_structure", gaql: fallbackGaql }
|
|
7404
8322
|
]);
|
|
7405
|
-
return ok({ biddingStrategies:
|
|
8323
|
+
return ok({ biddingStrategies: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7406
8324
|
} catch (e) {
|
|
7407
8325
|
return formatMcpToolError(e);
|
|
7408
8326
|
}
|
|
@@ -7413,13 +8331,13 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7413
8331
|
"Fetch search term performance from search_term_view or campaign_search_term_insight depending on reportType.",
|
|
7414
8332
|
{
|
|
7415
8333
|
customerId: customerIdSchema3,
|
|
7416
|
-
reportType:
|
|
8334
|
+
reportType: z7.enum(["search_term_view", "campaign_search_term_insight"]).optional().default("search_term_view"),
|
|
7417
8335
|
startDate: isoDateSchema2.describe("Start date YYYY-MM-DD"),
|
|
7418
8336
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
7419
8337
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7420
8338
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter. Applies directly to search_term_view and via segments.ad_group for insight reports."),
|
|
7421
|
-
searchTermContains:
|
|
7422
|
-
limit:
|
|
8339
|
+
searchTermContains: z7.string().min(1).optional().describe("Optional substring filter for search_term_view"),
|
|
8340
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7423
8341
|
},
|
|
7424
8342
|
async ({ customerId, reportType, startDate, endDate, campaignId, adGroupId, searchTermContains, limit }) => {
|
|
7425
8343
|
try {
|
|
@@ -7492,7 +8410,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7492
8410
|
startDate: isoDateSchema2.describe("Start date YYYY-MM-DD"),
|
|
7493
8411
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
7494
8412
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7495
|
-
limit:
|
|
8413
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7496
8414
|
},
|
|
7497
8415
|
async ({ customerId, startDate, endDate, campaignId, limit }) => {
|
|
7498
8416
|
try {
|
|
@@ -7536,11 +8454,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7536
8454
|
ORDER BY metrics.clicks DESC
|
|
7537
8455
|
LIMIT ${limit}
|
|
7538
8456
|
`);
|
|
7539
|
-
const
|
|
8457
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7540
8458
|
{ label: "landing_page_with_quality_metrics", gaql: richGaql, failureWarning: "Landing page query with quality metrics failed" },
|
|
7541
8459
|
{ label: "landing_page_minimal", gaql: fallbackGaql }
|
|
7542
8460
|
]);
|
|
7543
|
-
return ok({ landingPages:
|
|
8461
|
+
return ok({ landingPages: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7544
8462
|
} catch (e) {
|
|
7545
8463
|
return formatMcpToolError(e);
|
|
7546
8464
|
}
|
|
@@ -7553,27 +8471,30 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7553
8471
|
customerId: customerIdSchema3,
|
|
7554
8472
|
startDate: isoDateSchema2.optional().describe("Optional start date YYYY-MM-DD for performance metrics"),
|
|
7555
8473
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD for performance metrics"),
|
|
8474
|
+
metrics: z7.array(z7.enum(PMAX_METRIC_FIELDS)).min(1).max(32).optional().describe("Additional asset-compatible GAQL metric fields. Used with a date range; delivery metrics are always included. Omit for the default delivery/conversion fields."),
|
|
7556
8475
|
campaignId: numericIdSchema3.optional().describe("Optional Performance Max campaign ID filter"),
|
|
7557
8476
|
assetGroupId: numericIdSchema3.optional().describe("Optional asset group ID filter"),
|
|
8477
|
+
assetGroupIds: z7.array(numericIdSchema3).min(1).max(10).optional().describe("Restrict reporting to these asset groups."),
|
|
8478
|
+
assetId: numericIdSchema3.optional().describe("Restrict reporting to one asset."),
|
|
8479
|
+
daily: z7.boolean().optional().default(false).describe("Segment dated performance by segments.date."),
|
|
7558
8480
|
fieldType: gaqlEnumSchema.optional().describe("Optional AssetFieldType enum filter, e.g. HEADLINE, LONG_HEADLINE, MARKETING_IMAGE"),
|
|
7559
|
-
statusFilter:
|
|
7560
|
-
limit:
|
|
8481
|
+
statusFilter: z7.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
8482
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7561
8483
|
},
|
|
7562
|
-
async ({ customerId, startDate, endDate, campaignId, assetGroupId, fieldType, statusFilter, limit }) => {
|
|
8484
|
+
async ({ customerId, startDate, endDate, metrics, campaignId, assetGroupId, assetGroupIds, assetId, daily, fieldType, statusFilter, limit }) => {
|
|
7563
8485
|
try {
|
|
7564
8486
|
const warnings = [];
|
|
7565
8487
|
const where = ["campaign.advertising_channel_type = 'PERFORMANCE_MAX'", ...optionalDateRange(startDate, endDate)];
|
|
7566
8488
|
if (campaignId) where.push(`campaign.id = ${campaignId}`);
|
|
7567
8489
|
if (assetGroupId) where.push(`asset_group.id = ${assetGroupId}`);
|
|
8490
|
+
if (assetGroupIds) where.push(`asset_group.id IN (${assetGroupIds.join(",")})`);
|
|
8491
|
+
if (assetId) where.push(`asset.id = ${assetId}`);
|
|
8492
|
+
if (daily && (!startDate || !endDate)) throw new Error("Daily reporting requires both startDate and endDate.");
|
|
7568
8493
|
if (fieldType) where.push(`asset_group_asset.field_type = '${fieldType}'`);
|
|
7569
8494
|
if (statusFilter) where.push(`asset_group_asset.status = '${statusFilter}'`);
|
|
7570
8495
|
if (!startDate && !endDate) warnings.push("No date range provided; performance metrics were omitted and only asset structure was returned.");
|
|
7571
|
-
const
|
|
7572
|
-
|
|
7573
|
-
metrics.clicks,
|
|
7574
|
-
metrics.cost_micros,
|
|
7575
|
-
metrics.conversions,
|
|
7576
|
-
metrics.conversions_value` : "";
|
|
8496
|
+
const selectedMetrics = [.../* @__PURE__ */ new Set(["metrics.impressions", "metrics.cost_micros", ...metrics ?? PMAX_METRIC_FIELDS.slice(0, 5)])];
|
|
8497
|
+
const metricsFields = startDate || endDate ? `, ${selectedMetrics.join(", ")}${daily ? ", segments.date" : ""}` : "";
|
|
7577
8498
|
const richGaql = oneLineGaql(`
|
|
7578
8499
|
SELECT
|
|
7579
8500
|
campaign.id,
|
|
@@ -7620,11 +8541,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7620
8541
|
ORDER BY campaign.name ASC, asset_group.name ASC
|
|
7621
8542
|
LIMIT ${limit}
|
|
7622
8543
|
`);
|
|
7623
|
-
const
|
|
8544
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7624
8545
|
{ label: "pmax_assets_with_asset_details", gaql: richGaql, failureWarning: "PMax asset query with asset details failed" },
|
|
7625
8546
|
{ label: "pmax_assets_minimal", gaql: fallbackGaql }
|
|
7626
8547
|
]);
|
|
7627
|
-
return ok({ pmaxAssets:
|
|
8548
|
+
return ok({ pmaxAssets: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7628
8549
|
} catch (e) {
|
|
7629
8550
|
return formatMcpToolError(e);
|
|
7630
8551
|
}
|
|
@@ -7635,7 +8556,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7635
8556
|
"Read-only planning/forecast query for campaign, ad group, or portfolio bidding simulations. Returns simulation metadata and projected point lists when available, with a metadata fallback.",
|
|
7636
8557
|
{
|
|
7637
8558
|
customerId: customerIdSchema3,
|
|
7638
|
-
level:
|
|
8559
|
+
level: z7.enum(["campaign", "ad_group", "bidding_strategy"]).optional().default("campaign").describe("Simulation resource level to query"),
|
|
7639
8560
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter. Applies directly to campaign simulations and as campaign context for ad group simulations."),
|
|
7640
8561
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter for ad_group simulations"),
|
|
7641
8562
|
biddingStrategyId: numericIdSchema3.optional().describe("Optional portfolio bidding strategy ID filter for bidding_strategy simulations"),
|
|
@@ -7643,7 +8564,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7643
8564
|
simulationEndDate: isoDateSchema2.optional().describe("Optional maximum simulation end date YYYY-MM-DD"),
|
|
7644
8565
|
typeFilter: gaqlEnumSchema.optional().describe("Optional SimulationType enum, e.g. BUDGET, TARGET_CPA, TARGET_ROAS, CPC_BID"),
|
|
7645
8566
|
modificationMethod: gaqlEnumSchema.optional().describe("Optional SimulationModificationMethod enum, e.g. UNIFORM, SCALING, DEFAULT"),
|
|
7646
|
-
limit:
|
|
8567
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7647
8568
|
},
|
|
7648
8569
|
async ({ customerId, level, campaignId, adGroupId, biddingStrategyId, simulationStartDate, simulationEndDate, typeFilter, modificationMethod, limit }) => {
|
|
7649
8570
|
try {
|
|
@@ -7772,11 +8693,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7772
8693
|
ORDER BY ${resource}.start_date DESC
|
|
7773
8694
|
LIMIT ${limit}
|
|
7774
8695
|
`);
|
|
7775
|
-
const
|
|
8696
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7776
8697
|
{ label: `${resource}_with_points`, gaql: richGaql, failureWarning: "Simulation query with projected point lists failed" },
|
|
7777
8698
|
{ label: `${resource}_metadata`, gaql: fallbackGaql }
|
|
7778
8699
|
]);
|
|
7779
|
-
return ok({ simulations:
|
|
8700
|
+
return ok({ simulations: result2.rows, level, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7780
8701
|
} catch (e) {
|
|
7781
8702
|
return formatMcpToolError(e);
|
|
7782
8703
|
}
|
|
@@ -7791,9 +8712,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7791
8712
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
7792
8713
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7793
8714
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter"),
|
|
7794
|
-
searchTermContains:
|
|
7795
|
-
serpType:
|
|
7796
|
-
limit:
|
|
8715
|
+
searchTermContains: z7.string().min(1).optional().describe("Optional search term substring filter"),
|
|
8716
|
+
serpType: z7.enum(["ADS_AND_ORGANIC", "ADS_ONLY", "ORGANIC_ONLY", "UNKNOWN", "UNSPECIFIED"]).optional().describe("Optional search engine results page type segment filter"),
|
|
8717
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7797
8718
|
},
|
|
7798
8719
|
async ({ customerId, startDate, endDate, campaignId, adGroupId, searchTermContains, serpType, limit }) => {
|
|
7799
8720
|
try {
|
|
@@ -7853,11 +8774,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7853
8774
|
ORDER BY metrics.clicks DESC
|
|
7854
8775
|
LIMIT ${limit}
|
|
7855
8776
|
`);
|
|
7856
|
-
const
|
|
8777
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7857
8778
|
{ label: "paid_organic_search_term_view", gaql: richGaql, failureWarning: "Paid/organic search term query failed; organic reporting may require eligible linked organic search data or compatible fields" },
|
|
7858
8779
|
{ label: "search_term_view_paid_only", gaql: fallbackGaql }
|
|
7859
8780
|
]);
|
|
7860
|
-
return ok({ searchTerms:
|
|
8781
|
+
return ok({ searchTerms: result2.rows, source: result2.queryLabel, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7861
8782
|
} catch (e) {
|
|
7862
8783
|
return formatMcpToolError(e);
|
|
7863
8784
|
}
|
|
@@ -7868,17 +8789,17 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7868
8789
|
"Read-only Merchant Center product catalog/eligibility report via shopping_product. Supports account, campaign, and ad group scopes with performance metrics when the account has Shopping/PMax e-commerce data.",
|
|
7869
8790
|
{
|
|
7870
8791
|
customerId: customerIdSchema3,
|
|
7871
|
-
scope:
|
|
8792
|
+
scope: z7.enum(["account", "campaign", "ad_group"]).optional().default("account").describe("shopping_product scope. ad_group scope requires campaignId and adGroupId."),
|
|
7872
8793
|
startDate: isoDateSchema2.optional().describe("Optional start date YYYY-MM-DD. Used only as a WHERE filter; shopping_product cannot segment by date."),
|
|
7873
8794
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD. Used only as a WHERE filter; shopping_product cannot segment by date."),
|
|
7874
8795
|
campaignId: numericIdSchema3.optional().describe("Campaign ID for campaign or ad_group scope"),
|
|
7875
8796
|
adGroupId: numericIdSchema3.optional().describe("Ad group ID for ad_group scope"),
|
|
7876
8797
|
merchantCenterId: numericIdSchema3.optional().describe("Optional Merchant Center ID filter"),
|
|
7877
|
-
itemId:
|
|
7878
|
-
titleContains:
|
|
7879
|
-
feedLabel:
|
|
7880
|
-
statusFilter:
|
|
7881
|
-
limit:
|
|
8798
|
+
itemId: z7.string().min(1).optional().describe("Optional Merchant Center item ID filter"),
|
|
8799
|
+
titleContains: z7.string().min(1).optional().describe("Optional product title substring filter"),
|
|
8800
|
+
feedLabel: z7.string().min(1).optional().describe("Optional feed label filter"),
|
|
8801
|
+
statusFilter: z7.enum(["ELIGIBLE", "ELIGIBLE_LIMITED", "NOT_ELIGIBLE", "UNKNOWN", "UNSPECIFIED"]).optional(),
|
|
8802
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7882
8803
|
},
|
|
7883
8804
|
async ({ customerId, scope, startDate, endDate, campaignId, adGroupId, merchantCenterId, itemId, titleContains, feedLabel, statusFilter, limit }) => {
|
|
7884
8805
|
try {
|
|
@@ -7965,11 +8886,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7965
8886
|
ORDER BY shopping_product.title ASC
|
|
7966
8887
|
LIMIT ${limit}
|
|
7967
8888
|
`);
|
|
7968
|
-
const
|
|
8889
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7969
8890
|
{ label: "shopping_product_with_metrics_and_issues", gaql: richGaql, failureWarning: "Shopping product query with performance/cart/issue fields failed" },
|
|
7970
8891
|
{ label: "shopping_product_catalog", gaql: fallbackGaql }
|
|
7971
8892
|
]);
|
|
7972
|
-
return ok({ shoppingProducts:
|
|
8893
|
+
return ok({ shoppingProducts: result2.rows, scope, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7973
8894
|
} catch (e) {
|
|
7974
8895
|
return formatMcpToolError(e);
|
|
7975
8896
|
}
|
|
@@ -7985,11 +8906,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7985
8906
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7986
8907
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter"),
|
|
7987
8908
|
merchantCenterId: numericIdSchema3.optional().describe("Optional Merchant Center ID filter"),
|
|
7988
|
-
itemId:
|
|
7989
|
-
titleContains:
|
|
7990
|
-
brand:
|
|
7991
|
-
feedLabel:
|
|
7992
|
-
limit:
|
|
8909
|
+
itemId: z7.string().min(1).optional().describe("Optional Merchant Center item ID filter"),
|
|
8910
|
+
titleContains: z7.string().min(1).optional().describe("Optional product title substring filter"),
|
|
8911
|
+
brand: z7.string().min(1).optional().describe("Optional product brand filter"),
|
|
8912
|
+
feedLabel: z7.string().min(1).optional().describe("Optional product feed label filter"),
|
|
8913
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7993
8914
|
},
|
|
7994
8915
|
async ({ customerId, startDate, endDate, campaignId, adGroupId, merchantCenterId, itemId, titleContains, brand, feedLabel, limit }) => {
|
|
7995
8916
|
try {
|
|
@@ -8049,11 +8970,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8049
8970
|
ORDER BY metrics.cost_micros DESC
|
|
8050
8971
|
LIMIT ${limit}
|
|
8051
8972
|
`);
|
|
8052
|
-
const
|
|
8973
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
8053
8974
|
{ label: "shopping_performance_with_cart_metrics", gaql: richGaql, failureWarning: "Shopping performance query with cart/profit metrics failed" },
|
|
8054
8975
|
{ label: "shopping_performance_core", gaql: fallbackGaql }
|
|
8055
8976
|
]);
|
|
8056
|
-
return ok({ shoppingPerformance:
|
|
8977
|
+
return ok({ shoppingPerformance: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
8057
8978
|
} catch (e) {
|
|
8058
8979
|
return formatMcpToolError(e);
|
|
8059
8980
|
}
|
|
@@ -8067,9 +8988,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8067
8988
|
startDate: isoDateSchema2.describe("Start date YYYY-MM-DD"),
|
|
8068
8989
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
8069
8990
|
campaignId: numericIdSchema3.optional().describe("Optional Performance Max campaign ID filter"),
|
|
8070
|
-
placementType:
|
|
8071
|
-
placementContains:
|
|
8072
|
-
limit:
|
|
8991
|
+
placementType: z7.enum(["WEBSITE", "MOBILE_APPLICATION", "YOUTUBE_VIDEO", "YOUTUBE_CHANNEL", "GOOGLE_PRODUCTS", "UNKNOWN", "UNSPECIFIED"]).optional().describe("Optional PlacementType enum filter"),
|
|
8992
|
+
placementContains: z7.string().min(1).optional().describe("Optional substring filter for the placement string"),
|
|
8993
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(1e3)
|
|
8073
8994
|
},
|
|
8074
8995
|
async ({ customerId, startDate, endDate, campaignId, placementType, placementContains, limit }) => {
|
|
8075
8996
|
try {
|
|
@@ -8105,16 +9026,16 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8105
9026
|
ORDER BY metrics.impressions DESC
|
|
8106
9027
|
LIMIT ${limit}
|
|
8107
9028
|
`);
|
|
8108
|
-
const
|
|
9029
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
8109
9030
|
{ label: "pmax_placements_with_target_url_and_campaign", gaql: richGaql, failureWarning: "PMax placement query with target URL/campaign context failed" },
|
|
8110
9031
|
{ label: "pmax_placements_core", gaql: fallbackGaql }
|
|
8111
9032
|
]);
|
|
8112
9033
|
return ok({
|
|
8113
|
-
placements:
|
|
8114
|
-
pmaxPlacements:
|
|
8115
|
-
count:
|
|
8116
|
-
gaql:
|
|
8117
|
-
warnings: [...warnings, ...
|
|
9034
|
+
placements: result2.rows,
|
|
9035
|
+
pmaxPlacements: result2.rows,
|
|
9036
|
+
count: result2.rows.length,
|
|
9037
|
+
gaql: result2.gaql,
|
|
9038
|
+
warnings: [...warnings, ...result2.warnings]
|
|
8118
9039
|
});
|
|
8119
9040
|
} catch (e) {
|
|
8120
9041
|
return formatMcpToolError(e);
|
|
@@ -8130,9 +9051,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8130
9051
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD for asset group metrics/top combinations"),
|
|
8131
9052
|
campaignId: numericIdSchema3.optional().describe("Optional Performance Max campaign ID filter"),
|
|
8132
9053
|
assetGroupId: numericIdSchema3.optional().describe("Optional asset group ID filter"),
|
|
8133
|
-
statusFilter:
|
|
8134
|
-
includeTopCombinations:
|
|
8135
|
-
limit:
|
|
9054
|
+
statusFilter: z7.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
9055
|
+
includeTopCombinations: z7.boolean().optional().default(true).describe("Also query asset_group_top_combination_view when available"),
|
|
9056
|
+
limit: z7.number().int().min(1).max(1e3).optional().default(1e3)
|
|
8136
9057
|
},
|
|
8137
9058
|
async ({ customerId, startDate, endDate, campaignId, assetGroupId, statusFilter, includeTopCombinations, limit }) => {
|
|
8138
9059
|
try {
|
|
@@ -8232,6 +9153,207 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8232
9153
|
}
|
|
8233
9154
|
}
|
|
8234
9155
|
);
|
|
9156
|
+
server.tool(
|
|
9157
|
+
"google_ads_list_image_assets",
|
|
9158
|
+
"List the account's image asset library (FROM asset) with stable, publicly served full-size URLs on tpc.googlesyndication.com, dimensions, file size, and mime type. This is the whole stock and carries no notion of delivery: an asset uploaded in 2022 and never served appears exactly like one running today. Newest first, so a limit returns recent assets rather than an arbitrary slice, and the count in the response is the account total. For a visual-only gallery of assets with delivery over dates, use the hosted GetMCPAds service (Performance Max coverage). The local server returns media URLs and native report data. Shopping product imagery lives in Merchant Center, not in this library.",
|
|
9159
|
+
{
|
|
9160
|
+
customerId: customerIdSchema3,
|
|
9161
|
+
beforeAssetId: numericIdSchema3.optional().describe("Continue the descending library before this last-seen asset ID. Use the same filters."),
|
|
9162
|
+
assetIds: z7.array(numericIdSchema3).min(1).max(8).optional().describe("Resolve these exact native asset IDs, without scanning the library."),
|
|
9163
|
+
nameFilter: z7.string().optional().describe("Only assets whose file name contains this text"),
|
|
9164
|
+
minWidth: z7.number().int().min(1).optional().describe("Only images at least this wide, in pixels"),
|
|
9165
|
+
minHeight: z7.number().int().min(1).optional().describe("Only images at least this tall, in pixels"),
|
|
9166
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(500)
|
|
9167
|
+
},
|
|
9168
|
+
async ({ customerId, beforeAssetId, assetIds, nameFilter, minWidth, minHeight, limit }) => {
|
|
9169
|
+
try {
|
|
9170
|
+
const where = ["asset.type = 'IMAGE'"];
|
|
9171
|
+
if (beforeAssetId) where.push(`asset.id < ${beforeAssetId}`);
|
|
9172
|
+
if (assetIds?.length) where.push(`asset.id IN (${assetIds.join(",")})`);
|
|
9173
|
+
if (nameFilter) where.push(`asset.name LIKE '%${quoteGaqlString(nameFilter)}%'`);
|
|
9174
|
+
if (minWidth) where.push(`asset.image_asset.full_size.width_pixels >= ${minWidth}`);
|
|
9175
|
+
if (minHeight) where.push(`asset.image_asset.full_size.height_pixels >= ${minHeight}`);
|
|
9176
|
+
const { rows, gaql, warnings } = await runGaqlWithFallback(client, customerId, [{
|
|
9177
|
+
label: "image asset library",
|
|
9178
|
+
gaql: oneLineGaql(`
|
|
9179
|
+
SELECT
|
|
9180
|
+
asset.id, asset.name, asset.type, asset.source,
|
|
9181
|
+
asset.image_asset.full_size.url,
|
|
9182
|
+
asset.image_asset.full_size.width_pixels,
|
|
9183
|
+
asset.image_asset.full_size.height_pixels,
|
|
9184
|
+
asset.image_asset.file_size,
|
|
9185
|
+
asset.image_asset.mime_type
|
|
9186
|
+
FROM asset${buildWhere(where)} ORDER BY asset.id DESC LIMIT ${limit}`)
|
|
9187
|
+
}]);
|
|
9188
|
+
return ok({
|
|
9189
|
+
imageAssets: rows,
|
|
9190
|
+
count: rows.length,
|
|
9191
|
+
gaql,
|
|
9192
|
+
warnings,
|
|
9193
|
+
limitations: [
|
|
9194
|
+
"This is the account library, not a delivery report: it says nothing about what ran or spent in any period. Rows come newest first, so a limit returns the most recent assets. Use dated delivery reports to identify assets that actually served; the hosted GetMCPAds service provides interactive galleries.",
|
|
9195
|
+
"full_size.url points at tpc.googlesyndication.com/simgad and is publicly served without authentication or visible expiry: safe to display, hotlink, or download while the asset exists.",
|
|
9196
|
+
"Shopping/PLA product visuals come from Merchant Center feeds and never appear in this asset library."
|
|
9197
|
+
],
|
|
9198
|
+
nextActions: rows.length >= limit ? ["The LIMIT was reached; narrow with nameFilter or dimensions, or raise limit."] : []
|
|
9199
|
+
});
|
|
9200
|
+
} catch (e) {
|
|
9201
|
+
return formatMcpToolError(e);
|
|
9202
|
+
}
|
|
9203
|
+
}
|
|
9204
|
+
);
|
|
9205
|
+
server.tool(
|
|
9206
|
+
"google_ads_list_video_assets",
|
|
9207
|
+
"List the account's YouTube video assets (FROM asset) with derived watch, embed, and public thumbnail URLs. This is the whole stock and carries no notion of delivery: an asset uploaded years ago and never served appears exactly like one running today. Newest first, so a limit returns recent assets rather than an arbitrary slice, and the count in the response is the account total. For a visual-only gallery of assets with delivery over dates, use the hosted GetMCPAds service (Performance Max coverage). The local server returns media URLs and native report data. Google Ads exposes no downloadable video file: PMax and Demand Gen videos are hosted on YouTube, so display them via embed or thumbnails.",
|
|
9208
|
+
{
|
|
9209
|
+
customerId: customerIdSchema3,
|
|
9210
|
+
beforeAssetId: numericIdSchema3.optional().describe("Continue the descending library before this last-seen asset ID. Use the same filters."),
|
|
9211
|
+
assetIds: z7.array(numericIdSchema3).min(1).max(8).optional().describe("Resolve these exact native asset IDs, without scanning the library."),
|
|
9212
|
+
nameFilter: z7.string().optional().describe("Only assets whose name contains this text"),
|
|
9213
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(500)
|
|
9214
|
+
},
|
|
9215
|
+
async ({ customerId, beforeAssetId, assetIds, nameFilter, limit }) => {
|
|
9216
|
+
try {
|
|
9217
|
+
const where = ["asset.type = 'YOUTUBE_VIDEO'"];
|
|
9218
|
+
if (beforeAssetId) where.push(`asset.id < ${beforeAssetId}`);
|
|
9219
|
+
if (assetIds?.length) where.push(`asset.id IN (${assetIds.join(",")})`);
|
|
9220
|
+
if (nameFilter) where.push(`asset.name LIKE '%${quoteGaqlString(nameFilter)}%'`);
|
|
9221
|
+
const { rows, gaql, warnings } = await runGaqlWithFallback(client, customerId, [{
|
|
9222
|
+
label: "video asset library",
|
|
9223
|
+
gaql: oneLineGaql(`
|
|
9224
|
+
SELECT
|
|
9225
|
+
asset.id, asset.name, asset.type, asset.source,
|
|
9226
|
+
asset.youtube_video_asset.youtube_video_id,
|
|
9227
|
+
asset.youtube_video_asset.youtube_video_title
|
|
9228
|
+
FROM asset${buildWhere(where)} ORDER BY asset.id DESC LIMIT ${limit}`)
|
|
9229
|
+
}]);
|
|
9230
|
+
const videoAssets = rows.map((row) => {
|
|
9231
|
+
const asset = recordOf(recordOf(row)?.asset);
|
|
9232
|
+
const youtube = recordOf(asset?.youtubeVideoAsset);
|
|
9233
|
+
return { ...row, derivedUrls: youtubeDerivedUrls(youtube?.youtubeVideoId) };
|
|
9234
|
+
});
|
|
9235
|
+
return ok({
|
|
9236
|
+
videoAssets,
|
|
9237
|
+
count: videoAssets.length,
|
|
9238
|
+
gaql,
|
|
9239
|
+
warnings,
|
|
9240
|
+
limitations: [
|
|
9241
|
+
"This is the account library, not a delivery report: it says nothing about what ran or spent in any period. Rows come newest first, so a limit returns the most recent assets. Use dated delivery reports to identify assets that actually served; the hosted GetMCPAds service provides interactive galleries.",
|
|
9242
|
+
"No raw video file is available through the Google Ads API and YouTube's terms forbid downloading: use derivedUrls.embed_url (iframe) or watch_url, and the publicly served, stable i.ytimg.com thumbnails (maxresdefault with hqdefault as the always-present fallback)."
|
|
9243
|
+
],
|
|
9244
|
+
nextActions: videoAssets.length >= limit ? ["The LIMIT was reached; narrow with nameFilter or raise limit."] : []
|
|
9245
|
+
});
|
|
9246
|
+
} catch (e) {
|
|
9247
|
+
return formatMcpToolError(e);
|
|
9248
|
+
}
|
|
9249
|
+
}
|
|
9250
|
+
);
|
|
9251
|
+
server.tool(
|
|
9252
|
+
"google_ads_get_demand_gen_assets",
|
|
9253
|
+
"List Demand Gen ads (video responsive, multi-asset, carousel) with their referenced creatives resolved: stable image URLs and YouTube video IDs with embed and thumbnail URLs. Complements google_ads_get_pmax_assets, which covers Performance Max.",
|
|
9254
|
+
{
|
|
9255
|
+
customerId: customerIdSchema3,
|
|
9256
|
+
campaignId: numericIdSchema3.optional().describe("Optional Demand Gen campaign ID filter"),
|
|
9257
|
+
limit: z7.number().int().min(1).max(1e4).optional().default(500)
|
|
9258
|
+
},
|
|
9259
|
+
async ({ customerId, campaignId, limit }) => {
|
|
9260
|
+
try {
|
|
9261
|
+
const where = ["campaign.advertising_channel_type = 'DEMAND_GEN'"];
|
|
9262
|
+
if (campaignId) where.push(`campaign.id = ${campaignId}`);
|
|
9263
|
+
const demandGenFields = `
|
|
9264
|
+
ad_group_ad.ad.demand_gen_video_responsive_ad.videos,
|
|
9265
|
+
ad_group_ad.ad.demand_gen_video_responsive_ad.logo_images,
|
|
9266
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.marketing_images,
|
|
9267
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.square_marketing_images,
|
|
9268
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.portrait_marketing_images,
|
|
9269
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.logo_images,
|
|
9270
|
+
ad_group_ad.ad.demand_gen_carousel_ad.carousel_cards`;
|
|
9271
|
+
const baseFields = `
|
|
9272
|
+
campaign.id, campaign.name, ad_group.id, ad_group.name,
|
|
9273
|
+
ad_group_ad.status, ad_group_ad.ad.id, ad_group_ad.ad.name, ad_group_ad.ad.type`;
|
|
9274
|
+
const ads = await runGaqlWithFallback(client, customerId, [
|
|
9275
|
+
{
|
|
9276
|
+
label: "demand gen ads with creative references",
|
|
9277
|
+
gaql: oneLineGaql(`SELECT ${baseFields}, ${demandGenFields} FROM ad_group_ad${buildWhere(where)} LIMIT ${limit}`),
|
|
9278
|
+
failureWarning: "Full Demand Gen creative field selection failed; falling back to ad identifiers only"
|
|
9279
|
+
},
|
|
9280
|
+
{
|
|
9281
|
+
label: "demand gen ads without creative fields",
|
|
9282
|
+
gaql: oneLineGaql(`SELECT ${baseFields} FROM ad_group_ad${buildWhere(where)} LIMIT ${limit}`)
|
|
9283
|
+
}
|
|
9284
|
+
]);
|
|
9285
|
+
const referencedIds = /* @__PURE__ */ new Set();
|
|
9286
|
+
for (const row of ads.rows) {
|
|
9287
|
+
const ad = recordOf(recordOf(recordOf(row)?.adGroupAd)?.ad);
|
|
9288
|
+
if (!ad) continue;
|
|
9289
|
+
collectAssetIds(ad.demandGenVideoResponsiveAd, referencedIds);
|
|
9290
|
+
collectAssetIds(ad.demandGenMultiAssetAd, referencedIds);
|
|
9291
|
+
collectAssetIds(ad.demandGenCarouselAd, referencedIds);
|
|
9292
|
+
}
|
|
9293
|
+
const warnings = [...ads.warnings];
|
|
9294
|
+
const assetsById = /* @__PURE__ */ new Map();
|
|
9295
|
+
const idList = [...referencedIds];
|
|
9296
|
+
const chunkSize = 500;
|
|
9297
|
+
for (let index = 0; index < idList.length; index += chunkSize) {
|
|
9298
|
+
const chunk = idList.slice(index, index + chunkSize);
|
|
9299
|
+
try {
|
|
9300
|
+
const resolved = await client.searchStream(customerId, oneLineGaql(`
|
|
9301
|
+
SELECT
|
|
9302
|
+
asset.id, asset.name, asset.type,
|
|
9303
|
+
asset.image_asset.full_size.url,
|
|
9304
|
+
asset.image_asset.full_size.width_pixels,
|
|
9305
|
+
asset.image_asset.full_size.height_pixels,
|
|
9306
|
+
asset.youtube_video_asset.youtube_video_id,
|
|
9307
|
+
asset.youtube_video_asset.youtube_video_title
|
|
9308
|
+
FROM asset WHERE asset.id IN (${chunk.join(", ")})`));
|
|
9309
|
+
for (const row of resolved) {
|
|
9310
|
+
const asset = recordOf(recordOf(row)?.asset);
|
|
9311
|
+
const rawId = asset?.id;
|
|
9312
|
+
const id = typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : void 0;
|
|
9313
|
+
if (!asset || !id) continue;
|
|
9314
|
+
const youtube = recordOf(asset.youtubeVideoAsset);
|
|
9315
|
+
assetsById.set(id, {
|
|
9316
|
+
...asset,
|
|
9317
|
+
derivedUrls: youtubeDerivedUrls(youtube?.youtubeVideoId)
|
|
9318
|
+
});
|
|
9319
|
+
}
|
|
9320
|
+
} catch (error) {
|
|
9321
|
+
warnings.push(`Asset resolution failed for ${chunk.length} referenced assets: ${getErrorMessage(error)}`);
|
|
9322
|
+
}
|
|
9323
|
+
}
|
|
9324
|
+
const resolvedAds = ads.rows.map((row) => {
|
|
9325
|
+
const record = recordOf(row) ?? {};
|
|
9326
|
+
const ad = recordOf(recordOf(record.adGroupAd)?.ad);
|
|
9327
|
+
const ids = /* @__PURE__ */ new Set();
|
|
9328
|
+
if (ad) {
|
|
9329
|
+
collectAssetIds(ad.demandGenVideoResponsiveAd, ids);
|
|
9330
|
+
collectAssetIds(ad.demandGenMultiAssetAd, ids);
|
|
9331
|
+
collectAssetIds(ad.demandGenCarouselAd, ids);
|
|
9332
|
+
}
|
|
9333
|
+
return {
|
|
9334
|
+
...record,
|
|
9335
|
+
resolvedAssets: [...ids].map((id) => assetsById.get(id) ?? { id, unresolved: true })
|
|
9336
|
+
};
|
|
9337
|
+
});
|
|
9338
|
+
return ok({
|
|
9339
|
+
ads: resolvedAds,
|
|
9340
|
+
count: resolvedAds.length,
|
|
9341
|
+
referencedAssetCount: referencedIds.size,
|
|
9342
|
+
resolvedAssetCount: assetsById.size,
|
|
9343
|
+
gaql: ads.gaql,
|
|
9344
|
+
warnings,
|
|
9345
|
+
limitations: [
|
|
9346
|
+
"Demand Gen videos are YouTube assets: no raw file is available (YouTube terms), display them via resolvedAssets.derivedUrls.embed_url or the stable public i.ytimg.com thumbnails.",
|
|
9347
|
+
"Image full_size.url values on tpc.googlesyndication.com are publicly served with no visible expiry.",
|
|
9348
|
+
"Catalog-driven visuals come from Merchant Center feeds and are not part of ad creative references."
|
|
9349
|
+
],
|
|
9350
|
+
nextActions: resolvedAds.length >= limit ? ["The LIMIT was reached; filter by campaignId or raise limit."] : []
|
|
9351
|
+
});
|
|
9352
|
+
} catch (e) {
|
|
9353
|
+
return formatMcpToolError(e);
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
);
|
|
8235
9357
|
registerGoogleAdsKeywordPlannerTools(server, client, ok);
|
|
8236
9358
|
registerGoogleAdsDiscoveryTools(server, client, ok);
|
|
8237
9359
|
registerGoogleAdsReadOnlyRpcTool(server, client, ok);
|
|
@@ -8277,7 +9399,11 @@ var GOOGLE_ADS_TOOL_MANIFEST = {
|
|
|
8277
9399
|
{ name: "google_ads_run_readonly_rpc", purpose: "Call an allowlisted non-GAQL read service for Audience Insights, Reach Planner, benchmarks, suggestions, identity, invoices, or payments metadata." },
|
|
8278
9400
|
{ name: "google_ads_get_insights", purpose: "Generate validated performance GAQL using the metric and dimension catalogs." },
|
|
8279
9401
|
{ name: "google_ads_validate_query", purpose: "Validate metric/dimension/resource compatibility before querying." },
|
|
8280
|
-
{ name: "google_ads_run_gaql", purpose: "Run raw read-only GAQL SELECT queries for advanced reporting." }
|
|
9402
|
+
{ name: "google_ads_run_gaql", purpose: "Run raw read-only GAQL SELECT queries for advanced reporting." },
|
|
9403
|
+
{ name: "google_ads_list_image_assets", purpose: "List the account's image asset library with stable public full-size URLs, dimensions, and file metadata." },
|
|
9404
|
+
{ name: "google_ads_list_video_assets", purpose: "List YouTube video assets with derived watch, embed, and public thumbnail URLs." },
|
|
9405
|
+
{ name: "google_ads_get_demand_gen_assets", purpose: "List Demand Gen ads with their referenced image and YouTube creatives resolved." },
|
|
9406
|
+
{ "name": "google_ads_get_video_upload", "purpose": "Read the processing state and YouTube video ID of one Google Ads upload in the selected customer" }
|
|
8281
9407
|
],
|
|
8282
9408
|
resources: [
|
|
8283
9409
|
"google-ads://manifest",
|
|
@@ -8433,7 +9559,7 @@ function registerGoogleAdsResources(server) {
|
|
|
8433
9559
|
mimeType: "application/json",
|
|
8434
9560
|
text: JSON.stringify({
|
|
8435
9561
|
description: "Google Ads uses GAQL (SQL-like). The FROM clause determines the resource type. Some metrics are restricted to specific resources, and some segments are incompatible with certain metrics (especially impression_share).",
|
|
8436
|
-
resourceTypes: ["campaign", "ad_group", "ad_group_ad", "keyword_view", "search_term_view", "paid_organic_search_term_view", "shopping_performance_view", "shopping_product", "asset_group", "asset_group_asset", "asset_group_top_combination_view", "performance_max_placement_view", "campaign_simulation", "ad_group_simulation", "bidding_strategy_simulation"],
|
|
9562
|
+
resourceTypes: ["campaign", "ad_group", "ad_group_ad", "keyword_view", "search_term_view", "paid_organic_search_term_view", "shopping_performance_view", "shopping_product", "asset", "asset_group", "asset_group_asset", "asset_group_top_combination_view", "performance_max_placement_view", "campaign_simulation", "ad_group_simulation", "bidding_strategy_simulation"],
|
|
8437
9563
|
queryFormat: "SELECT ... FROM resource WHERE ... ORDER BY ... LIMIT N"
|
|
8438
9564
|
}, null, 2)
|
|
8439
9565
|
}]
|
|
@@ -8441,7 +9567,25 @@ function registerGoogleAdsResources(server) {
|
|
|
8441
9567
|
}
|
|
8442
9568
|
|
|
8443
9569
|
// src/platforms/google-ads/writes.ts
|
|
8444
|
-
import {
|
|
9570
|
+
import { randomUUID } from "crypto";
|
|
9571
|
+
import { z as z8 } from "zod";
|
|
9572
|
+
|
|
9573
|
+
// src/core/money.ts
|
|
9574
|
+
function guard(amount, currency) {
|
|
9575
|
+
if (!Number.isFinite(amount)) throw new Error(`Amount must be a finite number, received ${amount}.`);
|
|
9576
|
+
if (amount <= 0) throw new Error(`Amount must be greater than zero, received ${amount}.`);
|
|
9577
|
+
if (amount > 1e6) {
|
|
9578
|
+
throw new Error(
|
|
9579
|
+
`Amount ${amount} ${currency} is above the safety ceiling of 1,000,000. If this is intentional, make the change in the platform's own interface.`
|
|
9580
|
+
);
|
|
9581
|
+
}
|
|
9582
|
+
return amount;
|
|
9583
|
+
}
|
|
9584
|
+
function toMicros(amount) {
|
|
9585
|
+
return Math.round(guard(amount, "units") * 1e6);
|
|
9586
|
+
}
|
|
9587
|
+
|
|
9588
|
+
// src/platforms/google-ads/writes.ts
|
|
8445
9589
|
function ok2(data) {
|
|
8446
9590
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
8447
9591
|
}
|
|
@@ -8456,246 +9600,326 @@ function preview(action, details) {
|
|
|
8456
9600
|
message: "Preview only, nothing was changed. Repeat the same call with confirm: true to apply this change to the live account."
|
|
8457
9601
|
});
|
|
8458
9602
|
}
|
|
8459
|
-
var confirmSchema =
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
}
|
|
8467
|
-
|
|
8468
|
-
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
{
|
|
8475
|
-
customerId: z6.string().describe("Google Ads customer ID, with or without dashes."),
|
|
8476
|
-
[param]: z6.string().describe(`${label} ID.`),
|
|
8477
|
-
status: z6.enum(["PAUSED", "ENABLED", "REMOVED"]).describe("New status."),
|
|
8478
|
-
loginCustomerId: loginCustomerIdSchema2,
|
|
8479
|
-
confirm: confirmSchema
|
|
8480
|
-
},
|
|
8481
|
-
async (a) => {
|
|
8482
|
-
const cid = stripDashes(a.customerId);
|
|
8483
|
-
const id = String(a[param]);
|
|
8484
|
-
if (!a.confirm) {
|
|
8485
|
-
return preview(name, { customer: cid, target: id, newStatus: a.status });
|
|
8486
|
-
}
|
|
8487
|
-
const result = await client.mutate(cid, collection, [
|
|
8488
|
-
{ update: { resourceName: resourceName(cid, collection, id), status: a.status }, updateMask: "status" }
|
|
8489
|
-
], a.loginCustomerId);
|
|
8490
|
-
return ok2({ applied: true, action: name, result });
|
|
9603
|
+
var confirmSchema = z8.boolean().optional().describe("Set to true to actually apply the change. Without it, the tool only previews.");
|
|
9604
|
+
async function request(url, init, contexte) {
|
|
9605
|
+
const response = await fetch(url, { ...init, redirect: "error" });
|
|
9606
|
+
const body = await response.text();
|
|
9607
|
+
let parsed;
|
|
9608
|
+
try {
|
|
9609
|
+
parsed = JSON.parse(body);
|
|
9610
|
+
} catch {
|
|
9611
|
+
parsed = body;
|
|
9612
|
+
}
|
|
9613
|
+
if (!response.ok) {
|
|
9614
|
+
if (url.startsWith(ADS_BASE + "/")) {
|
|
9615
|
+
const error = parsed?.error;
|
|
9616
|
+
const details = error?.details?.flatMap((d) => d.errors ?? []).map((e) => ({ code: e.errorCode, message: e.message, location: e.location }));
|
|
9617
|
+
throw new Error(`${contexte}: ${JSON.stringify(details?.length ? details : error?.message ?? parsed).slice(0, 3e3)}`);
|
|
8491
9618
|
}
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
9619
|
+
throw new Error(`${contexte} : ${typeof parsed === "string" ? parsed.slice(0, 300) : JSON.stringify(parsed).slice(0, 300)}`);
|
|
9620
|
+
}
|
|
9621
|
+
return parsed;
|
|
9622
|
+
}
|
|
9623
|
+
var ADS_BASE = "https://googleads.googleapis.com/v25";
|
|
9624
|
+
function registerGoogleAdsWrites(c, config) {
|
|
9625
|
+
registerGoogleExtendedWrites(c, config);
|
|
9626
|
+
async function accessToken() {
|
|
9627
|
+
const r = await request(
|
|
9628
|
+
"https://oauth2.googleapis.com/token",
|
|
9629
|
+
{
|
|
9630
|
+
method: "POST",
|
|
9631
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
9632
|
+
body: new URLSearchParams({
|
|
9633
|
+
grant_type: "refresh_token",
|
|
9634
|
+
refresh_token: config.refreshToken,
|
|
9635
|
+
client_id: config.clientId,
|
|
9636
|
+
client_secret: config.clientSecret
|
|
9637
|
+
})
|
|
9638
|
+
},
|
|
9639
|
+
"Google token refresh"
|
|
9640
|
+
);
|
|
9641
|
+
return r.access_token;
|
|
9642
|
+
}
|
|
9643
|
+
const headers = async (loginCustomerId) => {
|
|
9644
|
+
const h = {
|
|
9645
|
+
authorization: `Bearer ${await accessToken()}`,
|
|
9646
|
+
"developer-token": config.developerToken,
|
|
9647
|
+
"content-type": "application/json"
|
|
9648
|
+
};
|
|
9649
|
+
const manager = loginCustomerId ?? config.loginCustomerId;
|
|
9650
|
+
if (manager) h["login-customer-id"] = manager.replace(/-/g, "");
|
|
9651
|
+
return h;
|
|
9652
|
+
};
|
|
9653
|
+
c.tool(
|
|
9654
|
+
"google_ads_update_campaign_status",
|
|
9655
|
+
"Pause, re-enable or remove a Google Ads campaign. Previews by default: without confirm: true, the tool describes the change without applying it.",
|
|
8498
9656
|
{
|
|
8499
|
-
customerId:
|
|
8500
|
-
campaignId:
|
|
8501
|
-
|
|
8502
|
-
loginCustomerId:
|
|
9657
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9658
|
+
campaignId: z8.string().describe("Campaign ID."),
|
|
9659
|
+
status: z8.enum(["PAUSED", "ENABLED", "REMOVED"]).describe("New status."),
|
|
9660
|
+
loginCustomerId: z8.string().optional().describe("Manager account, if the customer sits under an MCC."),
|
|
8503
9661
|
confirm: confirmSchema
|
|
8504
9662
|
},
|
|
8505
9663
|
async (a) => {
|
|
8506
|
-
const
|
|
8507
|
-
|
|
8508
|
-
|
|
9664
|
+
const { customerId, campaignId, status, loginCustomerId, confirm } = a;
|
|
9665
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9666
|
+
if (!confirm) {
|
|
9667
|
+
return preview("google_ads_update_campaign_status", { customer: cid, campaign: campaignId, newStatus: status });
|
|
8509
9668
|
}
|
|
8510
|
-
const
|
|
8511
|
-
{
|
|
8512
|
-
|
|
8513
|
-
|
|
9669
|
+
const result2 = await request(
|
|
9670
|
+
`${ADS_BASE}/customers/${cid}/campaigns:mutate`,
|
|
9671
|
+
{
|
|
9672
|
+
method: "POST",
|
|
9673
|
+
headers: await headers(loginCustomerId),
|
|
9674
|
+
body: JSON.stringify({
|
|
9675
|
+
operations: [
|
|
9676
|
+
status === "REMOVED" ? { remove: `customers/${cid}/campaigns/${campaignId}` } : { update: { resourceName: `customers/${cid}/campaigns/${campaignId}`, status }, updateMask: "status" }
|
|
9677
|
+
]
|
|
9678
|
+
})
|
|
9679
|
+
},
|
|
9680
|
+
"Google Ads status update"
|
|
9681
|
+
);
|
|
9682
|
+
return ok2({ applied: true, action: "google_ads_update_campaign_status", result: result2 });
|
|
8514
9683
|
}
|
|
8515
9684
|
);
|
|
8516
|
-
|
|
9685
|
+
c.tool(
|
|
8517
9686
|
"google_ads_create_campaign",
|
|
8518
9687
|
"Create a Google Ads campaign. It is always created PAUSED and there is no option to create it active: someone has to look at it before it spends. Creates the campaign budget too. Previews by default.",
|
|
8519
9688
|
{
|
|
8520
|
-
customerId:
|
|
8521
|
-
name:
|
|
8522
|
-
channelType:
|
|
8523
|
-
dailyBudget:
|
|
8524
|
-
|
|
8525
|
-
|
|
9689
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9690
|
+
name: z8.string().describe("Campaign name."),
|
|
9691
|
+
channelType: z8.enum(["SEARCH", "DISPLAY", "SHOPPING", "PERFORMANCE_MAX", "DEMAND_GEN"]).describe("Advertising channel. Classic VIDEO creation is not supported by Google Ads API; Demand Gen is a distinct, supported video-capable campaign type."),
|
|
9692
|
+
dailyBudget: z8.number().positive().describe("Daily budget in the account currency."),
|
|
9693
|
+
loginCustomerId: z8.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
9694
|
+
brandGuidelinesEnabled: z8.literal(false).optional().describe("For Performance Max, explicitly pass false to create an empty paused campaign without campaign-level brand assets. Brand-enabled creation is not supported by this tool."),
|
|
9695
|
+
euPoliticalAdvertising: z8.boolean().optional().describe("Required for execution: explicitly declare whether this campaign contains EU political advertising."),
|
|
9696
|
+
merchantId: z8.string().regex(/^\d+$/).optional().describe("Merchant Center account ID; required for Shopping."),
|
|
9697
|
+
feedLabel: z8.string().optional().describe("Merchant Center feed label for Shopping."),
|
|
9698
|
+
validateOnly: z8.boolean().optional().describe("Validate the atomic request with Google without creating any resources."),
|
|
8526
9699
|
confirm: confirmSchema
|
|
8527
9700
|
},
|
|
8528
9701
|
async (a) => {
|
|
8529
|
-
const
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
} catch (error) {
|
|
8534
|
-
return ko(error instanceof Error ? error.message : String(error));
|
|
8535
|
-
}
|
|
8536
|
-
if (!a.confirm) {
|
|
9702
|
+
const { customerId, name, channelType, dailyBudget, loginCustomerId, confirm } = a;
|
|
9703
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9704
|
+
const micros = toMicros(Number(dailyBudget));
|
|
9705
|
+
if (!confirm && !a.validateOnly) {
|
|
8537
9706
|
return preview("google_ads_create_campaign", {
|
|
8538
9707
|
customer: cid,
|
|
8539
|
-
name
|
|
8540
|
-
channelType
|
|
8541
|
-
dailyBudget
|
|
9708
|
+
name,
|
|
9709
|
+
channelType,
|
|
9710
|
+
dailyBudget,
|
|
8542
9711
|
inMicros: micros,
|
|
8543
|
-
|
|
8544
|
-
|
|
9712
|
+
status: "PAUSED",
|
|
9713
|
+
euPoliticalAdvertising: a.euPoliticalAdvertising ?? "declaration required before execution",
|
|
9714
|
+
brandGuidelinesEnabled: a.brandGuidelinesEnabled,
|
|
9715
|
+
merchantId: a.merchantId,
|
|
9716
|
+
feedLabel: a.feedLabel,
|
|
9717
|
+
biddingStrategy: ["PERFORMANCE_MAX", "DEMAND_GEN"].includes(String(channelType)) ? "MAXIMIZE_CONVERSIONS" : "MANUAL_CPC"
|
|
8545
9718
|
});
|
|
8546
9719
|
}
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
}
|
|
8561
|
-
const
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
}
|
|
8570
|
-
}
|
|
8571
|
-
], login);
|
|
9720
|
+
if (channelType === "VIDEO") throw new Error("Google Ads API cannot create VIDEO campaigns. Use an explicitly requested DEMAND_GEN video campaign through google_ads_apply_plan.");
|
|
9721
|
+
if (typeof a.euPoliticalAdvertising !== "boolean") throw new Error("Declare euPoliticalAdvertising explicitly before creating the campaign.");
|
|
9722
|
+
if (channelType === "PERFORMANCE_MAX" && a.brandGuidelinesEnabled !== false) throw new Error("Performance Max requires explicit brandGuidelinesEnabled: false for empty paused creation; brand assets are not supported by this tool.");
|
|
9723
|
+
if (channelType === "SHOPPING" && !a.merchantId) throw new Error("Shopping requires merchantId for an accessible Merchant Center account.");
|
|
9724
|
+
const budgetName = `customers/${cid}/campaignBudgets/-1`;
|
|
9725
|
+
const campaignData = {
|
|
9726
|
+
name,
|
|
9727
|
+
status: "PAUSED",
|
|
9728
|
+
advertisingChannelType: channelType,
|
|
9729
|
+
campaignBudget: budgetName,
|
|
9730
|
+
containsEuPoliticalAdvertising: a.euPoliticalAdvertising ? "CONTAINS_EU_POLITICAL_ADVERTISING" : "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING",
|
|
9731
|
+
...["PERFORMANCE_MAX", "DEMAND_GEN"].includes(String(channelType)) ? { maximizeConversions: {}, ...channelType === "PERFORMANCE_MAX" ? { brandGuidelinesEnabled: false } : {} } : { manualCpc: {} },
|
|
9732
|
+
...channelType === "SHOPPING" ? { shoppingSetting: { merchantId: a.merchantId, campaignPriority: 0, feedLabel: a.feedLabel } } : {}
|
|
9733
|
+
};
|
|
9734
|
+
const result2 = await request(`${ADS_BASE}/customers/${cid}/googleAds:mutate`, {
|
|
9735
|
+
method: "POST",
|
|
9736
|
+
headers: await headers(loginCustomerId ?? config.loginCustomerId),
|
|
9737
|
+
body: JSON.stringify({ partialFailure: false, validateOnly: a.validateOnly === true, mutateOperations: [
|
|
9738
|
+
{ campaignBudgetOperation: { create: { resourceName: budgetName, name: `${name} budget ${randomUUID()}`, amountMicros: String(micros), deliveryMethod: "STANDARD", explicitlyShared: false } } },
|
|
9739
|
+
{ campaignOperation: { create: campaignData } }
|
|
9740
|
+
] })
|
|
9741
|
+
}, "Google Ads campaign creation");
|
|
8572
9742
|
return ok2({
|
|
8573
|
-
applied: true,
|
|
9743
|
+
applied: a.validateOnly !== true,
|
|
9744
|
+
validatedByGoogle: a.validateOnly === true,
|
|
8574
9745
|
action: "google_ads_create_campaign",
|
|
8575
9746
|
status: "PAUSED",
|
|
8576
|
-
budget:
|
|
8577
|
-
result:
|
|
9747
|
+
budget: result2.mutateOperationResponses?.[0]?.campaignBudgetResult?.resourceName,
|
|
9748
|
+
result: { results: result2.mutateOperationResponses?.flatMap((r) => r.campaignResult ? [r.campaignResult] : []) ?? [] }
|
|
8578
9749
|
});
|
|
8579
9750
|
}
|
|
8580
9751
|
);
|
|
8581
|
-
|
|
9752
|
+
c.tool(
|
|
8582
9753
|
"google_ads_update_campaign_budget",
|
|
8583
9754
|
"Change the daily budget of a Google Ads campaign. The amount is in the account currency (12.50 for 12.50 EUR). Previews by default.",
|
|
8584
9755
|
{
|
|
8585
|
-
customerId:
|
|
8586
|
-
budgetId:
|
|
8587
|
-
dailyAmount:
|
|
8588
|
-
loginCustomerId:
|
|
9756
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9757
|
+
budgetId: z8.string().describe("Campaign budget ID (campaign_budget.id)."),
|
|
9758
|
+
dailyAmount: z8.number().positive().describe("New daily budget, in the account currency."),
|
|
9759
|
+
loginCustomerId: z8.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
8589
9760
|
confirm: confirmSchema
|
|
8590
9761
|
},
|
|
8591
9762
|
async (a) => {
|
|
8592
|
-
const
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
} catch (error) {
|
|
8597
|
-
return ko(error instanceof Error ? error.message : String(error));
|
|
8598
|
-
}
|
|
8599
|
-
if (!a.confirm) {
|
|
9763
|
+
const { customerId, budgetId, dailyAmount, loginCustomerId, confirm } = a;
|
|
9764
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9765
|
+
const micros = toMicros(Number(dailyAmount));
|
|
9766
|
+
if (!confirm) {
|
|
8600
9767
|
return preview("google_ads_update_campaign_budget", {
|
|
8601
9768
|
customer: cid,
|
|
8602
|
-
budget:
|
|
8603
|
-
newDailyBudget:
|
|
9769
|
+
budget: budgetId,
|
|
9770
|
+
newDailyBudget: dailyAmount,
|
|
8604
9771
|
inMicros: micros
|
|
8605
9772
|
});
|
|
8606
9773
|
}
|
|
8607
|
-
const
|
|
9774
|
+
const result2 = await request(
|
|
9775
|
+
`${ADS_BASE}/customers/${cid}/campaignBudgets:mutate`,
|
|
8608
9776
|
{
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
8612
|
-
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
9777
|
+
method: "POST",
|
|
9778
|
+
headers: await headers(loginCustomerId),
|
|
9779
|
+
body: JSON.stringify({
|
|
9780
|
+
operations: [
|
|
9781
|
+
{
|
|
9782
|
+
update: { resourceName: `customers/${cid}/campaignBudgets/${budgetId}`, amountMicros: String(micros) },
|
|
9783
|
+
updateMask: "amount_micros"
|
|
9784
|
+
}
|
|
9785
|
+
]
|
|
9786
|
+
})
|
|
9787
|
+
},
|
|
9788
|
+
"Google Ads budget update"
|
|
9789
|
+
);
|
|
9790
|
+
return ok2({ applied: true, action: "google_ads_update_campaign_budget", result: result2 });
|
|
8617
9791
|
}
|
|
8618
9792
|
);
|
|
8619
|
-
|
|
8620
|
-
"
|
|
8621
|
-
"
|
|
9793
|
+
c.tool(
|
|
9794
|
+
"google_ads_rename_campaign",
|
|
9795
|
+
"Rename a Google Ads campaign. The name is the only thing that changes: delivery, budget and targeting are untouched. Previews by default.",
|
|
8622
9796
|
{
|
|
8623
|
-
customerId:
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
loginCustomerId:
|
|
9797
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9798
|
+
campaignId: z8.string().describe("Campaign ID."),
|
|
9799
|
+
name: z8.string().min(1).max(255).describe("New campaign name."),
|
|
9800
|
+
loginCustomerId: z8.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
8627
9801
|
confirm: confirmSchema
|
|
8628
9802
|
},
|
|
8629
9803
|
async (a) => {
|
|
8630
|
-
const
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
return ko(error instanceof Error ? error.message : String(error));
|
|
8636
|
-
}
|
|
8637
|
-
if (!a.confirm) {
|
|
8638
|
-
return preview("google_ads_update_adgroup_bid", {
|
|
8639
|
-
customer: cid,
|
|
8640
|
-
adGroup: a.adGroupId,
|
|
8641
|
-
newCpcBid: a.cpcBid,
|
|
8642
|
-
inMicros: micros
|
|
8643
|
-
});
|
|
8644
|
-
}
|
|
8645
|
-
const result = await client.mutate(cid, "adGroups", [
|
|
9804
|
+
const { customerId, campaignId, name, loginCustomerId, confirm } = a;
|
|
9805
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9806
|
+
if (!confirm) return preview("google_ads_rename_campaign", { customer: cid, campaign: campaignId, newName: name });
|
|
9807
|
+
const result2 = await request(
|
|
9808
|
+
`${ADS_BASE}/customers/${cid}/campaigns:mutate`,
|
|
8646
9809
|
{
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
}
|
|
8653
|
-
|
|
8654
|
-
|
|
9810
|
+
method: "POST",
|
|
9811
|
+
headers: await headers(loginCustomerId),
|
|
9812
|
+
body: JSON.stringify({
|
|
9813
|
+
operations: [{ update: { resourceName: `customers/${cid}/campaigns/${campaignId}`, name }, updateMask: "name" }]
|
|
9814
|
+
})
|
|
9815
|
+
},
|
|
9816
|
+
"Google Ads rename"
|
|
9817
|
+
);
|
|
9818
|
+
return ok2({ applied: true, action: "google_ads_rename_campaign", result: result2 });
|
|
8655
9819
|
}
|
|
8656
9820
|
);
|
|
8657
|
-
|
|
9821
|
+
c.tool(
|
|
8658
9822
|
"google_ads_update_campaign_schedule",
|
|
8659
9823
|
"Change the start or end date of a Google Ads campaign. Dates are YYYY-MM-DD in the account time zone. Previews by default.",
|
|
8660
9824
|
{
|
|
8661
|
-
customerId:
|
|
8662
|
-
campaignId:
|
|
8663
|
-
startDate:
|
|
8664
|
-
endDate:
|
|
8665
|
-
loginCustomerId:
|
|
9825
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9826
|
+
campaignId: z8.string().describe("Campaign ID."),
|
|
9827
|
+
startDate: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Start date, YYYY-MM-DD, in the account time zone."),
|
|
9828
|
+
endDate: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Use 2037-12-30 to clear the end date (run indefinitely)."),
|
|
9829
|
+
loginCustomerId: z8.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
8666
9830
|
confirm: confirmSchema
|
|
8667
9831
|
},
|
|
8668
9832
|
async (a) => {
|
|
8669
|
-
const { startDate, endDate } = a;
|
|
9833
|
+
const { customerId, campaignId, startDate, endDate, loginCustomerId, confirm } = a;
|
|
8670
9834
|
if (!startDate && !endDate) return ko("Provide startDate, endDate, or both.");
|
|
8671
|
-
|
|
9835
|
+
for (const date of [startDate, endDate]) {
|
|
9836
|
+
if (date && (Number.isNaN(Date.parse(String(date))) || new Date(String(date)).toISOString().slice(0, 10) !== date)) return ko("Invalid calendar date.");
|
|
9837
|
+
}
|
|
9838
|
+
if (startDate && endDate && String(startDate) > String(endDate)) {
|
|
8672
9839
|
return ko(`startDate ${startDate} is after endDate ${endDate}.`);
|
|
8673
9840
|
}
|
|
8674
|
-
const cid =
|
|
8675
|
-
const update = {
|
|
8676
|
-
resourceName: resourceName(cid, "campaigns", a.campaignId)
|
|
8677
|
-
};
|
|
9841
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9842
|
+
const update = { resourceName: `customers/${cid}/campaigns/${campaignId}` };
|
|
8678
9843
|
const mask = [];
|
|
8679
9844
|
if (startDate) {
|
|
8680
|
-
update.
|
|
8681
|
-
mask.push("
|
|
9845
|
+
update.startDateTime = `${startDate} 00:00:00`;
|
|
9846
|
+
mask.push("start_date_time");
|
|
8682
9847
|
}
|
|
8683
9848
|
if (endDate) {
|
|
8684
|
-
update.
|
|
8685
|
-
mask.push("
|
|
8686
|
-
}
|
|
8687
|
-
if (!a.confirm) {
|
|
8688
|
-
return preview("google_ads_update_campaign_schedule", {
|
|
8689
|
-
customer: cid,
|
|
8690
|
-
campaign: a.campaignId,
|
|
8691
|
-
startDate,
|
|
8692
|
-
endDate
|
|
8693
|
-
});
|
|
9849
|
+
if (endDate !== "2037-12-30") update.endDateTime = `${endDate} 23:59:59`;
|
|
9850
|
+
mask.push("end_date_time");
|
|
8694
9851
|
}
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
9852
|
+
if (!confirm) return preview("google_ads_update_campaign_schedule", { customer: cid, campaign: campaignId, startDate, endDate });
|
|
9853
|
+
const result2 = await request(
|
|
9854
|
+
`${ADS_BASE}/customers/${cid}/campaigns:mutate`,
|
|
9855
|
+
{
|
|
9856
|
+
method: "POST",
|
|
9857
|
+
headers: await headers(loginCustomerId),
|
|
9858
|
+
body: JSON.stringify({ operations: [{ update, updateMask: mask.join(",") }] })
|
|
9859
|
+
},
|
|
9860
|
+
"Google Ads schedule update"
|
|
9861
|
+
);
|
|
9862
|
+
return ok2({ applied: true, action: "google_ads_update_campaign_schedule", result: result2 });
|
|
9863
|
+
}
|
|
9864
|
+
);
|
|
9865
|
+
c.tool(
|
|
9866
|
+
"google_ads_update_adgroup_status",
|
|
9867
|
+
"Pause, re-enable or remove a Google Ads ad group. Previews by default.",
|
|
9868
|
+
{
|
|
9869
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9870
|
+
adGroupId: z8.string().describe("Ad group ID."),
|
|
9871
|
+
status: z8.enum(["PAUSED", "ENABLED", "REMOVED"]).describe("New status."),
|
|
9872
|
+
loginCustomerId: z8.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
9873
|
+
confirm: confirmSchema
|
|
9874
|
+
},
|
|
9875
|
+
async (a) => {
|
|
9876
|
+
const { customerId, adGroupId, status, loginCustomerId, confirm } = a;
|
|
9877
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9878
|
+
if (!confirm) return preview("google_ads_update_adgroup_status", { customer: cid, adGroup: adGroupId, newStatus: status });
|
|
9879
|
+
const result2 = await request(
|
|
9880
|
+
`${ADS_BASE}/customers/${cid}/adGroups:mutate`,
|
|
9881
|
+
{
|
|
9882
|
+
method: "POST",
|
|
9883
|
+
headers: await headers(loginCustomerId),
|
|
9884
|
+
body: JSON.stringify({
|
|
9885
|
+
operations: [status === "REMOVED" ? { remove: `customers/${cid}/adGroups/${adGroupId}` } : { update: { resourceName: `customers/${cid}/adGroups/${adGroupId}`, status }, updateMask: "status" }]
|
|
9886
|
+
})
|
|
9887
|
+
},
|
|
9888
|
+
"Google Ads ad group status"
|
|
9889
|
+
);
|
|
9890
|
+
return ok2({ applied: true, action: "google_ads_update_adgroup_status", result: result2 });
|
|
9891
|
+
}
|
|
9892
|
+
);
|
|
9893
|
+
c.tool(
|
|
9894
|
+
"google_ads_update_adgroup_bid",
|
|
9895
|
+
"Change the default CPC bid of a Google Ads ad group. The amount is in the account currency (1.20 for 1.20 EUR). Has no effect on a campaign using an automated bidding strategy. Previews by default.",
|
|
9896
|
+
{
|
|
9897
|
+
customerId: z8.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9898
|
+
adGroupId: z8.string().describe("Ad group ID."),
|
|
9899
|
+
cpcBid: z8.number().positive().describe("New default CPC bid, in the account currency."),
|
|
9900
|
+
loginCustomerId: z8.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
9901
|
+
confirm: confirmSchema
|
|
9902
|
+
},
|
|
9903
|
+
async (a) => {
|
|
9904
|
+
const { customerId, adGroupId, cpcBid, loginCustomerId, confirm } = a;
|
|
9905
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9906
|
+
const micros = toMicros(Number(cpcBid));
|
|
9907
|
+
if (!confirm) return preview("google_ads_update_adgroup_bid", { customer: cid, adGroup: adGroupId, newCpcBid: cpcBid, inMicros: micros });
|
|
9908
|
+
const result2 = await request(
|
|
9909
|
+
`${ADS_BASE}/customers/${cid}/adGroups:mutate`,
|
|
9910
|
+
{
|
|
9911
|
+
method: "POST",
|
|
9912
|
+
headers: await headers(loginCustomerId),
|
|
9913
|
+
body: JSON.stringify({
|
|
9914
|
+
operations: [{
|
|
9915
|
+
update: { resourceName: `customers/${cid}/adGroups/${adGroupId}`, cpcBidMicros: String(micros) },
|
|
9916
|
+
updateMask: "cpc_bid_micros"
|
|
9917
|
+
}]
|
|
9918
|
+
})
|
|
9919
|
+
},
|
|
9920
|
+
"Google Ads ad group bid"
|
|
9921
|
+
);
|
|
9922
|
+
return ok2({ applied: true, action: "google_ads_update_adgroup_bid", result: result2 });
|
|
8699
9923
|
}
|
|
8700
9924
|
);
|
|
8701
9925
|
}
|
|
@@ -8703,21 +9927,23 @@ function registerGoogleAdsWrites(server, config) {
|
|
|
8703
9927
|
// src/platforms/google-ads/index.ts
|
|
8704
9928
|
function registerGoogleAds(server, config) {
|
|
8705
9929
|
registerGoogleAdsTools(server, config);
|
|
9930
|
+
registerGoogleVideoReads(server, config);
|
|
8706
9931
|
registerGoogleAdsResources(server);
|
|
8707
|
-
logger.info("google-ads", "Registered
|
|
9932
|
+
logger.info("google-ads", "Registered 35 read tools and 5 resources");
|
|
8708
9933
|
if (config.enableWrites) {
|
|
8709
9934
|
registerGoogleAdsWrites(server, config);
|
|
8710
|
-
logger.info("google-ads", "Registered
|
|
9935
|
+
logger.info("google-ads", "Registered 10 write tools (every one previews before it applies)");
|
|
8711
9936
|
}
|
|
8712
9937
|
}
|
|
8713
9938
|
|
|
8714
9939
|
// src/server.ts
|
|
8715
|
-
var PACKAGE_VERSION = "
|
|
9940
|
+
var PACKAGE_VERSION = "2.0.0";
|
|
8716
9941
|
function createServer(config) {
|
|
8717
9942
|
const server = new McpServer(
|
|
8718
|
-
{ name: "google-ads-mcp", version: PACKAGE_VERSION },
|
|
9943
|
+
{ name: "google-ads-mcp", version: PACKAGE_VERSION, title: "Google Ads", websiteUrl: "https://www.getmcpads.com/tools/google-ads", icons: [{ src: "https://mcp.getmcpads.com/icon.svg", mimeType: "image/svg+xml" }] },
|
|
8719
9944
|
{ capabilities: { tools: { listChanged: true }, resources: { subscribe: false, listChanged: true } } }
|
|
8720
9945
|
);
|
|
9946
|
+
installToolQuality(server);
|
|
8721
9947
|
registerGoogleAds(server, config);
|
|
8722
9948
|
logger.system(
|
|
8723
9949
|
`google-ads-mcp v${PACKAGE_VERSION} ready, writes ${config.enableWrites ? "enabled" : "disabled"}`
|