mcp-google-ads 1.4.2 → 1.4.4
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/dist/assetHelpers.d.ts +43 -0
- package/dist/assetHelpers.js +93 -0
- package/dist/build-info.json +2 -2
- package/dist/index.js +252 -2
- package/dist/tools.js +49 -0
- package/dist/writeGate.js +3 -1
- package/package.json +2 -2
package/dist/assetHelpers.d.ts
CHANGED
|
@@ -52,4 +52,47 @@ export interface PauseLinksDryRun {
|
|
|
52
52
|
ad_group_asset: string[];
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
|
+
export interface CreateSitelinkArgs {
|
|
56
|
+
customer_id?: string;
|
|
57
|
+
link_text: string;
|
|
58
|
+
final_urls: string[];
|
|
59
|
+
description1?: string;
|
|
60
|
+
description2?: string;
|
|
61
|
+
confirm?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface CreateSitelinkDryRun {
|
|
64
|
+
dry_run: true;
|
|
65
|
+
message: string;
|
|
66
|
+
customer_id: string;
|
|
67
|
+
link_text: string;
|
|
68
|
+
final_urls: string[];
|
|
69
|
+
description1?: string;
|
|
70
|
+
description2?: string;
|
|
71
|
+
}
|
|
72
|
+
export declare function normalizeCreateSitelinkArgs(raw: Record<string, unknown> | undefined): CreateSitelinkArgs | {
|
|
73
|
+
error: string;
|
|
74
|
+
};
|
|
75
|
+
export declare function buildCreateSitelinkDryRun(args: CreateSitelinkArgs): CreateSitelinkDryRun;
|
|
76
|
+
export interface ReplaceSitelinkArgs {
|
|
77
|
+
customer_id?: string;
|
|
78
|
+
old_asset_id: string;
|
|
79
|
+
new_final_urls: string[];
|
|
80
|
+
new_link_text?: string;
|
|
81
|
+
new_description1?: string;
|
|
82
|
+
new_description2?: string;
|
|
83
|
+
confirm?: boolean;
|
|
84
|
+
}
|
|
85
|
+
export interface ReplaceSitelinkDryRun {
|
|
86
|
+
dry_run: true;
|
|
87
|
+
message: string;
|
|
88
|
+
customer_id: string;
|
|
89
|
+
old_asset_id: string;
|
|
90
|
+
new_final_urls: string[];
|
|
91
|
+
new_link_text_override?: string;
|
|
92
|
+
warning: string;
|
|
93
|
+
}
|
|
94
|
+
export declare function normalizeReplaceSitelinkArgs(raw: Record<string, unknown> | undefined): ReplaceSitelinkArgs | {
|
|
95
|
+
error: string;
|
|
96
|
+
};
|
|
97
|
+
export declare function buildReplaceSitelinkDryRun(args: ReplaceSitelinkArgs): ReplaceSitelinkDryRun;
|
|
55
98
|
export declare function buildPauseLinksDryRun(args: PauseAssetLinksArgs): PauseLinksDryRun;
|
package/dist/assetHelpers.js
CHANGED
|
@@ -99,6 +99,95 @@ function buildUpdateUrlsDryRun(args) {
|
|
|
99
99
|
warning: "Updating an asset's final_urls affects EVERY campaign/ad group/customer link that uses this asset ID. Verify attachments before confirming."
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
function validateSitelinkText(value, field, max) {
|
|
103
|
+
if (typeof value !== "string") return { ok: false, error: `${field} must be a string` };
|
|
104
|
+
const t = value.trim();
|
|
105
|
+
if (!t) return { ok: false, error: `${field} must be non-empty` };
|
|
106
|
+
if (t.length > max) return { ok: false, error: `${field} exceeds ${max} chars (got ${t.length})` };
|
|
107
|
+
return { ok: true, value: t };
|
|
108
|
+
}
|
|
109
|
+
function normalizeCreateSitelinkArgs(raw) {
|
|
110
|
+
const r = raw ?? {};
|
|
111
|
+
const customer_id = typeof r.customer_id === "string" ? r.customer_id : void 0;
|
|
112
|
+
const linkText = validateSitelinkText(r.link_text, "link_text", 25);
|
|
113
|
+
if (!linkText.ok) return { error: linkText.error };
|
|
114
|
+
const urls = validateFinalUrls(r.final_urls);
|
|
115
|
+
if (!urls.ok) return { error: urls.error };
|
|
116
|
+
const out = {
|
|
117
|
+
customer_id,
|
|
118
|
+
link_text: linkText.value,
|
|
119
|
+
final_urls: urls.urls,
|
|
120
|
+
confirm: r.confirm === true || r.confirm === "true"
|
|
121
|
+
};
|
|
122
|
+
if (r.description1 !== void 0 && r.description1 !== null && r.description1 !== "") {
|
|
123
|
+
const d = validateSitelinkText(r.description1, "description1", 35);
|
|
124
|
+
if (!d.ok) return { error: d.error };
|
|
125
|
+
out.description1 = d.value;
|
|
126
|
+
}
|
|
127
|
+
if (r.description2 !== void 0 && r.description2 !== null && r.description2 !== "") {
|
|
128
|
+
const d = validateSitelinkText(r.description2, "description2", 35);
|
|
129
|
+
if (!d.ok) return { error: d.error };
|
|
130
|
+
out.description2 = d.value;
|
|
131
|
+
}
|
|
132
|
+
if (out.description1 && !out.description2 || !out.description1 && out.description2) {
|
|
133
|
+
return { error: "description1 and description2 must both be set or both omitted" };
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
function buildCreateSitelinkDryRun(args) {
|
|
138
|
+
return {
|
|
139
|
+
dry_run: true,
|
|
140
|
+
message: "DRY RUN. Nothing created. Pass confirm: true to actually create the sitelink asset.",
|
|
141
|
+
customer_id: args.customer_id ?? "",
|
|
142
|
+
link_text: args.link_text,
|
|
143
|
+
final_urls: args.final_urls,
|
|
144
|
+
description1: args.description1,
|
|
145
|
+
description2: args.description2
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function normalizeReplaceSitelinkArgs(raw) {
|
|
149
|
+
const r = raw ?? {};
|
|
150
|
+
const customer_id = typeof r.customer_id === "string" ? r.customer_id : void 0;
|
|
151
|
+
const assetIdRaw = typeof r.old_asset_id === "string" ? r.old_asset_id.trim() : typeof r.old_asset_id === "number" ? String(r.old_asset_id) : "";
|
|
152
|
+
if (!assetIdRaw || !/^\d+$/.test(assetIdRaw)) {
|
|
153
|
+
return { error: `invalid old_asset_id: ${JSON.stringify(r.old_asset_id)}. Expected numeric asset ID.` };
|
|
154
|
+
}
|
|
155
|
+
const urls = validateFinalUrls(r.new_final_urls);
|
|
156
|
+
if (!urls.ok) return { error: urls.error };
|
|
157
|
+
const out = {
|
|
158
|
+
customer_id,
|
|
159
|
+
old_asset_id: assetIdRaw,
|
|
160
|
+
new_final_urls: urls.urls,
|
|
161
|
+
confirm: r.confirm === true || r.confirm === "true"
|
|
162
|
+
};
|
|
163
|
+
if (r.new_link_text !== void 0 && r.new_link_text !== null && r.new_link_text !== "") {
|
|
164
|
+
const d = validateSitelinkText(r.new_link_text, "new_link_text", 25);
|
|
165
|
+
if (!d.ok) return { error: d.error };
|
|
166
|
+
out.new_link_text = d.value;
|
|
167
|
+
}
|
|
168
|
+
if (r.new_description1 !== void 0 && r.new_description1 !== null && r.new_description1 !== "") {
|
|
169
|
+
const d = validateSitelinkText(r.new_description1, "new_description1", 35);
|
|
170
|
+
if (!d.ok) return { error: d.error };
|
|
171
|
+
out.new_description1 = d.value;
|
|
172
|
+
}
|
|
173
|
+
if (r.new_description2 !== void 0 && r.new_description2 !== null && r.new_description2 !== "") {
|
|
174
|
+
const d = validateSitelinkText(r.new_description2, "new_description2", 35);
|
|
175
|
+
if (!d.ok) return { error: d.error };
|
|
176
|
+
out.new_description2 = d.value;
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
function buildReplaceSitelinkDryRun(args) {
|
|
181
|
+
return {
|
|
182
|
+
dry_run: true,
|
|
183
|
+
message: "DRY RUN. Nothing changed. Pass confirm: true to replace the sitelink.",
|
|
184
|
+
customer_id: args.customer_id ?? "",
|
|
185
|
+
old_asset_id: args.old_asset_id,
|
|
186
|
+
new_final_urls: args.new_final_urls,
|
|
187
|
+
new_link_text_override: args.new_link_text,
|
|
188
|
+
warning: "Will create a NEW sitelink asset, re-link every campaign/ad-group/customer link currently pointing at old_asset_id, then remove the old links. The old Asset itself is NOT deleted and can still be re-used manually."
|
|
189
|
+
};
|
|
190
|
+
}
|
|
102
191
|
function buildPauseLinksDryRun(args) {
|
|
103
192
|
const would = {
|
|
104
193
|
customer_asset: [],
|
|
@@ -119,9 +208,13 @@ function buildPauseLinksDryRun(args) {
|
|
|
119
208
|
};
|
|
120
209
|
}
|
|
121
210
|
export {
|
|
211
|
+
buildCreateSitelinkDryRun,
|
|
122
212
|
buildPauseLinksDryRun,
|
|
213
|
+
buildReplaceSitelinkDryRun,
|
|
123
214
|
buildUpdateUrlsDryRun,
|
|
215
|
+
normalizeCreateSitelinkArgs,
|
|
124
216
|
normalizePauseAssetLinksArgs,
|
|
217
|
+
normalizeReplaceSitelinkArgs,
|
|
125
218
|
normalizeUpdateAssetUrlsArgs,
|
|
126
219
|
parseAssetLinkResourceName,
|
|
127
220
|
validateFinalUrls
|
package/dist/build-info.json
CHANGED
package/dist/index.js
CHANGED
|
@@ -28,7 +28,11 @@ import {
|
|
|
28
28
|
normalizeUpdateAssetUrlsArgs,
|
|
29
29
|
normalizePauseAssetLinksArgs,
|
|
30
30
|
buildUpdateUrlsDryRun,
|
|
31
|
-
buildPauseLinksDryRun
|
|
31
|
+
buildPauseLinksDryRun,
|
|
32
|
+
normalizeCreateSitelinkArgs,
|
|
33
|
+
buildCreateSitelinkDryRun,
|
|
34
|
+
normalizeReplaceSitelinkArgs,
|
|
35
|
+
buildReplaceSitelinkDryRun
|
|
32
36
|
} from "./assetHelpers.js";
|
|
33
37
|
import {
|
|
34
38
|
buildCampaignCreatePayload
|
|
@@ -1211,7 +1215,7 @@ class GoogleAdsManager {
|
|
|
1211
1215
|
else if (/\/adGroupAssets\//.test(rn)) byLevel.ad_group_asset.push(rn);
|
|
1212
1216
|
else throw new Error(`Unrecognized asset-link resource name: ${rn}`);
|
|
1213
1217
|
}
|
|
1214
|
-
const PAUSED =
|
|
1218
|
+
const PAUSED = enums.AssetLinkStatus.PAUSED;
|
|
1215
1219
|
const result = { customer_id: cleanId, paused: { customer_asset: 0, campaign_asset: 0, ad_group_asset: 0 } };
|
|
1216
1220
|
if (byLevel.customer_asset.length > 0) {
|
|
1217
1221
|
const ops = byLevel.customer_asset.map((rn) => ({ resource_name: rn, status: PAUSED }));
|
|
@@ -1240,6 +1244,217 @@ class GoogleAdsManager {
|
|
|
1240
1244
|
return result;
|
|
1241
1245
|
}
|
|
1242
1246
|
// ============================================
|
|
1247
|
+
// SITELINK CREATE + REPLACE
|
|
1248
|
+
// ============================================
|
|
1249
|
+
async createSitelink(customerId, args) {
|
|
1250
|
+
const customer = this.getCustomer(customerId);
|
|
1251
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1252
|
+
const sitelink = { link_text: args.link_text };
|
|
1253
|
+
if (args.description1) sitelink.description1 = args.description1;
|
|
1254
|
+
if (args.description2) sitelink.description2 = args.description2;
|
|
1255
|
+
const result = await withResilience(
|
|
1256
|
+
() => customer.assets.create([
|
|
1257
|
+
{
|
|
1258
|
+
type: enums.AssetType.SITELINK,
|
|
1259
|
+
final_urls: args.final_urls,
|
|
1260
|
+
sitelink_asset: sitelink
|
|
1261
|
+
}
|
|
1262
|
+
]),
|
|
1263
|
+
"createSitelink"
|
|
1264
|
+
);
|
|
1265
|
+
const results = result.results || [];
|
|
1266
|
+
const resourceName = results[0]?.resource_name;
|
|
1267
|
+
const assetId = resourceName ? resourceName.split("/").pop() : void 0;
|
|
1268
|
+
if (resourceName) {
|
|
1269
|
+
await this.autoLabelCreated(customerId, [resourceName], "asset");
|
|
1270
|
+
}
|
|
1271
|
+
return {
|
|
1272
|
+
customer_id: cleanId,
|
|
1273
|
+
asset_id: assetId,
|
|
1274
|
+
resource_name: resourceName,
|
|
1275
|
+
link_text: args.link_text,
|
|
1276
|
+
final_urls: args.final_urls,
|
|
1277
|
+
description1: args.description1,
|
|
1278
|
+
description2: args.description2
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async replaceSitelinkUrl(customerId, args) {
|
|
1282
|
+
const customer = this.getCustomer(customerId);
|
|
1283
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1284
|
+
const oldAssetRows = await withResilience(
|
|
1285
|
+
() => customer.query(`
|
|
1286
|
+
SELECT
|
|
1287
|
+
asset.id,
|
|
1288
|
+
asset.type,
|
|
1289
|
+
asset.final_urls,
|
|
1290
|
+
asset.sitelink_asset.link_text,
|
|
1291
|
+
asset.sitelink_asset.description1,
|
|
1292
|
+
asset.sitelink_asset.description2
|
|
1293
|
+
FROM asset
|
|
1294
|
+
WHERE asset.id = ${args.old_asset_id}
|
|
1295
|
+
`),
|
|
1296
|
+
"replaceSitelinkUrl.queryAsset"
|
|
1297
|
+
);
|
|
1298
|
+
const oldAsset = oldAssetRows[0]?.asset;
|
|
1299
|
+
if (!oldAsset) {
|
|
1300
|
+
throw new Error(`Asset ${args.old_asset_id} not found in customer ${cleanId}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (oldAsset.type !== enums.AssetType.SITELINK && oldAsset.type !== 13) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`Asset ${args.old_asset_id} is type ${oldAsset.type} (not SITELINK). This tool only replaces sitelink assets; use google_ads_update_asset_urls for other asset types.`
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
const link_text = args.new_link_text ?? oldAsset.sitelink_asset?.link_text;
|
|
1308
|
+
if (!link_text) {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`Old asset has no link_text and none was provided. Pass new_link_text to override.`
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
const description1 = args.new_description1 ?? oldAsset.sitelink_asset?.description1 ?? void 0;
|
|
1314
|
+
const description2 = args.new_description2 ?? oldAsset.sitelink_asset?.description2 ?? void 0;
|
|
1315
|
+
const campaignLinksRows = await withResilience(
|
|
1316
|
+
() => customer.query(`
|
|
1317
|
+
SELECT
|
|
1318
|
+
campaign_asset.resource_name,
|
|
1319
|
+
campaign_asset.campaign,
|
|
1320
|
+
campaign_asset.field_type,
|
|
1321
|
+
campaign_asset.status
|
|
1322
|
+
FROM campaign_asset
|
|
1323
|
+
WHERE campaign_asset.asset = 'customers/${cleanId}/assets/${args.old_asset_id}'
|
|
1324
|
+
AND campaign_asset.field_type = 'SITELINK'
|
|
1325
|
+
AND campaign_asset.status = 'ENABLED'
|
|
1326
|
+
`),
|
|
1327
|
+
"replaceSitelinkUrl.queryCampaignAssets"
|
|
1328
|
+
);
|
|
1329
|
+
const adGroupLinksRows = await withResilience(
|
|
1330
|
+
() => customer.query(`
|
|
1331
|
+
SELECT
|
|
1332
|
+
ad_group_asset.resource_name,
|
|
1333
|
+
ad_group_asset.ad_group,
|
|
1334
|
+
ad_group_asset.field_type,
|
|
1335
|
+
ad_group_asset.status
|
|
1336
|
+
FROM ad_group_asset
|
|
1337
|
+
WHERE ad_group_asset.asset = 'customers/${cleanId}/assets/${args.old_asset_id}'
|
|
1338
|
+
AND ad_group_asset.field_type = 'SITELINK'
|
|
1339
|
+
AND ad_group_asset.status = 'ENABLED'
|
|
1340
|
+
`),
|
|
1341
|
+
"replaceSitelinkUrl.queryAdGroupAssets"
|
|
1342
|
+
);
|
|
1343
|
+
const customerLinksRows = await withResilience(
|
|
1344
|
+
() => customer.query(`
|
|
1345
|
+
SELECT
|
|
1346
|
+
customer_asset.resource_name,
|
|
1347
|
+
customer_asset.field_type,
|
|
1348
|
+
customer_asset.status
|
|
1349
|
+
FROM customer_asset
|
|
1350
|
+
WHERE customer_asset.asset = 'customers/${cleanId}/assets/${args.old_asset_id}'
|
|
1351
|
+
AND customer_asset.field_type = 'SITELINK'
|
|
1352
|
+
AND customer_asset.status = 'ENABLED'
|
|
1353
|
+
`),
|
|
1354
|
+
"replaceSitelinkUrl.queryCustomerAssets"
|
|
1355
|
+
);
|
|
1356
|
+
const campaignLinks = campaignLinksRows.map((r) => ({
|
|
1357
|
+
resource_name: r.campaign_asset.resource_name,
|
|
1358
|
+
campaign: r.campaign_asset.campaign
|
|
1359
|
+
}));
|
|
1360
|
+
const adGroupLinks = adGroupLinksRows.map((r) => ({
|
|
1361
|
+
resource_name: r.ad_group_asset.resource_name,
|
|
1362
|
+
ad_group: r.ad_group_asset.ad_group
|
|
1363
|
+
}));
|
|
1364
|
+
const customerLinks = customerLinksRows.map((r) => ({
|
|
1365
|
+
resource_name: r.customer_asset.resource_name
|
|
1366
|
+
}));
|
|
1367
|
+
const newSitelink = { link_text };
|
|
1368
|
+
if (description1) newSitelink.description1 = description1;
|
|
1369
|
+
if (description2) newSitelink.description2 = description2;
|
|
1370
|
+
const createResult = await withResilience(
|
|
1371
|
+
() => customer.assets.create([
|
|
1372
|
+
{
|
|
1373
|
+
type: enums.AssetType.SITELINK,
|
|
1374
|
+
final_urls: args.new_final_urls,
|
|
1375
|
+
sitelink_asset: newSitelink
|
|
1376
|
+
}
|
|
1377
|
+
]),
|
|
1378
|
+
"replaceSitelinkUrl.createAsset"
|
|
1379
|
+
);
|
|
1380
|
+
const newAssetResource = createResult.results?.[0]?.resource_name;
|
|
1381
|
+
if (!newAssetResource) {
|
|
1382
|
+
throw new Error("Failed to create replacement sitelink asset (no resource_name in response)");
|
|
1383
|
+
}
|
|
1384
|
+
const newAssetId = newAssetResource.split("/").pop() || "";
|
|
1385
|
+
await this.autoLabelCreated(customerId, [newAssetResource], "asset");
|
|
1386
|
+
const SITELINK_FIELD_TYPE = enums.AssetFieldType.SITELINK;
|
|
1387
|
+
if (campaignLinks.length > 0) {
|
|
1388
|
+
await withResilience(
|
|
1389
|
+
() => customer.campaignAssets.create(
|
|
1390
|
+
campaignLinks.map((l) => ({
|
|
1391
|
+
campaign: l.campaign,
|
|
1392
|
+
asset: newAssetResource,
|
|
1393
|
+
field_type: SITELINK_FIELD_TYPE
|
|
1394
|
+
}))
|
|
1395
|
+
),
|
|
1396
|
+
"replaceSitelinkUrl.createCampaignAssets"
|
|
1397
|
+
);
|
|
1398
|
+
}
|
|
1399
|
+
if (adGroupLinks.length > 0) {
|
|
1400
|
+
await withResilience(
|
|
1401
|
+
() => customer.adGroupAssets.create(
|
|
1402
|
+
adGroupLinks.map((l) => ({
|
|
1403
|
+
ad_group: l.ad_group,
|
|
1404
|
+
asset: newAssetResource,
|
|
1405
|
+
field_type: SITELINK_FIELD_TYPE
|
|
1406
|
+
}))
|
|
1407
|
+
),
|
|
1408
|
+
"replaceSitelinkUrl.createAdGroupAssets"
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
if (customerLinks.length > 0) {
|
|
1412
|
+
await withResilience(
|
|
1413
|
+
() => customer.customerAssets.create(
|
|
1414
|
+
customerLinks.map(() => ({
|
|
1415
|
+
asset: newAssetResource,
|
|
1416
|
+
field_type: SITELINK_FIELD_TYPE
|
|
1417
|
+
}))
|
|
1418
|
+
),
|
|
1419
|
+
"replaceSitelinkUrl.createCustomerAssets"
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
if (campaignLinks.length > 0) {
|
|
1423
|
+
await withResilience(
|
|
1424
|
+
() => customer.campaignAssets.remove(campaignLinks.map((l) => l.resource_name)),
|
|
1425
|
+
"replaceSitelinkUrl.removeCampaignAssets"
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
if (adGroupLinks.length > 0) {
|
|
1429
|
+
await withResilience(
|
|
1430
|
+
() => customer.adGroupAssets.remove(adGroupLinks.map((l) => l.resource_name)),
|
|
1431
|
+
"replaceSitelinkUrl.removeAdGroupAssets"
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
if (customerLinks.length > 0) {
|
|
1435
|
+
await withResilience(
|
|
1436
|
+
() => customer.customerAssets.remove(customerLinks.map((l) => l.resource_name)),
|
|
1437
|
+
"replaceSitelinkUrl.removeCustomerAssets"
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
return {
|
|
1441
|
+
customer_id: cleanId,
|
|
1442
|
+
old_asset_id: args.old_asset_id,
|
|
1443
|
+
new_asset_id: newAssetId,
|
|
1444
|
+
new_asset_resource_name: newAssetResource,
|
|
1445
|
+
link_text,
|
|
1446
|
+
final_urls: args.new_final_urls,
|
|
1447
|
+
description1,
|
|
1448
|
+
description2,
|
|
1449
|
+
relinked: {
|
|
1450
|
+
campaign_assets: campaignLinks.length,
|
|
1451
|
+
ad_group_assets: adGroupLinks.length,
|
|
1452
|
+
customer_assets: customerLinks.length
|
|
1453
|
+
},
|
|
1454
|
+
note: "The old Asset is not deleted; only its ENABLED links were migrated. Paused/removed links on the old asset were left alone."
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
// ============================================
|
|
1243
1458
|
// REPORTING METHODS
|
|
1244
1459
|
// ============================================
|
|
1245
1460
|
// Get keyword performance report
|
|
@@ -2444,6 +2659,41 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
2444
2659
|
const result = await adsManager.updateAssetUrls(customerId, normalized.updates);
|
|
2445
2660
|
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
2446
2661
|
}
|
|
2662
|
+
case "google_ads_create_sitelink": {
|
|
2663
|
+
const normalized = normalizeCreateSitelinkArgs(args);
|
|
2664
|
+
if ("error" in normalized) {
|
|
2665
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: normalized.error }, null, 2) }] };
|
|
2666
|
+
}
|
|
2667
|
+
if (!normalized.confirm) {
|
|
2668
|
+
return { content: [{ type: "text", text: JSON.stringify(buildCreateSitelinkDryRun(normalized), null, 2) }] };
|
|
2669
|
+
}
|
|
2670
|
+
const customerId = normalized.customer_id || "";
|
|
2671
|
+
const result = await adsManager.createSitelink(customerId, {
|
|
2672
|
+
link_text: normalized.link_text,
|
|
2673
|
+
final_urls: normalized.final_urls,
|
|
2674
|
+
description1: normalized.description1,
|
|
2675
|
+
description2: normalized.description2
|
|
2676
|
+
});
|
|
2677
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
2678
|
+
}
|
|
2679
|
+
case "google_ads_replace_sitelink_url": {
|
|
2680
|
+
const normalized = normalizeReplaceSitelinkArgs(args);
|
|
2681
|
+
if ("error" in normalized) {
|
|
2682
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: normalized.error }, null, 2) }] };
|
|
2683
|
+
}
|
|
2684
|
+
if (!normalized.confirm) {
|
|
2685
|
+
return { content: [{ type: "text", text: JSON.stringify(buildReplaceSitelinkDryRun(normalized), null, 2) }] };
|
|
2686
|
+
}
|
|
2687
|
+
const customerId = normalized.customer_id || "";
|
|
2688
|
+
const result = await adsManager.replaceSitelinkUrl(customerId, {
|
|
2689
|
+
old_asset_id: normalized.old_asset_id,
|
|
2690
|
+
new_final_urls: normalized.new_final_urls,
|
|
2691
|
+
new_link_text: normalized.new_link_text,
|
|
2692
|
+
new_description1: normalized.new_description1,
|
|
2693
|
+
new_description2: normalized.new_description2
|
|
2694
|
+
});
|
|
2695
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
2696
|
+
}
|
|
2447
2697
|
case "google_ads_pause_asset_links": {
|
|
2448
2698
|
const normalized = normalizePauseAssetLinksArgs(args);
|
|
2449
2699
|
if ("error" in normalized) {
|
package/dist/tools.js
CHANGED
|
@@ -748,6 +748,55 @@ const tools = [
|
|
|
748
748
|
required: ["updates"]
|
|
749
749
|
}
|
|
750
750
|
},
|
|
751
|
+
{
|
|
752
|
+
name: "google_ads_create_sitelink",
|
|
753
|
+
description: "Create a new sitelink Asset (link_text + final_urls, optional two description lines). Sitelinks are shared across campaigns/ad groups -- use google_ads_replace_sitelink_url if the goal is to fix a broken URL on an existing sitelink (sitelink final_urls are immutable; the correct pattern is create new + re-link). DRY-RUN BY DEFAULT: omit `confirm` or pass `confirm: false` to preview.",
|
|
754
|
+
inputSchema: {
|
|
755
|
+
additionalProperties: false,
|
|
756
|
+
type: "object",
|
|
757
|
+
properties: {
|
|
758
|
+
customer_id: { type: "string" },
|
|
759
|
+
link_text: { type: "string", description: "Clickable sitelink label (max 25 chars)." },
|
|
760
|
+
final_urls: {
|
|
761
|
+
type: "array",
|
|
762
|
+
items: { type: "string" },
|
|
763
|
+
description: "Destination URLs. Must start with http:// or https://."
|
|
764
|
+
},
|
|
765
|
+
description1: { type: "string", description: "Optional description line 1 (max 35 chars). If set, description2 must also be set." },
|
|
766
|
+
description2: { type: "string", description: "Optional description line 2 (max 35 chars). If set, description1 must also be set." },
|
|
767
|
+
confirm: {
|
|
768
|
+
type: "boolean",
|
|
769
|
+
description: "Must be true to actually create. Omit or false for dry-run preview."
|
|
770
|
+
}
|
|
771
|
+
},
|
|
772
|
+
required: ["link_text", "final_urls"]
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
name: "google_ads_replace_sitelink_url",
|
|
777
|
+
description: "Fix a broken/outdated sitelink URL. Google Ads treats sitelink Asset.final_urls as immutable, so this creates a new sitelink asset with the corrected URL (preserving link_text + descriptions from the old one unless overridden), re-links every campaign / ad-group / customer-level attachment to the new asset, then removes the old links. The old Asset itself is NOT deleted. DRY-RUN BY DEFAULT.",
|
|
778
|
+
inputSchema: {
|
|
779
|
+
additionalProperties: false,
|
|
780
|
+
type: "object",
|
|
781
|
+
properties: {
|
|
782
|
+
customer_id: { type: "string" },
|
|
783
|
+
old_asset_id: { type: "string", description: "Numeric ID of the existing sitelink asset to replace." },
|
|
784
|
+
new_final_urls: {
|
|
785
|
+
type: "array",
|
|
786
|
+
items: { type: "string" },
|
|
787
|
+
description: "New destination URLs for the replacement sitelink. Must start with http:// or https://."
|
|
788
|
+
},
|
|
789
|
+
new_link_text: { type: "string", description: "Optional override for the sitelink click text (default: preserve from old asset). Max 25 chars." },
|
|
790
|
+
new_description1: { type: "string", description: "Optional override for description line 1 (default: preserve from old asset). Max 35 chars." },
|
|
791
|
+
new_description2: { type: "string", description: "Optional override for description line 2 (default: preserve from old asset). Max 35 chars." },
|
|
792
|
+
confirm: {
|
|
793
|
+
type: "boolean",
|
|
794
|
+
description: "Must be true to actually replace. Omit or false for dry-run preview."
|
|
795
|
+
}
|
|
796
|
+
},
|
|
797
|
+
required: ["old_asset_id", "new_final_urls"]
|
|
798
|
+
}
|
|
799
|
+
},
|
|
751
800
|
{
|
|
752
801
|
name: "google_ads_pause_asset_links",
|
|
753
802
|
description: "Pause asset links (customer_asset, campaign_asset, or ad_group_asset). Use this to stop a sitelink from serving without deleting the underlying asset. DRY-RUN BY DEFAULT: omit `confirm` or pass `confirm: false` to get a preview. Resource name form: customers/{cid}/customerAssets/{assetId}~SITELINK, customers/{cid}/campaignAssets/{campId}~{assetId}~SITELINK, or customers/{cid}/adGroupAssets/{agId}~{assetId}~SITELINK.",
|
package/dist/writeGate.js
CHANGED
|
@@ -22,7 +22,9 @@ const WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
22
22
|
"google_ads_update_campaign_tracking",
|
|
23
23
|
"google_ads_update_campaign_budget",
|
|
24
24
|
"google_ads_update_campaign_bidding",
|
|
25
|
-
"google_ads_update_asset_urls"
|
|
25
|
+
"google_ads_update_asset_urls",
|
|
26
|
+
"google_ads_create_sitelink",
|
|
27
|
+
"google_ads_replace_sitelink_url"
|
|
26
28
|
]);
|
|
27
29
|
function isWriteTool(name) {
|
|
28
30
|
return WRITE_TOOLS.has(name);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-ads",
|
|
3
3
|
"mcpName": "io.github.mharnett/google-ads",
|
|
4
|
-
"version": "1.4.
|
|
5
|
-
"description": "MCP server for Google Ads API with MCC support,
|
|
4
|
+
"version": "1.4.4",
|
|
5
|
+
"description": "MCP server for Google Ads API with MCC support, 43 tools for campaign management, reporting, and optimization. Read-only by default -- mutating tools require GOOGLE_ADS_MCP_WRITE=true. All creates/updates land PAUSED.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"mcp-google-ads": "dist/index.js",
|