mcp-google-ads 1.1.0 → 1.2.2
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/README.md +55 -4
- package/dist/adGroupBuilder.d.ts +26 -0
- package/dist/adGroupBuilder.js +17 -0
- package/dist/assetHelpers.d.ts +55 -0
- package/dist/assetHelpers.js +129 -0
- package/dist/auth-cli.js +13 -2
- package/dist/build-info.json +2 -2
- package/dist/campaignBuilder.d.ts +39 -0
- package/dist/campaignBuilder.js +65 -0
- package/dist/imageAsset.d.ts +51 -0
- package/dist/imageAsset.js +153 -0
- package/dist/index.js +714 -58
- package/dist/removeHelpers.d.ts +41 -0
- package/dist/removeHelpers.js +68 -0
- package/dist/tools.js +268 -8
- package/dist/validateDemandGenAd.d.ts +62 -0
- package/dist/validateDemandGenAd.js +96 -0
- package/dist/validateRsa.d.ts +11 -0
- package/dist/validateRsa.js +23 -18
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -12,6 +12,32 @@ import {
|
|
|
12
12
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
13
|
import { tools } from "./tools.js";
|
|
14
14
|
import { validateRsa } from "./validateRsa.js";
|
|
15
|
+
import {
|
|
16
|
+
validateRemoveInput,
|
|
17
|
+
buildRemovePreview,
|
|
18
|
+
orderRemovalsChildUp,
|
|
19
|
+
normalizeRemoveArgs,
|
|
20
|
+
coerceStringArray
|
|
21
|
+
} from "./removeHelpers.js";
|
|
22
|
+
import {
|
|
23
|
+
normalizeUpdateAssetUrlsArgs,
|
|
24
|
+
normalizePauseAssetLinksArgs,
|
|
25
|
+
buildUpdateUrlsDryRun,
|
|
26
|
+
buildPauseLinksDryRun
|
|
27
|
+
} from "./assetHelpers.js";
|
|
28
|
+
import {
|
|
29
|
+
buildCampaignCreatePayload
|
|
30
|
+
} from "./campaignBuilder.js";
|
|
31
|
+
import {
|
|
32
|
+
buildAdGroupCreatePayload
|
|
33
|
+
} from "./adGroupBuilder.js";
|
|
34
|
+
import {
|
|
35
|
+
prepareImageForUpload
|
|
36
|
+
} from "./imageAsset.js";
|
|
37
|
+
import {
|
|
38
|
+
validateDemandGenAd,
|
|
39
|
+
buildDemandGenAdPayload
|
|
40
|
+
} from "./validateDemandGenAd.js";
|
|
15
41
|
import { GoogleAdsApi, enums } from "google-ads-api";
|
|
16
42
|
import { readFileSync, existsSync } from "fs";
|
|
17
43
|
import v8 from "v8";
|
|
@@ -113,6 +139,16 @@ import {
|
|
|
113
139
|
validateResolvedCredentials
|
|
114
140
|
} from "./credentials.js";
|
|
115
141
|
import { onPosixSignal } from "./platform.js";
|
|
142
|
+
function mimeToAssetMimeEnum(mime) {
|
|
143
|
+
switch (mime) {
|
|
144
|
+
case "image/png":
|
|
145
|
+
return enums.MimeType.IMAGE_PNG;
|
|
146
|
+
case "image/jpeg":
|
|
147
|
+
return enums.MimeType.IMAGE_JPEG;
|
|
148
|
+
case "image/gif":
|
|
149
|
+
return enums.MimeType.IMAGE_GIF;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
116
152
|
class GoogleAdsManager {
|
|
117
153
|
api;
|
|
118
154
|
config;
|
|
@@ -426,6 +462,21 @@ class GoogleAdsManager {
|
|
|
426
462
|
}
|
|
427
463
|
break;
|
|
428
464
|
}
|
|
465
|
+
case "asset": {
|
|
466
|
+
const customer = this.getCustomer(customerId);
|
|
467
|
+
const customerAssetLabels = customer.customerAssetLabels || customer.assetLabels;
|
|
468
|
+
if (customerAssetLabels?.create) {
|
|
469
|
+
const ops = resourceNames.map((rn) => ({ asset: rn, label: labelRN }));
|
|
470
|
+
try {
|
|
471
|
+
await withResilience(() => customerAssetLabels.create(ops), "labelAssets");
|
|
472
|
+
} catch (e) {
|
|
473
|
+
console.error(`[WARN] asset labeling failed: ${e.message}`);
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
console.error(`[INFO] customerAssetLabels endpoint not available \u2014 skipping auto-label for asset`);
|
|
477
|
+
}
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
429
480
|
}
|
|
430
481
|
return labelName;
|
|
431
482
|
} catch (e) {
|
|
@@ -497,53 +548,125 @@ class GoogleAdsManager {
|
|
|
497
548
|
};
|
|
498
549
|
}
|
|
499
550
|
}
|
|
500
|
-
// Create a campaign (paused by default)
|
|
551
|
+
// Create a campaign (paused by default). Supports SEARCH (back-compat) and
|
|
552
|
+
// DEMAND_GEN channels, plus richer bidding strategies, geo/language targeting,
|
|
553
|
+
// and start/end dates. The pure payload shape is built by
|
|
554
|
+
// buildCampaignCreatePayload (see campaignBuilder.ts) so it can be unit-tested
|
|
555
|
+
// independently of the live API.
|
|
501
556
|
async createCampaign(customerId, campaign) {
|
|
502
557
|
const customer = this.getCustomer(customerId);
|
|
558
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
559
|
+
const plan = buildCampaignCreatePayload(campaign);
|
|
503
560
|
const budgetResult = await withResilience(
|
|
504
|
-
() => customer.campaignBudgets.create([
|
|
505
|
-
name: `${campaign.name} Budget`,
|
|
506
|
-
amount_micros: campaign.budget_amount_micros,
|
|
507
|
-
delivery_method: enums.BudgetDeliveryMethod.STANDARD
|
|
508
|
-
}]),
|
|
561
|
+
() => customer.campaignBudgets.create([plan.budget]),
|
|
509
562
|
"createCampaign.budget"
|
|
510
563
|
);
|
|
511
564
|
const budgetResourceName = budgetResult.results[0].resource_name;
|
|
512
565
|
const campaignResult = await withResilience(
|
|
513
566
|
() => customer.campaigns.create([{
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
// Always create paused
|
|
517
|
-
advertising_channel_type: enums.AdvertisingChannelType.SEARCH,
|
|
518
|
-
campaign_budget: budgetResourceName,
|
|
519
|
-
manual_cpc: {}
|
|
520
|
-
// Default to manual CPC
|
|
567
|
+
...plan.campaign,
|
|
568
|
+
campaign_budget: budgetResourceName
|
|
521
569
|
}]),
|
|
522
570
|
"createCampaign"
|
|
523
571
|
);
|
|
524
572
|
const campaignRNs = (campaignResult.results || []).map((r) => r.resource_name).filter(Boolean);
|
|
573
|
+
if (plan.criteria.length > 0 && campaignRNs.length > 0) {
|
|
574
|
+
const campaignResourceName = campaignRNs[0];
|
|
575
|
+
const criterionPayloads = plan.criteria.map((c) => ({
|
|
576
|
+
...c,
|
|
577
|
+
campaign: campaignResourceName
|
|
578
|
+
}));
|
|
579
|
+
await withResilience(
|
|
580
|
+
() => customer.campaignCriteria.create(criterionPayloads),
|
|
581
|
+
"createCampaign.criteria"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
525
584
|
await this.autoLabelCreated(customerId, campaignRNs, "campaign");
|
|
526
585
|
return campaignResult;
|
|
527
586
|
}
|
|
528
|
-
// Create an ad group (paused by default)
|
|
587
|
+
// Create an ad group (paused by default). Supports SEARCH_STANDARD (default,
|
|
588
|
+
// back-compat) and DEMAND_GEN_MULTI_ASSET_AD_GROUP. DG path flows through
|
|
589
|
+
// mutateResources because the v23 client enum map is missing the DG value;
|
|
590
|
+
// see adGroupBuilder.ts for details.
|
|
529
591
|
async createAdGroup(customerId, adGroup) {
|
|
530
592
|
const customer = this.getCustomer(customerId);
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
593
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
594
|
+
const payload = buildAdGroupCreatePayload({
|
|
595
|
+
customer_id_clean: cleanId,
|
|
596
|
+
name: adGroup.name,
|
|
597
|
+
campaign_id: adGroup.campaign_id,
|
|
598
|
+
cpc_bid_micros: adGroup.cpc_bid_micros,
|
|
599
|
+
type: adGroup.type
|
|
600
|
+
});
|
|
601
|
+
let result;
|
|
602
|
+
if (adGroup.type === "DEMAND_GEN_MULTI_ASSET_AD_GROUP") {
|
|
603
|
+
const mutateResp = await withResilience(
|
|
604
|
+
() => customer.mutateResources([
|
|
605
|
+
{
|
|
606
|
+
entity: "ad_group",
|
|
607
|
+
operation: "create",
|
|
608
|
+
resource: payload
|
|
609
|
+
}
|
|
610
|
+
]),
|
|
611
|
+
"createAdGroup.demandGen"
|
|
612
|
+
);
|
|
613
|
+
result = {
|
|
614
|
+
results: (mutateResp.mutate_operation_responses || []).map((r) => r.ad_group_result).filter(Boolean)
|
|
615
|
+
};
|
|
616
|
+
} else {
|
|
617
|
+
result = await withResilience(
|
|
618
|
+
() => customer.adGroups.create([payload]),
|
|
619
|
+
"createAdGroup"
|
|
620
|
+
);
|
|
621
|
+
}
|
|
543
622
|
const adGroupRNs = (result.results || []).map((r) => r.resource_name).filter(Boolean);
|
|
544
623
|
await this.autoLabelCreated(customerId, adGroupRNs, "ad_group");
|
|
545
624
|
return result;
|
|
546
625
|
}
|
|
626
|
+
// Create an image asset for use in Demand Gen (or other image-capable) ads.
|
|
627
|
+
// Validates mime type (PNG/JPEG/GIF), size (≤5MB), and min dimensions (600x314)
|
|
628
|
+
// before hitting the API. Auto-labels the created asset.
|
|
629
|
+
async createImageAsset(customerId, input) {
|
|
630
|
+
const prepared = prepareImageForUpload(input);
|
|
631
|
+
if (!prepared.valid) {
|
|
632
|
+
throw new Error("Image validation failed:\n" + prepared.errors.join("\n"));
|
|
633
|
+
}
|
|
634
|
+
const customer = this.getCustomer(customerId);
|
|
635
|
+
const result = await withResilience(
|
|
636
|
+
() => customer.assets.create([
|
|
637
|
+
{
|
|
638
|
+
name: input.name,
|
|
639
|
+
type: enums.AssetType.IMAGE,
|
|
640
|
+
image_asset: {
|
|
641
|
+
data: prepared.bytes,
|
|
642
|
+
file_size: prepared.bytes.length,
|
|
643
|
+
mime_type: mimeToAssetMimeEnum(prepared.mime_type),
|
|
644
|
+
full_size: {
|
|
645
|
+
width_pixels: prepared.width,
|
|
646
|
+
height_pixels: prepared.height,
|
|
647
|
+
url: ""
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
]),
|
|
652
|
+
"createImageAsset"
|
|
653
|
+
);
|
|
654
|
+
const results = result.results || [];
|
|
655
|
+
const resourceName = results[0]?.resource_name;
|
|
656
|
+
const assetId = resourceName ? resourceName.split("/").pop() : void 0;
|
|
657
|
+
if (resourceName) {
|
|
658
|
+
await this.autoLabelCreated(customerId, [resourceName], "asset");
|
|
659
|
+
}
|
|
660
|
+
return {
|
|
661
|
+
asset_id: assetId,
|
|
662
|
+
resource_name: resourceName,
|
|
663
|
+
name: input.name,
|
|
664
|
+
bytes: prepared.bytes.length,
|
|
665
|
+
mime_type: prepared.mime_type,
|
|
666
|
+
width: prepared.width,
|
|
667
|
+
height: prepared.height
|
|
668
|
+
};
|
|
669
|
+
}
|
|
547
670
|
// Create a responsive search ad (paused by default)
|
|
548
671
|
async createResponsiveSearchAd(customerId, ad) {
|
|
549
672
|
const customer = this.getCustomer(customerId);
|
|
@@ -593,6 +716,68 @@ class GoogleAdsManager {
|
|
|
593
716
|
await this.autoLabelCreated(customerId, adRNs, "ad");
|
|
594
717
|
return result;
|
|
595
718
|
}
|
|
719
|
+
// Create a Demand Gen multi-asset ad (paused by default). Validates character
|
|
720
|
+
// limits and count caps before hitting the API. Fails fast if the ad_group
|
|
721
|
+
// isn't a DEMAND_GEN_MULTI_ASSET_AD_GROUP. Uses mutateResources because the
|
|
722
|
+
// v23 typed helper doesn't know about DG ad types.
|
|
723
|
+
async createDemandGenMultiAssetAd(customerId, input) {
|
|
724
|
+
const validation = validateDemandGenAd(input);
|
|
725
|
+
if (!validation.valid) {
|
|
726
|
+
throw new Error("Demand Gen ad validation failed:\n" + validation.errors.join("\n"));
|
|
727
|
+
}
|
|
728
|
+
const customer = this.getCustomer(customerId);
|
|
729
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
730
|
+
const agRows = await withResilience(
|
|
731
|
+
() => customer.query(
|
|
732
|
+
`SELECT ad_group.id, ad_group.type FROM ad_group WHERE ad_group.id = ${sanitizeNumericId(
|
|
733
|
+
input.ad_group_id
|
|
734
|
+
)}`
|
|
735
|
+
),
|
|
736
|
+
"createDemandGenMultiAssetAd.adGroupLookup"
|
|
737
|
+
);
|
|
738
|
+
if (!agRows || agRows.length === 0) {
|
|
739
|
+
throw new Error(`Ad group ${input.ad_group_id} not found`);
|
|
740
|
+
}
|
|
741
|
+
const agType = agRows[0]?.ad_group?.type;
|
|
742
|
+
const isDgAdGroup = agType === 21 || agType === "DEMAND_GEN_MULTI_ASSET_AD_GROUP" || agType === "21";
|
|
743
|
+
if (!isDgAdGroup) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`Ad group ${input.ad_group_id} has type '${agType}', not DEMAND_GEN_MULTI_ASSET_AD_GROUP. Use google_ads_create_ad_group with type=DEMAND_GEN_MULTI_ASSET_AD_GROUP first.`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
const payload = buildDemandGenAdPayload({
|
|
749
|
+
customer_id_clean: cleanId,
|
|
750
|
+
ad_group_id: input.ad_group_id,
|
|
751
|
+
input
|
|
752
|
+
});
|
|
753
|
+
const mutateResp = await withResilience(
|
|
754
|
+
() => customer.mutateResources([
|
|
755
|
+
{
|
|
756
|
+
entity: "ad_group_ad",
|
|
757
|
+
operation: "create",
|
|
758
|
+
resource: payload
|
|
759
|
+
}
|
|
760
|
+
]),
|
|
761
|
+
"createDemandGenMultiAssetAd"
|
|
762
|
+
);
|
|
763
|
+
const adGroupAdResult = (mutateResp.mutate_operation_responses || []).map((r) => r.ad_group_ad_result).filter(Boolean)[0];
|
|
764
|
+
const resourceName = adGroupAdResult?.resource_name;
|
|
765
|
+
if (resourceName) {
|
|
766
|
+
await this.autoLabelCreated(customerId, [resourceName], "ad");
|
|
767
|
+
for (const lbl of input.labels ?? []) {
|
|
768
|
+
try {
|
|
769
|
+
const labelRN = await this.ensureLabelExists(customerId, lbl);
|
|
770
|
+
await this.labelAdGroupAds(customerId, [resourceName], labelRN);
|
|
771
|
+
} catch (e) {
|
|
772
|
+
console.error(`[WARN] extra label '${lbl}' failed: ${e.message}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return {
|
|
777
|
+
resource_name: resourceName,
|
|
778
|
+
ad_id: resourceName ? resourceName.split("~").pop() : void 0
|
|
779
|
+
};
|
|
780
|
+
}
|
|
596
781
|
// Create keywords (paused by default, auto-labeled for discoverability)
|
|
597
782
|
async createKeywords(customerId, keywords) {
|
|
598
783
|
const customer = this.getCustomer(customerId);
|
|
@@ -709,64 +894,167 @@ class GoogleAdsManager {
|
|
|
709
894
|
const result = await withResilience(() => customer.adGroupCriteria.remove(resourceNames), "removeAdGroupNegativeKeywords");
|
|
710
895
|
return result;
|
|
711
896
|
}
|
|
712
|
-
// Enable ads (
|
|
713
|
-
async enableAds(customerId, adIds) {
|
|
897
|
+
// Enable ads (auto-labels with today's Claude-MM-DD-YY + any custom labels)
|
|
898
|
+
async enableAds(customerId, adIds, labels) {
|
|
714
899
|
const customer = this.getCustomer(customerId);
|
|
715
|
-
const
|
|
716
|
-
|
|
900
|
+
const resourceNames = adIds.map(
|
|
901
|
+
(adId) => `customers/${customerId.replace(/-/g, "")}/adGroupAds/${adId}`
|
|
902
|
+
);
|
|
903
|
+
const operations = resourceNames.map((rn) => ({
|
|
904
|
+
resource_name: rn,
|
|
717
905
|
status: enums.AdGroupAdStatus.ENABLED
|
|
718
906
|
}));
|
|
719
907
|
const result = await withResilience(() => customer.adGroupAds.update(operations), "enableAds");
|
|
908
|
+
await this.autoLabelCreated(customerId, resourceNames, "ad");
|
|
909
|
+
await this.applyCustomLabels(customerId, resourceNames, "ad", labels);
|
|
720
910
|
return result;
|
|
721
911
|
}
|
|
722
|
-
// Enable ad groups
|
|
723
|
-
async enableAdGroups(customerId, adGroupIds) {
|
|
912
|
+
// Enable ad groups (auto-labels with today's Claude-MM-DD-YY + any custom labels)
|
|
913
|
+
async enableAdGroups(customerId, adGroupIds, labels) {
|
|
724
914
|
const customer = this.getCustomer(customerId);
|
|
725
|
-
const
|
|
726
|
-
|
|
915
|
+
const resourceNames = adGroupIds.map(
|
|
916
|
+
(id) => `customers/${customerId.replace(/-/g, "")}/adGroups/${id}`
|
|
917
|
+
);
|
|
918
|
+
const operations = resourceNames.map((rn) => ({
|
|
919
|
+
resource_name: rn,
|
|
727
920
|
status: enums.AdGroupStatus.ENABLED
|
|
728
921
|
}));
|
|
729
922
|
const result = await withResilience(() => customer.adGroups.update(operations), "enableAdGroups");
|
|
923
|
+
await this.autoLabelCreated(customerId, resourceNames, "ad_group");
|
|
924
|
+
await this.applyCustomLabels(customerId, resourceNames, "ad_group", labels);
|
|
730
925
|
return result;
|
|
731
926
|
}
|
|
732
|
-
// Enable campaigns
|
|
733
|
-
async enableCampaigns(customerId, campaignIds) {
|
|
927
|
+
// Enable campaigns (auto-labels with today's Claude-MM-DD-YY + any custom labels)
|
|
928
|
+
async enableCampaigns(customerId, campaignIds, labels) {
|
|
734
929
|
const customer = this.getCustomer(customerId);
|
|
735
|
-
const
|
|
736
|
-
|
|
930
|
+
const resourceNames = campaignIds.map(
|
|
931
|
+
(id) => `customers/${customerId.replace(/-/g, "")}/campaigns/${id}`
|
|
932
|
+
);
|
|
933
|
+
const operations = resourceNames.map((rn) => ({
|
|
934
|
+
resource_name: rn,
|
|
737
935
|
status: enums.CampaignStatus.ENABLED
|
|
738
936
|
}));
|
|
739
937
|
const result = await withResilience(() => customer.campaigns.update(operations), "enableCampaigns");
|
|
938
|
+
await this.autoLabelCreated(customerId, resourceNames, "campaign");
|
|
939
|
+
await this.applyCustomLabels(customerId, resourceNames, "campaign", labels);
|
|
740
940
|
return result;
|
|
741
941
|
}
|
|
742
|
-
//
|
|
743
|
-
|
|
942
|
+
// Apply one or more custom labels to resources of a given asset type.
|
|
943
|
+
// Best-effort: logs and swallows errors so the calling mutation isn't rolled
|
|
944
|
+
// back on a labeling failure. Creates labels that don't yet exist.
|
|
945
|
+
async applyCustomLabels(customerId, resourceNames, assetType, labelNames) {
|
|
946
|
+
const clean = (labelNames ?? []).map((l) => l?.trim()).filter((l) => !!l);
|
|
947
|
+
if (clean.length === 0 || resourceNames.length === 0) return;
|
|
948
|
+
for (const labelName of clean) {
|
|
949
|
+
try {
|
|
950
|
+
const labelRN = await this.ensureLabelExists(customerId, labelName);
|
|
951
|
+
switch (assetType) {
|
|
952
|
+
case "campaign":
|
|
953
|
+
await this.labelCampaigns(customerId, resourceNames, labelRN);
|
|
954
|
+
break;
|
|
955
|
+
case "ad_group":
|
|
956
|
+
await this.labelAdGroups(customerId, resourceNames, labelRN);
|
|
957
|
+
break;
|
|
958
|
+
case "ad":
|
|
959
|
+
await this.labelAdGroupAds(customerId, resourceNames, labelRN);
|
|
960
|
+
break;
|
|
961
|
+
case "keyword":
|
|
962
|
+
await this.labelAdGroupCriteria(customerId, resourceNames, labelRN);
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
} catch (e) {
|
|
966
|
+
console.error(`[WARN] applyCustomLabels(${assetType}, '${labelName}') failed: ${e.message}`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// Pause ads (auto-labels with today's Claude-MM-DD-YY + any custom labels)
|
|
971
|
+
async pauseAds(customerId, adIds, labels) {
|
|
744
972
|
const customer = this.getCustomer(customerId);
|
|
745
|
-
const
|
|
746
|
-
|
|
973
|
+
const resourceNames = adIds.map(
|
|
974
|
+
(adId) => `customers/${customerId.replace(/-/g, "")}/adGroupAds/${adId}`
|
|
975
|
+
);
|
|
976
|
+
const operations = resourceNames.map((rn) => ({
|
|
977
|
+
resource_name: rn,
|
|
747
978
|
status: enums.AdGroupAdStatus.PAUSED
|
|
748
979
|
}));
|
|
749
980
|
const result = await withResilience(() => customer.adGroupAds.update(operations), "pauseAds");
|
|
981
|
+
await this.autoLabelCreated(customerId, resourceNames, "ad");
|
|
982
|
+
await this.applyCustomLabels(customerId, resourceNames, "ad", labels);
|
|
750
983
|
return result;
|
|
751
984
|
}
|
|
752
|
-
// Pause ad groups
|
|
753
|
-
async pauseAdGroups(customerId, adGroupIds) {
|
|
985
|
+
// Pause ad groups (auto-labels with today's Claude-MM-DD-YY + any custom labels)
|
|
986
|
+
async pauseAdGroups(customerId, adGroupIds, labels) {
|
|
754
987
|
const customer = this.getCustomer(customerId);
|
|
755
|
-
const
|
|
756
|
-
|
|
988
|
+
const resourceNames = adGroupIds.map(
|
|
989
|
+
(id) => `customers/${customerId.replace(/-/g, "")}/adGroups/${id}`
|
|
990
|
+
);
|
|
991
|
+
const operations = resourceNames.map((rn) => ({
|
|
992
|
+
resource_name: rn,
|
|
757
993
|
status: enums.AdGroupStatus.PAUSED
|
|
758
994
|
}));
|
|
759
995
|
const result = await withResilience(() => customer.adGroups.update(operations), "pauseAdGroups");
|
|
996
|
+
await this.autoLabelCreated(customerId, resourceNames, "ad_group");
|
|
997
|
+
await this.applyCustomLabels(customerId, resourceNames, "ad_group", labels);
|
|
760
998
|
return result;
|
|
761
999
|
}
|
|
762
|
-
// Pause campaigns
|
|
763
|
-
async pauseCampaigns(customerId, campaignIds) {
|
|
1000
|
+
// Pause campaigns (auto-labels with today's Claude-MM-DD-YY + any custom labels)
|
|
1001
|
+
async pauseCampaigns(customerId, campaignIds, labels) {
|
|
764
1002
|
const customer = this.getCustomer(customerId);
|
|
765
|
-
const
|
|
766
|
-
|
|
1003
|
+
const resourceNames = campaignIds.map(
|
|
1004
|
+
(id) => `customers/${customerId.replace(/-/g, "")}/campaigns/${id}`
|
|
1005
|
+
);
|
|
1006
|
+
const operations = resourceNames.map((rn) => ({
|
|
1007
|
+
resource_name: rn,
|
|
767
1008
|
status: enums.CampaignStatus.PAUSED
|
|
768
1009
|
}));
|
|
769
1010
|
const result = await withResilience(() => customer.campaigns.update(operations), "pauseCampaigns");
|
|
1011
|
+
await this.autoLabelCreated(customerId, resourceNames, "campaign");
|
|
1012
|
+
await this.applyCustomLabels(customerId, resourceNames, "campaign", labels);
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
// Remove ads permanently. Reports on removed ads still work.
|
|
1016
|
+
async removeAds(customerId, adIds) {
|
|
1017
|
+
const customer = this.getCustomer(customerId);
|
|
1018
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1019
|
+
const resourceNames = adIds.map((id) => `customers/${cleanId}/adGroupAds/${id}`);
|
|
1020
|
+
return withResilience(() => customer.adGroupAds.remove(resourceNames), "removeAds");
|
|
1021
|
+
}
|
|
1022
|
+
// Remove ad groups permanently. Cascades to child ads.
|
|
1023
|
+
async removeAdGroups(customerId, adGroupIds) {
|
|
1024
|
+
const customer = this.getCustomer(customerId);
|
|
1025
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1026
|
+
const resourceNames = adGroupIds.map((id) => `customers/${cleanId}/adGroups/${id}`);
|
|
1027
|
+
return withResilience(() => customer.adGroups.remove(resourceNames), "removeAdGroups");
|
|
1028
|
+
}
|
|
1029
|
+
// Remove campaigns permanently. Cascades to child ad groups and ads.
|
|
1030
|
+
async removeCampaigns(customerId, campaignIds) {
|
|
1031
|
+
const customer = this.getCustomer(customerId);
|
|
1032
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1033
|
+
const resourceNames = campaignIds.map((id) => `customers/${cleanId}/campaigns/${id}`);
|
|
1034
|
+
return withResilience(() => customer.campaigns.remove(resourceNames), "removeCampaigns");
|
|
1035
|
+
}
|
|
1036
|
+
// Attach a label to existing resources without changing status.
|
|
1037
|
+
// Unlike applyCustomLabels (which swallows errors to protect a parent mutation),
|
|
1038
|
+
// this surfaces errors to the caller so label-first-then-remove can abort cleanly.
|
|
1039
|
+
async applyLabel(customerId, label, targets) {
|
|
1040
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1041
|
+
const labelRN = await this.ensureLabelExists(customerId, label);
|
|
1042
|
+
const result = { label, label_resource_name: labelRN };
|
|
1043
|
+
if (targets.campaignIds?.length) {
|
|
1044
|
+
const rns = targets.campaignIds.map((id) => `customers/${cleanId}/campaigns/${id}`);
|
|
1045
|
+
await this.labelCampaigns(customerId, rns, labelRN);
|
|
1046
|
+
result.campaigns_labeled = rns.length;
|
|
1047
|
+
}
|
|
1048
|
+
if (targets.adGroupIds?.length) {
|
|
1049
|
+
const rns = targets.adGroupIds.map((id) => `customers/${cleanId}/adGroups/${id}`);
|
|
1050
|
+
await this.labelAdGroups(customerId, rns, labelRN);
|
|
1051
|
+
result.ad_groups_labeled = rns.length;
|
|
1052
|
+
}
|
|
1053
|
+
if (targets.adIds?.length) {
|
|
1054
|
+
const rns = targets.adIds.map((id) => `customers/${cleanId}/adGroupAds/${id}`);
|
|
1055
|
+
await this.labelAdGroupAds(customerId, rns, labelRN);
|
|
1056
|
+
result.ads_labeled = rns.length;
|
|
1057
|
+
}
|
|
770
1058
|
return result;
|
|
771
1059
|
}
|
|
772
1060
|
// Update campaign tracking parameters (final_url_suffix, tracking_url_template, custom params)
|
|
@@ -788,6 +1076,163 @@ class GoogleAdsManager {
|
|
|
788
1076
|
const result = await withResilience(() => customer.campaigns.update([campaignUpdate]), "updateCampaignTracking");
|
|
789
1077
|
return result;
|
|
790
1078
|
}
|
|
1079
|
+
// Update campaign bidding strategy (and/or target CPA / target ROAS).
|
|
1080
|
+
// If `strategy` is omitted, the current strategy is preserved and only the target is updated.
|
|
1081
|
+
async updateCampaignBidding(customerId, campaignId, updates) {
|
|
1082
|
+
const customer = this.getCustomer(customerId);
|
|
1083
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1084
|
+
const [current] = await withResilience(
|
|
1085
|
+
() => customer.query(`
|
|
1086
|
+
SELECT campaign.id, campaign.name, campaign.bidding_strategy_type
|
|
1087
|
+
FROM campaign
|
|
1088
|
+
WHERE campaign.id = ${campaignId}
|
|
1089
|
+
`),
|
|
1090
|
+
"updateCampaignBidding.query"
|
|
1091
|
+
);
|
|
1092
|
+
if (!current?.campaign?.name) {
|
|
1093
|
+
throw new Error(`Campaign ${campaignId} not found`);
|
|
1094
|
+
}
|
|
1095
|
+
const currentTypeEnum = current.campaign.bidding_strategy_type;
|
|
1096
|
+
const typeEnumToName = {
|
|
1097
|
+
2: "MANUAL_CPC",
|
|
1098
|
+
6: "MAXIMIZE_CONVERSIONS",
|
|
1099
|
+
8: "TARGET_CPA",
|
|
1100
|
+
9: "TARGET_ROAS",
|
|
1101
|
+
10: "MAXIMIZE_CONVERSIONS",
|
|
1102
|
+
11: "MAXIMIZE_CONVERSION_VALUE",
|
|
1103
|
+
12: "TARGET_SPEND"
|
|
1104
|
+
};
|
|
1105
|
+
const currentStrategy = typeEnumToName[currentTypeEnum] || "UNKNOWN";
|
|
1106
|
+
const resolvedStrategy = updates.strategy || currentStrategy;
|
|
1107
|
+
const targetCpaMicros = updates.target_cpa_dollars !== void 0 ? Math.round(updates.target_cpa_dollars * 1e6) : void 0;
|
|
1108
|
+
const campaignUpdate = {
|
|
1109
|
+
resource_name: `customers/${cleanId}/campaigns/${campaignId}`
|
|
1110
|
+
};
|
|
1111
|
+
switch (resolvedStrategy) {
|
|
1112
|
+
case "MAXIMIZE_CONVERSIONS":
|
|
1113
|
+
campaignUpdate.maximize_conversions = targetCpaMicros !== void 0 ? { target_cpa_micros: targetCpaMicros } : {};
|
|
1114
|
+
break;
|
|
1115
|
+
case "MAXIMIZE_CONVERSION_VALUE":
|
|
1116
|
+
campaignUpdate.maximize_conversion_value = updates.target_roas !== void 0 ? { target_roas: updates.target_roas } : {};
|
|
1117
|
+
break;
|
|
1118
|
+
case "TARGET_CPA":
|
|
1119
|
+
if (targetCpaMicros === void 0) {
|
|
1120
|
+
throw new Error("TARGET_CPA strategy requires target_cpa_dollars");
|
|
1121
|
+
}
|
|
1122
|
+
campaignUpdate.target_cpa = { target_cpa_micros: targetCpaMicros };
|
|
1123
|
+
break;
|
|
1124
|
+
case "TARGET_ROAS":
|
|
1125
|
+
if (updates.target_roas === void 0) {
|
|
1126
|
+
throw new Error("TARGET_ROAS strategy requires target_roas");
|
|
1127
|
+
}
|
|
1128
|
+
campaignUpdate.target_roas = { target_roas: updates.target_roas };
|
|
1129
|
+
break;
|
|
1130
|
+
case "MANUAL_CPC":
|
|
1131
|
+
campaignUpdate.manual_cpc = {};
|
|
1132
|
+
break;
|
|
1133
|
+
case "MAXIMIZE_CLICKS":
|
|
1134
|
+
campaignUpdate.target_spend = {};
|
|
1135
|
+
break;
|
|
1136
|
+
default:
|
|
1137
|
+
throw new Error(`Unsupported strategy: ${resolvedStrategy}`);
|
|
1138
|
+
}
|
|
1139
|
+
await withResilience(
|
|
1140
|
+
() => customer.campaigns.update([campaignUpdate]),
|
|
1141
|
+
"updateCampaignBidding.update"
|
|
1142
|
+
);
|
|
1143
|
+
return {
|
|
1144
|
+
campaign_id: campaignId,
|
|
1145
|
+
campaign_name: current.campaign.name,
|
|
1146
|
+
previous_strategy: currentStrategy,
|
|
1147
|
+
new_strategy: resolvedStrategy,
|
|
1148
|
+
target_cpa_dollars: updates.target_cpa_dollars,
|
|
1149
|
+
target_roas: updates.target_roas
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
async updateAssetUrls(customerId, updates) {
|
|
1153
|
+
const customer = this.getCustomer(customerId);
|
|
1154
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1155
|
+
const assetIds = updates.map((u) => u.asset_id);
|
|
1156
|
+
const idList = assetIds.join(",");
|
|
1157
|
+
const currentRows = await withResilience(
|
|
1158
|
+
() => customer.query(`
|
|
1159
|
+
SELECT asset.id, asset.name, asset.final_urls, asset.sitelink_asset.link_text
|
|
1160
|
+
FROM asset
|
|
1161
|
+
WHERE asset.id IN (${idList})
|
|
1162
|
+
`),
|
|
1163
|
+
"updateAssetUrls.query"
|
|
1164
|
+
);
|
|
1165
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1166
|
+
for (const row of currentRows) {
|
|
1167
|
+
if (row.asset?.id) byId.set(String(row.asset.id), row.asset);
|
|
1168
|
+
}
|
|
1169
|
+
const missing = assetIds.filter((id) => !byId.has(id));
|
|
1170
|
+
if (missing.length > 0) {
|
|
1171
|
+
throw new Error(`Asset ID(s) not found in customer ${customerId}: ${missing.join(", ")}`);
|
|
1172
|
+
}
|
|
1173
|
+
const operations = updates.map((u) => ({
|
|
1174
|
+
resource_name: `customers/${cleanId}/assets/${u.asset_id}`,
|
|
1175
|
+
final_urls: u.final_urls
|
|
1176
|
+
}));
|
|
1177
|
+
await withResilience(
|
|
1178
|
+
() => customer.assets.update(operations),
|
|
1179
|
+
"updateAssetUrls.update"
|
|
1180
|
+
);
|
|
1181
|
+
return {
|
|
1182
|
+
customer_id: cleanId,
|
|
1183
|
+
updated: updates.map((u) => {
|
|
1184
|
+
const before = byId.get(u.asset_id);
|
|
1185
|
+
return {
|
|
1186
|
+
asset_id: u.asset_id,
|
|
1187
|
+
link_text: before?.sitelink_asset?.link_text ?? null,
|
|
1188
|
+
previous_final_urls: before?.final_urls ?? [],
|
|
1189
|
+
new_final_urls: u.final_urls
|
|
1190
|
+
};
|
|
1191
|
+
})
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
async pauseAssetLinks(customerId, resourceNames) {
|
|
1195
|
+
const customer = this.getCustomer(customerId);
|
|
1196
|
+
const cleanId = customerId.replace(/-/g, "");
|
|
1197
|
+
const byLevel = {
|
|
1198
|
+
customer_asset: [],
|
|
1199
|
+
campaign_asset: [],
|
|
1200
|
+
ad_group_asset: []
|
|
1201
|
+
};
|
|
1202
|
+
for (const rn of resourceNames) {
|
|
1203
|
+
if (/\/customerAssets\//.test(rn)) byLevel.customer_asset.push(rn);
|
|
1204
|
+
else if (/\/campaignAssets\//.test(rn)) byLevel.campaign_asset.push(rn);
|
|
1205
|
+
else if (/\/adGroupAssets\//.test(rn)) byLevel.ad_group_asset.push(rn);
|
|
1206
|
+
else throw new Error(`Unrecognized asset-link resource name: ${rn}`);
|
|
1207
|
+
}
|
|
1208
|
+
const PAUSED = 2;
|
|
1209
|
+
const result = { customer_id: cleanId, paused: { customer_asset: 0, campaign_asset: 0, ad_group_asset: 0 } };
|
|
1210
|
+
if (byLevel.customer_asset.length > 0) {
|
|
1211
|
+
const ops = byLevel.customer_asset.map((rn) => ({ resource_name: rn, status: PAUSED }));
|
|
1212
|
+
await withResilience(
|
|
1213
|
+
() => customer.customerAssets.update(ops),
|
|
1214
|
+
"pauseAssetLinks.customerAssets"
|
|
1215
|
+
);
|
|
1216
|
+
result.paused.customer_asset = ops.length;
|
|
1217
|
+
}
|
|
1218
|
+
if (byLevel.campaign_asset.length > 0) {
|
|
1219
|
+
const ops = byLevel.campaign_asset.map((rn) => ({ resource_name: rn, status: PAUSED }));
|
|
1220
|
+
await withResilience(
|
|
1221
|
+
() => customer.campaignAssets.update(ops),
|
|
1222
|
+
"pauseAssetLinks.campaignAssets"
|
|
1223
|
+
);
|
|
1224
|
+
result.paused.campaign_asset = ops.length;
|
|
1225
|
+
}
|
|
1226
|
+
if (byLevel.ad_group_asset.length > 0) {
|
|
1227
|
+
const ops = byLevel.ad_group_asset.map((rn) => ({ resource_name: rn, status: PAUSED }));
|
|
1228
|
+
await withResilience(
|
|
1229
|
+
() => customer.adGroupAssets.update(ops),
|
|
1230
|
+
"pauseAssetLinks.adGroupAssets"
|
|
1231
|
+
);
|
|
1232
|
+
result.paused.ad_group_asset = ops.length;
|
|
1233
|
+
}
|
|
1234
|
+
return result;
|
|
1235
|
+
}
|
|
791
1236
|
// ============================================
|
|
792
1237
|
// REPORTING METHODS
|
|
793
1238
|
// ============================================
|
|
@@ -1305,7 +1750,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1305
1750
|
}
|
|
1306
1751
|
const result = await adsManager.createCampaign(customerId, {
|
|
1307
1752
|
name: sanitizedName,
|
|
1308
|
-
budget_amount_micros: Math.round(daily_budget * 1e6)
|
|
1753
|
+
budget_amount_micros: Math.round(daily_budget * 1e6),
|
|
1754
|
+
channel_type: args?.channel_type,
|
|
1755
|
+
bidding_strategy: args?.bidding_strategy,
|
|
1756
|
+
target_cpa: args?.target_cpa,
|
|
1757
|
+
target_cpc_cap: args?.target_cpc_cap,
|
|
1758
|
+
geo_target_ids: args?.geo_target_ids,
|
|
1759
|
+
language_id: args?.language_id,
|
|
1760
|
+
start_date: args?.start_date,
|
|
1761
|
+
end_date: args?.end_date
|
|
1309
1762
|
});
|
|
1310
1763
|
return {
|
|
1311
1764
|
content: [{
|
|
@@ -1323,7 +1776,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1323
1776
|
const result = await adsManager.createAdGroup(customerId, {
|
|
1324
1777
|
name: args?.name,
|
|
1325
1778
|
campaign_id: args?.campaign_id,
|
|
1326
|
-
cpc_bid_micros: args?.cpc_bid ? Math.round(args.cpc_bid * 1e6) : void 0
|
|
1779
|
+
cpc_bid_micros: args?.cpc_bid ? Math.round(args.cpc_bid * 1e6) : void 0,
|
|
1780
|
+
type: args?.type
|
|
1327
1781
|
});
|
|
1328
1782
|
return {
|
|
1329
1783
|
content: [{
|
|
@@ -1414,15 +1868,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1414
1868
|
}]
|
|
1415
1869
|
};
|
|
1416
1870
|
}
|
|
1871
|
+
const labels = args?.labels;
|
|
1417
1872
|
const results = {};
|
|
1418
1873
|
if (hasCampaignIds) {
|
|
1419
|
-
results.campaigns = await adsManager.enableCampaigns(customerId, args.campaign_ids);
|
|
1874
|
+
results.campaigns = await adsManager.enableCampaigns(customerId, args.campaign_ids, labels);
|
|
1420
1875
|
}
|
|
1421
1876
|
if (hasAdGroupIds) {
|
|
1422
|
-
results.adGroups = await adsManager.enableAdGroups(customerId, args.ad_group_ids);
|
|
1877
|
+
results.adGroups = await adsManager.enableAdGroups(customerId, args.ad_group_ids, labels);
|
|
1423
1878
|
}
|
|
1424
1879
|
if (hasAdIds) {
|
|
1425
|
-
results.ads = await adsManager.enableAds(customerId, args.ad_ids);
|
|
1880
|
+
results.ads = await adsManager.enableAds(customerId, args.ad_ids, labels);
|
|
1426
1881
|
}
|
|
1427
1882
|
return {
|
|
1428
1883
|
content: [{
|
|
@@ -1448,15 +1903,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1448
1903
|
}]
|
|
1449
1904
|
};
|
|
1450
1905
|
}
|
|
1906
|
+
const labels = args?.labels;
|
|
1451
1907
|
const results = {};
|
|
1452
1908
|
if (hasCampaignIds) {
|
|
1453
|
-
results.campaigns = await adsManager.pauseCampaigns(customerId, args.campaign_ids);
|
|
1909
|
+
results.campaigns = await adsManager.pauseCampaigns(customerId, args.campaign_ids, labels);
|
|
1454
1910
|
}
|
|
1455
1911
|
if (hasAdGroupIds) {
|
|
1456
|
-
results.adGroups = await adsManager.pauseAdGroups(customerId, args.ad_group_ids);
|
|
1912
|
+
results.adGroups = await adsManager.pauseAdGroups(customerId, args.ad_group_ids, labels);
|
|
1457
1913
|
}
|
|
1458
1914
|
if (hasAdIds) {
|
|
1459
|
-
results.ads = await adsManager.pauseAds(customerId, args.ad_ids);
|
|
1915
|
+
results.ads = await adsManager.pauseAds(customerId, args.ad_ids, labels);
|
|
1460
1916
|
}
|
|
1461
1917
|
return {
|
|
1462
1918
|
content: [{
|
|
@@ -1469,6 +1925,81 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1469
1925
|
}]
|
|
1470
1926
|
};
|
|
1471
1927
|
}
|
|
1928
|
+
case "google_ads_remove_items": {
|
|
1929
|
+
const removeArgs = normalizeRemoveArgs(args);
|
|
1930
|
+
const customerId = removeArgs.customer_id ?? "";
|
|
1931
|
+
const validation = validateRemoveInput(removeArgs);
|
|
1932
|
+
if (!validation.ok) {
|
|
1933
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: validation.error }, null, 2) }] };
|
|
1934
|
+
}
|
|
1935
|
+
if (removeArgs.confirm !== true) {
|
|
1936
|
+
return {
|
|
1937
|
+
content: [{ type: "text", text: JSON.stringify(buildRemovePreview(removeArgs), null, 2) }]
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
const autoLabel = adsManager.todayClaudeLabel ? adsManager.todayClaudeLabel() : void 0;
|
|
1941
|
+
const allLabels = [autoLabel, ...removeArgs.labels ?? []].filter(
|
|
1942
|
+
(l) => typeof l === "string" && !!l
|
|
1943
|
+
);
|
|
1944
|
+
const labelResults = [];
|
|
1945
|
+
for (const labelName of allLabels) {
|
|
1946
|
+
try {
|
|
1947
|
+
const r = await adsManager.applyLabel(customerId, labelName, {
|
|
1948
|
+
campaignIds: removeArgs.campaign_ids,
|
|
1949
|
+
adGroupIds: removeArgs.ad_group_ids,
|
|
1950
|
+
adIds: removeArgs.ad_ids
|
|
1951
|
+
});
|
|
1952
|
+
labelResults.push(r);
|
|
1953
|
+
} catch (e) {
|
|
1954
|
+
return {
|
|
1955
|
+
content: [{
|
|
1956
|
+
type: "text",
|
|
1957
|
+
text: JSON.stringify({
|
|
1958
|
+
error: `Label attach failed before removal: ${e.message}. Nothing removed.`,
|
|
1959
|
+
partial_labels_applied: labelResults
|
|
1960
|
+
}, null, 2)
|
|
1961
|
+
}]
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
const steps = orderRemovalsChildUp(removeArgs);
|
|
1966
|
+
const removed = {};
|
|
1967
|
+
for (const step of steps) {
|
|
1968
|
+
if (step.type === "ads") {
|
|
1969
|
+
removed.ads = await adsManager.removeAds(customerId, step.ids);
|
|
1970
|
+
} else if (step.type === "ad_groups") {
|
|
1971
|
+
removed.ad_groups = await adsManager.removeAdGroups(customerId, step.ids);
|
|
1972
|
+
} else {
|
|
1973
|
+
removed.campaigns = await adsManager.removeCampaigns(customerId, step.ids);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
return {
|
|
1977
|
+
content: [{
|
|
1978
|
+
type: "text",
|
|
1979
|
+
text: JSON.stringify({
|
|
1980
|
+
success: true,
|
|
1981
|
+
message: "Items removed. Reports on removed resources remain queryable.",
|
|
1982
|
+
labels_applied: allLabels,
|
|
1983
|
+
removed
|
|
1984
|
+
}, null, 2)
|
|
1985
|
+
}]
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
case "google_ads_apply_label": {
|
|
1989
|
+
const customerId = args?.customer_id ?? "";
|
|
1990
|
+
const label = args?.label;
|
|
1991
|
+
if (!label) {
|
|
1992
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "label is required" }, null, 2) }] };
|
|
1993
|
+
}
|
|
1994
|
+
const result = await adsManager.applyLabel(customerId, label, {
|
|
1995
|
+
campaignIds: coerceStringArray(args?.campaign_ids),
|
|
1996
|
+
adGroupIds: coerceStringArray(args?.ad_group_ids),
|
|
1997
|
+
adIds: coerceStringArray(args?.ad_ids)
|
|
1998
|
+
});
|
|
1999
|
+
return {
|
|
2000
|
+
content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }]
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
1472
2003
|
case "google_ads_update_campaign_tracking": {
|
|
1473
2004
|
const customerId = args?.customer_id || "";
|
|
1474
2005
|
const campaignId = args?.campaign_id;
|
|
@@ -1852,6 +2383,72 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1852
2383
|
}]
|
|
1853
2384
|
};
|
|
1854
2385
|
}
|
|
2386
|
+
case "google_ads_update_campaign_bidding": {
|
|
2387
|
+
const customerId = args?.customer_id || "";
|
|
2388
|
+
const campaignId = args?.campaign_id;
|
|
2389
|
+
const strategy = args?.strategy;
|
|
2390
|
+
const targetCpaDollars = args?.target_cpa_dollars;
|
|
2391
|
+
const targetRoas = args?.target_roas;
|
|
2392
|
+
if (strategy === void 0 && targetCpaDollars === void 0 && targetRoas === void 0) {
|
|
2393
|
+
return {
|
|
2394
|
+
content: [{
|
|
2395
|
+
type: "text",
|
|
2396
|
+
text: JSON.stringify({ error: "Must provide at least one of: strategy, target_cpa_dollars, target_roas" }, null, 2)
|
|
2397
|
+
}]
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
if (targetCpaDollars !== void 0 && targetCpaDollars <= 0) {
|
|
2401
|
+
return {
|
|
2402
|
+
content: [{
|
|
2403
|
+
type: "text",
|
|
2404
|
+
text: JSON.stringify({ error: "target_cpa_dollars must be positive" }, null, 2)
|
|
2405
|
+
}]
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
if (targetRoas !== void 0 && targetRoas <= 0) {
|
|
2409
|
+
return {
|
|
2410
|
+
content: [{
|
|
2411
|
+
type: "text",
|
|
2412
|
+
text: JSON.stringify({ error: "target_roas must be positive (decimal, e.g., 3.0 = 300%)" }, null, 2)
|
|
2413
|
+
}]
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
const result = await adsManager.updateCampaignBidding(customerId, campaignId, {
|
|
2417
|
+
strategy,
|
|
2418
|
+
target_cpa_dollars: targetCpaDollars,
|
|
2419
|
+
target_roas: targetRoas
|
|
2420
|
+
});
|
|
2421
|
+
return {
|
|
2422
|
+
content: [{
|
|
2423
|
+
type: "text",
|
|
2424
|
+
text: JSON.stringify({ success: true, ...result }, null, 2)
|
|
2425
|
+
}]
|
|
2426
|
+
};
|
|
2427
|
+
}
|
|
2428
|
+
case "google_ads_update_asset_urls": {
|
|
2429
|
+
const normalized = normalizeUpdateAssetUrlsArgs(args);
|
|
2430
|
+
if ("error" in normalized) {
|
|
2431
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: normalized.error }, null, 2) }] };
|
|
2432
|
+
}
|
|
2433
|
+
if (!normalized.confirm) {
|
|
2434
|
+
return { content: [{ type: "text", text: JSON.stringify(buildUpdateUrlsDryRun(normalized), null, 2) }] };
|
|
2435
|
+
}
|
|
2436
|
+
const customerId = normalized.customer_id || "";
|
|
2437
|
+
const result = await adsManager.updateAssetUrls(customerId, normalized.updates);
|
|
2438
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
2439
|
+
}
|
|
2440
|
+
case "google_ads_pause_asset_links": {
|
|
2441
|
+
const normalized = normalizePauseAssetLinksArgs(args);
|
|
2442
|
+
if ("error" in normalized) {
|
|
2443
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: normalized.error }, null, 2) }] };
|
|
2444
|
+
}
|
|
2445
|
+
if (!normalized.confirm) {
|
|
2446
|
+
return { content: [{ type: "text", text: JSON.stringify(buildPauseLinksDryRun(normalized), null, 2) }] };
|
|
2447
|
+
}
|
|
2448
|
+
const customerId = normalized.customer_id || "";
|
|
2449
|
+
const result = await adsManager.pauseAssetLinks(customerId, normalized.resource_names);
|
|
2450
|
+
return { content: [{ type: "text", text: JSON.stringify({ success: true, ...result }, null, 2) }] };
|
|
2451
|
+
}
|
|
1855
2452
|
case "google_ads_gaql_query": {
|
|
1856
2453
|
const customerId = args?.customer_id || "";
|
|
1857
2454
|
const query = args?.query;
|
|
@@ -1893,6 +2490,65 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
1893
2490
|
}]
|
|
1894
2491
|
};
|
|
1895
2492
|
}
|
|
2493
|
+
case "google_ads_create_demand_gen_multi_asset_ad": {
|
|
2494
|
+
const customerId = args?.customer_id || "";
|
|
2495
|
+
try {
|
|
2496
|
+
const result = await adsManager.createDemandGenMultiAssetAd(customerId, {
|
|
2497
|
+
ad_group_id: args?.ad_group_id,
|
|
2498
|
+
final_urls: args?.final_urls,
|
|
2499
|
+
business_name: args?.business_name,
|
|
2500
|
+
call_to_action: args?.call_to_action,
|
|
2501
|
+
marketing_image_asset_ids: args?.marketing_image_asset_ids,
|
|
2502
|
+
square_marketing_image_asset_ids: args?.square_marketing_image_asset_ids,
|
|
2503
|
+
portrait_marketing_image_asset_ids: args?.portrait_marketing_image_asset_ids,
|
|
2504
|
+
logo_image_asset_ids: args?.logo_image_asset_ids,
|
|
2505
|
+
headlines: args?.headlines,
|
|
2506
|
+
long_headlines: args?.long_headlines,
|
|
2507
|
+
descriptions: args?.descriptions,
|
|
2508
|
+
labels: args?.labels
|
|
2509
|
+
});
|
|
2510
|
+
return {
|
|
2511
|
+
content: [{
|
|
2512
|
+
type: "text",
|
|
2513
|
+
text: JSON.stringify({
|
|
2514
|
+
success: true,
|
|
2515
|
+
message: "Demand Gen multi-asset ad created (PAUSED). Review in Google Ads before enabling.",
|
|
2516
|
+
...result
|
|
2517
|
+
}, null, 2)
|
|
2518
|
+
}]
|
|
2519
|
+
};
|
|
2520
|
+
} catch (e) {
|
|
2521
|
+
return {
|
|
2522
|
+
content: [{
|
|
2523
|
+
type: "text",
|
|
2524
|
+
text: JSON.stringify({ success: false, error: e.message }, null, 2)
|
|
2525
|
+
}]
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2528
|
+
}
|
|
2529
|
+
case "google_ads_create_image_asset": {
|
|
2530
|
+
const customerId = args?.customer_id || "";
|
|
2531
|
+
try {
|
|
2532
|
+
const result = await adsManager.createImageAsset(customerId, {
|
|
2533
|
+
name: args?.name,
|
|
2534
|
+
file_path: args?.file_path,
|
|
2535
|
+
base64_data: args?.base64_data
|
|
2536
|
+
});
|
|
2537
|
+
return {
|
|
2538
|
+
content: [{
|
|
2539
|
+
type: "text",
|
|
2540
|
+
text: JSON.stringify({ success: true, ...result }, null, 2)
|
|
2541
|
+
}]
|
|
2542
|
+
};
|
|
2543
|
+
} catch (e) {
|
|
2544
|
+
return {
|
|
2545
|
+
content: [{
|
|
2546
|
+
type: "text",
|
|
2547
|
+
text: JSON.stringify({ success: false, error: e.message }, null, 2)
|
|
2548
|
+
}]
|
|
2549
|
+
};
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
1896
2552
|
default:
|
|
1897
2553
|
throw new Error(`Unknown tool: ${name}`);
|
|
1898
2554
|
}
|