mcp-google-ads 1.3.0 → 1.4.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/README.md +12 -0
- package/dist/build-info.json +2 -2
- package/dist/campaignBuilder.js +1 -1
- package/dist/index.js +19 -7
- package/dist/validateDemandGenAd.d.ts +12 -0
- package/dist/validateDemandGenAd.js +34 -1
- package/dist/writeGate.d.ts +18 -0
- package/dist/writeGate.js +54 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -103,6 +103,18 @@ Alternatively, set credentials via environment variables (these override `config
|
|
|
103
103
|
| `GOOGLE_ADS_CLIENT_ID` | Yes | OAuth 2.0 client ID |
|
|
104
104
|
| `GOOGLE_ADS_CLIENT_SECRET` | Yes | OAuth 2.0 client secret |
|
|
105
105
|
| `GOOGLE_ADS_REFRESH_TOKEN` | Yes | OAuth 2.0 refresh token |
|
|
106
|
+
| `GOOGLE_ADS_MCP_WRITE` | No | Set to `true` to expose mutating tools (create/update/pause/enable/remove/apply). Default: read-only. |
|
|
107
|
+
|
|
108
|
+
### Read-only by default
|
|
109
|
+
|
|
110
|
+
The server ships read-only. Mutating tools (anything that creates, updates,
|
|
111
|
+
pauses, enables, removes, links, or applies) are hidden from the tool list
|
|
112
|
+
until you set `GOOGLE_ADS_MCP_WRITE=true` in the MCP server environment.
|
|
113
|
+
If a write tool is somehow invoked without that flag, the server returns a
|
|
114
|
+
clear error pointing at the env var.
|
|
115
|
+
|
|
116
|
+
This is deliberate: a casual chat message like "activate the Fundraising
|
|
117
|
+
campaign" should not move live ad spend without an explicit opt-in.
|
|
106
118
|
|
|
107
119
|
### 4. Add to Claude Code
|
|
108
120
|
|
package/dist/build-info.json
CHANGED
package/dist/campaignBuilder.js
CHANGED
|
@@ -23,7 +23,7 @@ function buildCampaignCreatePayload(input) {
|
|
|
23
23
|
campaign.network_settings = {
|
|
24
24
|
target_google_search: false,
|
|
25
25
|
target_search_network: false,
|
|
26
|
-
target_content_network:
|
|
26
|
+
target_content_network: true,
|
|
27
27
|
target_partner_search_network: false
|
|
28
28
|
};
|
|
29
29
|
}
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,11 @@ import {
|
|
|
11
11
|
ListToolsRequestSchema
|
|
12
12
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
13
13
|
import { tools } from "./tools.js";
|
|
14
|
+
import {
|
|
15
|
+
assertWriteAllowed,
|
|
16
|
+
filterTools,
|
|
17
|
+
isWriteEnabled
|
|
18
|
+
} from "./writeGate.js";
|
|
14
19
|
import { validateRsa } from "./validateRsa.js";
|
|
15
20
|
import {
|
|
16
21
|
validateRemoveInput,
|
|
@@ -36,7 +41,8 @@ import {
|
|
|
36
41
|
} from "./imageAsset.js";
|
|
37
42
|
import {
|
|
38
43
|
validateDemandGenAd,
|
|
39
|
-
buildDemandGenAdPayload
|
|
44
|
+
buildDemandGenAdPayload,
|
|
45
|
+
isDemandGenAdGroup
|
|
40
46
|
} from "./validateDemandGenAd.js";
|
|
41
47
|
import { GoogleAdsApi, enums } from "google-ads-api";
|
|
42
48
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -729,7 +735,7 @@ class GoogleAdsManager {
|
|
|
729
735
|
const cleanId = customerId.replace(/-/g, "");
|
|
730
736
|
const agRows = await withResilience(
|
|
731
737
|
() => customer.query(
|
|
732
|
-
`SELECT ad_group.id, ad_group.type FROM ad_group WHERE ad_group.id = ${sanitizeNumericId(
|
|
738
|
+
`SELECT ad_group.id, ad_group.type, campaign.advertising_channel_type FROM ad_group WHERE ad_group.id = ${sanitizeNumericId(
|
|
733
739
|
input.ad_group_id
|
|
734
740
|
)}`
|
|
735
741
|
),
|
|
@@ -738,11 +744,11 @@ class GoogleAdsManager {
|
|
|
738
744
|
if (!agRows || agRows.length === 0) {
|
|
739
745
|
throw new Error(`Ad group ${input.ad_group_id} not found`);
|
|
740
746
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
747
|
+
if (!isDemandGenAdGroup(agRows[0])) {
|
|
748
|
+
const agType = agRows[0]?.ad_group?.type;
|
|
749
|
+
const cType = agRows[0]?.campaign?.advertising_channel_type;
|
|
744
750
|
throw new Error(
|
|
745
|
-
`Ad group ${input.ad_group_id}
|
|
751
|
+
`Ad group ${input.ad_group_id} is not a Demand Gen ad group (ad_group.type='${agType}', campaign.advertising_channel_type='${cType}'). It must belong to a DEMAND_GEN campaign.`
|
|
746
752
|
);
|
|
747
753
|
}
|
|
748
754
|
const payload = buildDemandGenAdPayload({
|
|
@@ -1637,11 +1643,12 @@ const server = new Server(
|
|
|
1637
1643
|
}
|
|
1638
1644
|
);
|
|
1639
1645
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
1640
|
-
return { tools };
|
|
1646
|
+
return { tools: filterTools(tools) };
|
|
1641
1647
|
});
|
|
1642
1648
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1643
1649
|
const { name, arguments: args } = request.params;
|
|
1644
1650
|
try {
|
|
1651
|
+
assertWriteAllowed(name);
|
|
1645
1652
|
switch (name) {
|
|
1646
1653
|
case "google_ads_get_client_context": {
|
|
1647
1654
|
const cwd = args?.working_directory;
|
|
@@ -2598,6 +2605,11 @@ async function main() {
|
|
|
2598
2605
|
logger.warn({ error: err.message }, "Auth check returned non-auth error (may be OK)");
|
|
2599
2606
|
}
|
|
2600
2607
|
}
|
|
2608
|
+
const writeMode = isWriteEnabled();
|
|
2609
|
+
logger.info(
|
|
2610
|
+
{ writeEnabled: writeMode, envVar: "GOOGLE_ADS_MCP_WRITE" },
|
|
2611
|
+
writeMode ? "Write operations ENABLED (mutating tools exposed)" : "Read-only mode (write tools hidden -- set GOOGLE_ADS_MCP_WRITE=true to enable)"
|
|
2612
|
+
);
|
|
2601
2613
|
const transport = new StdioServerTransport();
|
|
2602
2614
|
await server.connect(transport);
|
|
2603
2615
|
logger.info("MCP Google Ads server running");
|
|
@@ -60,3 +60,15 @@ export declare function buildDemandGenAdPayload(args: {
|
|
|
60
60
|
input: DemandGenAdInput;
|
|
61
61
|
}): DemandGenAdPayload;
|
|
62
62
|
export declare function validateDemandGenAd(ad: DemandGenAdInput): DemandGenAdValidationResult;
|
|
63
|
+
/**
|
|
64
|
+
* Decide whether a GAQL row describes a Demand Gen ad group. Accepts two
|
|
65
|
+
* orthogonal signals:
|
|
66
|
+
* 1) ad_group.type matches DEMAND_GEN_MULTI_ASSET_AD_GROUP (proto value 21
|
|
67
|
+
* or its string name when future libs add it).
|
|
68
|
+
* 2) campaign.advertising_channel_type === DEMAND_GEN (14). This is the
|
|
69
|
+
* authoritative check because DG campaigns can only contain DG ad groups,
|
|
70
|
+
* and google-ads-api v23 returns undefined for ad_group.type when the
|
|
71
|
+
* stored proto value isn't in its local enum map.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isDemandGenAdGroup(row: any): boolean;
|
|
74
|
+
export declare function normalizeCallToAction(input: string): string;
|
|
@@ -12,7 +12,7 @@ function buildDemandGenAdPayload(args) {
|
|
|
12
12
|
const assetRef = (id) => ({ asset: `customers/${customer_id_clean}/assets/${id}` });
|
|
13
13
|
const dgAd = {
|
|
14
14
|
business_name: input.business_name,
|
|
15
|
-
call_to_action_text: input.call_to_action,
|
|
15
|
+
call_to_action_text: normalizeCallToAction(input.call_to_action),
|
|
16
16
|
marketing_images: input.marketing_image_asset_ids.map(assetRef),
|
|
17
17
|
headlines: input.headlines.map((h) => ({ text: headlineText(h) })),
|
|
18
18
|
descriptions: input.descriptions.map((t) => ({ text: t }))
|
|
@@ -83,6 +83,37 @@ function validateDemandGenAd(ad) {
|
|
|
83
83
|
});
|
|
84
84
|
return { valid: errors.length === 0, errors };
|
|
85
85
|
}
|
|
86
|
+
const DEMAND_GEN_CHANNEL_TYPE = 14;
|
|
87
|
+
function isDemandGenAdGroup(row) {
|
|
88
|
+
if (!row) return false;
|
|
89
|
+
const agType = row.ad_group?.type;
|
|
90
|
+
const campaignChannelType = row.campaign?.advertising_channel_type;
|
|
91
|
+
return agType === 21 || agType === "21" || agType === "DEMAND_GEN_MULTI_ASSET_AD_GROUP" || campaignChannelType === DEMAND_GEN_CHANNEL_TYPE || campaignChannelType === "DEMAND_GEN";
|
|
92
|
+
}
|
|
93
|
+
const CTA_DISPLAY_MAP = {
|
|
94
|
+
LEARN_MORE: "Learn more",
|
|
95
|
+
GET_QUOTE: "Get quote",
|
|
96
|
+
APPLY_NOW: "Apply now",
|
|
97
|
+
SIGN_UP: "Sign up",
|
|
98
|
+
CONTACT_US: "Contact us",
|
|
99
|
+
SUBSCRIBE: "Subscribe",
|
|
100
|
+
DOWNLOAD: "Download",
|
|
101
|
+
BOOK_NOW: "Book now",
|
|
102
|
+
SHOP_NOW: "Shop now",
|
|
103
|
+
BUY_NOW: "Buy now",
|
|
104
|
+
DONATE_NOW: "Donate now",
|
|
105
|
+
ORDER_NOW: "Order now",
|
|
106
|
+
PLAY_NOW: "Play now",
|
|
107
|
+
SEE_MORE: "See more",
|
|
108
|
+
START_NOW: "Start now",
|
|
109
|
+
VISIT_SITE: "Visit site",
|
|
110
|
+
WATCH_NOW: "Watch now"
|
|
111
|
+
};
|
|
112
|
+
function normalizeCallToAction(input) {
|
|
113
|
+
if (!input) return input;
|
|
114
|
+
const key = input.toUpperCase();
|
|
115
|
+
return CTA_DISPLAY_MAP[key] ?? input;
|
|
116
|
+
}
|
|
86
117
|
export {
|
|
87
118
|
MAX_DESCRIPTIONS,
|
|
88
119
|
MAX_DESCRIPTION_LEN,
|
|
@@ -91,6 +122,8 @@ export {
|
|
|
91
122
|
MAX_LONG_HEADLINES,
|
|
92
123
|
MAX_LONG_HEADLINE_LEN,
|
|
93
124
|
buildDemandGenAdPayload,
|
|
125
|
+
isDemandGenAdGroup,
|
|
126
|
+
normalizeCallToAction,
|
|
94
127
|
validateDemandGenAd
|
|
95
128
|
};
|
|
96
129
|
//# sourceMappingURL=validateDemandGenAd.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Tools that mutate Google Ads state. These are hidden from the tool list
|
|
4
|
+
* and refused at call time unless GOOGLE_ADS_MCP_WRITE=true.
|
|
5
|
+
*
|
|
6
|
+
* Adding a new tool? Put it in this set if it creates, modifies, pauses,
|
|
7
|
+
* enables, removes, links, unlinks, or applies anything.
|
|
8
|
+
*/
|
|
9
|
+
export declare const WRITE_TOOLS: ReadonlySet<string>;
|
|
10
|
+
export declare function isWriteTool(name: string): boolean;
|
|
11
|
+
export declare function isWriteEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
12
|
+
export declare function filterTools(allTools: readonly Tool[], env?: NodeJS.ProcessEnv): Tool[];
|
|
13
|
+
export declare const WRITE_DISABLED_MESSAGE = "Write operations are disabled. Set GOOGLE_ADS_MCP_WRITE=true in the MCP server environment to enable mutating tools (create/update/pause/enable/remove/apply).";
|
|
14
|
+
/**
|
|
15
|
+
* Assert that a tool call is allowed under the current write-mode setting.
|
|
16
|
+
* Throws a clear Error if the tool mutates state and writes are disabled.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertWriteAllowed(toolName: string, env?: NodeJS.ProcessEnv): void;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
2
|
+
"google_ads_create_campaign",
|
|
3
|
+
"google_ads_create_ad_group",
|
|
4
|
+
"google_ads_create_responsive_search_ad",
|
|
5
|
+
"google_ads_create_keywords",
|
|
6
|
+
"google_ads_create_shared_set",
|
|
7
|
+
"google_ads_create_demand_gen_multi_asset_ad",
|
|
8
|
+
"google_ads_create_image_asset",
|
|
9
|
+
"google_ads_enable_items",
|
|
10
|
+
"google_ads_pause_items",
|
|
11
|
+
"google_ads_pause_keywords",
|
|
12
|
+
"google_ads_pause_asset_links",
|
|
13
|
+
"google_ads_remove_items",
|
|
14
|
+
"google_ads_remove_shared_negatives",
|
|
15
|
+
"google_ads_remove_campaign_negatives",
|
|
16
|
+
"google_ads_remove_adgroup_negatives",
|
|
17
|
+
"google_ads_add_shared_negatives",
|
|
18
|
+
"google_ads_add_campaign_negatives",
|
|
19
|
+
"google_ads_link_shared_set",
|
|
20
|
+
"google_ads_unlink_shared_set",
|
|
21
|
+
"google_ads_apply_label",
|
|
22
|
+
"google_ads_update_campaign_tracking",
|
|
23
|
+
"google_ads_update_campaign_budget",
|
|
24
|
+
"google_ads_update_campaign_bidding",
|
|
25
|
+
"google_ads_update_asset_urls"
|
|
26
|
+
]);
|
|
27
|
+
function isWriteTool(name) {
|
|
28
|
+
return WRITE_TOOLS.has(name);
|
|
29
|
+
}
|
|
30
|
+
function isWriteEnabled(env = process.env) {
|
|
31
|
+
const v = (env.GOOGLE_ADS_MCP_WRITE || "").trim().toLowerCase();
|
|
32
|
+
return v === "true" || v === "1" || v === "yes";
|
|
33
|
+
}
|
|
34
|
+
function filterTools(allTools, env = process.env) {
|
|
35
|
+
if (isWriteEnabled(env)) return [...allTools];
|
|
36
|
+
return allTools.filter((t) => !WRITE_TOOLS.has(t.name));
|
|
37
|
+
}
|
|
38
|
+
const WRITE_DISABLED_MESSAGE = "Write operations are disabled. Set GOOGLE_ADS_MCP_WRITE=true in the MCP server environment to enable mutating tools (create/update/pause/enable/remove/apply).";
|
|
39
|
+
function assertWriteAllowed(toolName, env = process.env) {
|
|
40
|
+
if (!isWriteTool(toolName)) return;
|
|
41
|
+
if (isWriteEnabled(env)) return;
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Tool "${toolName}" is a write operation. ${WRITE_DISABLED_MESSAGE}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
WRITE_DISABLED_MESSAGE,
|
|
48
|
+
WRITE_TOOLS,
|
|
49
|
+
assertWriteAllowed,
|
|
50
|
+
filterTools,
|
|
51
|
+
isWriteEnabled,
|
|
52
|
+
isWriteTool
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=writeGate.js.map
|
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.
|
|
5
|
-
"description": "MCP server for Google Ads API with MCC support,
|
|
4
|
+
"version": "1.4.0",
|
|
5
|
+
"description": "MCP server for Google Ads API with MCC support, 41 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",
|