@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/index.js
CHANGED
|
@@ -1,8 +1,822 @@
|
|
|
1
|
+
// src/parameter-descriptions.ts
|
|
2
|
+
var PARAMETER_DESCRIPTIONS = {
|
|
3
|
+
"filters": "Filters combined according to this tool's query schema. Use provider field names and the declared operators.",
|
|
4
|
+
"searchType": "Search surface to report, such as web, image, video, news or Discover. Availability depends on the property.",
|
|
5
|
+
"searchTypes": "Search surfaces to compare. Each surface is queried separately; do not sum overlapping surfaces.",
|
|
6
|
+
"datePreset": "Named reporting period. Use the custom date range when you need exact start and end dates.",
|
|
7
|
+
"dateRange": "Explicit reporting start and end dates in YYYY-MM-DD format.",
|
|
8
|
+
"timeRange": "Explicit provider reporting period with since and until dates.",
|
|
9
|
+
"startDate": "First date of the reporting period, in YYYY-MM-DD format.",
|
|
10
|
+
"endDate": "Last date of the reporting period, in YYYY-MM-DD format, on or after startDate.",
|
|
11
|
+
"since": "Beginning of the requested reporting period; use the format accepted by the provider.",
|
|
12
|
+
"until": "End of the requested reporting period; use the format accepted by the provider.",
|
|
13
|
+
"orderBy": "Field used to order report rows.",
|
|
14
|
+
"orderField": "Provider field used to order report rows.",
|
|
15
|
+
"orderDirection": "Sort direction for the selected ordering field.",
|
|
16
|
+
"orderType": "Sort direction for the selected ordering field.",
|
|
17
|
+
"sortBy": "Metric or dimension used to rank the returned results.",
|
|
18
|
+
"sort": "Native provider ordering expressions.",
|
|
19
|
+
"limit": "Maximum number of returned items or rows. The declared bounds and default apply; use pagination for additional results.",
|
|
20
|
+
"topN": "Maximum number of highest-ranked results to return.",
|
|
21
|
+
"pageSize": "Maximum items requested per page. Follow the returned pagination information for remaining results.",
|
|
22
|
+
"page": "Page number to request, starting at 1.",
|
|
23
|
+
"offset": "Zero-based number of rows to skip before returning results.",
|
|
24
|
+
"bookmark": "Opaque Pinterest pagination bookmark returned by the previous response. Omit on the first request.",
|
|
25
|
+
"after": "Opaque cursor for the next page returned by Meta.",
|
|
26
|
+
"before": "Opaque cursor for the previous page returned by Meta.",
|
|
27
|
+
"includeUnchanged": "Include entries with no observed change in the comparison.",
|
|
28
|
+
"minImpressions": "Minimum reported impressions required for a row to enter the analysis.",
|
|
29
|
+
"objectives": "Analysis goals that determine the recommended large-site sampling strategy.",
|
|
30
|
+
"pageLimit": "Maximum number of pages considered for the sampling plan.",
|
|
31
|
+
"maxInspectionUrls": "Maximum URL Inspection candidates included in the plan; inspection calls consume provider quota.",
|
|
32
|
+
"minImpressionsForLowCtr": "Minimum impressions before a page is considered for low-CTR analysis.",
|
|
33
|
+
"staleSitemapDays": "Number of days after which a sitemap's last download is considered stale.",
|
|
34
|
+
"continueOnError": "Continue with other URLs when one inspection fails, returning individual failures.",
|
|
35
|
+
"dimension": "Reporting dimension used to group and compare results.",
|
|
36
|
+
"maxClusters": "Maximum query clusters to return.",
|
|
37
|
+
"topQueriesPerCluster": "Maximum example queries returned for each cluster.",
|
|
38
|
+
"minPages": "Minimum distinct pages ranking for a query before it is considered a cannibalization candidate.",
|
|
39
|
+
"adAccountId": "Owning advertising account ID. Use the exact ID returned by account discovery; do not substitute a campaign or business ID.",
|
|
40
|
+
"advertiserId": "TikTok advertiser account ID as a string.",
|
|
41
|
+
"customerId": "Google Ads customer ID, without hyphens.",
|
|
42
|
+
"propertyId": "Google Analytics property ID; use the property, not the account or measurement ID.",
|
|
43
|
+
"level": "Entity or aggregation level for the requested report.",
|
|
44
|
+
"dataLevel": "TikTok reporting aggregation level. Metrics and dimensions must be compatible with this level.",
|
|
45
|
+
"granularity": "Time bucket used to aggregate reporting rows.",
|
|
46
|
+
"columns": "Native Pinterest reporting column names. Discover supported columns before selecting metrics.",
|
|
47
|
+
"executionMode": "Choose the supported synchronous, asynchronous or automatic report execution strategy.",
|
|
48
|
+
"entityIds": "Exact IDs of the entities to include in the selected reporting level.",
|
|
49
|
+
"targetingTypes": "Native targeting categories used to break down delivery.",
|
|
50
|
+
"clickWindowDays": "Click-through attribution window, in days.",
|
|
51
|
+
"engagementWindowDays": "Engagement-through attribution window, in days.",
|
|
52
|
+
"viewWindowDays": "View-through attribution window, in days.",
|
|
53
|
+
"conversionReportTime": "Whether conversions are assigned to the ad action date or the conversion date.",
|
|
54
|
+
"attributionTypes": "Provider attribution categories included in the report.",
|
|
55
|
+
"reportingTimezone": "Provider reporting timezone selector. Keep it consistent when comparing periods.",
|
|
56
|
+
"campaignIds": "Restrict results to these campaign IDs.",
|
|
57
|
+
"adGroupIds": "Restrict results to these ad group IDs.",
|
|
58
|
+
"adgroupIds": "Restrict TikTok results to these ad group IDs.",
|
|
59
|
+
"adIds": "Restrict results to these ad IDs.",
|
|
60
|
+
"campaignId": "Exact campaign ID in the selected advertising account.",
|
|
61
|
+
"adGroupId": "Exact ad group ID in the selected advertising account.",
|
|
62
|
+
"adSetId": "Exact Meta ad set ID in the selected advertising account.",
|
|
63
|
+
"adId": "Exact ad ID in the selected advertising account.",
|
|
64
|
+
"entityId": "Exact ID of the entity selected by this operation. It must belong to the selected account.",
|
|
65
|
+
"onlyWithPeriodDelivery": "Keep only creatives with observed delivery in the requested date range.",
|
|
66
|
+
"breakdown": "Native reporting breakdown used to group results.",
|
|
67
|
+
"breakdowns": "Native reporting breakdowns. Check compatibility before combining them.",
|
|
68
|
+
"productGroupIds": "Restrict reporting to these product group IDs.",
|
|
69
|
+
"productItemIds": "Restrict reporting to these product item IDs.",
|
|
70
|
+
"includeProductSamples": "Include a limited sample of product records alongside inventory diagnostics.",
|
|
71
|
+
"sampleProductGroups": "Maximum product groups to sample for inventory diagnostics.",
|
|
72
|
+
"reportName": "Provider report name identifying the requested report recipe.",
|
|
73
|
+
"conversionProductAttributionType": "Pinterest product-conversion attribution category.",
|
|
74
|
+
"conversionProductBreakdown": "Pinterest product-conversion grouping level.",
|
|
75
|
+
"productSkuIds": "Restrict product-conversion reporting to these SKU IDs.",
|
|
76
|
+
"request": "Exact request schema name to inspect before composing a write payload.",
|
|
77
|
+
"entity": "Entity collection or level to inspect.",
|
|
78
|
+
"entityType": "Provider entity type to retrieve.",
|
|
79
|
+
"entityStatuses": "Native status values used to filter the entity collection.",
|
|
80
|
+
"statusFilter": "Native status values used to filter the returned entities.",
|
|
81
|
+
"targetingType": "Targeting dictionary to inspect, such as interests, locations or demographics.",
|
|
82
|
+
"interestId": "Native interest ID for the targeting lookup.",
|
|
83
|
+
"mode": "Operation or analysis mode. Only the values declared in this schema are supported.",
|
|
84
|
+
"matchTypes": "Keyword match types included in the lookup.",
|
|
85
|
+
"countryCode": "Country code used to localize keyword or targeting results.",
|
|
86
|
+
"keywords": "Keyword phrases to analyze; use the language and market of the intended audience.",
|
|
87
|
+
"term": "Search term used for the keyword lookup.",
|
|
88
|
+
"terms": "Search terms used for the keyword lookup.",
|
|
89
|
+
"audienceId": "Exact ID of the audience to inspect or modify.",
|
|
90
|
+
"customerListId": "Exact customer-list ID. Reading metadata does not upload customer data.",
|
|
91
|
+
"businessId": "Exact business account ID used for the lookup.",
|
|
92
|
+
"accountType": "Provider account category used to filter results.",
|
|
93
|
+
"ownershipType": "Filter audiences by provider ownership category.",
|
|
94
|
+
"excludeNca": "Whether to exclude Pinterest new-customer-acquisition audiences from this lookup.",
|
|
95
|
+
"audienceInsightType": "Pinterest audience-insights category to request.",
|
|
96
|
+
"surfaces": "Conversion-setup surfaces to inspect, according to the supported values.",
|
|
97
|
+
"includeDeletedTags": "Include deleted conversion tags in diagnostics.",
|
|
98
|
+
"lookbackPeriod": "Provider lookback period used for conversion diagnostics.",
|
|
99
|
+
"sourcePlatform": "Filter conversion data by source platform.",
|
|
100
|
+
"ingestionSource": "Filter conversion events by ingestion source.",
|
|
101
|
+
"catalogId": "Exact product catalog ID accessible to the selected account.",
|
|
102
|
+
"feedId": "Exact catalog feed ID.",
|
|
103
|
+
"processingResultId": "Exact catalog feed processing result ID to inspect.",
|
|
104
|
+
"productGroupId": "Exact product group ID.",
|
|
105
|
+
"productSetId": "Exact Meta product set ID belonging to the selected catalog.",
|
|
106
|
+
"exportType": "Provider export category to request.",
|
|
107
|
+
"action": "Supported operation within this tool's scoped provider surface.",
|
|
108
|
+
"includeDetails": "Include detailed provider records in addition to summary information.",
|
|
109
|
+
"leadFormId": "Exact lead form ID to inspect.",
|
|
110
|
+
"subscriptionId": "Exact subscription ID to inspect.",
|
|
111
|
+
"assetId": "Exact provider asset ID.",
|
|
112
|
+
"memberId": "Exact business member ID used for the read lookup.",
|
|
113
|
+
"partnerId": "Exact business partner ID used for the read lookup.",
|
|
114
|
+
"pinId": "Exact Pinterest pin ID.",
|
|
115
|
+
"pinIds": "Exact Pinterest pin IDs to include.",
|
|
116
|
+
"metrics": "Native or documented calculated metric names to request. Check compatibility with the selected dimensions.",
|
|
117
|
+
"metric": "Metric used for the analysis; use a name from the platform metric catalogue.",
|
|
118
|
+
"boardId": "Exact Pinterest board ID.",
|
|
119
|
+
"searchTerm": "Text used to filter or search the selected collection.",
|
|
120
|
+
"region": "Provider region selector used to localize trends.",
|
|
121
|
+
"productRegion": "Region used for product trend analysis.",
|
|
122
|
+
"trendType": "Provider trend category to return.",
|
|
123
|
+
"interests": "Native interest categories used to filter trends.",
|
|
124
|
+
"genders": "Native gender categories used to filter aggregate trends.",
|
|
125
|
+
"ageBuckets": "Native age groups used to filter aggregate trends.",
|
|
126
|
+
"includeKeywords": "Include related keyword details in the trend response.",
|
|
127
|
+
"includeDemographics": "Include available aggregate demographic breakdowns.",
|
|
128
|
+
"productCategories": "Native product category IDs or values used to filter trends.",
|
|
129
|
+
"verticals": "Native business verticals used to filter trends.",
|
|
130
|
+
"productLookbackDays": "Product-trend lookback window, in days.",
|
|
131
|
+
"productEngagementType": "Engagement signal used for product-trend analysis.",
|
|
132
|
+
"featuredInterest": "Featured interest category for the trends lookup.",
|
|
133
|
+
"reportType": "Native provider report type; determines supported metrics and dimensions.",
|
|
134
|
+
"promotionId": "Exact Pinterest product-group promotion ID.",
|
|
135
|
+
"type": "Provider object or operation type selected from this schema's allowed values.",
|
|
136
|
+
"productBreakdown": "Native product grouping used to join catalog items with reported insights.",
|
|
137
|
+
"catalogProductLimit": "Maximum catalog product records read for the join.",
|
|
138
|
+
"includeAdsets": "Include ad set configuration when checking brand safety.",
|
|
139
|
+
"includeBlockLists": "Include accessible brand-safety block-list metadata.",
|
|
140
|
+
"includeRawTargeting": "Include the native targeting specification for diagnostics.",
|
|
141
|
+
"cellEntityType": "Entity type represented by experiment cells.",
|
|
142
|
+
"includePagePosts": "Read related Facebook Page posts when the token has permission.",
|
|
143
|
+
"includeInstagramMedia": "Read related Instagram media when the token has permission.",
|
|
144
|
+
"includeInsights": "Include available insight metrics; additional provider permissions may be required.",
|
|
145
|
+
"edge": "Allowlisted Meta Graph edge to read on the selected node.",
|
|
146
|
+
"fields": "Native provider fields to return. Select only fields supported by the chosen object or report.",
|
|
147
|
+
"filtering": "Native provider filter expressions; credentials and account overrides are not allowed.",
|
|
148
|
+
"includeSummary": "Request the provider's available summary or total-count metadata.",
|
|
149
|
+
"actionBreakdowns": "Meta action-level breakdowns, subject to Insights compatibility rules.",
|
|
150
|
+
"actionAttributionWindows": "Meta attribution windows used to attribute action metrics.",
|
|
151
|
+
"timeIncrement": "Provider time-bucket size or supported aggregate value.",
|
|
152
|
+
"locationTypes": "Location dictionary categories to include in targeting search.",
|
|
153
|
+
"adFormat": "Native Meta ad preview format; must be compatible with this ad's creative.",
|
|
154
|
+
"handle": "Asynchronous catalog batch handle returned by the submission to inspect.",
|
|
155
|
+
"keepEmptyRows": "Keep provider rows whose selected metrics are all zero, where supported.",
|
|
156
|
+
"sampleLimit": "Maximum records included in the diagnostic sample.",
|
|
157
|
+
"isOpenFunnel": "Allow users to enter at any funnel step instead of requiring the first step.",
|
|
158
|
+
"visualizationType": "Supported funnel visualization mode.",
|
|
159
|
+
"breakdownLimit": "Maximum breakdown values to include in the funnel response.",
|
|
160
|
+
"nextActionLimit": "Maximum next actions returned for each funnel step.",
|
|
161
|
+
"fallbackToStepCounts": "If the advanced funnel is unavailable, return independent step counts with the limitation made explicit.",
|
|
162
|
+
"report": "Native GA4 report request object for this endpoint.",
|
|
163
|
+
"reports": "Native GA4 report request objects to execute in one batch.",
|
|
164
|
+
"dimensions": "Native reporting dimensions. Validate compatibility with the selected metrics.",
|
|
165
|
+
"compatibilityFilter": "Limit compatibility results to the requested GA4 compatibility category.",
|
|
166
|
+
"collection": "Allowlisted administrative resource collection to read.",
|
|
167
|
+
"settings": "Property configuration surfaces to inspect.",
|
|
168
|
+
"includeRecurring": "Include recurring audience-export configurations.",
|
|
169
|
+
"audienceExportName": "Full GA4 audience-export resource name returned by list_audience_exports.",
|
|
170
|
+
"queryLifetime": "Request lifetime reporting instead of the explicit reporting period where TikTok supports it.",
|
|
171
|
+
"declineThresholdPct": "Percentage decline used as a candidate signal for creative fatigue, requiring further evidence.",
|
|
172
|
+
"includeSavedAudiences": "Include accessible saved-audience metadata in the overlap analysis.",
|
|
173
|
+
"overlapThreshold": "Overlap threshold used to flag audience pairs, in the units declared by this schema.",
|
|
174
|
+
"serviceType": "TikTok report service category.",
|
|
175
|
+
"catalog": "Allowlisted targeting dictionary or catalog to inspect.",
|
|
176
|
+
"query": "Search text used to find matching entries in the selected catalogue.",
|
|
177
|
+
"parameters": "Native query parameters for this allowlisted read endpoint. Do not provide credentials or account overrides.",
|
|
178
|
+
"account_id": "Exact account ID selected for this platform in the GetMCPAds workspace.",
|
|
179
|
+
"platform": "Platform key identifying the connected advertising or analytics source.",
|
|
180
|
+
"id": "Exact saved object ID returned by the corresponding create or list tool.",
|
|
181
|
+
"revision": "Last known object revision, used to reject conflicting updates. Read the object before editing.",
|
|
182
|
+
"name": "Human-readable name for this object.",
|
|
183
|
+
"title": "Human-readable title for the resulting object or review.",
|
|
184
|
+
"clientLabel": "Display label identifying the client in the saved view.",
|
|
185
|
+
"config": "Complete saved-view configuration, including source, selected account, period and display options.",
|
|
186
|
+
"subject": "Entity or creative subject whose dated performance will be analyzed.",
|
|
187
|
+
"compare_previous": "Include the immediately preceding period of equal duration for comparison.",
|
|
188
|
+
"profile": "Creative brief profile containing the client's business context and analysis preferences.",
|
|
189
|
+
"primaryMetric": "Metric used to rank or evaluate the review; it must be available for the selected platform.",
|
|
190
|
+
"direction": "Whether higher or lower values of the primary metric represent improvement.",
|
|
191
|
+
"creative_ids": "Exact creative IDs to include in this review.",
|
|
192
|
+
"request_id": "Caller-generated request identifier. Reuse it for a retry of the same operation to avoid duplicate work.",
|
|
193
|
+
"expiresInDays": "Number of days before the public review sharing link expires.",
|
|
194
|
+
"client_id": "Exact saved business-client workspace ID.",
|
|
195
|
+
"review_id": "Exact review ID returned by the review creation or listing tool.",
|
|
196
|
+
"page_id": "Exact page identifier within the landing-page review.",
|
|
197
|
+
"entity_id": "Exact entity identifier in the selected review or platform context.",
|
|
198
|
+
"source_key": "Source identifier returned by the business review; keep the platform/account association intact.",
|
|
199
|
+
"metric_key": "Exact metric key in the review evidence.",
|
|
200
|
+
"follow_up_on": "Follow-up date for the decision, using the format declared in this schema.",
|
|
201
|
+
"hypothesis": "Specific hypothesis to record, supported by the review evidence.",
|
|
202
|
+
"successCriterion": "Measurable condition used to decide whether the recorded action succeeded.",
|
|
203
|
+
"outcome": "Observed outcome to record; distinguish evidence from an untested expectation.",
|
|
204
|
+
"status": "Status selected from the allowed values for this object or operation.",
|
|
205
|
+
"retry_source": "Source identifier to retry after reviewing its reported error.",
|
|
206
|
+
"site_origin": "Website origin to analyze, including https:// and hostname, without credentials or a path.",
|
|
207
|
+
"matchType": "Google Ads keyword match type for the forecast.",
|
|
208
|
+
"negativeKeywords": "Keyword phrases excluded from the keyword forecast.",
|
|
209
|
+
"negativeMatchType": "Match type applied to the negative keywords.",
|
|
210
|
+
"geoTargetIds": "Google Ads geo-target constant IDs for the intended locations.",
|
|
211
|
+
"languageId": "Google Ads language constant ID for the keyword request.",
|
|
212
|
+
"languageIds": "Google Ads language constant IDs for the intended audience.",
|
|
213
|
+
"network": "Google Ads search network selector used for planning.",
|
|
214
|
+
"biddingStrategy": "Supported bidding strategy for this temporary forecast. The request does not create a live campaign.",
|
|
215
|
+
"includeAdultKeywords": "Allow adult keyword ideas where supported by the provider and the selected market.",
|
|
216
|
+
"seedKeywords": "Seed keyword phrases from which Google generates keyword ideas.",
|
|
217
|
+
"includeAverageCpc": "Request average cost-per-click metrics when available.",
|
|
218
|
+
"includeDeviceBreakdown": "Include device-specific planning metrics when supported.",
|
|
219
|
+
"includeKeywordConcepts": "Include Google's keyword concept grouping metadata.",
|
|
220
|
+
"startYearMonth": "First year and month of the historical metrics period.",
|
|
221
|
+
"endYearMonth": "Last year and month of the historical metrics period.",
|
|
222
|
+
"includeDismissed": "Include recommendations already dismissed in the account.",
|
|
223
|
+
"operation": "Exact allowlisted operation to execute. Arbitrary provider operations are not accepted.",
|
|
224
|
+
"category": "Category selected from the provider dictionary or field catalogue.",
|
|
225
|
+
"selectable": "Filter the field catalogue by whether the field can appear in SELECT.",
|
|
226
|
+
"filterable": "Filter the field catalogue by whether the field can appear in WHERE.",
|
|
227
|
+
"sortable": "Filter the field catalogue by whether the field can appear in ORDER BY.",
|
|
228
|
+
"pageToken": "Opaque continuation token returned by the preceding provider response.",
|
|
229
|
+
"locationNames": "Human-readable location names for Google to resolve into geo-target constants.",
|
|
230
|
+
"locale": "Locale used for human-readable lookup results.",
|
|
231
|
+
"resource": "Native Google Ads resource from which the GAQL query selects rows.",
|
|
232
|
+
"cursor": "Opaque pagination cursor returned by the preceding response; omit for the first page.",
|
|
233
|
+
"kind": "Entity or request kind whose schema or data is being requested.",
|
|
234
|
+
"startTime": "Beginning of the reporting period as an ISO 8601 timestamp with an explicit timezone.",
|
|
235
|
+
"endTime": "End of the reporting period as an ISO 8601 timestamp with an explicit timezone, after startTime.",
|
|
236
|
+
"reportDimension": "Native dimension used to break down the Snapchat report."
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
// src/tool-quality.ts
|
|
240
|
+
import { z } from "zod";
|
|
241
|
+
var RESULT_SCHEMA = {
|
|
242
|
+
type: "object",
|
|
243
|
+
properties: {
|
|
244
|
+
result: {
|
|
245
|
+
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.",
|
|
246
|
+
type: ["object", "array", "string", "number", "boolean", "null"]
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
required: ["result"],
|
|
250
|
+
additionalProperties: true
|
|
251
|
+
};
|
|
252
|
+
function toolAnnotations(write) {
|
|
253
|
+
return { readOnlyHint: !write, destructiveHint: write, idempotentHint: !write, openWorldHint: true };
|
|
254
|
+
}
|
|
255
|
+
function structuredResult(value) {
|
|
256
|
+
if (value.isError) return value;
|
|
257
|
+
let result2 = value.content;
|
|
258
|
+
const first = value.content[0];
|
|
259
|
+
if (value.content.length === 1 && first?.type === "text" && typeof first.text === "string") {
|
|
260
|
+
result2 = first.text;
|
|
261
|
+
try {
|
|
262
|
+
result2 = JSON.parse(first.text);
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { ...value, structuredContent: { ...value.structuredContent, result: result2 } };
|
|
267
|
+
}
|
|
268
|
+
var resultShape = { result: z.union([z.object({}).passthrough(), z.array(z.unknown()), z.string(), z.number(), z.boolean(), z.null()]).describe(RESULT_SCHEMA.properties.result.description) };
|
|
269
|
+
function installToolQuality(server) {
|
|
270
|
+
server.tool = ((name, description, inputSchema, handler) => {
|
|
271
|
+
const write = Object.hasOwn(inputSchema, "confirm");
|
|
272
|
+
inputSchema = Object.fromEntries(Object.entries(inputSchema).map(([key, schema]) => [key, schema.description || PARAMETER_DESCRIPTIONS[key] ? schema.describe(schema.description || PARAMETER_DESCRIPTIONS[key]) : schema]));
|
|
273
|
+
return server.registerTool(name, {
|
|
274
|
+
title: name.replace(/_/g, " "),
|
|
275
|
+
description,
|
|
276
|
+
inputSchema,
|
|
277
|
+
outputSchema: resultShape,
|
|
278
|
+
annotations: toolAnnotations(write)
|
|
279
|
+
}, async (...args) => structuredResult(await handler(...args)));
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
1
283
|
// src/server.ts
|
|
2
284
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
285
|
|
|
286
|
+
// src/platforms/google-ads/extended-writes.ts
|
|
287
|
+
import { webcrypto } from "crypto";
|
|
288
|
+
import { z as z2 } from "zod";
|
|
289
|
+
var BASE = "https://googleads.googleapis.com/v25";
|
|
290
|
+
var resources = {
|
|
291
|
+
conversionActionOperation: "conversionActions",
|
|
292
|
+
campaignBudgetOperation: "campaignBudgets",
|
|
293
|
+
campaignOperation: "campaigns",
|
|
294
|
+
adGroupOperation: "adGroups",
|
|
295
|
+
adGroupAdOperation: "adGroupAds",
|
|
296
|
+
adOperation: "ads",
|
|
297
|
+
assetOperation: "assets",
|
|
298
|
+
assetGroupOperation: "assetGroups",
|
|
299
|
+
assetGroupAssetOperation: "assetGroupAssets",
|
|
300
|
+
campaignAssetOperation: "campaignAssets",
|
|
301
|
+
campaignCriterionOperation: "campaignCriteria",
|
|
302
|
+
adGroupCriterionOperation: "adGroupCriteria",
|
|
303
|
+
assetGroupListingGroupFilterOperation: "assetGroupListingGroupFilters"
|
|
304
|
+
};
|
|
305
|
+
var json = z2.record(z2.unknown());
|
|
306
|
+
var operation = z2.object({
|
|
307
|
+
kind: z2.enum(
|
|
308
|
+
Object.keys(resources)
|
|
309
|
+
),
|
|
310
|
+
create: json.optional(),
|
|
311
|
+
update: json.optional(),
|
|
312
|
+
remove: z2.string().optional(),
|
|
313
|
+
updateMask: z2.string().min(1).optional()
|
|
314
|
+
}).strict();
|
|
315
|
+
var result = (v) => ({
|
|
316
|
+
content: [{ type: "text", text: JSON.stringify(v, null, 2) }]
|
|
317
|
+
});
|
|
318
|
+
function canonical(v) {
|
|
319
|
+
if (Array.isArray(v)) return `[${v.map(canonical).join(",")}]`;
|
|
320
|
+
if (v && typeof v === "object")
|
|
321
|
+
return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(",")}}`;
|
|
322
|
+
return JSON.stringify(v);
|
|
323
|
+
}
|
|
324
|
+
async function digest(v) {
|
|
325
|
+
return Array.from(
|
|
326
|
+
new Uint8Array(
|
|
327
|
+
await webcrypto.subtle.digest(
|
|
328
|
+
"SHA-256",
|
|
329
|
+
new TextEncoder().encode(canonical(v))
|
|
330
|
+
)
|
|
331
|
+
)
|
|
332
|
+
).map((n) => n.toString(16).padStart(2, "0")).join("");
|
|
333
|
+
}
|
|
334
|
+
function previewData(v) {
|
|
335
|
+
if (Array.isArray(v)) return v.map(previewData);
|
|
336
|
+
if (v && typeof v === "object")
|
|
337
|
+
return Object.fromEntries(
|
|
338
|
+
Object.entries(v).map(([k, x]) => [
|
|
339
|
+
k,
|
|
340
|
+
k === "data" && typeof x === "string" ? `[base64 media: ${x.length} characters]` : previewData(x)
|
|
341
|
+
])
|
|
342
|
+
);
|
|
343
|
+
return v;
|
|
344
|
+
}
|
|
345
|
+
function normalizeGooglePlan(customerId, input) {
|
|
346
|
+
if (!/^\d{10}$/.test(customerId))
|
|
347
|
+
throw new Error("Use a ten-digit customer ID.");
|
|
348
|
+
const prefix = `customers/${customerId}/`;
|
|
349
|
+
function check(v) {
|
|
350
|
+
if (typeof v === "string" && v.startsWith("customers/") && !v.startsWith(prefix))
|
|
351
|
+
throw new Error(
|
|
352
|
+
"Every customer resource must belong to the selected customerId."
|
|
353
|
+
);
|
|
354
|
+
if (Array.isArray(v)) v.forEach(check);
|
|
355
|
+
else if (v && typeof v === "object")
|
|
356
|
+
for (const [k, x] of Object.entries(v)) {
|
|
357
|
+
if (/access.?token|refresh.?token|developer.?token|authorization|client.?secret/i.test(
|
|
358
|
+
k
|
|
359
|
+
))
|
|
360
|
+
throw new Error("Do not put credentials in a plan.");
|
|
361
|
+
if (k === "data" && typeof x === "string") {
|
|
362
|
+
let valid = x.length > 0 && x.length <= 6990508 && /^[A-Za-z0-9+/]*={0,2}$/.test(x);
|
|
363
|
+
try {
|
|
364
|
+
valid = valid && atob(x).length <= 5 * 1024 * 1024;
|
|
365
|
+
} catch {
|
|
366
|
+
valid = false;
|
|
367
|
+
}
|
|
368
|
+
if (!valid) throw new Error("Image data must be base64, at most 5 MiB decoded.");
|
|
369
|
+
}
|
|
370
|
+
check(x);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return input.map((raw) => {
|
|
374
|
+
const op = operation.parse(raw);
|
|
375
|
+
if ([op.create, op.update, op.remove].filter((x) => x !== void 0).length !== 1)
|
|
376
|
+
throw new Error(
|
|
377
|
+
"Each operation needs exactly one of create, update or remove."
|
|
378
|
+
);
|
|
379
|
+
if (op.update && !op.updateMask)
|
|
380
|
+
throw new Error("Updates require an explicit updateMask.");
|
|
381
|
+
if (op.kind === "adOperation" && op.update && ("name" in op.update || op.updateMask?.split(",").some((field) => field.trim() === "name")))
|
|
382
|
+
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.");
|
|
383
|
+
if (!op.update && op.updateMask)
|
|
384
|
+
throw new Error("updateMask is only valid on updates.");
|
|
385
|
+
if (op.updateMask && /(^|,)\s*(\*|resource_name)\s*(,|$)/.test(op.updateMask))
|
|
386
|
+
throw new Error(
|
|
387
|
+
"Use a precise updateMask; never wildcard or resource_name."
|
|
388
|
+
);
|
|
389
|
+
const body = structuredClone(op.create ?? op.update);
|
|
390
|
+
const expected = prefix + resources[op.kind] + "/";
|
|
391
|
+
const resource = op.remove ?? body?.resourceName;
|
|
392
|
+
if ((op.update || op.remove) && !resource)
|
|
393
|
+
throw new Error("Update/remove requires a resource name.");
|
|
394
|
+
if (resource && (!resource.startsWith(expected) || !/^-?\d+(~[A-Za-z0-9_-]+)*$/.test(resource.slice(expected.length))))
|
|
395
|
+
throw new Error(`Expected resource in ${expected}.`);
|
|
396
|
+
if (op.create && [
|
|
397
|
+
"campaignOperation",
|
|
398
|
+
"adGroupOperation",
|
|
399
|
+
"adGroupAdOperation",
|
|
400
|
+
"assetGroupOperation"
|
|
401
|
+
].includes(op.kind)) {
|
|
402
|
+
if (body.status && body.status !== "PAUSED")
|
|
403
|
+
throw new Error("New campaigns, groups and ads must be PAUSED.");
|
|
404
|
+
body.status = "PAUSED";
|
|
405
|
+
}
|
|
406
|
+
if (op.create && op.kind === "campaignOperation") {
|
|
407
|
+
if (body.advertisingChannelType === "VIDEO")
|
|
408
|
+
throw new Error(
|
|
409
|
+
"Google Ads API cannot create VIDEO campaigns. Use an explicitly requested DEMAND_GEN video campaign instead."
|
|
410
|
+
);
|
|
411
|
+
if (![
|
|
412
|
+
"CONTAINS_EU_POLITICAL_ADVERTISING",
|
|
413
|
+
"DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING"
|
|
414
|
+
].includes(body.containsEuPoliticalAdvertising))
|
|
415
|
+
throw new Error(
|
|
416
|
+
"Explicit EU political advertising declaration required."
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const value = op.remove ? { remove: op.remove } : op.create ? { create: body } : { update: body, updateMask: op.updateMask };
|
|
420
|
+
check(value);
|
|
421
|
+
return { [op.kind]: value };
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function registerGoogleVideoReads(c, config) {
|
|
425
|
+
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.", {
|
|
426
|
+
customerId: z2.string().regex(/^[\d-]+$/),
|
|
427
|
+
currency: z2.string().regex(/^[A-Z]{3}$/).describe("Actual customer currency, verified before reading the upload."),
|
|
428
|
+
uploadId: z2.string().regex(/^\d+$/).describe("Numeric upload ID returned by google_ads_upload_video."),
|
|
429
|
+
loginCustomerId: z2.string().regex(/^[\d-]+$/).optional().describe("Optional MCC manager ID; defaults to the configured manager."),
|
|
430
|
+
testAccountOnly: z2.boolean().optional().describe("Require customer.test_account=true; enable for sandbox checks.")
|
|
431
|
+
}, async (raw) => {
|
|
432
|
+
const a = z2.object({ customerId: z2.string().regex(/^[\d-]+$/), currency: z2.string().regex(/^[A-Z]{3}$/), uploadId: z2.string().regex(/^\d+$/), loginCustomerId: z2.string().regex(/^[\d-]+$/).optional(), testAccountOnly: z2.boolean().optional() }).strict().parse(raw);
|
|
433
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
434
|
+
const { api } = await googleSession(config, cid, a.currency, a.testAccountOnly === true, (a.loginCustomerId ?? config.loginCustomerId)?.replace(/-/g, ""));
|
|
435
|
+
const resourceName = `customers/${cid}/youTubeVideoUploads/${a.uploadId}`;
|
|
436
|
+
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` });
|
|
437
|
+
const video = response.results?.[0]?.youTubeVideoUpload;
|
|
438
|
+
return result({ uploadId: a.uploadId, state: video?.state ?? "NOT_FOUND", ...video?.state === "PROCESSED" && video.videoId ? { videoId: video.videoId } : {} });
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function registerGoogleExtendedWrites(c, config) {
|
|
442
|
+
const original = c;
|
|
443
|
+
const descriptions = {
|
|
444
|
+
customerId: "Selected Google Ads customer ID, not the manager account.",
|
|
445
|
+
currency: "Actual account currency as a three-letter ISO code; verified natively before writes.",
|
|
446
|
+
loginCustomerId: "Optional MCC ID; defaults to the selected account's inferred manager.",
|
|
447
|
+
testAccountOnly: "Require customer.test_account=true before writing. Always use true for sandbox acceptance.",
|
|
448
|
+
confirm: "Apply only when true and expectedPlanHash matches the exact preview.",
|
|
449
|
+
validateOnly: "Ask Google to validate without applying. Takes priority over confirm.",
|
|
450
|
+
expectedPlanHash: "SHA-256 planHash from the preview of this exact payload, required for application.",
|
|
451
|
+
uploadId: "Numeric ID of a video uploaded through Google Ads in this customer.",
|
|
452
|
+
title: "Nonempty title for this new video upload.",
|
|
453
|
+
mimeType: "Actual MIME type of the supplied video bytes."
|
|
454
|
+
};
|
|
455
|
+
c = {
|
|
456
|
+
tool: (n, d, shape, h) => original.tool(
|
|
457
|
+
n,
|
|
458
|
+
d,
|
|
459
|
+
Object.fromEntries(
|
|
460
|
+
Object.entries(shape).map(([k, v]) => [
|
|
461
|
+
k,
|
|
462
|
+
v.description ? v : v.describe(descriptions[k] ?? k)
|
|
463
|
+
])
|
|
464
|
+
),
|
|
465
|
+
h
|
|
466
|
+
)
|
|
467
|
+
};
|
|
468
|
+
registerGoogleVideoWrites(c, config);
|
|
469
|
+
c.tool(
|
|
470
|
+
"google_ads_apply_plan",
|
|
471
|
+
"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.",
|
|
472
|
+
{
|
|
473
|
+
customerId: z2.string().regex(/^[\d-]+$/),
|
|
474
|
+
currency: z2.string().regex(/^[A-Z]{3}$/).describe(
|
|
475
|
+
"Actual account currency, mandatory even when only linking assets."
|
|
476
|
+
),
|
|
477
|
+
operations: z2.array(operation).min(1).max(100).describe(
|
|
478
|
+
"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)."
|
|
479
|
+
),
|
|
480
|
+
loginCustomerId: z2.string().regex(/^[\d-]+$/).optional(),
|
|
481
|
+
testAccountOnly: z2.boolean().optional().describe(
|
|
482
|
+
"Require Google's customer.test_account=true before any mutation. Use true for acceptance tests."
|
|
483
|
+
),
|
|
484
|
+
validateOnly: z2.boolean().optional(),
|
|
485
|
+
confirm: z2.boolean().optional(),
|
|
486
|
+
expectedPlanHash: z2.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
487
|
+
},
|
|
488
|
+
async (a) => {
|
|
489
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
490
|
+
const operations = normalizeGooglePlan(cid, a.operations);
|
|
491
|
+
if (JSON.stringify(operations).length > 12e6)
|
|
492
|
+
throw new Error(
|
|
493
|
+
"Plan too large; upload assets in separate confirmed plans and reference their resource names."
|
|
494
|
+
);
|
|
495
|
+
const manager = (a.loginCustomerId ?? config.loginCustomerId)?.replace(
|
|
496
|
+
/-/g,
|
|
497
|
+
""
|
|
498
|
+
);
|
|
499
|
+
const planHash = await digest({
|
|
500
|
+
customerId: cid,
|
|
501
|
+
currency: a.currency,
|
|
502
|
+
manager: manager ?? null,
|
|
503
|
+
testAccountOnly: a.testAccountOnly === true,
|
|
504
|
+
operations
|
|
505
|
+
});
|
|
506
|
+
if (!a.confirm && !a.validateOnly)
|
|
507
|
+
return result({
|
|
508
|
+
applied: false,
|
|
509
|
+
planHash,
|
|
510
|
+
customerId: cid,
|
|
511
|
+
currency: a.currency,
|
|
512
|
+
operationCount: operations.length,
|
|
513
|
+
operations: previewData(operations)
|
|
514
|
+
});
|
|
515
|
+
if (a.confirm && !a.validateOnly && a.expectedPlanHash !== planHash)
|
|
516
|
+
throw new Error(
|
|
517
|
+
"Preview this exact plan first and provide its matching expectedPlanHash."
|
|
518
|
+
);
|
|
519
|
+
const { api, customer } = await googleSession(
|
|
520
|
+
config,
|
|
521
|
+
cid,
|
|
522
|
+
a.currency,
|
|
523
|
+
a.testAccountOnly,
|
|
524
|
+
manager
|
|
525
|
+
);
|
|
526
|
+
const response = await api("googleAds:mutate", {
|
|
527
|
+
mutateOperations: operations,
|
|
528
|
+
partialFailure: false,
|
|
529
|
+
validateOnly: a.validateOnly === true
|
|
530
|
+
});
|
|
531
|
+
return result({
|
|
532
|
+
applied: a.validateOnly !== true,
|
|
533
|
+
validatedByGoogle: true,
|
|
534
|
+
planHash,
|
|
535
|
+
operationCount: operations.length,
|
|
536
|
+
customerId: cid,
|
|
537
|
+
currency: a.currency,
|
|
538
|
+
testAccount: customer.testAccount === true,
|
|
539
|
+
result: response
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
async function googleSession(config, cid, currency, testAccountOnly, manager) {
|
|
545
|
+
const auth = await fetch("https://oauth2.googleapis.com/token", {
|
|
546
|
+
method: "POST",
|
|
547
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
548
|
+
body: new URLSearchParams({
|
|
549
|
+
grant_type: "refresh_token",
|
|
550
|
+
client_id: config.clientId,
|
|
551
|
+
client_secret: config.clientSecret,
|
|
552
|
+
refresh_token: config.refreshToken
|
|
553
|
+
}),
|
|
554
|
+
redirect: "error"
|
|
555
|
+
});
|
|
556
|
+
const token = await auth.json();
|
|
557
|
+
if (!auth.ok || !token.access_token)
|
|
558
|
+
throw new Error(
|
|
559
|
+
"Google authorization could not be refreshed. Reconnect the Google Ads source."
|
|
560
|
+
);
|
|
561
|
+
const headers = {
|
|
562
|
+
authorization: `Bearer ${token.access_token}`,
|
|
563
|
+
"developer-token": config.developerToken,
|
|
564
|
+
"content-type": "application/json"
|
|
565
|
+
};
|
|
566
|
+
if (manager) headers["login-customer-id"] = manager;
|
|
567
|
+
async function api(path, body) {
|
|
568
|
+
const r = await fetch(`${BASE}/customers/${cid}/${path}`, {
|
|
569
|
+
method: "POST",
|
|
570
|
+
headers,
|
|
571
|
+
body: JSON.stringify(body),
|
|
572
|
+
redirect: "error"
|
|
573
|
+
});
|
|
574
|
+
const x = await r.json();
|
|
575
|
+
if (!r.ok) {
|
|
576
|
+
const errors = x.error?.details?.flatMap((d) => d.errors ?? []).map((e) => ({
|
|
577
|
+
code: e.errorCode,
|
|
578
|
+
message: e.message,
|
|
579
|
+
location: e.location
|
|
580
|
+
}));
|
|
581
|
+
throw new Error(
|
|
582
|
+
`Google Ads: ${JSON.stringify(errors?.length ? errors : x.error?.message ?? r.status).slice(0, 4e3)}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
return x;
|
|
586
|
+
}
|
|
587
|
+
const read = await api("googleAds:search", {
|
|
588
|
+
query: "SELECT customer.id, customer.currency_code, customer.test_account FROM customer"
|
|
589
|
+
});
|
|
590
|
+
const customer = read.results?.[0]?.customer;
|
|
591
|
+
if (customer?.id !== cid || customer?.currencyCode !== currency)
|
|
592
|
+
throw new Error("Customer identity or currency does not match the plan.");
|
|
593
|
+
if (testAccountOnly && customer.testAccount !== true)
|
|
594
|
+
throw new Error("This plan requires a Google test account.");
|
|
595
|
+
return { headers, api, customer };
|
|
596
|
+
}
|
|
597
|
+
function registerGoogleVideoWrites(c, config) {
|
|
598
|
+
const common = {
|
|
599
|
+
customerId: z2.string().regex(/^[\d-]+$/),
|
|
600
|
+
currency: z2.string().regex(/^[A-Z]{3}$/),
|
|
601
|
+
loginCustomerId: z2.string().regex(/^[\d-]+$/).optional(),
|
|
602
|
+
testAccountOnly: z2.boolean().optional(),
|
|
603
|
+
confirm: z2.boolean().optional(),
|
|
604
|
+
expectedPlanHash: z2.string().regex(/^[a-f0-9]{64}$/).optional()
|
|
605
|
+
};
|
|
606
|
+
c.tool(
|
|
607
|
+
"google_ads_upload_video",
|
|
608
|
+
"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.",
|
|
609
|
+
{
|
|
610
|
+
...common,
|
|
611
|
+
bytesBase64: z2.string().min(4).max(22369624).describe(
|
|
612
|
+
"Raw base64 video bytes, no data URL. Prefer an existing YouTube asset for large videos."
|
|
613
|
+
),
|
|
614
|
+
mimeType: z2.enum(["video/mp4", "video/webm", "video/quicktime"]),
|
|
615
|
+
title: z2.string().trim().min(1).max(100),
|
|
616
|
+
description: z2.string().trim().min(1).max(5e3).describe(
|
|
617
|
+
"Nonempty video description; Google rejects an empty value during upload."
|
|
618
|
+
),
|
|
619
|
+
visibility: z2.literal("UNLISTED").describe(
|
|
620
|
+
"Explicit acknowledgment of the visibility of this YouTube upload."
|
|
621
|
+
)
|
|
622
|
+
},
|
|
623
|
+
async (a) => {
|
|
624
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
625
|
+
if (!/^\d{10}$/.test(cid))
|
|
626
|
+
throw new Error("Use a ten-digit customer ID.");
|
|
627
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(a.bytesBase64))
|
|
628
|
+
throw new Error("Invalid base64 video.");
|
|
629
|
+
let binary;
|
|
630
|
+
try {
|
|
631
|
+
binary = atob(a.bytesBase64);
|
|
632
|
+
} catch {
|
|
633
|
+
throw new Error("Invalid base64 video.");
|
|
634
|
+
}
|
|
635
|
+
if (binary.length > 16 * 1024 * 1024)
|
|
636
|
+
throw new Error("Video exceeds 16 MiB.");
|
|
637
|
+
const manager = (a.loginCustomerId ?? config.loginCustomerId)?.replace(
|
|
638
|
+
/-/g,
|
|
639
|
+
""
|
|
640
|
+
);
|
|
641
|
+
const planHash = await digest({
|
|
642
|
+
customerId: cid,
|
|
643
|
+
currency: a.currency,
|
|
644
|
+
manager: manager ?? null,
|
|
645
|
+
testAccountOnly: a.testAccountOnly === true,
|
|
646
|
+
title: a.title,
|
|
647
|
+
description: a.description ?? "",
|
|
648
|
+
mimeType: a.mimeType,
|
|
649
|
+
visibility: a.visibility,
|
|
650
|
+
bytes: a.bytesBase64
|
|
651
|
+
});
|
|
652
|
+
if (!a.confirm)
|
|
653
|
+
return result({
|
|
654
|
+
applied: false,
|
|
655
|
+
planHash,
|
|
656
|
+
customerId: cid,
|
|
657
|
+
title: a.title,
|
|
658
|
+
description: a.description,
|
|
659
|
+
mimeType: a.mimeType,
|
|
660
|
+
visibility: a.visibility,
|
|
661
|
+
byteLength: binary.length
|
|
662
|
+
});
|
|
663
|
+
if (a.expectedPlanHash !== planHash)
|
|
664
|
+
throw new Error(
|
|
665
|
+
"Preview this exact upload first and supply expectedPlanHash."
|
|
666
|
+
);
|
|
667
|
+
const { headers } = await googleSession(
|
|
668
|
+
config,
|
|
669
|
+
cid,
|
|
670
|
+
a.currency,
|
|
671
|
+
a.testAccountOnly,
|
|
672
|
+
manager
|
|
673
|
+
);
|
|
674
|
+
const start = await fetch(
|
|
675
|
+
`https://googleads.googleapis.com/resumable/upload/v25/customers/${cid}/youTubeVideoUploads:create`,
|
|
676
|
+
{
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
...headers,
|
|
680
|
+
"X-Goog-Upload-Protocol": "resumable",
|
|
681
|
+
"X-Goog-Upload-Command": "start",
|
|
682
|
+
"X-Goog-Upload-Header-Content-Length": String(binary.length)
|
|
683
|
+
},
|
|
684
|
+
body: JSON.stringify({
|
|
685
|
+
customer_id: cid,
|
|
686
|
+
you_tube_video_upload: {
|
|
687
|
+
video_title: a.title,
|
|
688
|
+
video_description: a.description ?? "",
|
|
689
|
+
video_privacy: "UNLISTED"
|
|
690
|
+
}
|
|
691
|
+
}),
|
|
692
|
+
redirect: "error"
|
|
693
|
+
}
|
|
694
|
+
);
|
|
695
|
+
if (!start.ok)
|
|
696
|
+
throw new Error(
|
|
697
|
+
`Google video upload initiation failed (${start.status}). No automatic retry.`
|
|
698
|
+
);
|
|
699
|
+
const target = start.headers.get("x-goog-upload-url");
|
|
700
|
+
if (!target || new URL(target).origin !== "https://googleads.googleapis.com")
|
|
701
|
+
throw new Error(
|
|
702
|
+
"Unexpected Google upload URL; no bytes or credentials sent."
|
|
703
|
+
);
|
|
704
|
+
const uploaded = await fetch(target, {
|
|
705
|
+
method: "PUT",
|
|
706
|
+
headers: {
|
|
707
|
+
...headers,
|
|
708
|
+
"content-type": a.mimeType,
|
|
709
|
+
"X-Goog-Upload-Offset": "0",
|
|
710
|
+
"X-Goog-Upload-Command": "upload, finalize"
|
|
711
|
+
},
|
|
712
|
+
body: Uint8Array.from(binary, (c2) => c2.charCodeAt(0)),
|
|
713
|
+
redirect: "error"
|
|
714
|
+
});
|
|
715
|
+
if (!uploaded.ok) {
|
|
716
|
+
const error = await uploaded.json().catch(() => ({}));
|
|
717
|
+
throw new Error(
|
|
718
|
+
`Google video upload failed (${uploaded.status}): ${String(error.error?.message ?? "unknown error").slice(0, 700)}. Inspect you_tube_video_upload before retrying.`
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
const response = await uploaded.json();
|
|
722
|
+
if (!String(response.resourceName).startsWith(
|
|
723
|
+
`customers/${cid}/youTubeVideoUploads/`
|
|
724
|
+
))
|
|
725
|
+
throw new Error(
|
|
726
|
+
"Google did not return an owned video upload resource. Inspect uploads before retrying."
|
|
727
|
+
);
|
|
728
|
+
return result({
|
|
729
|
+
applied: true,
|
|
730
|
+
planHash,
|
|
731
|
+
resourceName: response.resourceName,
|
|
732
|
+
uploadId: String(response.resourceName).split("/").pop(),
|
|
733
|
+
visibility: "UNLISTED",
|
|
734
|
+
nextAction: "Query you_tube_video_upload.state and video_id; wait for PROCESSED before creating the video asset."
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
c.tool(
|
|
739
|
+
"google_ads_remove_video_upload",
|
|
740
|
+
"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.",
|
|
741
|
+
{ ...common, uploadId: z2.string().regex(/^\d+$/) },
|
|
742
|
+
async (a) => {
|
|
743
|
+
const cid = a.customerId.replace(/-/g, "");
|
|
744
|
+
if (!/^\d{10}$/.test(cid))
|
|
745
|
+
throw new Error("Use a ten-digit customer ID.");
|
|
746
|
+
const manager = (a.loginCustomerId ?? config.loginCustomerId)?.replace(
|
|
747
|
+
/-/g,
|
|
748
|
+
""
|
|
749
|
+
);
|
|
750
|
+
const resourceName = `customers/${cid}/youTubeVideoUploads/${a.uploadId}`;
|
|
751
|
+
const planHash = await digest({
|
|
752
|
+
customerId: cid,
|
|
753
|
+
currency: a.currency,
|
|
754
|
+
manager: manager ?? null,
|
|
755
|
+
testAccountOnly: a.testAccountOnly === true,
|
|
756
|
+
remove: resourceName
|
|
757
|
+
});
|
|
758
|
+
if (!a.confirm)
|
|
759
|
+
return result({ applied: false, planHash, remove: resourceName });
|
|
760
|
+
if (a.expectedPlanHash !== planHash)
|
|
761
|
+
throw new Error(
|
|
762
|
+
"Preview this exact removal first and supply expectedPlanHash."
|
|
763
|
+
);
|
|
764
|
+
const { api } = await googleSession(
|
|
765
|
+
config,
|
|
766
|
+
cid,
|
|
767
|
+
a.currency,
|
|
768
|
+
a.testAccountOnly,
|
|
769
|
+
manager
|
|
770
|
+
);
|
|
771
|
+
return result({
|
|
772
|
+
applied: true,
|
|
773
|
+
planHash,
|
|
774
|
+
result: await api("youTubeVideoUploads:remove", {
|
|
775
|
+
resourceNames: [resourceName]
|
|
776
|
+
})
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/platforms/google-ads/pmax-metrics.ts
|
|
783
|
+
var PMAX_METRIC_FIELDS = [
|
|
784
|
+
"metrics.impressions",
|
|
785
|
+
"metrics.clicks",
|
|
786
|
+
"metrics.cost_micros",
|
|
787
|
+
"metrics.conversions",
|
|
788
|
+
"metrics.conversions_value",
|
|
789
|
+
"metrics.all_conversions",
|
|
790
|
+
"metrics.all_conversions_from_interactions_rate",
|
|
791
|
+
"metrics.all_conversions_from_interactions_value_per_interaction",
|
|
792
|
+
"metrics.all_conversions_value",
|
|
793
|
+
"metrics.all_conversions_value_per_cost",
|
|
794
|
+
"metrics.average_cost",
|
|
795
|
+
"metrics.average_cpc",
|
|
796
|
+
"metrics.average_cpe",
|
|
797
|
+
"metrics.average_cpm",
|
|
798
|
+
"metrics.conversions_from_interactions_rate",
|
|
799
|
+
"metrics.conversions_from_interactions_value_per_interaction",
|
|
800
|
+
"metrics.conversions_value_per_cost",
|
|
801
|
+
"metrics.cost_per_all_conversions",
|
|
802
|
+
"metrics.cost_per_conversion",
|
|
803
|
+
"metrics.cross_device_conversions",
|
|
804
|
+
"metrics.cross_device_conversions_value",
|
|
805
|
+
"metrics.ctr",
|
|
806
|
+
"metrics.engagement_rate",
|
|
807
|
+
"metrics.engagements",
|
|
808
|
+
"metrics.interaction_rate",
|
|
809
|
+
"metrics.interactions",
|
|
810
|
+
"metrics.trueview_average_cpv",
|
|
811
|
+
"metrics.value_per_all_conversions",
|
|
812
|
+
"metrics.value_per_conversion",
|
|
813
|
+
"metrics.video_trueview_view_rate",
|
|
814
|
+
"metrics.video_trueview_views",
|
|
815
|
+
"metrics.view_through_conversions"
|
|
816
|
+
];
|
|
817
|
+
|
|
4
818
|
// src/platforms/google-ads/tools.ts
|
|
5
|
-
import { z as
|
|
819
|
+
import { z as z6 } from "zod";
|
|
6
820
|
|
|
7
821
|
// src/platforms/google-ads/types.ts
|
|
8
822
|
var GoogleAdsApiException = class extends Error {
|
|
@@ -3640,9 +4454,9 @@ var BIDDING_TARGET_DIMENSIONS = [
|
|
|
3640
4454
|
{
|
|
3641
4455
|
key: "campaignStartDate",
|
|
3642
4456
|
name: "Campaign Start Date",
|
|
3643
|
-
description: "The start date of the campaign in YYYY-MM-DD
|
|
4457
|
+
description: "The start date and time of the campaign in the account time zone (YYYY-MM-DD HH:mm:ss).",
|
|
3644
4458
|
category: "entity",
|
|
3645
|
-
apiField: "campaign.
|
|
4459
|
+
apiField: "campaign.start_date_time",
|
|
3646
4460
|
isSegment: false,
|
|
3647
4461
|
isResourceAttribute: true,
|
|
3648
4462
|
compatibleResources: ["campaign"]
|
|
@@ -3650,9 +4464,9 @@ var BIDDING_TARGET_DIMENSIONS = [
|
|
|
3650
4464
|
{
|
|
3651
4465
|
key: "campaignEndDate",
|
|
3652
4466
|
name: "Campaign End Date",
|
|
3653
|
-
description: "The end date of the campaign in YYYY-MM-DD
|
|
4467
|
+
description: "The end date and time of the campaign in the account time zone (YYYY-MM-DD HH:mm:ss), if set.",
|
|
3654
4468
|
category: "entity",
|
|
3655
|
-
apiField: "campaign.
|
|
4469
|
+
apiField: "campaign.end_date_time",
|
|
3656
4470
|
isSegment: false,
|
|
3657
4471
|
isResourceAttribute: true,
|
|
3658
4472
|
compatibleResources: ["campaign"]
|
|
@@ -4085,7 +4899,7 @@ function buildDateWhereClause(startDate, endDate, datePreset) {
|
|
|
4085
4899
|
}
|
|
4086
4900
|
return { clause: null, warnings: [] };
|
|
4087
4901
|
}
|
|
4088
|
-
function planQuery(
|
|
4902
|
+
function planQuery(request2) {
|
|
4089
4903
|
const {
|
|
4090
4904
|
resource,
|
|
4091
4905
|
metrics,
|
|
@@ -4097,7 +4911,7 @@ function planQuery(request) {
|
|
|
4097
4911
|
orderBy,
|
|
4098
4912
|
orderDirection,
|
|
4099
4913
|
limit
|
|
4100
|
-
} =
|
|
4914
|
+
} = request2;
|
|
4101
4915
|
const errors = [];
|
|
4102
4916
|
const warnings = [];
|
|
4103
4917
|
const calculatedMetricKeys = metrics.filter((k) => isCalculatedMetric(k));
|
|
@@ -4474,13 +5288,6 @@ var PlatformApiError = class extends Error {
|
|
|
4474
5288
|
this.retryAfter = retryAfter;
|
|
4475
5289
|
this.name = "PlatformApiError";
|
|
4476
5290
|
}
|
|
4477
|
-
platform;
|
|
4478
|
-
code;
|
|
4479
|
-
isRateLimit;
|
|
4480
|
-
isAuth;
|
|
4481
|
-
isPermission;
|
|
4482
|
-
suggestion;
|
|
4483
|
-
retryAfter;
|
|
4484
5291
|
toMcpError() {
|
|
4485
5292
|
return {
|
|
4486
5293
|
error: this.message,
|
|
@@ -4615,7 +5422,7 @@ var KeywordPlannerRateLimiter = class {
|
|
|
4615
5422
|
};
|
|
4616
5423
|
|
|
4617
5424
|
// src/platforms/google-ads/read-rpc.ts
|
|
4618
|
-
import { z } from "zod";
|
|
5425
|
+
import { z as z3 } from "zod";
|
|
4619
5426
|
var READ_ONLY_OPERATION_NAMES = [
|
|
4620
5427
|
"generateAudienceCompositionInsights",
|
|
4621
5428
|
"generateAudienceDefinition",
|
|
@@ -4731,13 +5538,13 @@ function validateGoogleAdsReadOnlyRequest(value, path = "request", depth = 0, st
|
|
|
4731
5538
|
}
|
|
4732
5539
|
throw new Error(`${path} must contain JSON-compatible values.`);
|
|
4733
5540
|
}
|
|
4734
|
-
function buildGoogleAdsReadOnlyPath(
|
|
4735
|
-
const config = GOOGLE_ADS_READ_ONLY_OPERATIONS[
|
|
4736
|
-
if (!config) throw new Error(`Unsupported read-only operation: ${
|
|
5541
|
+
function buildGoogleAdsReadOnlyPath(operation2, customerId) {
|
|
5542
|
+
const config = GOOGLE_ADS_READ_ONLY_OPERATIONS[operation2];
|
|
5543
|
+
if (!config) throw new Error(`Unsupported read-only operation: ${operation2}.`);
|
|
4737
5544
|
if (config.scope === "customer") {
|
|
4738
5545
|
const cleanCustomerId = customerId?.replace(/-/g, "");
|
|
4739
5546
|
if (!cleanCustomerId || !/^\d+$/.test(cleanCustomerId)) {
|
|
4740
|
-
throw new Error(`${
|
|
5547
|
+
throw new Error(`${operation2} requires a numeric customerId.`);
|
|
4741
5548
|
}
|
|
4742
5549
|
return { path: `customers/${cleanCustomerId}${config.path}`, config };
|
|
4743
5550
|
}
|
|
@@ -4748,20 +5555,20 @@ function registerGoogleAdsReadOnlyRpcTool(server, client, ok3) {
|
|
|
4748
5555
|
"google_ads_run_readonly_rpc",
|
|
4749
5556
|
"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.",
|
|
4750
5557
|
{
|
|
4751
|
-
operation:
|
|
4752
|
-
customerId:
|
|
4753
|
-
request:
|
|
5558
|
+
operation: z3.enum(READ_ONLY_OPERATION_NAMES),
|
|
5559
|
+
customerId: z3.string().regex(/^\d[\d-]*\d$|^\d$/).optional().describe("Required for customer-scoped operations; omit for global planning catalogs"),
|
|
5560
|
+
request: z3.record(z3.unknown()).optional().default({}).describe("Official REST JSON request body for POST operations, or query parameters for GET operations")
|
|
4754
5561
|
},
|
|
4755
|
-
async ({ operation, customerId, request }) => {
|
|
5562
|
+
async ({ operation: operation2, customerId, request: request2 }) => {
|
|
4756
5563
|
try {
|
|
4757
|
-
validateGoogleAdsReadOnlyRequest(
|
|
4758
|
-
const encodedLength = JSON.stringify(
|
|
5564
|
+
validateGoogleAdsReadOnlyRequest(request2);
|
|
5565
|
+
const encodedLength = JSON.stringify(request2).length;
|
|
4759
5566
|
if (encodedLength > 1e5) throw new Error("Encoded request exceeds 100,000 characters.");
|
|
4760
|
-
const { path, config } = buildGoogleAdsReadOnlyPath(
|
|
5567
|
+
const { path, config } = buildGoogleAdsReadOnlyPath(operation2, customerId);
|
|
4761
5568
|
let response;
|
|
4762
5569
|
if (config.method === "GET") {
|
|
4763
5570
|
const query = {};
|
|
4764
|
-
for (const [key, value] of Object.entries(
|
|
5571
|
+
for (const [key, value] of Object.entries(request2)) {
|
|
4765
5572
|
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
4766
5573
|
throw new Error(`GET query parameter ${key} must be a string, number, or boolean.`);
|
|
4767
5574
|
}
|
|
@@ -4769,17 +5576,17 @@ function registerGoogleAdsReadOnlyRpcTool(server, client, ok3) {
|
|
|
4769
5576
|
}
|
|
4770
5577
|
response = await client.runReadOnlyService(path, "GET", void 0, query);
|
|
4771
5578
|
} else {
|
|
4772
|
-
response = await client.runReadOnlyService(path, "POST",
|
|
5579
|
+
response = await client.runReadOnlyService(path, "POST", request2);
|
|
4773
5580
|
}
|
|
4774
5581
|
return ok3({
|
|
4775
5582
|
dataKind: "google_ads_readonly_rpc",
|
|
4776
|
-
operation,
|
|
5583
|
+
operation: operation2,
|
|
4777
5584
|
purpose: config.purpose,
|
|
4778
5585
|
scope: config.scope,
|
|
4779
5586
|
method: config.method,
|
|
4780
5587
|
response,
|
|
4781
5588
|
readOnly: true,
|
|
4782
|
-
warnings:
|
|
5589
|
+
warnings: operation2 === "generateShareablePreviews" ? ["The returned preview URL is shareable and can expose ad creative until it expires; disclose it only to intended recipients."] : [],
|
|
4783
5590
|
limitations: [
|
|
4784
5591
|
"The request body follows Google's native REST schema and is intentionally not translated into this server's metric aliases.",
|
|
4785
5592
|
"Availability depends on developer-token access level, account eligibility, OAuth permissions, and operation-specific quotas."
|
|
@@ -4794,8 +5601,20 @@ function registerGoogleAdsReadOnlyRpcTool(server, client, ok3) {
|
|
|
4794
5601
|
);
|
|
4795
5602
|
}
|
|
4796
5603
|
|
|
5604
|
+
// src/core/redirects.ts
|
|
5605
|
+
function refuseRedirect(response, api) {
|
|
5606
|
+
if (response.status < 300 || response.status >= 400) return;
|
|
5607
|
+
throw new Error(`${api} returned an unexpected HTTP ${response.status} redirect. Redirects are never followed. Check the configured endpoint and authorization.`);
|
|
5608
|
+
}
|
|
5609
|
+
|
|
4797
5610
|
// src/platforms/google-ads/client.ts
|
|
4798
5611
|
var DEFAULT_GOOGLE_ADS_REQUEST_TIMEOUT_MS = 3e4;
|
|
5612
|
+
function withManagerHint(message, url, announcedManager) {
|
|
5613
|
+
if (announcedManager) return message;
|
|
5614
|
+
if (!/permission|PERMISSION_DENIED/i.test(message)) return message;
|
|
5615
|
+
const customer = url.match(/customers\/(\d+)/)?.[1];
|
|
5616
|
+
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}` : "") + `.`;
|
|
5617
|
+
}
|
|
4799
5618
|
function googleAdsRequestTimeoutMs() {
|
|
4800
5619
|
const parsed = Number(process.env["GOOGLE_ADS_REQUEST_TIMEOUT_MS"]);
|
|
4801
5620
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_GOOGLE_ADS_REQUEST_TIMEOUT_MS;
|
|
@@ -4808,6 +5627,14 @@ var GoogleAdsClient = class {
|
|
|
4808
5627
|
loginCustomerId;
|
|
4809
5628
|
accessToken = "";
|
|
4810
5629
|
tokenExpiresAt = 0;
|
|
5630
|
+
/**
|
|
5631
|
+
* Le gestionnaire résolu pour un compte, ou `undefined` s'il n'en a pas.
|
|
5632
|
+
*
|
|
5633
|
+
* La distinction compte : `undefined` mémorisé veut dire « on a cherché et il
|
|
5634
|
+
* n'y en a pas », ce qui évite de refaire la découverte à chaque appel sur un
|
|
5635
|
+
* compte accessible en direct.
|
|
5636
|
+
*/
|
|
5637
|
+
managerCache = /* @__PURE__ */ new Map();
|
|
4811
5638
|
rateLimiter = new RateLimiter();
|
|
4812
5639
|
keywordPlannerRateLimiter = new KeywordPlannerRateLimiter();
|
|
4813
5640
|
constructor(config) {
|
|
@@ -4864,18 +5691,66 @@ var GoogleAdsClient = class {
|
|
|
4864
5691
|
// ============================================
|
|
4865
5692
|
// PRIVATE METHODS
|
|
4866
5693
|
// ============================================
|
|
4867
|
-
getHeaders() {
|
|
5694
|
+
getHeaders(loginCustomerId) {
|
|
4868
5695
|
const headers = {
|
|
4869
5696
|
Authorization: `Bearer ${this.accessToken}`,
|
|
4870
5697
|
"developer-token": this.developerToken,
|
|
4871
5698
|
"Content-Type": "application/json"
|
|
4872
5699
|
};
|
|
4873
|
-
|
|
4874
|
-
|
|
5700
|
+
const manager = loginCustomerId ?? this.loginCustomerId;
|
|
5701
|
+
if (manager) {
|
|
5702
|
+
headers["login-customer-id"] = stripCustomerId(manager);
|
|
4875
5703
|
}
|
|
4876
5704
|
return headers;
|
|
4877
5705
|
}
|
|
4878
|
-
|
|
5706
|
+
/**
|
|
5707
|
+
* The manager account to announce when querying a given customer.
|
|
5708
|
+
*
|
|
5709
|
+
* Google rejects any request against an account managed by an MCC unless the
|
|
5710
|
+
* call names that MCC in `login-customer-id`. The rejection is a bare
|
|
5711
|
+
* `USER_PERMISSION_DENIED` that names neither the account nor the manager, so
|
|
5712
|
+
* it reads as missing access rather than as a missing header, and that is
|
|
5713
|
+
* where people lose an afternoon.
|
|
5714
|
+
*
|
|
5715
|
+
* `GOOGLE_ADS_LOGIN_CUSTOMER_ID` solves it when you know the answer in
|
|
5716
|
+
* advance. This resolves it when you do not: if the customer is not directly
|
|
5717
|
+
* accessible, the accessible managers are asked which accounts they hold, and
|
|
5718
|
+
* the one holding this customer is used.
|
|
5719
|
+
*
|
|
5720
|
+
* Resolved once and cached for the life of the client. The hierarchy of an
|
|
5721
|
+
* advertising account does not change between two tool calls, and paying two
|
|
5722
|
+
* extra requests on every query to discover that it has not would be worse
|
|
5723
|
+
* than the problem.
|
|
5724
|
+
*/
|
|
5725
|
+
async resolveLoginCustomerId(customerId) {
|
|
5726
|
+
if (this.loginCustomerId) return this.loginCustomerId;
|
|
5727
|
+
const wanted = stripCustomerId(customerId);
|
|
5728
|
+
if (this.managerCache.has(wanted)) return this.managerCache.get(wanted);
|
|
5729
|
+
try {
|
|
5730
|
+
const accessible = (await this.listAccessibleCustomers()).map((name) => stripCustomerId(name.replace(/^customers\//, "")));
|
|
5731
|
+
if (accessible.includes(wanted)) {
|
|
5732
|
+
this.managerCache.set(wanted, void 0);
|
|
5733
|
+
return void 0;
|
|
5734
|
+
}
|
|
5735
|
+
for (const candidate of accessible) {
|
|
5736
|
+
const children = await this.getClientAccounts(candidate);
|
|
5737
|
+
for (const child of children) {
|
|
5738
|
+
if (stripCustomerId(child.id) === wanted) {
|
|
5739
|
+
this.managerCache.set(wanted, candidate);
|
|
5740
|
+
return candidate;
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
} catch (error) {
|
|
5745
|
+
logger.debug(
|
|
5746
|
+
"google-ads",
|
|
5747
|
+
`Could not resolve a manager for ${wanted}: ${error instanceof Error ? error.message : error}`
|
|
5748
|
+
);
|
|
5749
|
+
}
|
|
5750
|
+
this.managerCache.set(wanted, void 0);
|
|
5751
|
+
return void 0;
|
|
5752
|
+
}
|
|
5753
|
+
async request(url, options = {}, beforeAttempt, deadlineAtMs, loginCustomerId) {
|
|
4879
5754
|
await this.ensureValidToken();
|
|
4880
5755
|
return this.rateLimiter.execute(async () => {
|
|
4881
5756
|
await beforeAttempt?.();
|
|
@@ -4900,10 +5775,11 @@ var GoogleAdsClient = class {
|
|
|
4900
5775
|
// whatever host the redirect names.
|
|
4901
5776
|
redirect: "error",
|
|
4902
5777
|
headers: {
|
|
4903
|
-
...this.getHeaders(),
|
|
5778
|
+
...this.getHeaders(loginCustomerId),
|
|
4904
5779
|
...options.headers
|
|
4905
5780
|
}
|
|
4906
5781
|
});
|
|
5782
|
+
refuseRedirect(response, "Google Ads API");
|
|
4907
5783
|
if (!response.ok) {
|
|
4908
5784
|
const errorBody = await response.json().catch(() => ({}));
|
|
4909
5785
|
const errorDetails = errorBody?.error;
|
|
@@ -4912,7 +5788,7 @@ var GoogleAdsClient = class {
|
|
|
4912
5788
|
const requestId = errorDetails?.details?.[0]?.requestId ?? errorBody?.[0]?.error?.details?.[0]?.requestId;
|
|
4913
5789
|
const detailedMessage = gaErrors?.[0]?.message || errorDetails?.message || `Request failed: ${response.statusText}`;
|
|
4914
5790
|
throw new GoogleAdsApiException(
|
|
4915
|
-
detailedMessage,
|
|
5791
|
+
withManagerHint(detailedMessage, url, loginCustomerId),
|
|
4916
5792
|
response.status,
|
|
4917
5793
|
errorDetails?.status,
|
|
4918
5794
|
requestId,
|
|
@@ -4973,12 +5849,16 @@ var GoogleAdsClient = class {
|
|
|
4973
5849
|
async searchStream(customerId, gaqlQuery) {
|
|
4974
5850
|
const cleanCustomerId = stripCustomerId(customerId);
|
|
4975
5851
|
const url = `${GOOGLE_ADS_API_BASE_URL}/customers/${cleanCustomerId}/googleAds:searchStream`;
|
|
5852
|
+
const manager = await this.resolveLoginCustomerId(cleanCustomerId);
|
|
4976
5853
|
const response = await this.request(
|
|
4977
5854
|
url,
|
|
4978
5855
|
{
|
|
4979
5856
|
method: "POST",
|
|
4980
5857
|
body: JSON.stringify({ query: gaqlQuery })
|
|
4981
|
-
}
|
|
5858
|
+
},
|
|
5859
|
+
void 0,
|
|
5860
|
+
void 0,
|
|
5861
|
+
manager
|
|
4982
5862
|
);
|
|
4983
5863
|
const allRows = [];
|
|
4984
5864
|
for (const batch of response) {
|
|
@@ -4995,12 +5875,19 @@ var GoogleAdsClient = class {
|
|
|
4995
5875
|
async search(customerId, gaqlQuery, pageToken) {
|
|
4996
5876
|
const cleanCustomerId = stripCustomerId(customerId);
|
|
4997
5877
|
const url = `${GOOGLE_ADS_API_BASE_URL}/customers/${cleanCustomerId}/googleAds:search`;
|
|
5878
|
+
const manager = await this.resolveLoginCustomerId(cleanCustomerId);
|
|
4998
5879
|
const body = { query: gaqlQuery };
|
|
4999
5880
|
if (pageToken) body.pageToken = pageToken;
|
|
5000
|
-
return this.request(
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5881
|
+
return this.request(
|
|
5882
|
+
url,
|
|
5883
|
+
{
|
|
5884
|
+
method: "POST",
|
|
5885
|
+
body: JSON.stringify(body)
|
|
5886
|
+
},
|
|
5887
|
+
void 0,
|
|
5888
|
+
void 0,
|
|
5889
|
+
manager
|
|
5890
|
+
);
|
|
5004
5891
|
}
|
|
5005
5892
|
// ============================================
|
|
5006
5893
|
// KEYWORD PLANNER (PLANLESS, READ-ONLY RPCS)
|
|
@@ -5153,8 +6040,8 @@ var GoogleAdsClient = class {
|
|
|
5153
6040
|
return {
|
|
5154
6041
|
id: String(row.customer?.id || cleanCustomerId),
|
|
5155
6042
|
descriptiveName: row.customer?.descriptiveName || "Unknown Account",
|
|
5156
|
-
currencyCode: row.customer?.currencyCode || "
|
|
5157
|
-
timeZone: row.customer?.timeZone || "
|
|
6043
|
+
currencyCode: row.customer?.currencyCode || "",
|
|
6044
|
+
timeZone: row.customer?.timeZone || "",
|
|
5158
6045
|
manager: row.customer?.manager || false,
|
|
5159
6046
|
testAccount: row.customer?.testAccount || false,
|
|
5160
6047
|
resourceName: row.customer?.resourceName || `customers/${cleanCustomerId}`
|
|
@@ -5222,9 +6109,9 @@ var GoogleAdsClient = class {
|
|
|
5222
6109
|
/**
|
|
5223
6110
|
* Execute a query request with automatic query planning
|
|
5224
6111
|
*/
|
|
5225
|
-
async executeQuery(
|
|
6112
|
+
async executeQuery(request2) {
|
|
5226
6113
|
const startTime = Date.now();
|
|
5227
|
-
const plan = planQuery(
|
|
6114
|
+
const plan = planQuery(request2);
|
|
5228
6115
|
const errors = [...plan.errors];
|
|
5229
6116
|
const warnings = [...plan.warnings];
|
|
5230
6117
|
const gaqlQueries = [];
|
|
@@ -5252,7 +6139,7 @@ var GoogleAdsClient = class {
|
|
|
5252
6139
|
gaqlQueries.push(query.gaql);
|
|
5253
6140
|
rawRequests.push({ gaql: query.gaql, resource: query.resource });
|
|
5254
6141
|
try {
|
|
5255
|
-
const apiRows = (await this.search(
|
|
6142
|
+
const apiRows = (await this.search(request2.customerId, query.gaql)).results ?? [];
|
|
5256
6143
|
rawResponses.push(apiRows.slice(0, 5));
|
|
5257
6144
|
const flatRows = apiRows.map((row) => flattenGoogleAdsRow(row));
|
|
5258
6145
|
queryResults.push(flatRows);
|
|
@@ -5463,7 +6350,7 @@ function camelToSnakeResource(key) {
|
|
|
5463
6350
|
}
|
|
5464
6351
|
|
|
5465
6352
|
// src/platforms/google-ads/keyword-planner.ts
|
|
5466
|
-
import { z as
|
|
6353
|
+
import { z as z4 } from "zod";
|
|
5467
6354
|
var FORECAST_WITH_BREAKDOWN_TIME_BUDGET_MS = 45e3;
|
|
5468
6355
|
var MONTHS = [
|
|
5469
6356
|
"JANUARY",
|
|
@@ -5482,17 +6369,17 @@ var MONTHS = [
|
|
|
5482
6369
|
var MONTH_NUMBER = new Map(
|
|
5483
6370
|
MONTHS.map((month, index) => [month, index + 1])
|
|
5484
6371
|
);
|
|
5485
|
-
var customerIdSchema =
|
|
5486
|
-
var numericIdSchema =
|
|
5487
|
-
var keywordTextSchema =
|
|
6372
|
+
var customerIdSchema = z4.string().regex(/^\d[\d-]*\d$|^\d$/, "Expected a numeric Google Ads customer ID, with or without dashes");
|
|
6373
|
+
var numericIdSchema = z4.string().regex(/^\d+$/, "Expected a numeric Google Ads criterion ID");
|
|
6374
|
+
var keywordTextSchema = z4.string().trim().min(1).max(80).refine(
|
|
5488
6375
|
(value) => value.split(/\s+/).length <= 10,
|
|
5489
6376
|
"Google Ads keywords can contain at most 10 words"
|
|
5490
6377
|
);
|
|
5491
|
-
var yearMonthSchema =
|
|
5492
|
-
var isoDateSchema =
|
|
5493
|
-
var networkSchema =
|
|
5494
|
-
var matchTypeSchema =
|
|
5495
|
-
var currencyAmountSchema =
|
|
6378
|
+
var yearMonthSchema = z4.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Expected YYYY-MM");
|
|
6379
|
+
var isoDateSchema = z4.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD");
|
|
6380
|
+
var networkSchema = z4.enum(["GOOGLE_SEARCH", "GOOGLE_SEARCH_AND_PARTNERS"]);
|
|
6381
|
+
var matchTypeSchema = z4.enum(["EXACT", "PHRASE", "BROAD"]);
|
|
6382
|
+
var currencyAmountSchema = z4.number().finite().min(1e-6).max(1e9);
|
|
5496
6383
|
function compactObject(value) {
|
|
5497
6384
|
return Object.fromEntries(
|
|
5498
6385
|
Object.entries(value).filter(([, fieldValue]) => fieldValue !== void 0)
|
|
@@ -5764,22 +6651,22 @@ function normalizeKeywordHistoricalMetrics(metrics, includeMonthlySearchVolumes
|
|
|
5764
6651
|
trends: computeKeywordPlannerTrends(metrics?.monthlySearchVolumes)
|
|
5765
6652
|
};
|
|
5766
6653
|
}
|
|
5767
|
-
function normalizeHistoricalResult(
|
|
6654
|
+
function normalizeHistoricalResult(result2, includeMonthlySearchVolumes) {
|
|
5768
6655
|
return {
|
|
5769
|
-
keyword:
|
|
5770
|
-
closeVariants:
|
|
6656
|
+
keyword: result2.text ?? null,
|
|
6657
|
+
closeVariants: result2.closeVariants ?? [],
|
|
5771
6658
|
metrics: normalizeKeywordHistoricalMetrics(
|
|
5772
|
-
|
|
6659
|
+
result2.keywordMetrics,
|
|
5773
6660
|
includeMonthlySearchVolumes
|
|
5774
6661
|
)
|
|
5775
6662
|
};
|
|
5776
6663
|
}
|
|
5777
|
-
function normalizeIdeaResult(
|
|
6664
|
+
function normalizeIdeaResult(result2) {
|
|
5778
6665
|
return {
|
|
5779
|
-
keyword:
|
|
5780
|
-
closeVariants:
|
|
5781
|
-
metrics: normalizeKeywordHistoricalMetrics(
|
|
5782
|
-
concepts:
|
|
6666
|
+
keyword: result2.text ?? null,
|
|
6667
|
+
closeVariants: result2.closeVariants ?? [],
|
|
6668
|
+
metrics: normalizeKeywordHistoricalMetrics(result2.keywordIdeaMetrics),
|
|
6669
|
+
concepts: result2.keywordAnnotations?.concepts ?? []
|
|
5783
6670
|
};
|
|
5784
6671
|
}
|
|
5785
6672
|
function normalizeAggregateMetrics(results) {
|
|
@@ -5956,15 +6843,15 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
5956
6843
|
"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.",
|
|
5957
6844
|
{
|
|
5958
6845
|
customerId: customerIdSchema.describe("Google Ads serving customer ID; use the client account, not its MCC manager"),
|
|
5959
|
-
keywords:
|
|
5960
|
-
geoTargetIds:
|
|
6846
|
+
keywords: z4.array(keywordTextSchema).min(1).max(1e4).describe("Keywords to analyze; Google may combine near-exact close variants"),
|
|
6847
|
+
geoTargetIds: z4.array(numericIdSchema).max(10).optional().default([]).describe("Geo target criterion IDs, e.g. 2250 for France; empty means all geographies"),
|
|
5961
6848
|
languageId: numericIdSchema.optional().describe("Optional language criterion ID, e.g. 1002 for French; omit for all languages"),
|
|
5962
6849
|
network: networkSchema.optional().default("GOOGLE_SEARCH"),
|
|
5963
|
-
includeAdultKeywords:
|
|
5964
|
-
includeAverageCpc:
|
|
5965
|
-
includeDeviceBreakdown:
|
|
5966
|
-
includeMonthlySearchVolumes:
|
|
5967
|
-
historyMonths:
|
|
6850
|
+
includeAdultKeywords: z4.boolean().optional().default(false),
|
|
6851
|
+
includeAverageCpc: z4.boolean().optional().default(true).describe("Request legacy average CPC in addition to top-of-page bid ranges"),
|
|
6852
|
+
includeDeviceBreakdown: z4.boolean().optional().default(false).describe("Return aggregate searches by device across all requested keywords"),
|
|
6853
|
+
includeMonthlySearchVolumes: z4.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"),
|
|
6854
|
+
historyMonths: z4.number().int().min(3).max(48).optional().default(24).describe("History length when no explicit YYYY-MM range is supplied; 24 enables YoY calculation"),
|
|
5968
6855
|
startYearMonth: yearMonthSchema.optional().describe("Optional inclusive historical range start YYYY-MM"),
|
|
5969
6856
|
endYearMonth: yearMonthSchema.optional().describe("Optional inclusive historical range end YYYY-MM")
|
|
5970
6857
|
},
|
|
@@ -5984,7 +6871,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
5984
6871
|
}) => {
|
|
5985
6872
|
try {
|
|
5986
6873
|
const startedAt = Date.now();
|
|
5987
|
-
const { request, historyRange } = buildKeywordHistoricalMetricsRequest({
|
|
6874
|
+
const { request: request2, historyRange } = buildKeywordHistoricalMetricsRequest({
|
|
5988
6875
|
keywords,
|
|
5989
6876
|
geoTargetIds,
|
|
5990
6877
|
languageId,
|
|
@@ -6000,9 +6887,9 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6000
6887
|
keywords.length,
|
|
6001
6888
|
historyRange.monthCount
|
|
6002
6889
|
);
|
|
6003
|
-
const response = await client.generateKeywordHistoricalMetrics(customerId,
|
|
6004
|
-
const results = (response.results ?? []).map((
|
|
6005
|
-
|
|
6890
|
+
const response = await client.generateKeywordHistoricalMetrics(customerId, request2);
|
|
6891
|
+
const results = (response.results ?? []).map((result2) => normalizeHistoricalResult(
|
|
6892
|
+
result2,
|
|
6006
6893
|
includeMonthlySearchVolumes
|
|
6007
6894
|
));
|
|
6008
6895
|
const warnings = [];
|
|
@@ -6056,21 +6943,21 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6056
6943
|
"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.",
|
|
6057
6944
|
{
|
|
6058
6945
|
customerId: customerIdSchema.describe("Google Ads serving customer ID; use the client account, not its MCC manager"),
|
|
6059
|
-
seedKeywords:
|
|
6060
|
-
url:
|
|
6061
|
-
site:
|
|
6062
|
-
geoTargetIds:
|
|
6946
|
+
seedKeywords: z4.array(keywordTextSchema).min(1).max(20).optional(),
|
|
6947
|
+
url: z4.string().trim().min(1).optional().describe("Specific page URL to crawl; combine with seedKeywords if desired"),
|
|
6948
|
+
site: z4.string().trim().min(1).optional().describe("Whole-domain seed; exclusive with seedKeywords/url"),
|
|
6949
|
+
geoTargetIds: z4.array(numericIdSchema).max(10).optional().default([]),
|
|
6063
6950
|
languageId: numericIdSchema.optional(),
|
|
6064
6951
|
network: networkSchema.optional().default("GOOGLE_SEARCH"),
|
|
6065
|
-
includeAdultKeywords:
|
|
6066
|
-
includeAverageCpc:
|
|
6067
|
-
includeDeviceBreakdown:
|
|
6068
|
-
includeKeywordConcepts:
|
|
6069
|
-
historyMonths:
|
|
6952
|
+
includeAdultKeywords: z4.boolean().optional().default(false),
|
|
6953
|
+
includeAverageCpc: z4.boolean().optional().default(true),
|
|
6954
|
+
includeDeviceBreakdown: z4.boolean().optional().default(false),
|
|
6955
|
+
includeKeywordConcepts: z4.boolean().optional().default(true),
|
|
6956
|
+
historyMonths: z4.number().int().min(3).max(48).optional().default(13).describe("13 months is enough to compute latest-month YoY while limiting response size"),
|
|
6070
6957
|
startYearMonth: yearMonthSchema.optional(),
|
|
6071
6958
|
endYearMonth: yearMonthSchema.optional(),
|
|
6072
|
-
pageSize:
|
|
6073
|
-
pageToken:
|
|
6959
|
+
pageSize: z4.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"),
|
|
6960
|
+
pageToken: z4.string().optional().describe("nextPageToken from a prior identical request")
|
|
6074
6961
|
},
|
|
6075
6962
|
async ({
|
|
6076
6963
|
customerId,
|
|
@@ -6092,7 +6979,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6092
6979
|
}) => {
|
|
6093
6980
|
try {
|
|
6094
6981
|
const startedAt = Date.now();
|
|
6095
|
-
const { request, historyRange, seedType } = buildKeywordIdeasRequest({
|
|
6982
|
+
const { request: request2, historyRange, seedType } = buildKeywordIdeasRequest({
|
|
6096
6983
|
seedKeywords,
|
|
6097
6984
|
url,
|
|
6098
6985
|
site,
|
|
@@ -6113,7 +7000,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6113
7000
|
pageSize,
|
|
6114
7001
|
historyRange.monthCount
|
|
6115
7002
|
);
|
|
6116
|
-
const response = await client.generateKeywordIdeas(customerId,
|
|
7003
|
+
const response = await client.generateKeywordIdeas(customerId, request2);
|
|
6117
7004
|
const ideas = (response.results ?? []).map(normalizeIdeaResult);
|
|
6118
7005
|
const nextPageToken = response.nextPageToken ?? null;
|
|
6119
7006
|
return ok3({
|
|
@@ -6162,23 +7049,23 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6162
7049
|
"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.",
|
|
6163
7050
|
{
|
|
6164
7051
|
customerId: customerIdSchema.describe("Google Ads serving customer ID; use a relevant client account for better estimates"),
|
|
6165
|
-
keywords:
|
|
7052
|
+
keywords: z4.array(keywordTextSchema).min(1).max(1e3),
|
|
6166
7053
|
matchType: matchTypeSchema.optional().default("BROAD"),
|
|
6167
|
-
negativeKeywords:
|
|
7054
|
+
negativeKeywords: z4.array(keywordTextSchema).max(1e3).optional().default([]),
|
|
6168
7055
|
negativeMatchType: matchTypeSchema.optional().default("BROAD"),
|
|
6169
|
-
geoTargetIds:
|
|
6170
|
-
languageIds:
|
|
7056
|
+
geoTargetIds: z4.array(numericIdSchema).max(20).optional().default([]),
|
|
7057
|
+
languageIds: z4.array(numericIdSchema).max(10).optional().default([]),
|
|
6171
7058
|
network: networkSchema.optional().default("GOOGLE_SEARCH"),
|
|
6172
|
-
biddingStrategy:
|
|
7059
|
+
biddingStrategy: z4.enum(["MANUAL_CPC", "MAXIMIZE_CLICKS", "MAXIMIZE_CONVERSIONS"]).optional().default("MANUAL_CPC"),
|
|
6173
7060
|
maxCpcBid: currencyAmountSchema.optional().describe("Bid in standard account-currency units; required for MANUAL_CPC"),
|
|
6174
7061
|
dailyBudget: currencyAmountSchema.optional().describe("Daily amount in standard currency units; required for maximize strategies and optional for MANUAL_CPC"),
|
|
6175
7062
|
maxCpcBidCeiling: currencyAmountSchema.optional().describe("Optional standard-currency CPC ceiling for MAXIMIZE_CLICKS"),
|
|
6176
|
-
conversionRate:
|
|
6177
|
-
currencyCode:
|
|
7063
|
+
conversionRate: z4.number().finite().min(0).max(1).optional().describe("Expected conversion rate as a decimal, e.g. 0.02 for 2%"),
|
|
7064
|
+
currencyCode: z4.string().regex(/^[A-Z]{3}$/).optional().describe("Optional ISO 4217 conversion currency; account currency is used by default"),
|
|
6178
7065
|
startDate: isoDateSchema.optional().describe("Optional inclusive future forecast start YYYY-MM-DD; supply with endDate"),
|
|
6179
7066
|
endDate: isoDateSchema.optional().describe("Optional inclusive forecast end YYYY-MM-DD, no more than one year ahead; supply with startDate"),
|
|
6180
|
-
includeKeywordBreakdown:
|
|
6181
|
-
keywordBreakdownLimit:
|
|
7067
|
+
includeKeywordBreakdown: z4.boolean().optional().default(false).describe("Make independent one-keyword forecast calls in addition to the combined campaign forecast"),
|
|
7068
|
+
keywordBreakdownLimit: z4.number().int().min(1).max(20).optional().default(10).describe("Safety cap for additional rate-limited Keyword Planner requests")
|
|
6182
7069
|
},
|
|
6183
7070
|
async ({
|
|
6184
7071
|
customerId,
|
|
@@ -6219,11 +7106,11 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6219
7106
|
startDate,
|
|
6220
7107
|
endDate
|
|
6221
7108
|
};
|
|
6222
|
-
const { request, periodDays } = buildKeywordForecastRequest(baseInput);
|
|
7109
|
+
const { request: request2, periodDays } = buildKeywordForecastRequest(baseInput);
|
|
6223
7110
|
const deadlineAtMs = includeKeywordBreakdown ? startedAt + FORECAST_WITH_BREAKDOWN_TIME_BUDGET_MS : void 0;
|
|
6224
7111
|
const response = await client.generateKeywordForecastMetrics(
|
|
6225
7112
|
customerId,
|
|
6226
|
-
|
|
7113
|
+
request2,
|
|
6227
7114
|
deadlineAtMs
|
|
6228
7115
|
);
|
|
6229
7116
|
const campaignForecast = normalizeKeywordForecastMetrics(
|
|
@@ -6313,7 +7200,7 @@ function registerGoogleAdsKeywordPlannerTools(server, client, ok3) {
|
|
|
6313
7200
|
}
|
|
6314
7201
|
|
|
6315
7202
|
// src/platforms/google-ads/discovery-tools.ts
|
|
6316
|
-
import { z as
|
|
7203
|
+
import { z as z5 } from "zod";
|
|
6317
7204
|
var FIELD_COLUMNS = [
|
|
6318
7205
|
"name",
|
|
6319
7206
|
"category",
|
|
@@ -6329,12 +7216,12 @@ var FIELD_COLUMNS = [
|
|
|
6329
7216
|
"metrics",
|
|
6330
7217
|
"segments"
|
|
6331
7218
|
].join(", ");
|
|
6332
|
-
var customerIdSchema2 =
|
|
7219
|
+
var customerIdSchema2 = z5.string().regex(
|
|
6333
7220
|
/^\d[\d-]*\d$|^\d$/,
|
|
6334
7221
|
"Expected a numeric Google Ads customer ID, with or without dashes"
|
|
6335
7222
|
);
|
|
6336
|
-
var numericIdSchema2 =
|
|
6337
|
-
var fieldCategorySchema =
|
|
7223
|
+
var numericIdSchema2 = z5.string().regex(/^\d+$/, "Expected a numeric Google Ads ID");
|
|
7224
|
+
var fieldCategorySchema = z5.enum([
|
|
6338
7225
|
"RESOURCE",
|
|
6339
7226
|
"ATTRIBUTE",
|
|
6340
7227
|
"SEGMENT",
|
|
@@ -6389,14 +7276,14 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6389
7276
|
"google_ads_search_fields",
|
|
6390
7277
|
"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.",
|
|
6391
7278
|
{
|
|
6392
|
-
query:
|
|
6393
|
-
nameContains:
|
|
7279
|
+
query: z5.string().trim().min(6).max(5e4).optional().describe("Optional raw GoogleAdsField SELECT query. When provided it replaces the structured filters."),
|
|
7280
|
+
nameContains: z5.string().trim().min(1).max(250).optional().describe("Case-sensitive field-name substring, e.g. conversion or asset_group"),
|
|
6394
7281
|
category: fieldCategorySchema.optional(),
|
|
6395
|
-
selectable:
|
|
6396
|
-
filterable:
|
|
6397
|
-
sortable:
|
|
6398
|
-
pageSize:
|
|
6399
|
-
pageToken:
|
|
7282
|
+
selectable: z5.boolean().optional(),
|
|
7283
|
+
filterable: z5.boolean().optional(),
|
|
7284
|
+
sortable: z5.boolean().optional(),
|
|
7285
|
+
pageSize: z5.number().int().min(1).max(1e4).optional().default(500),
|
|
7286
|
+
pageToken: z5.string().trim().min(1).max(1e4).optional()
|
|
6400
7287
|
},
|
|
6401
7288
|
async ({ query, nameContains, category, selectable, filterable, sortable, pageSize, pageToken }) => {
|
|
6402
7289
|
try {
|
|
@@ -6439,22 +7326,22 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6439
7326
|
"google_ads_suggest_geo_targets",
|
|
6440
7327
|
"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.",
|
|
6441
7328
|
{
|
|
6442
|
-
locationNames:
|
|
6443
|
-
geoTargetIds:
|
|
6444
|
-
locale:
|
|
6445
|
-
countryCode:
|
|
7329
|
+
locationNames: z5.array(z5.string().trim().min(1).max(250)).min(1).max(25).optional(),
|
|
7330
|
+
geoTargetIds: z5.array(numericIdSchema2).min(1).max(25).optional(),
|
|
7331
|
+
locale: z5.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"),
|
|
7332
|
+
countryCode: z5.string().trim().regex(/^[A-Za-z]{2}$/, "Expected an ISO-3166 alpha-2 country code").optional().transform((value) => value?.toUpperCase())
|
|
6446
7333
|
},
|
|
6447
7334
|
async ({ locationNames, geoTargetIds, locale, countryCode }) => {
|
|
6448
7335
|
try {
|
|
6449
7336
|
if (Boolean(locationNames?.length) === Boolean(geoTargetIds?.length)) {
|
|
6450
7337
|
throw new Error("Provide exactly one of locationNames or geoTargetIds.");
|
|
6451
7338
|
}
|
|
6452
|
-
const
|
|
7339
|
+
const request2 = {
|
|
6453
7340
|
locale,
|
|
6454
7341
|
...countryCode ? { countryCode } : {},
|
|
6455
7342
|
...locationNames ? { locationNames: { names: [...new Set(locationNames)] } } : { geoTargets: { geoTargetConstants: [...new Set(geoTargetIds)].map((id) => `geoTargetConstants/${id}`) } }
|
|
6456
7343
|
};
|
|
6457
|
-
const response = await client.suggestGeoTargetConstants(
|
|
7344
|
+
const response = await client.suggestGeoTargetConstants(request2);
|
|
6458
7345
|
const suggestions = (response.geoTargetConstantSuggestions ?? []).map((suggestion) => ({
|
|
6459
7346
|
searchTerm: suggestion.searchTerm ?? null,
|
|
6460
7347
|
locale: suggestion.locale ?? locale,
|
|
@@ -6468,7 +7355,7 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6468
7355
|
dataKind: "geo_target_suggestions",
|
|
6469
7356
|
suggestions,
|
|
6470
7357
|
count: suggestions.length,
|
|
6471
|
-
request,
|
|
7358
|
+
request: request2,
|
|
6472
7359
|
readOnly: true,
|
|
6473
7360
|
warnings: [],
|
|
6474
7361
|
limitations: ["Reach is approximate and rounded by Google."],
|
|
@@ -6484,19 +7371,19 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6484
7371
|
"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.",
|
|
6485
7372
|
{
|
|
6486
7373
|
customerId: customerIdSchema2.describe("Serving customer ID containing the existing ad groups"),
|
|
6487
|
-
keywords:
|
|
6488
|
-
adGroupIds:
|
|
7374
|
+
keywords: z5.array(z5.string().trim().min(1).max(80)).min(1).max(1e3),
|
|
7375
|
+
adGroupIds: z5.array(numericIdSchema2).min(1).max(200).describe("Existing ad group IDs in the same customer account")
|
|
6489
7376
|
},
|
|
6490
7377
|
async ({ customerId, keywords, adGroupIds }) => {
|
|
6491
7378
|
try {
|
|
6492
7379
|
const cleanCustomerId = customerId.replace(/-/g, "");
|
|
6493
|
-
const
|
|
7380
|
+
const request2 = {
|
|
6494
7381
|
keywords: [...new Set(keywords)],
|
|
6495
7382
|
adGroups: [...new Set(adGroupIds)].map(
|
|
6496
7383
|
(id) => `customers/${cleanCustomerId}/adGroups/${id}`
|
|
6497
7384
|
)
|
|
6498
7385
|
};
|
|
6499
|
-
const response = await client.generateAdGroupThemes(cleanCustomerId,
|
|
7386
|
+
const response = await client.generateAdGroupThemes(cleanCustomerId, request2);
|
|
6500
7387
|
const suggestions = response.adGroupKeywordSuggestions ?? [];
|
|
6501
7388
|
const unusableAdGroups = response.unusableAdGroups ?? [];
|
|
6502
7389
|
return ok3({
|
|
@@ -6505,8 +7392,8 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6505
7392
|
suggestionCount: suggestions.length,
|
|
6506
7393
|
unusableAdGroups,
|
|
6507
7394
|
unusableAdGroupCount: unusableAdGroups.length,
|
|
6508
|
-
requestedKeywordCount:
|
|
6509
|
-
requestedAdGroupCount:
|
|
7395
|
+
requestedKeywordCount: request2.keywords.length,
|
|
7396
|
+
requestedAdGroupCount: request2.adGroups.length,
|
|
6510
7397
|
readOnly: true,
|
|
6511
7398
|
warnings: unusableAdGroups.length ? ["Google could not use one or more supplied ad groups; inspect unusableAdGroups for their campaign context."] : [],
|
|
6512
7399
|
limitations: ["This RPC suggests organization into existing ad groups; it does not create themes, ad groups, or keywords."],
|
|
@@ -6520,11 +7407,11 @@ function registerGoogleAdsDiscoveryTools(server, client, ok3) {
|
|
|
6520
7407
|
}
|
|
6521
7408
|
|
|
6522
7409
|
// src/platforms/google-ads/tools.ts
|
|
6523
|
-
var customerIdSchema3 =
|
|
6524
|
-
var loginCustomerIdSchema =
|
|
6525
|
-
var isoDateSchema2 =
|
|
6526
|
-
var numericIdSchema3 =
|
|
6527
|
-
var gaqlEnumSchema =
|
|
7410
|
+
var customerIdSchema3 = z6.string().describe("Google Ads customer ID (without dashes, e.g., 1234567890)");
|
|
7411
|
+
var loginCustomerIdSchema = z6.string().optional().describe("MCC Manager account ID (required for sub-accounts managed by an MCC)");
|
|
7412
|
+
var isoDateSchema2 = z6.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected date format YYYY-MM-DD");
|
|
7413
|
+
var numericIdSchema3 = z6.string().regex(/^\d+$/, "Expected a numeric Google Ads ID");
|
|
7414
|
+
var gaqlEnumSchema = z6.string().regex(/^[A-Z0-9_]+$/, "Expected an uppercase Google Ads enum value");
|
|
6528
7415
|
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.";
|
|
6529
7416
|
function isAgentResponseRecord(value) {
|
|
6530
7417
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -6687,6 +7574,35 @@ function normalizeChangeEventRange(startDate, endDate) {
|
|
|
6687
7574
|
warnings
|
|
6688
7575
|
};
|
|
6689
7576
|
}
|
|
7577
|
+
function recordOf(value) {
|
|
7578
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
7579
|
+
}
|
|
7580
|
+
function assetIdFromResourceName(value) {
|
|
7581
|
+
if (typeof value !== "string") return void 0;
|
|
7582
|
+
return value.match(/\/assets\/(\d+)$/)?.[1];
|
|
7583
|
+
}
|
|
7584
|
+
function youtubeDerivedUrls(videoId) {
|
|
7585
|
+
if (typeof videoId !== "string" || !/^[A-Za-z0-9_-]{5,20}$/.test(videoId)) return void 0;
|
|
7586
|
+
return {
|
|
7587
|
+
watch_url: `https://www.youtube.com/watch?v=${videoId}`,
|
|
7588
|
+
embed_url: `https://www.youtube.com/embed/${videoId}`,
|
|
7589
|
+
thumbnail_url: `https://i.ytimg.com/vi/${videoId}/maxresdefault.jpg`,
|
|
7590
|
+
thumbnail_fallback_url: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`
|
|
7591
|
+
};
|
|
7592
|
+
}
|
|
7593
|
+
function collectAssetIds(value, into) {
|
|
7594
|
+
const id = assetIdFromResourceName(value);
|
|
7595
|
+
if (id) {
|
|
7596
|
+
into.add(id);
|
|
7597
|
+
return;
|
|
7598
|
+
}
|
|
7599
|
+
if (Array.isArray(value)) {
|
|
7600
|
+
for (const item of value) collectAssetIds(item, into);
|
|
7601
|
+
return;
|
|
7602
|
+
}
|
|
7603
|
+
const record = recordOf(value);
|
|
7604
|
+
if (record) for (const item of Object.values(record)) collectAssetIds(item, into);
|
|
7605
|
+
}
|
|
6690
7606
|
async function runGaqlWithFallback(client, customerId, attempts) {
|
|
6691
7607
|
const warnings = [];
|
|
6692
7608
|
let lastError;
|
|
@@ -6774,7 +7690,7 @@ function registerGoogleAdsTools(server, config) {
|
|
|
6774
7690
|
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`,
|
|
6775
7691
|
{
|
|
6776
7692
|
customerId: customerIdSchema3,
|
|
6777
|
-
query:
|
|
7693
|
+
query: z6.string().min(10).describe("GAQL query string (SELECT ... FROM ... WHERE ...)")
|
|
6778
7694
|
},
|
|
6779
7695
|
async ({ customerId, query }) => {
|
|
6780
7696
|
try {
|
|
@@ -6791,12 +7707,12 @@ Example: SELECT campaign.name, metrics.impressions FROM campaign WHERE campaign.
|
|
|
6791
7707
|
Use google-ads://metrics for available metrics, google-ads://dimensions for dimensions.`,
|
|
6792
7708
|
{
|
|
6793
7709
|
customerId: customerIdSchema3,
|
|
6794
|
-
resource:
|
|
6795
|
-
metrics:
|
|
6796
|
-
dimensions:
|
|
6797
|
-
startDate:
|
|
6798
|
-
endDate:
|
|
6799
|
-
datePreset:
|
|
7710
|
+
resource: z6.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.)"),
|
|
7711
|
+
metrics: z6.array(z6.string()).min(1).describe("Metric keys (e.g., impressions, clicks, cost_micros, conversions)"),
|
|
7712
|
+
dimensions: z6.array(z6.string()).optional().describe("Dimension keys (e.g., date, campaignName, device)"),
|
|
7713
|
+
startDate: z6.string().optional().describe("Start date YYYY-MM-DD"),
|
|
7714
|
+
endDate: z6.string().optional().describe("End date YYYY-MM-DD"),
|
|
7715
|
+
datePreset: z6.enum([
|
|
6800
7716
|
"TODAY",
|
|
6801
7717
|
"YESTERDAY",
|
|
6802
7718
|
"LAST_7_DAYS",
|
|
@@ -6808,9 +7724,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6808
7724
|
"THIS_QUARTER",
|
|
6809
7725
|
"LAST_QUARTER"
|
|
6810
7726
|
]).optional().describe("Predefined date range"),
|
|
6811
|
-
orderBy:
|
|
6812
|
-
orderDirection:
|
|
6813
|
-
limit:
|
|
7727
|
+
orderBy: z6.string().optional().describe("Optional GAQL field to order by, e.g. metrics.impressions or campaign.name"),
|
|
7728
|
+
orderDirection: z6.enum(["ASC", "DESC"]).optional().default("DESC"),
|
|
7729
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(500)
|
|
6814
7730
|
},
|
|
6815
7731
|
async ({ customerId, resource, metrics, dimensions, startDate, endDate, datePreset, orderBy, orderDirection, limit }) => {
|
|
6816
7732
|
try {
|
|
@@ -6837,7 +7753,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6837
7753
|
gaqlPreview: generateQueryPreview(plan)
|
|
6838
7754
|
});
|
|
6839
7755
|
}
|
|
6840
|
-
const
|
|
7756
|
+
const result2 = await client.executeQuery({
|
|
6841
7757
|
customerId,
|
|
6842
7758
|
resource,
|
|
6843
7759
|
metrics,
|
|
@@ -6850,18 +7766,18 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6850
7766
|
orderDirection,
|
|
6851
7767
|
limit
|
|
6852
7768
|
});
|
|
6853
|
-
const debugWarnings = Array.isArray(
|
|
6854
|
-
const debugErrors = Array.isArray(
|
|
6855
|
-
const zeroMetricLimitWarnings =
|
|
7769
|
+
const debugWarnings = Array.isArray(result2.debug.warnings) ? result2.debug.warnings : [];
|
|
7770
|
+
const debugErrors = Array.isArray(result2.debug.errors) ? result2.debug.errors : [];
|
|
7771
|
+
const zeroMetricLimitWarnings = result2.data.length === limit && allReturnedMetricValuesAreZero(result2.data, metrics) ? [ZERO_METRIC_LIMIT_WARNING] : [];
|
|
6856
7772
|
const warnings = [...dateInput.warnings, ...debugWarnings, ...zeroMetricLimitWarnings];
|
|
6857
7773
|
return ok({
|
|
6858
7774
|
status: debugErrors.length > 0 ? "error" : "ok",
|
|
6859
|
-
data:
|
|
6860
|
-
rowCount:
|
|
7775
|
+
data: result2.data,
|
|
7776
|
+
rowCount: result2.data.length,
|
|
6861
7777
|
errors: debugErrors,
|
|
6862
7778
|
warnings,
|
|
6863
7779
|
debug: {
|
|
6864
|
-
...
|
|
7780
|
+
...result2.debug,
|
|
6865
7781
|
warnings,
|
|
6866
7782
|
executionTimeMs: Date.now() - startTime,
|
|
6867
7783
|
gaqlPreview: generateQueryPreview(plan)
|
|
@@ -6877,8 +7793,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6877
7793
|
"List campaigns for a Google Ads account with status, budget, channel type, and bidding strategy.",
|
|
6878
7794
|
{
|
|
6879
7795
|
customerId: customerIdSchema3,
|
|
6880
|
-
statusFilter:
|
|
6881
|
-
limit:
|
|
7796
|
+
statusFilter: z6.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
7797
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(100)
|
|
6882
7798
|
},
|
|
6883
7799
|
async ({ customerId, statusFilter, limit }) => {
|
|
6884
7800
|
try {
|
|
@@ -6897,9 +7813,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6897
7813
|
"List ad groups for a Google Ads account, optionally filtered by campaign.",
|
|
6898
7814
|
{
|
|
6899
7815
|
customerId: customerIdSchema3,
|
|
6900
|
-
campaignId:
|
|
6901
|
-
statusFilter:
|
|
6902
|
-
limit:
|
|
7816
|
+
campaignId: z6.string().optional().describe("Filter by campaign ID"),
|
|
7817
|
+
statusFilter: z6.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
7818
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(100)
|
|
6903
7819
|
},
|
|
6904
7820
|
async ({ customerId, campaignId, statusFilter, limit }) => {
|
|
6905
7821
|
try {
|
|
@@ -6921,9 +7837,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6921
7837
|
"Get keyword-level performance data from keyword_view resource. Shows quality score, impressions, clicks, cost.",
|
|
6922
7838
|
{
|
|
6923
7839
|
customerId: customerIdSchema3,
|
|
6924
|
-
startDate:
|
|
6925
|
-
endDate:
|
|
6926
|
-
limit:
|
|
7840
|
+
startDate: z6.string().describe("Start date YYYY-MM-DD"),
|
|
7841
|
+
endDate: z6.string().describe("End date YYYY-MM-DD"),
|
|
7842
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(100)
|
|
6927
7843
|
},
|
|
6928
7844
|
async ({ customerId, startDate, endDate, limit }) => {
|
|
6929
7845
|
try {
|
|
@@ -6939,14 +7855,14 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6939
7855
|
"google_ads_validate_query",
|
|
6940
7856
|
"Validate metric/dimension/resource compatibility BEFORE executing a query. Checks segment restrictions and resource availability.",
|
|
6941
7857
|
{
|
|
6942
|
-
metrics:
|
|
6943
|
-
dimensions:
|
|
6944
|
-
resource:
|
|
7858
|
+
metrics: z6.array(z6.string()).min(1).describe("Metric keys to validate"),
|
|
7859
|
+
dimensions: z6.array(z6.string()).optional().describe("Dimension keys to validate"),
|
|
7860
|
+
resource: z6.string().optional().default("campaign")
|
|
6945
7861
|
},
|
|
6946
7862
|
async ({ metrics, dimensions, resource }) => {
|
|
6947
7863
|
try {
|
|
6948
|
-
const
|
|
6949
|
-
return ok(
|
|
7864
|
+
const result2 = validateQuerySelection(metrics, dimensions ?? [], resource, dimensions ?? []);
|
|
7865
|
+
return ok(result2);
|
|
6950
7866
|
} catch (e) {
|
|
6951
7867
|
return formatMcpToolError(e);
|
|
6952
7868
|
}
|
|
@@ -6976,7 +7892,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
6976
7892
|
warnings.push("GOOGLE_ADS_LOGIN_CUSTOMER_ID is configured but was not returned by listAccessibleCustomers; verify MCC access if child account queries fail.");
|
|
6977
7893
|
}
|
|
6978
7894
|
if (!loginCustomerId && accessibleCustomers.some((customer) => customer.manager)) {
|
|
6979
|
-
warnings.push(
|
|
7895
|
+
warnings.push(
|
|
7896
|
+
"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."
|
|
7897
|
+
);
|
|
6980
7898
|
}
|
|
6981
7899
|
return ok({
|
|
6982
7900
|
status: "ok",
|
|
@@ -7007,7 +7925,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7007
7925
|
"List accessible customers and, where possible, manager/client relationships from GAQL customer_client. Falls back to accessible customers if hierarchy queries are unavailable.",
|
|
7008
7926
|
{
|
|
7009
7927
|
managerCustomerId: loginCustomerIdSchema,
|
|
7010
|
-
includeInactive:
|
|
7928
|
+
includeInactive: z6.boolean().optional().default(false).describe("Include non-ENABLED customer_client links")
|
|
7011
7929
|
},
|
|
7012
7930
|
async ({ managerCustomerId, includeInactive }) => {
|
|
7013
7931
|
try {
|
|
@@ -7082,8 +8000,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7082
8000
|
"List conversion actions with status, type, category, primary/include-in-conversions flags, owner customer, and last activity dates when supported.",
|
|
7083
8001
|
{
|
|
7084
8002
|
customerId: customerIdSchema3,
|
|
7085
|
-
statusFilter:
|
|
7086
|
-
limit:
|
|
8003
|
+
statusFilter: z6.enum(["ENABLED", "HIDDEN", "REMOVED"]).optional(),
|
|
8004
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7087
8005
|
},
|
|
7088
8006
|
async ({ customerId, statusFilter, limit }) => {
|
|
7089
8007
|
try {
|
|
@@ -7122,11 +8040,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7122
8040
|
ORDER BY conversion_action.name ASC
|
|
7123
8041
|
LIMIT ${limit}
|
|
7124
8042
|
`);
|
|
7125
|
-
const
|
|
8043
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7126
8044
|
{ label: "conversion_action_with_last_activity", gaql: richGaql, failureWarning: "Conversion action query with last activity metrics failed" },
|
|
7127
8045
|
{ label: "conversion_action_minimal", gaql: fallbackGaql }
|
|
7128
8046
|
]);
|
|
7129
|
-
return ok({ conversionActions:
|
|
8047
|
+
return ok({ conversionActions: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7130
8048
|
} catch (e) {
|
|
7131
8049
|
return formatMcpToolError(e);
|
|
7132
8050
|
}
|
|
@@ -7141,10 +8059,10 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7141
8059
|
endDate: isoDateSchema2.optional().describe("End date YYYY-MM-DD. Defaults to today."),
|
|
7142
8060
|
resourceType: gaqlEnumSchema.optional().describe("Optional ChangeEventResourceType filter, e.g. CAMPAIGN or AD_GROUP_AD"),
|
|
7143
8061
|
operation: gaqlEnumSchema.optional().describe("Optional ResourceChangeOperation filter, e.g. CREATE, UPDATE, REMOVE"),
|
|
7144
|
-
userEmail:
|
|
7145
|
-
limit:
|
|
8062
|
+
userEmail: z6.string().email().optional().describe("Optional user email filter"),
|
|
8063
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7146
8064
|
},
|
|
7147
|
-
async ({ customerId, startDate, endDate, resourceType, operation, userEmail, limit }) => {
|
|
8065
|
+
async ({ customerId, startDate, endDate, resourceType, operation: operation2, userEmail, limit }) => {
|
|
7148
8066
|
try {
|
|
7149
8067
|
const range = normalizeChangeEventRange(startDate, endDate);
|
|
7150
8068
|
const where = [
|
|
@@ -7152,7 +8070,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7152
8070
|
`change_event.change_date_time < '${range.endExclusiveDate}'`
|
|
7153
8071
|
];
|
|
7154
8072
|
if (resourceType) where.push(`change_event.change_resource_type = '${resourceType}'`);
|
|
7155
|
-
if (
|
|
8073
|
+
if (operation2) where.push(`change_event.resource_change_operation = '${operation2}'`);
|
|
7156
8074
|
if (userEmail) where.push(`change_event.user_email = '${quoteGaqlString(userEmail)}'`);
|
|
7157
8075
|
const gaql = oneLineGaql(`
|
|
7158
8076
|
SELECT
|
|
@@ -7190,8 +8108,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7190
8108
|
{
|
|
7191
8109
|
customerId: customerIdSchema3,
|
|
7192
8110
|
typeFilter: gaqlEnumSchema.optional().describe("Optional RecommendationType enum filter"),
|
|
7193
|
-
includeDismissed:
|
|
7194
|
-
limit:
|
|
8111
|
+
includeDismissed: z6.boolean().optional().default(false),
|
|
8112
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7195
8113
|
},
|
|
7196
8114
|
async ({ customerId, typeFilter, includeDismissed, limit }) => {
|
|
7197
8115
|
try {
|
|
@@ -7231,11 +8149,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7231
8149
|
ORDER BY recommendation.type ASC
|
|
7232
8150
|
LIMIT ${limit}
|
|
7233
8151
|
`);
|
|
7234
|
-
const
|
|
8152
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7235
8153
|
{ label: "recommendations_with_impact", gaql: richGaql, failureWarning: "Recommendation query with attributed resources/impact failed" },
|
|
7236
8154
|
{ label: "recommendations_minimal", gaql: fallbackGaql }
|
|
7237
8155
|
]);
|
|
7238
|
-
return ok({ recommendations:
|
|
8156
|
+
return ok({ recommendations: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7239
8157
|
} catch (e) {
|
|
7240
8158
|
return formatMcpToolError(e);
|
|
7241
8159
|
}
|
|
@@ -7246,8 +8164,8 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7246
8164
|
"List campaign budgets with amount, status, delivery method, and recommended budget fields when supported.",
|
|
7247
8165
|
{
|
|
7248
8166
|
customerId: customerIdSchema3,
|
|
7249
|
-
statusFilter:
|
|
7250
|
-
limit:
|
|
8167
|
+
statusFilter: z6.enum(["ENABLED", "REMOVED", "UNKNOWN", "UNSPECIFIED"]).optional(),
|
|
8168
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7251
8169
|
},
|
|
7252
8170
|
async ({ customerId, statusFilter, limit }) => {
|
|
7253
8171
|
try {
|
|
@@ -7291,11 +8209,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7291
8209
|
ORDER BY campaign_budget.name ASC
|
|
7292
8210
|
LIMIT ${limit}
|
|
7293
8211
|
`);
|
|
7294
|
-
const
|
|
8212
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7295
8213
|
{ label: "campaign_budget_with_recommendations", gaql: richGaql, failureWarning: "Campaign budget query with recommended budget fields failed" },
|
|
7296
8214
|
{ label: "campaign_budget_minimal", gaql: fallbackGaql }
|
|
7297
8215
|
]);
|
|
7298
|
-
return ok({ budgets:
|
|
8216
|
+
return ok({ budgets: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7299
8217
|
} catch (e) {
|
|
7300
8218
|
return formatMcpToolError(e);
|
|
7301
8219
|
}
|
|
@@ -7309,7 +8227,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7309
8227
|
startDate: isoDateSchema2.optional().describe("Optional start date YYYY-MM-DD for metrics"),
|
|
7310
8228
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD for metrics"),
|
|
7311
8229
|
typeFilter: gaqlEnumSchema.optional().describe("Optional BiddingStrategyType enum filter"),
|
|
7312
|
-
limit:
|
|
8230
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7313
8231
|
},
|
|
7314
8232
|
async ({ customerId, startDate, endDate, typeFilter, limit }) => {
|
|
7315
8233
|
try {
|
|
@@ -7356,11 +8274,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7356
8274
|
ORDER BY bidding_strategy.name ASC
|
|
7357
8275
|
LIMIT ${limit}
|
|
7358
8276
|
`);
|
|
7359
|
-
const
|
|
8277
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7360
8278
|
{ label: "bidding_strategy_requested", gaql, failureWarning: "Bidding strategy query with requested fields failed" },
|
|
7361
8279
|
{ label: "bidding_strategy_structure", gaql: fallbackGaql }
|
|
7362
8280
|
]);
|
|
7363
|
-
return ok({ biddingStrategies:
|
|
8281
|
+
return ok({ biddingStrategies: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7364
8282
|
} catch (e) {
|
|
7365
8283
|
return formatMcpToolError(e);
|
|
7366
8284
|
}
|
|
@@ -7371,13 +8289,13 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7371
8289
|
"Fetch search term performance from search_term_view or campaign_search_term_insight depending on reportType.",
|
|
7372
8290
|
{
|
|
7373
8291
|
customerId: customerIdSchema3,
|
|
7374
|
-
reportType:
|
|
8292
|
+
reportType: z6.enum(["search_term_view", "campaign_search_term_insight"]).optional().default("search_term_view"),
|
|
7375
8293
|
startDate: isoDateSchema2.describe("Start date YYYY-MM-DD"),
|
|
7376
8294
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
7377
8295
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7378
8296
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter. Applies directly to search_term_view and via segments.ad_group for insight reports."),
|
|
7379
|
-
searchTermContains:
|
|
7380
|
-
limit:
|
|
8297
|
+
searchTermContains: z6.string().min(1).optional().describe("Optional substring filter for search_term_view"),
|
|
8298
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7381
8299
|
},
|
|
7382
8300
|
async ({ customerId, reportType, startDate, endDate, campaignId, adGroupId, searchTermContains, limit }) => {
|
|
7383
8301
|
try {
|
|
@@ -7450,7 +8368,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7450
8368
|
startDate: isoDateSchema2.describe("Start date YYYY-MM-DD"),
|
|
7451
8369
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
7452
8370
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7453
|
-
limit:
|
|
8371
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7454
8372
|
},
|
|
7455
8373
|
async ({ customerId, startDate, endDate, campaignId, limit }) => {
|
|
7456
8374
|
try {
|
|
@@ -7494,11 +8412,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7494
8412
|
ORDER BY metrics.clicks DESC
|
|
7495
8413
|
LIMIT ${limit}
|
|
7496
8414
|
`);
|
|
7497
|
-
const
|
|
8415
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7498
8416
|
{ label: "landing_page_with_quality_metrics", gaql: richGaql, failureWarning: "Landing page query with quality metrics failed" },
|
|
7499
8417
|
{ label: "landing_page_minimal", gaql: fallbackGaql }
|
|
7500
8418
|
]);
|
|
7501
|
-
return ok({ landingPages:
|
|
8419
|
+
return ok({ landingPages: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7502
8420
|
} catch (e) {
|
|
7503
8421
|
return formatMcpToolError(e);
|
|
7504
8422
|
}
|
|
@@ -7511,27 +8429,30 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7511
8429
|
customerId: customerIdSchema3,
|
|
7512
8430
|
startDate: isoDateSchema2.optional().describe("Optional start date YYYY-MM-DD for performance metrics"),
|
|
7513
8431
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD for performance metrics"),
|
|
8432
|
+
metrics: z6.array(z6.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."),
|
|
7514
8433
|
campaignId: numericIdSchema3.optional().describe("Optional Performance Max campaign ID filter"),
|
|
7515
8434
|
assetGroupId: numericIdSchema3.optional().describe("Optional asset group ID filter"),
|
|
8435
|
+
assetGroupIds: z6.array(numericIdSchema3).min(1).max(10).optional().describe("Restrict reporting to these asset groups."),
|
|
8436
|
+
assetId: numericIdSchema3.optional().describe("Restrict reporting to one asset."),
|
|
8437
|
+
daily: z6.boolean().optional().default(false).describe("Segment dated performance by segments.date."),
|
|
7516
8438
|
fieldType: gaqlEnumSchema.optional().describe("Optional AssetFieldType enum filter, e.g. HEADLINE, LONG_HEADLINE, MARKETING_IMAGE"),
|
|
7517
|
-
statusFilter:
|
|
7518
|
-
limit:
|
|
8439
|
+
statusFilter: z6.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
8440
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7519
8441
|
},
|
|
7520
|
-
async ({ customerId, startDate, endDate, campaignId, assetGroupId, fieldType, statusFilter, limit }) => {
|
|
8442
|
+
async ({ customerId, startDate, endDate, metrics, campaignId, assetGroupId, assetGroupIds, assetId, daily, fieldType, statusFilter, limit }) => {
|
|
7521
8443
|
try {
|
|
7522
8444
|
const warnings = [];
|
|
7523
8445
|
const where = ["campaign.advertising_channel_type = 'PERFORMANCE_MAX'", ...optionalDateRange(startDate, endDate)];
|
|
7524
8446
|
if (campaignId) where.push(`campaign.id = ${campaignId}`);
|
|
7525
8447
|
if (assetGroupId) where.push(`asset_group.id = ${assetGroupId}`);
|
|
8448
|
+
if (assetGroupIds) where.push(`asset_group.id IN (${assetGroupIds.join(",")})`);
|
|
8449
|
+
if (assetId) where.push(`asset.id = ${assetId}`);
|
|
8450
|
+
if (daily && (!startDate || !endDate)) throw new Error("Daily reporting requires both startDate and endDate.");
|
|
7526
8451
|
if (fieldType) where.push(`asset_group_asset.field_type = '${fieldType}'`);
|
|
7527
8452
|
if (statusFilter) where.push(`asset_group_asset.status = '${statusFilter}'`);
|
|
7528
8453
|
if (!startDate && !endDate) warnings.push("No date range provided; performance metrics were omitted and only asset structure was returned.");
|
|
7529
|
-
const
|
|
7530
|
-
|
|
7531
|
-
metrics.clicks,
|
|
7532
|
-
metrics.cost_micros,
|
|
7533
|
-
metrics.conversions,
|
|
7534
|
-
metrics.conversions_value` : "";
|
|
8454
|
+
const selectedMetrics = [.../* @__PURE__ */ new Set(["metrics.impressions", "metrics.cost_micros", ...metrics ?? PMAX_METRIC_FIELDS.slice(0, 5)])];
|
|
8455
|
+
const metricsFields = startDate || endDate ? `, ${selectedMetrics.join(", ")}${daily ? ", segments.date" : ""}` : "";
|
|
7535
8456
|
const richGaql = oneLineGaql(`
|
|
7536
8457
|
SELECT
|
|
7537
8458
|
campaign.id,
|
|
@@ -7578,11 +8499,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7578
8499
|
ORDER BY campaign.name ASC, asset_group.name ASC
|
|
7579
8500
|
LIMIT ${limit}
|
|
7580
8501
|
`);
|
|
7581
|
-
const
|
|
8502
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7582
8503
|
{ label: "pmax_assets_with_asset_details", gaql: richGaql, failureWarning: "PMax asset query with asset details failed" },
|
|
7583
8504
|
{ label: "pmax_assets_minimal", gaql: fallbackGaql }
|
|
7584
8505
|
]);
|
|
7585
|
-
return ok({ pmaxAssets:
|
|
8506
|
+
return ok({ pmaxAssets: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7586
8507
|
} catch (e) {
|
|
7587
8508
|
return formatMcpToolError(e);
|
|
7588
8509
|
}
|
|
@@ -7593,7 +8514,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7593
8514
|
"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.",
|
|
7594
8515
|
{
|
|
7595
8516
|
customerId: customerIdSchema3,
|
|
7596
|
-
level:
|
|
8517
|
+
level: z6.enum(["campaign", "ad_group", "bidding_strategy"]).optional().default("campaign").describe("Simulation resource level to query"),
|
|
7597
8518
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter. Applies directly to campaign simulations and as campaign context for ad group simulations."),
|
|
7598
8519
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter for ad_group simulations"),
|
|
7599
8520
|
biddingStrategyId: numericIdSchema3.optional().describe("Optional portfolio bidding strategy ID filter for bidding_strategy simulations"),
|
|
@@ -7601,7 +8522,7 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7601
8522
|
simulationEndDate: isoDateSchema2.optional().describe("Optional maximum simulation end date YYYY-MM-DD"),
|
|
7602
8523
|
typeFilter: gaqlEnumSchema.optional().describe("Optional SimulationType enum, e.g. BUDGET, TARGET_CPA, TARGET_ROAS, CPC_BID"),
|
|
7603
8524
|
modificationMethod: gaqlEnumSchema.optional().describe("Optional SimulationModificationMethod enum, e.g. UNIFORM, SCALING, DEFAULT"),
|
|
7604
|
-
limit:
|
|
8525
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(1e3)
|
|
7605
8526
|
},
|
|
7606
8527
|
async ({ customerId, level, campaignId, adGroupId, biddingStrategyId, simulationStartDate, simulationEndDate, typeFilter, modificationMethod, limit }) => {
|
|
7607
8528
|
try {
|
|
@@ -7730,11 +8651,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7730
8651
|
ORDER BY ${resource}.start_date DESC
|
|
7731
8652
|
LIMIT ${limit}
|
|
7732
8653
|
`);
|
|
7733
|
-
const
|
|
8654
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7734
8655
|
{ label: `${resource}_with_points`, gaql: richGaql, failureWarning: "Simulation query with projected point lists failed" },
|
|
7735
8656
|
{ label: `${resource}_metadata`, gaql: fallbackGaql }
|
|
7736
8657
|
]);
|
|
7737
|
-
return ok({ simulations:
|
|
8658
|
+
return ok({ simulations: result2.rows, level, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7738
8659
|
} catch (e) {
|
|
7739
8660
|
return formatMcpToolError(e);
|
|
7740
8661
|
}
|
|
@@ -7749,9 +8670,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7749
8670
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
7750
8671
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7751
8672
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter"),
|
|
7752
|
-
searchTermContains:
|
|
7753
|
-
serpType:
|
|
7754
|
-
limit:
|
|
8673
|
+
searchTermContains: z6.string().min(1).optional().describe("Optional search term substring filter"),
|
|
8674
|
+
serpType: z6.enum(["ADS_AND_ORGANIC", "ADS_ONLY", "ORGANIC_ONLY", "UNKNOWN", "UNSPECIFIED"]).optional().describe("Optional search engine results page type segment filter"),
|
|
8675
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7755
8676
|
},
|
|
7756
8677
|
async ({ customerId, startDate, endDate, campaignId, adGroupId, searchTermContains, serpType, limit }) => {
|
|
7757
8678
|
try {
|
|
@@ -7811,11 +8732,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7811
8732
|
ORDER BY metrics.clicks DESC
|
|
7812
8733
|
LIMIT ${limit}
|
|
7813
8734
|
`);
|
|
7814
|
-
const
|
|
8735
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7815
8736
|
{ 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" },
|
|
7816
8737
|
{ label: "search_term_view_paid_only", gaql: fallbackGaql }
|
|
7817
8738
|
]);
|
|
7818
|
-
return ok({ searchTerms:
|
|
8739
|
+
return ok({ searchTerms: result2.rows, source: result2.queryLabel, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
7819
8740
|
} catch (e) {
|
|
7820
8741
|
return formatMcpToolError(e);
|
|
7821
8742
|
}
|
|
@@ -7826,17 +8747,17 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7826
8747
|
"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.",
|
|
7827
8748
|
{
|
|
7828
8749
|
customerId: customerIdSchema3,
|
|
7829
|
-
scope:
|
|
8750
|
+
scope: z6.enum(["account", "campaign", "ad_group"]).optional().default("account").describe("shopping_product scope. ad_group scope requires campaignId and adGroupId."),
|
|
7830
8751
|
startDate: isoDateSchema2.optional().describe("Optional start date YYYY-MM-DD. Used only as a WHERE filter; shopping_product cannot segment by date."),
|
|
7831
8752
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD. Used only as a WHERE filter; shopping_product cannot segment by date."),
|
|
7832
8753
|
campaignId: numericIdSchema3.optional().describe("Campaign ID for campaign or ad_group scope"),
|
|
7833
8754
|
adGroupId: numericIdSchema3.optional().describe("Ad group ID for ad_group scope"),
|
|
7834
8755
|
merchantCenterId: numericIdSchema3.optional().describe("Optional Merchant Center ID filter"),
|
|
7835
|
-
itemId:
|
|
7836
|
-
titleContains:
|
|
7837
|
-
feedLabel:
|
|
7838
|
-
statusFilter:
|
|
7839
|
-
limit:
|
|
8756
|
+
itemId: z6.string().min(1).optional().describe("Optional Merchant Center item ID filter"),
|
|
8757
|
+
titleContains: z6.string().min(1).optional().describe("Optional product title substring filter"),
|
|
8758
|
+
feedLabel: z6.string().min(1).optional().describe("Optional feed label filter"),
|
|
8759
|
+
statusFilter: z6.enum(["ELIGIBLE", "ELIGIBLE_LIMITED", "NOT_ELIGIBLE", "UNKNOWN", "UNSPECIFIED"]).optional(),
|
|
8760
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7840
8761
|
},
|
|
7841
8762
|
async ({ customerId, scope, startDate, endDate, campaignId, adGroupId, merchantCenterId, itemId, titleContains, feedLabel, statusFilter, limit }) => {
|
|
7842
8763
|
try {
|
|
@@ -7923,11 +8844,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7923
8844
|
ORDER BY shopping_product.title ASC
|
|
7924
8845
|
LIMIT ${limit}
|
|
7925
8846
|
`);
|
|
7926
|
-
const
|
|
8847
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
7927
8848
|
{ label: "shopping_product_with_metrics_and_issues", gaql: richGaql, failureWarning: "Shopping product query with performance/cart/issue fields failed" },
|
|
7928
8849
|
{ label: "shopping_product_catalog", gaql: fallbackGaql }
|
|
7929
8850
|
]);
|
|
7930
|
-
return ok({ shoppingProducts:
|
|
8851
|
+
return ok({ shoppingProducts: result2.rows, scope, count: result2.rows.length, gaql: result2.gaql, warnings: [...warnings, ...result2.warnings] });
|
|
7931
8852
|
} catch (e) {
|
|
7932
8853
|
return formatMcpToolError(e);
|
|
7933
8854
|
}
|
|
@@ -7943,11 +8864,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
7943
8864
|
campaignId: numericIdSchema3.optional().describe("Optional campaign ID filter"),
|
|
7944
8865
|
adGroupId: numericIdSchema3.optional().describe("Optional ad group ID filter"),
|
|
7945
8866
|
merchantCenterId: numericIdSchema3.optional().describe("Optional Merchant Center ID filter"),
|
|
7946
|
-
itemId:
|
|
7947
|
-
titleContains:
|
|
7948
|
-
brand:
|
|
7949
|
-
feedLabel:
|
|
7950
|
-
limit:
|
|
8867
|
+
itemId: z6.string().min(1).optional().describe("Optional Merchant Center item ID filter"),
|
|
8868
|
+
titleContains: z6.string().min(1).optional().describe("Optional product title substring filter"),
|
|
8869
|
+
brand: z6.string().min(1).optional().describe("Optional product brand filter"),
|
|
8870
|
+
feedLabel: z6.string().min(1).optional().describe("Optional product feed label filter"),
|
|
8871
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
7951
8872
|
},
|
|
7952
8873
|
async ({ customerId, startDate, endDate, campaignId, adGroupId, merchantCenterId, itemId, titleContains, brand, feedLabel, limit }) => {
|
|
7953
8874
|
try {
|
|
@@ -8007,11 +8928,11 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8007
8928
|
ORDER BY metrics.cost_micros DESC
|
|
8008
8929
|
LIMIT ${limit}
|
|
8009
8930
|
`);
|
|
8010
|
-
const
|
|
8931
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
8011
8932
|
{ label: "shopping_performance_with_cart_metrics", gaql: richGaql, failureWarning: "Shopping performance query with cart/profit metrics failed" },
|
|
8012
8933
|
{ label: "shopping_performance_core", gaql: fallbackGaql }
|
|
8013
8934
|
]);
|
|
8014
|
-
return ok({ shoppingPerformance:
|
|
8935
|
+
return ok({ shoppingPerformance: result2.rows, count: result2.rows.length, gaql: result2.gaql, warnings: result2.warnings });
|
|
8015
8936
|
} catch (e) {
|
|
8016
8937
|
return formatMcpToolError(e);
|
|
8017
8938
|
}
|
|
@@ -8025,9 +8946,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8025
8946
|
startDate: isoDateSchema2.describe("Start date YYYY-MM-DD"),
|
|
8026
8947
|
endDate: isoDateSchema2.describe("End date YYYY-MM-DD"),
|
|
8027
8948
|
campaignId: numericIdSchema3.optional().describe("Optional Performance Max campaign ID filter"),
|
|
8028
|
-
placementType:
|
|
8029
|
-
placementContains:
|
|
8030
|
-
limit:
|
|
8949
|
+
placementType: z6.enum(["WEBSITE", "MOBILE_APPLICATION", "YOUTUBE_VIDEO", "YOUTUBE_CHANNEL", "GOOGLE_PRODUCTS", "UNKNOWN", "UNSPECIFIED"]).optional().describe("Optional PlacementType enum filter"),
|
|
8950
|
+
placementContains: z6.string().min(1).optional().describe("Optional substring filter for the placement string"),
|
|
8951
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(1e3)
|
|
8031
8952
|
},
|
|
8032
8953
|
async ({ customerId, startDate, endDate, campaignId, placementType, placementContains, limit }) => {
|
|
8033
8954
|
try {
|
|
@@ -8063,16 +8984,16 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8063
8984
|
ORDER BY metrics.impressions DESC
|
|
8064
8985
|
LIMIT ${limit}
|
|
8065
8986
|
`);
|
|
8066
|
-
const
|
|
8987
|
+
const result2 = await runGaqlWithFallback(client, customerId, [
|
|
8067
8988
|
{ label: "pmax_placements_with_target_url_and_campaign", gaql: richGaql, failureWarning: "PMax placement query with target URL/campaign context failed" },
|
|
8068
8989
|
{ label: "pmax_placements_core", gaql: fallbackGaql }
|
|
8069
8990
|
]);
|
|
8070
8991
|
return ok({
|
|
8071
|
-
placements:
|
|
8072
|
-
pmaxPlacements:
|
|
8073
|
-
count:
|
|
8074
|
-
gaql:
|
|
8075
|
-
warnings: [...warnings, ...
|
|
8992
|
+
placements: result2.rows,
|
|
8993
|
+
pmaxPlacements: result2.rows,
|
|
8994
|
+
count: result2.rows.length,
|
|
8995
|
+
gaql: result2.gaql,
|
|
8996
|
+
warnings: [...warnings, ...result2.warnings]
|
|
8076
8997
|
});
|
|
8077
8998
|
} catch (e) {
|
|
8078
8999
|
return formatMcpToolError(e);
|
|
@@ -8088,9 +9009,9 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8088
9009
|
endDate: isoDateSchema2.optional().describe("Optional end date YYYY-MM-DD for asset group metrics/top combinations"),
|
|
8089
9010
|
campaignId: numericIdSchema3.optional().describe("Optional Performance Max campaign ID filter"),
|
|
8090
9011
|
assetGroupId: numericIdSchema3.optional().describe("Optional asset group ID filter"),
|
|
8091
|
-
statusFilter:
|
|
8092
|
-
includeTopCombinations:
|
|
8093
|
-
limit:
|
|
9012
|
+
statusFilter: z6.enum(["ENABLED", "PAUSED", "REMOVED"]).optional(),
|
|
9013
|
+
includeTopCombinations: z6.boolean().optional().default(true).describe("Also query asset_group_top_combination_view when available"),
|
|
9014
|
+
limit: z6.number().int().min(1).max(1e3).optional().default(1e3)
|
|
8094
9015
|
},
|
|
8095
9016
|
async ({ customerId, startDate, endDate, campaignId, assetGroupId, statusFilter, includeTopCombinations, limit }) => {
|
|
8096
9017
|
try {
|
|
@@ -8190,6 +9111,207 @@ Use google-ads://metrics for available metrics, google-ads://dimensions for dime
|
|
|
8190
9111
|
}
|
|
8191
9112
|
}
|
|
8192
9113
|
);
|
|
9114
|
+
server.tool(
|
|
9115
|
+
"google_ads_list_image_assets",
|
|
9116
|
+
"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.",
|
|
9117
|
+
{
|
|
9118
|
+
customerId: customerIdSchema3,
|
|
9119
|
+
beforeAssetId: numericIdSchema3.optional().describe("Continue the descending library before this last-seen asset ID. Use the same filters."),
|
|
9120
|
+
assetIds: z6.array(numericIdSchema3).min(1).max(8).optional().describe("Resolve these exact native asset IDs, without scanning the library."),
|
|
9121
|
+
nameFilter: z6.string().optional().describe("Only assets whose file name contains this text"),
|
|
9122
|
+
minWidth: z6.number().int().min(1).optional().describe("Only images at least this wide, in pixels"),
|
|
9123
|
+
minHeight: z6.number().int().min(1).optional().describe("Only images at least this tall, in pixels"),
|
|
9124
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(500)
|
|
9125
|
+
},
|
|
9126
|
+
async ({ customerId, beforeAssetId, assetIds, nameFilter, minWidth, minHeight, limit }) => {
|
|
9127
|
+
try {
|
|
9128
|
+
const where = ["asset.type = 'IMAGE'"];
|
|
9129
|
+
if (beforeAssetId) where.push(`asset.id < ${beforeAssetId}`);
|
|
9130
|
+
if (assetIds?.length) where.push(`asset.id IN (${assetIds.join(",")})`);
|
|
9131
|
+
if (nameFilter) where.push(`asset.name LIKE '%${quoteGaqlString(nameFilter)}%'`);
|
|
9132
|
+
if (minWidth) where.push(`asset.image_asset.full_size.width_pixels >= ${minWidth}`);
|
|
9133
|
+
if (minHeight) where.push(`asset.image_asset.full_size.height_pixels >= ${minHeight}`);
|
|
9134
|
+
const { rows, gaql, warnings } = await runGaqlWithFallback(client, customerId, [{
|
|
9135
|
+
label: "image asset library",
|
|
9136
|
+
gaql: oneLineGaql(`
|
|
9137
|
+
SELECT
|
|
9138
|
+
asset.id, asset.name, asset.type, asset.source,
|
|
9139
|
+
asset.image_asset.full_size.url,
|
|
9140
|
+
asset.image_asset.full_size.width_pixels,
|
|
9141
|
+
asset.image_asset.full_size.height_pixels,
|
|
9142
|
+
asset.image_asset.file_size,
|
|
9143
|
+
asset.image_asset.mime_type
|
|
9144
|
+
FROM asset${buildWhere(where)} ORDER BY asset.id DESC LIMIT ${limit}`)
|
|
9145
|
+
}]);
|
|
9146
|
+
return ok({
|
|
9147
|
+
imageAssets: rows,
|
|
9148
|
+
count: rows.length,
|
|
9149
|
+
gaql,
|
|
9150
|
+
warnings,
|
|
9151
|
+
limitations: [
|
|
9152
|
+
"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.",
|
|
9153
|
+
"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.",
|
|
9154
|
+
"Shopping/PLA product visuals come from Merchant Center feeds and never appear in this asset library."
|
|
9155
|
+
],
|
|
9156
|
+
nextActions: rows.length >= limit ? ["The LIMIT was reached; narrow with nameFilter or dimensions, or raise limit."] : []
|
|
9157
|
+
});
|
|
9158
|
+
} catch (e) {
|
|
9159
|
+
return formatMcpToolError(e);
|
|
9160
|
+
}
|
|
9161
|
+
}
|
|
9162
|
+
);
|
|
9163
|
+
server.tool(
|
|
9164
|
+
"google_ads_list_video_assets",
|
|
9165
|
+
"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.",
|
|
9166
|
+
{
|
|
9167
|
+
customerId: customerIdSchema3,
|
|
9168
|
+
beforeAssetId: numericIdSchema3.optional().describe("Continue the descending library before this last-seen asset ID. Use the same filters."),
|
|
9169
|
+
assetIds: z6.array(numericIdSchema3).min(1).max(8).optional().describe("Resolve these exact native asset IDs, without scanning the library."),
|
|
9170
|
+
nameFilter: z6.string().optional().describe("Only assets whose name contains this text"),
|
|
9171
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(500)
|
|
9172
|
+
},
|
|
9173
|
+
async ({ customerId, beforeAssetId, assetIds, nameFilter, limit }) => {
|
|
9174
|
+
try {
|
|
9175
|
+
const where = ["asset.type = 'YOUTUBE_VIDEO'"];
|
|
9176
|
+
if (beforeAssetId) where.push(`asset.id < ${beforeAssetId}`);
|
|
9177
|
+
if (assetIds?.length) where.push(`asset.id IN (${assetIds.join(",")})`);
|
|
9178
|
+
if (nameFilter) where.push(`asset.name LIKE '%${quoteGaqlString(nameFilter)}%'`);
|
|
9179
|
+
const { rows, gaql, warnings } = await runGaqlWithFallback(client, customerId, [{
|
|
9180
|
+
label: "video asset library",
|
|
9181
|
+
gaql: oneLineGaql(`
|
|
9182
|
+
SELECT
|
|
9183
|
+
asset.id, asset.name, asset.type, asset.source,
|
|
9184
|
+
asset.youtube_video_asset.youtube_video_id,
|
|
9185
|
+
asset.youtube_video_asset.youtube_video_title
|
|
9186
|
+
FROM asset${buildWhere(where)} ORDER BY asset.id DESC LIMIT ${limit}`)
|
|
9187
|
+
}]);
|
|
9188
|
+
const videoAssets = rows.map((row) => {
|
|
9189
|
+
const asset = recordOf(recordOf(row)?.asset);
|
|
9190
|
+
const youtube = recordOf(asset?.youtubeVideoAsset);
|
|
9191
|
+
return { ...row, derivedUrls: youtubeDerivedUrls(youtube?.youtubeVideoId) };
|
|
9192
|
+
});
|
|
9193
|
+
return ok({
|
|
9194
|
+
videoAssets,
|
|
9195
|
+
count: videoAssets.length,
|
|
9196
|
+
gaql,
|
|
9197
|
+
warnings,
|
|
9198
|
+
limitations: [
|
|
9199
|
+
"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.",
|
|
9200
|
+
"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)."
|
|
9201
|
+
],
|
|
9202
|
+
nextActions: videoAssets.length >= limit ? ["The LIMIT was reached; narrow with nameFilter or raise limit."] : []
|
|
9203
|
+
});
|
|
9204
|
+
} catch (e) {
|
|
9205
|
+
return formatMcpToolError(e);
|
|
9206
|
+
}
|
|
9207
|
+
}
|
|
9208
|
+
);
|
|
9209
|
+
server.tool(
|
|
9210
|
+
"google_ads_get_demand_gen_assets",
|
|
9211
|
+
"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.",
|
|
9212
|
+
{
|
|
9213
|
+
customerId: customerIdSchema3,
|
|
9214
|
+
campaignId: numericIdSchema3.optional().describe("Optional Demand Gen campaign ID filter"),
|
|
9215
|
+
limit: z6.number().int().min(1).max(1e4).optional().default(500)
|
|
9216
|
+
},
|
|
9217
|
+
async ({ customerId, campaignId, limit }) => {
|
|
9218
|
+
try {
|
|
9219
|
+
const where = ["campaign.advertising_channel_type = 'DEMAND_GEN'"];
|
|
9220
|
+
if (campaignId) where.push(`campaign.id = ${campaignId}`);
|
|
9221
|
+
const demandGenFields = `
|
|
9222
|
+
ad_group_ad.ad.demand_gen_video_responsive_ad.videos,
|
|
9223
|
+
ad_group_ad.ad.demand_gen_video_responsive_ad.logo_images,
|
|
9224
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.marketing_images,
|
|
9225
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.square_marketing_images,
|
|
9226
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.portrait_marketing_images,
|
|
9227
|
+
ad_group_ad.ad.demand_gen_multi_asset_ad.logo_images,
|
|
9228
|
+
ad_group_ad.ad.demand_gen_carousel_ad.carousel_cards`;
|
|
9229
|
+
const baseFields = `
|
|
9230
|
+
campaign.id, campaign.name, ad_group.id, ad_group.name,
|
|
9231
|
+
ad_group_ad.status, ad_group_ad.ad.id, ad_group_ad.ad.name, ad_group_ad.ad.type`;
|
|
9232
|
+
const ads = await runGaqlWithFallback(client, customerId, [
|
|
9233
|
+
{
|
|
9234
|
+
label: "demand gen ads with creative references",
|
|
9235
|
+
gaql: oneLineGaql(`SELECT ${baseFields}, ${demandGenFields} FROM ad_group_ad${buildWhere(where)} LIMIT ${limit}`),
|
|
9236
|
+
failureWarning: "Full Demand Gen creative field selection failed; falling back to ad identifiers only"
|
|
9237
|
+
},
|
|
9238
|
+
{
|
|
9239
|
+
label: "demand gen ads without creative fields",
|
|
9240
|
+
gaql: oneLineGaql(`SELECT ${baseFields} FROM ad_group_ad${buildWhere(where)} LIMIT ${limit}`)
|
|
9241
|
+
}
|
|
9242
|
+
]);
|
|
9243
|
+
const referencedIds = /* @__PURE__ */ new Set();
|
|
9244
|
+
for (const row of ads.rows) {
|
|
9245
|
+
const ad = recordOf(recordOf(recordOf(row)?.adGroupAd)?.ad);
|
|
9246
|
+
if (!ad) continue;
|
|
9247
|
+
collectAssetIds(ad.demandGenVideoResponsiveAd, referencedIds);
|
|
9248
|
+
collectAssetIds(ad.demandGenMultiAssetAd, referencedIds);
|
|
9249
|
+
collectAssetIds(ad.demandGenCarouselAd, referencedIds);
|
|
9250
|
+
}
|
|
9251
|
+
const warnings = [...ads.warnings];
|
|
9252
|
+
const assetsById = /* @__PURE__ */ new Map();
|
|
9253
|
+
const idList = [...referencedIds];
|
|
9254
|
+
const chunkSize = 500;
|
|
9255
|
+
for (let index = 0; index < idList.length; index += chunkSize) {
|
|
9256
|
+
const chunk = idList.slice(index, index + chunkSize);
|
|
9257
|
+
try {
|
|
9258
|
+
const resolved = await client.searchStream(customerId, oneLineGaql(`
|
|
9259
|
+
SELECT
|
|
9260
|
+
asset.id, asset.name, asset.type,
|
|
9261
|
+
asset.image_asset.full_size.url,
|
|
9262
|
+
asset.image_asset.full_size.width_pixels,
|
|
9263
|
+
asset.image_asset.full_size.height_pixels,
|
|
9264
|
+
asset.youtube_video_asset.youtube_video_id,
|
|
9265
|
+
asset.youtube_video_asset.youtube_video_title
|
|
9266
|
+
FROM asset WHERE asset.id IN (${chunk.join(", ")})`));
|
|
9267
|
+
for (const row of resolved) {
|
|
9268
|
+
const asset = recordOf(recordOf(row)?.asset);
|
|
9269
|
+
const rawId = asset?.id;
|
|
9270
|
+
const id = typeof rawId === "string" || typeof rawId === "number" ? String(rawId) : void 0;
|
|
9271
|
+
if (!asset || !id) continue;
|
|
9272
|
+
const youtube = recordOf(asset.youtubeVideoAsset);
|
|
9273
|
+
assetsById.set(id, {
|
|
9274
|
+
...asset,
|
|
9275
|
+
derivedUrls: youtubeDerivedUrls(youtube?.youtubeVideoId)
|
|
9276
|
+
});
|
|
9277
|
+
}
|
|
9278
|
+
} catch (error) {
|
|
9279
|
+
warnings.push(`Asset resolution failed for ${chunk.length} referenced assets: ${getErrorMessage(error)}`);
|
|
9280
|
+
}
|
|
9281
|
+
}
|
|
9282
|
+
const resolvedAds = ads.rows.map((row) => {
|
|
9283
|
+
const record = recordOf(row) ?? {};
|
|
9284
|
+
const ad = recordOf(recordOf(record.adGroupAd)?.ad);
|
|
9285
|
+
const ids = /* @__PURE__ */ new Set();
|
|
9286
|
+
if (ad) {
|
|
9287
|
+
collectAssetIds(ad.demandGenVideoResponsiveAd, ids);
|
|
9288
|
+
collectAssetIds(ad.demandGenMultiAssetAd, ids);
|
|
9289
|
+
collectAssetIds(ad.demandGenCarouselAd, ids);
|
|
9290
|
+
}
|
|
9291
|
+
return {
|
|
9292
|
+
...record,
|
|
9293
|
+
resolvedAssets: [...ids].map((id) => assetsById.get(id) ?? { id, unresolved: true })
|
|
9294
|
+
};
|
|
9295
|
+
});
|
|
9296
|
+
return ok({
|
|
9297
|
+
ads: resolvedAds,
|
|
9298
|
+
count: resolvedAds.length,
|
|
9299
|
+
referencedAssetCount: referencedIds.size,
|
|
9300
|
+
resolvedAssetCount: assetsById.size,
|
|
9301
|
+
gaql: ads.gaql,
|
|
9302
|
+
warnings,
|
|
9303
|
+
limitations: [
|
|
9304
|
+
"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.",
|
|
9305
|
+
"Image full_size.url values on tpc.googlesyndication.com are publicly served with no visible expiry.",
|
|
9306
|
+
"Catalog-driven visuals come from Merchant Center feeds and are not part of ad creative references."
|
|
9307
|
+
],
|
|
9308
|
+
nextActions: resolvedAds.length >= limit ? ["The LIMIT was reached; filter by campaignId or raise limit."] : []
|
|
9309
|
+
});
|
|
9310
|
+
} catch (e) {
|
|
9311
|
+
return formatMcpToolError(e);
|
|
9312
|
+
}
|
|
9313
|
+
}
|
|
9314
|
+
);
|
|
8193
9315
|
registerGoogleAdsKeywordPlannerTools(server, client, ok);
|
|
8194
9316
|
registerGoogleAdsDiscoveryTools(server, client, ok);
|
|
8195
9317
|
registerGoogleAdsReadOnlyRpcTool(server, client, ok);
|
|
@@ -8235,7 +9357,11 @@ var GOOGLE_ADS_TOOL_MANIFEST = {
|
|
|
8235
9357
|
{ 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." },
|
|
8236
9358
|
{ name: "google_ads_get_insights", purpose: "Generate validated performance GAQL using the metric and dimension catalogs." },
|
|
8237
9359
|
{ name: "google_ads_validate_query", purpose: "Validate metric/dimension/resource compatibility before querying." },
|
|
8238
|
-
{ name: "google_ads_run_gaql", purpose: "Run raw read-only GAQL SELECT queries for advanced reporting." }
|
|
9360
|
+
{ name: "google_ads_run_gaql", purpose: "Run raw read-only GAQL SELECT queries for advanced reporting." },
|
|
9361
|
+
{ name: "google_ads_list_image_assets", purpose: "List the account's image asset library with stable public full-size URLs, dimensions, and file metadata." },
|
|
9362
|
+
{ name: "google_ads_list_video_assets", purpose: "List YouTube video assets with derived watch, embed, and public thumbnail URLs." },
|
|
9363
|
+
{ name: "google_ads_get_demand_gen_assets", purpose: "List Demand Gen ads with their referenced image and YouTube creatives resolved." },
|
|
9364
|
+
{ "name": "google_ads_get_video_upload", "purpose": "Read the processing state and YouTube video ID of one Google Ads upload in the selected customer" }
|
|
8239
9365
|
],
|
|
8240
9366
|
resources: [
|
|
8241
9367
|
"google-ads://manifest",
|
|
@@ -8391,7 +9517,7 @@ function registerGoogleAdsResources(server) {
|
|
|
8391
9517
|
mimeType: "application/json",
|
|
8392
9518
|
text: JSON.stringify({
|
|
8393
9519
|
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).",
|
|
8394
|
-
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"],
|
|
9520
|
+
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"],
|
|
8395
9521
|
queryFormat: "SELECT ... FROM resource WHERE ... ORDER BY ... LIMIT N"
|
|
8396
9522
|
}, null, 2)
|
|
8397
9523
|
}]
|
|
@@ -8399,7 +9525,25 @@ function registerGoogleAdsResources(server) {
|
|
|
8399
9525
|
}
|
|
8400
9526
|
|
|
8401
9527
|
// src/platforms/google-ads/writes.ts
|
|
8402
|
-
import {
|
|
9528
|
+
import { randomUUID } from "crypto";
|
|
9529
|
+
import { z as z7 } from "zod";
|
|
9530
|
+
|
|
9531
|
+
// src/core/money.ts
|
|
9532
|
+
function guard(amount, currency) {
|
|
9533
|
+
if (!Number.isFinite(amount)) throw new Error(`Amount must be a finite number, received ${amount}.`);
|
|
9534
|
+
if (amount <= 0) throw new Error(`Amount must be greater than zero, received ${amount}.`);
|
|
9535
|
+
if (amount > 1e6) {
|
|
9536
|
+
throw new Error(
|
|
9537
|
+
`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.`
|
|
9538
|
+
);
|
|
9539
|
+
}
|
|
9540
|
+
return amount;
|
|
9541
|
+
}
|
|
9542
|
+
function toMicros(amount) {
|
|
9543
|
+
return Math.round(guard(amount, "units") * 1e6);
|
|
9544
|
+
}
|
|
9545
|
+
|
|
9546
|
+
// src/platforms/google-ads/writes.ts
|
|
8403
9547
|
function ok2(data) {
|
|
8404
9548
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
8405
9549
|
}
|
|
@@ -8414,246 +9558,326 @@ function preview(action, details) {
|
|
|
8414
9558
|
message: "Preview only, nothing was changed. Repeat the same call with confirm: true to apply this change to the live account."
|
|
8415
9559
|
});
|
|
8416
9560
|
}
|
|
8417
|
-
var confirmSchema =
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
}
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
{
|
|
8433
|
-
customerId: z5.string().describe("Google Ads customer ID, with or without dashes."),
|
|
8434
|
-
[param]: z5.string().describe(`${label} ID.`),
|
|
8435
|
-
status: z5.enum(["PAUSED", "ENABLED", "REMOVED"]).describe("New status."),
|
|
8436
|
-
loginCustomerId: loginCustomerIdSchema2,
|
|
8437
|
-
confirm: confirmSchema
|
|
8438
|
-
},
|
|
8439
|
-
async (a) => {
|
|
8440
|
-
const cid = stripDashes(a.customerId);
|
|
8441
|
-
const id = String(a[param]);
|
|
8442
|
-
if (!a.confirm) {
|
|
8443
|
-
return preview(name, { customer: cid, target: id, newStatus: a.status });
|
|
8444
|
-
}
|
|
8445
|
-
const result = await client.mutate(cid, collection, [
|
|
8446
|
-
{ update: { resourceName: resourceName(cid, collection, id), status: a.status }, updateMask: "status" }
|
|
8447
|
-
], a.loginCustomerId);
|
|
8448
|
-
return ok2({ applied: true, action: name, result });
|
|
9561
|
+
var confirmSchema = z7.boolean().optional().describe("Set to true to actually apply the change. Without it, the tool only previews.");
|
|
9562
|
+
async function request(url, init, contexte) {
|
|
9563
|
+
const response = await fetch(url, { ...init, redirect: "error" });
|
|
9564
|
+
const body = await response.text();
|
|
9565
|
+
let parsed;
|
|
9566
|
+
try {
|
|
9567
|
+
parsed = JSON.parse(body);
|
|
9568
|
+
} catch {
|
|
9569
|
+
parsed = body;
|
|
9570
|
+
}
|
|
9571
|
+
if (!response.ok) {
|
|
9572
|
+
if (url.startsWith(ADS_BASE + "/")) {
|
|
9573
|
+
const error = parsed?.error;
|
|
9574
|
+
const details = error?.details?.flatMap((d) => d.errors ?? []).map((e) => ({ code: e.errorCode, message: e.message, location: e.location }));
|
|
9575
|
+
throw new Error(`${contexte}: ${JSON.stringify(details?.length ? details : error?.message ?? parsed).slice(0, 3e3)}`);
|
|
8449
9576
|
}
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
|
|
8453
|
-
|
|
8454
|
-
|
|
8455
|
-
|
|
9577
|
+
throw new Error(`${contexte} : ${typeof parsed === "string" ? parsed.slice(0, 300) : JSON.stringify(parsed).slice(0, 300)}`);
|
|
9578
|
+
}
|
|
9579
|
+
return parsed;
|
|
9580
|
+
}
|
|
9581
|
+
var ADS_BASE = "https://googleads.googleapis.com/v25";
|
|
9582
|
+
function registerGoogleAdsWrites(c, config) {
|
|
9583
|
+
registerGoogleExtendedWrites(c, config);
|
|
9584
|
+
async function accessToken() {
|
|
9585
|
+
const r = await request(
|
|
9586
|
+
"https://oauth2.googleapis.com/token",
|
|
9587
|
+
{
|
|
9588
|
+
method: "POST",
|
|
9589
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
9590
|
+
body: new URLSearchParams({
|
|
9591
|
+
grant_type: "refresh_token",
|
|
9592
|
+
refresh_token: config.refreshToken,
|
|
9593
|
+
client_id: config.clientId,
|
|
9594
|
+
client_secret: config.clientSecret
|
|
9595
|
+
})
|
|
9596
|
+
},
|
|
9597
|
+
"Google token refresh"
|
|
9598
|
+
);
|
|
9599
|
+
return r.access_token;
|
|
9600
|
+
}
|
|
9601
|
+
const headers = async (loginCustomerId) => {
|
|
9602
|
+
const h = {
|
|
9603
|
+
authorization: `Bearer ${await accessToken()}`,
|
|
9604
|
+
"developer-token": config.developerToken,
|
|
9605
|
+
"content-type": "application/json"
|
|
9606
|
+
};
|
|
9607
|
+
const manager = loginCustomerId ?? config.loginCustomerId;
|
|
9608
|
+
if (manager) h["login-customer-id"] = manager.replace(/-/g, "");
|
|
9609
|
+
return h;
|
|
9610
|
+
};
|
|
9611
|
+
c.tool(
|
|
9612
|
+
"google_ads_update_campaign_status",
|
|
9613
|
+
"Pause, re-enable or remove a Google Ads campaign. Previews by default: without confirm: true, the tool describes the change without applying it.",
|
|
8456
9614
|
{
|
|
8457
|
-
customerId:
|
|
8458
|
-
campaignId:
|
|
8459
|
-
|
|
8460
|
-
loginCustomerId:
|
|
9615
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9616
|
+
campaignId: z7.string().describe("Campaign ID."),
|
|
9617
|
+
status: z7.enum(["PAUSED", "ENABLED", "REMOVED"]).describe("New status."),
|
|
9618
|
+
loginCustomerId: z7.string().optional().describe("Manager account, if the customer sits under an MCC."),
|
|
8461
9619
|
confirm: confirmSchema
|
|
8462
9620
|
},
|
|
8463
9621
|
async (a) => {
|
|
8464
|
-
const
|
|
8465
|
-
|
|
8466
|
-
|
|
9622
|
+
const { customerId, campaignId, status, loginCustomerId, confirm } = a;
|
|
9623
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9624
|
+
if (!confirm) {
|
|
9625
|
+
return preview("google_ads_update_campaign_status", { customer: cid, campaign: campaignId, newStatus: status });
|
|
8467
9626
|
}
|
|
8468
|
-
const
|
|
8469
|
-
{
|
|
8470
|
-
|
|
8471
|
-
|
|
9627
|
+
const result2 = await request(
|
|
9628
|
+
`${ADS_BASE}/customers/${cid}/campaigns:mutate`,
|
|
9629
|
+
{
|
|
9630
|
+
method: "POST",
|
|
9631
|
+
headers: await headers(loginCustomerId),
|
|
9632
|
+
body: JSON.stringify({
|
|
9633
|
+
operations: [
|
|
9634
|
+
status === "REMOVED" ? { remove: `customers/${cid}/campaigns/${campaignId}` } : { update: { resourceName: `customers/${cid}/campaigns/${campaignId}`, status }, updateMask: "status" }
|
|
9635
|
+
]
|
|
9636
|
+
})
|
|
9637
|
+
},
|
|
9638
|
+
"Google Ads status update"
|
|
9639
|
+
);
|
|
9640
|
+
return ok2({ applied: true, action: "google_ads_update_campaign_status", result: result2 });
|
|
8472
9641
|
}
|
|
8473
9642
|
);
|
|
8474
|
-
|
|
9643
|
+
c.tool(
|
|
8475
9644
|
"google_ads_create_campaign",
|
|
8476
9645
|
"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.",
|
|
8477
9646
|
{
|
|
8478
|
-
customerId:
|
|
8479
|
-
name:
|
|
8480
|
-
channelType:
|
|
8481
|
-
dailyBudget:
|
|
8482
|
-
|
|
8483
|
-
|
|
9647
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9648
|
+
name: z7.string().describe("Campaign name."),
|
|
9649
|
+
channelType: z7.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."),
|
|
9650
|
+
dailyBudget: z7.number().positive().describe("Daily budget in the account currency."),
|
|
9651
|
+
loginCustomerId: z7.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
9652
|
+
brandGuidelinesEnabled: z7.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."),
|
|
9653
|
+
euPoliticalAdvertising: z7.boolean().optional().describe("Required for execution: explicitly declare whether this campaign contains EU political advertising."),
|
|
9654
|
+
merchantId: z7.string().regex(/^\d+$/).optional().describe("Merchant Center account ID; required for Shopping."),
|
|
9655
|
+
feedLabel: z7.string().optional().describe("Merchant Center feed label for Shopping."),
|
|
9656
|
+
validateOnly: z7.boolean().optional().describe("Validate the atomic request with Google without creating any resources."),
|
|
8484
9657
|
confirm: confirmSchema
|
|
8485
9658
|
},
|
|
8486
9659
|
async (a) => {
|
|
8487
|
-
const
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
} catch (error) {
|
|
8492
|
-
return ko(error instanceof Error ? error.message : String(error));
|
|
8493
|
-
}
|
|
8494
|
-
if (!a.confirm) {
|
|
9660
|
+
const { customerId, name, channelType, dailyBudget, loginCustomerId, confirm } = a;
|
|
9661
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9662
|
+
const micros = toMicros(Number(dailyBudget));
|
|
9663
|
+
if (!confirm && !a.validateOnly) {
|
|
8495
9664
|
return preview("google_ads_create_campaign", {
|
|
8496
9665
|
customer: cid,
|
|
8497
|
-
name
|
|
8498
|
-
channelType
|
|
8499
|
-
dailyBudget
|
|
9666
|
+
name,
|
|
9667
|
+
channelType,
|
|
9668
|
+
dailyBudget,
|
|
8500
9669
|
inMicros: micros,
|
|
8501
|
-
|
|
8502
|
-
|
|
9670
|
+
status: "PAUSED",
|
|
9671
|
+
euPoliticalAdvertising: a.euPoliticalAdvertising ?? "declaration required before execution",
|
|
9672
|
+
brandGuidelinesEnabled: a.brandGuidelinesEnabled,
|
|
9673
|
+
merchantId: a.merchantId,
|
|
9674
|
+
feedLabel: a.feedLabel,
|
|
9675
|
+
biddingStrategy: ["PERFORMANCE_MAX", "DEMAND_GEN"].includes(String(channelType)) ? "MAXIMIZE_CONVERSIONS" : "MANUAL_CPC"
|
|
8503
9676
|
});
|
|
8504
9677
|
}
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
}
|
|
8519
|
-
const
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
}
|
|
8528
|
-
}
|
|
8529
|
-
], login);
|
|
9678
|
+
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.");
|
|
9679
|
+
if (typeof a.euPoliticalAdvertising !== "boolean") throw new Error("Declare euPoliticalAdvertising explicitly before creating the campaign.");
|
|
9680
|
+
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.");
|
|
9681
|
+
if (channelType === "SHOPPING" && !a.merchantId) throw new Error("Shopping requires merchantId for an accessible Merchant Center account.");
|
|
9682
|
+
const budgetName = `customers/${cid}/campaignBudgets/-1`;
|
|
9683
|
+
const campaignData = {
|
|
9684
|
+
name,
|
|
9685
|
+
status: "PAUSED",
|
|
9686
|
+
advertisingChannelType: channelType,
|
|
9687
|
+
campaignBudget: budgetName,
|
|
9688
|
+
containsEuPoliticalAdvertising: a.euPoliticalAdvertising ? "CONTAINS_EU_POLITICAL_ADVERTISING" : "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING",
|
|
9689
|
+
...["PERFORMANCE_MAX", "DEMAND_GEN"].includes(String(channelType)) ? { maximizeConversions: {}, ...channelType === "PERFORMANCE_MAX" ? { brandGuidelinesEnabled: false } : {} } : { manualCpc: {} },
|
|
9690
|
+
...channelType === "SHOPPING" ? { shoppingSetting: { merchantId: a.merchantId, campaignPriority: 0, feedLabel: a.feedLabel } } : {}
|
|
9691
|
+
};
|
|
9692
|
+
const result2 = await request(`${ADS_BASE}/customers/${cid}/googleAds:mutate`, {
|
|
9693
|
+
method: "POST",
|
|
9694
|
+
headers: await headers(loginCustomerId ?? config.loginCustomerId),
|
|
9695
|
+
body: JSON.stringify({ partialFailure: false, validateOnly: a.validateOnly === true, mutateOperations: [
|
|
9696
|
+
{ campaignBudgetOperation: { create: { resourceName: budgetName, name: `${name} budget ${randomUUID()}`, amountMicros: String(micros), deliveryMethod: "STANDARD", explicitlyShared: false } } },
|
|
9697
|
+
{ campaignOperation: { create: campaignData } }
|
|
9698
|
+
] })
|
|
9699
|
+
}, "Google Ads campaign creation");
|
|
8530
9700
|
return ok2({
|
|
8531
|
-
applied: true,
|
|
9701
|
+
applied: a.validateOnly !== true,
|
|
9702
|
+
validatedByGoogle: a.validateOnly === true,
|
|
8532
9703
|
action: "google_ads_create_campaign",
|
|
8533
9704
|
status: "PAUSED",
|
|
8534
|
-
budget:
|
|
8535
|
-
result:
|
|
9705
|
+
budget: result2.mutateOperationResponses?.[0]?.campaignBudgetResult?.resourceName,
|
|
9706
|
+
result: { results: result2.mutateOperationResponses?.flatMap((r) => r.campaignResult ? [r.campaignResult] : []) ?? [] }
|
|
8536
9707
|
});
|
|
8537
9708
|
}
|
|
8538
9709
|
);
|
|
8539
|
-
|
|
9710
|
+
c.tool(
|
|
8540
9711
|
"google_ads_update_campaign_budget",
|
|
8541
9712
|
"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.",
|
|
8542
9713
|
{
|
|
8543
|
-
customerId:
|
|
8544
|
-
budgetId:
|
|
8545
|
-
dailyAmount:
|
|
8546
|
-
loginCustomerId:
|
|
9714
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9715
|
+
budgetId: z7.string().describe("Campaign budget ID (campaign_budget.id)."),
|
|
9716
|
+
dailyAmount: z7.number().positive().describe("New daily budget, in the account currency."),
|
|
9717
|
+
loginCustomerId: z7.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
8547
9718
|
confirm: confirmSchema
|
|
8548
9719
|
},
|
|
8549
9720
|
async (a) => {
|
|
8550
|
-
const
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
} catch (error) {
|
|
8555
|
-
return ko(error instanceof Error ? error.message : String(error));
|
|
8556
|
-
}
|
|
8557
|
-
if (!a.confirm) {
|
|
9721
|
+
const { customerId, budgetId, dailyAmount, loginCustomerId, confirm } = a;
|
|
9722
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9723
|
+
const micros = toMicros(Number(dailyAmount));
|
|
9724
|
+
if (!confirm) {
|
|
8558
9725
|
return preview("google_ads_update_campaign_budget", {
|
|
8559
9726
|
customer: cid,
|
|
8560
|
-
budget:
|
|
8561
|
-
newDailyBudget:
|
|
9727
|
+
budget: budgetId,
|
|
9728
|
+
newDailyBudget: dailyAmount,
|
|
8562
9729
|
inMicros: micros
|
|
8563
9730
|
});
|
|
8564
9731
|
}
|
|
8565
|
-
const
|
|
9732
|
+
const result2 = await request(
|
|
9733
|
+
`${ADS_BASE}/customers/${cid}/campaignBudgets:mutate`,
|
|
8566
9734
|
{
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
9735
|
+
method: "POST",
|
|
9736
|
+
headers: await headers(loginCustomerId),
|
|
9737
|
+
body: JSON.stringify({
|
|
9738
|
+
operations: [
|
|
9739
|
+
{
|
|
9740
|
+
update: { resourceName: `customers/${cid}/campaignBudgets/${budgetId}`, amountMicros: String(micros) },
|
|
9741
|
+
updateMask: "amount_micros"
|
|
9742
|
+
}
|
|
9743
|
+
]
|
|
9744
|
+
})
|
|
9745
|
+
},
|
|
9746
|
+
"Google Ads budget update"
|
|
9747
|
+
);
|
|
9748
|
+
return ok2({ applied: true, action: "google_ads_update_campaign_budget", result: result2 });
|
|
8575
9749
|
}
|
|
8576
9750
|
);
|
|
8577
|
-
|
|
8578
|
-
"
|
|
8579
|
-
"
|
|
9751
|
+
c.tool(
|
|
9752
|
+
"google_ads_rename_campaign",
|
|
9753
|
+
"Rename a Google Ads campaign. The name is the only thing that changes: delivery, budget and targeting are untouched. Previews by default.",
|
|
8580
9754
|
{
|
|
8581
|
-
customerId:
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
loginCustomerId:
|
|
9755
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9756
|
+
campaignId: z7.string().describe("Campaign ID."),
|
|
9757
|
+
name: z7.string().min(1).max(255).describe("New campaign name."),
|
|
9758
|
+
loginCustomerId: z7.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
8585
9759
|
confirm: confirmSchema
|
|
8586
9760
|
},
|
|
8587
9761
|
async (a) => {
|
|
8588
|
-
const
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
return ko(error instanceof Error ? error.message : String(error));
|
|
8594
|
-
}
|
|
8595
|
-
if (!a.confirm) {
|
|
8596
|
-
return preview("google_ads_update_adgroup_bid", {
|
|
8597
|
-
customer: cid,
|
|
8598
|
-
adGroup: a.adGroupId,
|
|
8599
|
-
newCpcBid: a.cpcBid,
|
|
8600
|
-
inMicros: micros
|
|
8601
|
-
});
|
|
8602
|
-
}
|
|
8603
|
-
const result = await client.mutate(cid, "adGroups", [
|
|
9762
|
+
const { customerId, campaignId, name, loginCustomerId, confirm } = a;
|
|
9763
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9764
|
+
if (!confirm) return preview("google_ads_rename_campaign", { customer: cid, campaign: campaignId, newName: name });
|
|
9765
|
+
const result2 = await request(
|
|
9766
|
+
`${ADS_BASE}/customers/${cid}/campaigns:mutate`,
|
|
8604
9767
|
{
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
}
|
|
8611
|
-
|
|
8612
|
-
|
|
9768
|
+
method: "POST",
|
|
9769
|
+
headers: await headers(loginCustomerId),
|
|
9770
|
+
body: JSON.stringify({
|
|
9771
|
+
operations: [{ update: { resourceName: `customers/${cid}/campaigns/${campaignId}`, name }, updateMask: "name" }]
|
|
9772
|
+
})
|
|
9773
|
+
},
|
|
9774
|
+
"Google Ads rename"
|
|
9775
|
+
);
|
|
9776
|
+
return ok2({ applied: true, action: "google_ads_rename_campaign", result: result2 });
|
|
8613
9777
|
}
|
|
8614
9778
|
);
|
|
8615
|
-
|
|
9779
|
+
c.tool(
|
|
8616
9780
|
"google_ads_update_campaign_schedule",
|
|
8617
9781
|
"Change the start or end date of a Google Ads campaign. Dates are YYYY-MM-DD in the account time zone. Previews by default.",
|
|
8618
9782
|
{
|
|
8619
|
-
customerId:
|
|
8620
|
-
campaignId:
|
|
8621
|
-
startDate:
|
|
8622
|
-
endDate:
|
|
8623
|
-
loginCustomerId:
|
|
9783
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9784
|
+
campaignId: z7.string().describe("Campaign ID."),
|
|
9785
|
+
startDate: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Start date, YYYY-MM-DD, in the account time zone."),
|
|
9786
|
+
endDate: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("Use 2037-12-30 to clear the end date (run indefinitely)."),
|
|
9787
|
+
loginCustomerId: z7.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
8624
9788
|
confirm: confirmSchema
|
|
8625
9789
|
},
|
|
8626
9790
|
async (a) => {
|
|
8627
|
-
const { startDate, endDate } = a;
|
|
9791
|
+
const { customerId, campaignId, startDate, endDate, loginCustomerId, confirm } = a;
|
|
8628
9792
|
if (!startDate && !endDate) return ko("Provide startDate, endDate, or both.");
|
|
8629
|
-
|
|
9793
|
+
for (const date of [startDate, endDate]) {
|
|
9794
|
+
if (date && (Number.isNaN(Date.parse(String(date))) || new Date(String(date)).toISOString().slice(0, 10) !== date)) return ko("Invalid calendar date.");
|
|
9795
|
+
}
|
|
9796
|
+
if (startDate && endDate && String(startDate) > String(endDate)) {
|
|
8630
9797
|
return ko(`startDate ${startDate} is after endDate ${endDate}.`);
|
|
8631
9798
|
}
|
|
8632
|
-
const cid =
|
|
8633
|
-
const update = {
|
|
8634
|
-
resourceName: resourceName(cid, "campaigns", a.campaignId)
|
|
8635
|
-
};
|
|
9799
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9800
|
+
const update = { resourceName: `customers/${cid}/campaigns/${campaignId}` };
|
|
8636
9801
|
const mask = [];
|
|
8637
9802
|
if (startDate) {
|
|
8638
|
-
update.
|
|
8639
|
-
mask.push("
|
|
9803
|
+
update.startDateTime = `${startDate} 00:00:00`;
|
|
9804
|
+
mask.push("start_date_time");
|
|
8640
9805
|
}
|
|
8641
9806
|
if (endDate) {
|
|
8642
|
-
update.
|
|
8643
|
-
mask.push("
|
|
8644
|
-
}
|
|
8645
|
-
if (!a.confirm) {
|
|
8646
|
-
return preview("google_ads_update_campaign_schedule", {
|
|
8647
|
-
customer: cid,
|
|
8648
|
-
campaign: a.campaignId,
|
|
8649
|
-
startDate,
|
|
8650
|
-
endDate
|
|
8651
|
-
});
|
|
9807
|
+
if (endDate !== "2037-12-30") update.endDateTime = `${endDate} 23:59:59`;
|
|
9808
|
+
mask.push("end_date_time");
|
|
8652
9809
|
}
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
9810
|
+
if (!confirm) return preview("google_ads_update_campaign_schedule", { customer: cid, campaign: campaignId, startDate, endDate });
|
|
9811
|
+
const result2 = await request(
|
|
9812
|
+
`${ADS_BASE}/customers/${cid}/campaigns:mutate`,
|
|
9813
|
+
{
|
|
9814
|
+
method: "POST",
|
|
9815
|
+
headers: await headers(loginCustomerId),
|
|
9816
|
+
body: JSON.stringify({ operations: [{ update, updateMask: mask.join(",") }] })
|
|
9817
|
+
},
|
|
9818
|
+
"Google Ads schedule update"
|
|
9819
|
+
);
|
|
9820
|
+
return ok2({ applied: true, action: "google_ads_update_campaign_schedule", result: result2 });
|
|
9821
|
+
}
|
|
9822
|
+
);
|
|
9823
|
+
c.tool(
|
|
9824
|
+
"google_ads_update_adgroup_status",
|
|
9825
|
+
"Pause, re-enable or remove a Google Ads ad group. Previews by default.",
|
|
9826
|
+
{
|
|
9827
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9828
|
+
adGroupId: z7.string().describe("Ad group ID."),
|
|
9829
|
+
status: z7.enum(["PAUSED", "ENABLED", "REMOVED"]).describe("New status."),
|
|
9830
|
+
loginCustomerId: z7.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
9831
|
+
confirm: confirmSchema
|
|
9832
|
+
},
|
|
9833
|
+
async (a) => {
|
|
9834
|
+
const { customerId, adGroupId, status, loginCustomerId, confirm } = a;
|
|
9835
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9836
|
+
if (!confirm) return preview("google_ads_update_adgroup_status", { customer: cid, adGroup: adGroupId, newStatus: status });
|
|
9837
|
+
const result2 = await request(
|
|
9838
|
+
`${ADS_BASE}/customers/${cid}/adGroups:mutate`,
|
|
9839
|
+
{
|
|
9840
|
+
method: "POST",
|
|
9841
|
+
headers: await headers(loginCustomerId),
|
|
9842
|
+
body: JSON.stringify({
|
|
9843
|
+
operations: [status === "REMOVED" ? { remove: `customers/${cid}/adGroups/${adGroupId}` } : { update: { resourceName: `customers/${cid}/adGroups/${adGroupId}`, status }, updateMask: "status" }]
|
|
9844
|
+
})
|
|
9845
|
+
},
|
|
9846
|
+
"Google Ads ad group status"
|
|
9847
|
+
);
|
|
9848
|
+
return ok2({ applied: true, action: "google_ads_update_adgroup_status", result: result2 });
|
|
9849
|
+
}
|
|
9850
|
+
);
|
|
9851
|
+
c.tool(
|
|
9852
|
+
"google_ads_update_adgroup_bid",
|
|
9853
|
+
"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.",
|
|
9854
|
+
{
|
|
9855
|
+
customerId: z7.string().describe("Google Ads customer ID, with or without dashes."),
|
|
9856
|
+
adGroupId: z7.string().describe("Ad group ID."),
|
|
9857
|
+
cpcBid: z7.number().positive().describe("New default CPC bid, in the account currency."),
|
|
9858
|
+
loginCustomerId: z7.string().optional().describe("Manager account ID, when the customer sits under an MCC. Omit otherwise."),
|
|
9859
|
+
confirm: confirmSchema
|
|
9860
|
+
},
|
|
9861
|
+
async (a) => {
|
|
9862
|
+
const { customerId, adGroupId, cpcBid, loginCustomerId, confirm } = a;
|
|
9863
|
+
const cid = String(customerId).replace(/-/g, "");
|
|
9864
|
+
const micros = toMicros(Number(cpcBid));
|
|
9865
|
+
if (!confirm) return preview("google_ads_update_adgroup_bid", { customer: cid, adGroup: adGroupId, newCpcBid: cpcBid, inMicros: micros });
|
|
9866
|
+
const result2 = await request(
|
|
9867
|
+
`${ADS_BASE}/customers/${cid}/adGroups:mutate`,
|
|
9868
|
+
{
|
|
9869
|
+
method: "POST",
|
|
9870
|
+
headers: await headers(loginCustomerId),
|
|
9871
|
+
body: JSON.stringify({
|
|
9872
|
+
operations: [{
|
|
9873
|
+
update: { resourceName: `customers/${cid}/adGroups/${adGroupId}`, cpcBidMicros: String(micros) },
|
|
9874
|
+
updateMask: "cpc_bid_micros"
|
|
9875
|
+
}]
|
|
9876
|
+
})
|
|
9877
|
+
},
|
|
9878
|
+
"Google Ads ad group bid"
|
|
9879
|
+
);
|
|
9880
|
+
return ok2({ applied: true, action: "google_ads_update_adgroup_bid", result: result2 });
|
|
8657
9881
|
}
|
|
8658
9882
|
);
|
|
8659
9883
|
}
|
|
@@ -8661,21 +9885,23 @@ function registerGoogleAdsWrites(server, config) {
|
|
|
8661
9885
|
// src/platforms/google-ads/index.ts
|
|
8662
9886
|
function registerGoogleAds(server, config) {
|
|
8663
9887
|
registerGoogleAdsTools(server, config);
|
|
9888
|
+
registerGoogleVideoReads(server, config);
|
|
8664
9889
|
registerGoogleAdsResources(server);
|
|
8665
|
-
logger.info("google-ads", "Registered
|
|
9890
|
+
logger.info("google-ads", "Registered 35 read tools and 5 resources");
|
|
8666
9891
|
if (config.enableWrites) {
|
|
8667
9892
|
registerGoogleAdsWrites(server, config);
|
|
8668
|
-
logger.info("google-ads", "Registered
|
|
9893
|
+
logger.info("google-ads", "Registered 10 write tools (every one previews before it applies)");
|
|
8669
9894
|
}
|
|
8670
9895
|
}
|
|
8671
9896
|
|
|
8672
9897
|
// src/server.ts
|
|
8673
|
-
var PACKAGE_VERSION = "
|
|
9898
|
+
var PACKAGE_VERSION = "2.0.0";
|
|
8674
9899
|
function createServer(config) {
|
|
8675
9900
|
const server = new McpServer(
|
|
8676
|
-
{ name: "google-ads-mcp", version: PACKAGE_VERSION },
|
|
9901
|
+
{ 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" }] },
|
|
8677
9902
|
{ capabilities: { tools: { listChanged: true }, resources: { subscribe: false, listChanged: true } } }
|
|
8678
9903
|
);
|
|
9904
|
+
installToolQuality(server);
|
|
8679
9905
|
registerGoogleAds(server, config);
|
|
8680
9906
|
logger.system(
|
|
8681
9907
|
`google-ads-mcp v${PACKAGE_VERSION} ready, writes ${config.enableWrites ? "enabled" : "disabled"}`
|
|
@@ -8684,16 +9910,16 @@ function createServer(config) {
|
|
|
8684
9910
|
}
|
|
8685
9911
|
|
|
8686
9912
|
// src/config.ts
|
|
8687
|
-
import { z as
|
|
8688
|
-
var configSchema =
|
|
8689
|
-
developerToken:
|
|
8690
|
-
clientId:
|
|
8691
|
-
clientSecret:
|
|
8692
|
-
refreshToken:
|
|
8693
|
-
loginCustomerId:
|
|
9913
|
+
import { z as z8 } from "zod";
|
|
9914
|
+
var configSchema = z8.object({
|
|
9915
|
+
developerToken: z8.string().min(1, "GOOGLE_ADS_DEVELOPER_TOKEN is required"),
|
|
9916
|
+
clientId: z8.string().min(1, "GOOGLE_ADS_CLIENT_ID is required"),
|
|
9917
|
+
clientSecret: z8.string().min(1, "GOOGLE_ADS_CLIENT_SECRET is required"),
|
|
9918
|
+
refreshToken: z8.string().min(1, "GOOGLE_ADS_REFRESH_TOKEN is required"),
|
|
9919
|
+
loginCustomerId: z8.string().optional(),
|
|
8694
9920
|
/** Write tools are registered only when this is true. */
|
|
8695
|
-
enableWrites:
|
|
8696
|
-
logLevel:
|
|
9921
|
+
enableWrites: z8.boolean().optional(),
|
|
9922
|
+
logLevel: z8.enum(["debug", "info", "warn", "error"]).default("info")
|
|
8697
9923
|
});
|
|
8698
9924
|
function loadConfig() {
|
|
8699
9925
|
const raw = {
|
|
@@ -8705,13 +9931,13 @@ function loadConfig() {
|
|
|
8705
9931
|
enableWrites: isTruthy(process.env["GOOGLE_ADS_ENABLE_WRITES"]),
|
|
8706
9932
|
logLevel: process.env["LOG_LEVEL"] ?? "info"
|
|
8707
9933
|
};
|
|
8708
|
-
const
|
|
8709
|
-
if (!
|
|
8710
|
-
const missing =
|
|
9934
|
+
const result2 = configSchema.safeParse(raw);
|
|
9935
|
+
if (!result2.success) {
|
|
9936
|
+
const missing = result2.error.issues.map((i) => i.message).join(", ");
|
|
8711
9937
|
logger.error("config", `Missing credentials: ${missing}`);
|
|
8712
9938
|
throw new Error(`Missing Google Ads credentials: ${missing}`);
|
|
8713
9939
|
}
|
|
8714
|
-
return
|
|
9940
|
+
return result2.data;
|
|
8715
9941
|
}
|
|
8716
9942
|
function isTruthy(value) {
|
|
8717
9943
|
if (!value) return false;
|