mcp-google-ads 1.2.5 → 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 +8 -0
- package/dist/doctor-cli.d.ts +12 -0
- package/dist/doctor-cli.js +144 -0
- package/dist/index.js +19 -7
- package/dist/install-cli.d.ts +12 -0
- package/dist/install-cli.js +127 -0
- 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 +13 -3
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
|
@@ -19,6 +19,14 @@ function buildCampaignCreatePayload(input) {
|
|
|
19
19
|
status: enums.CampaignStatus.PAUSED,
|
|
20
20
|
advertising_channel_type: channelEnum
|
|
21
21
|
};
|
|
22
|
+
if (channelType === "DEMAND_GEN") {
|
|
23
|
+
campaign.network_settings = {
|
|
24
|
+
target_google_search: false,
|
|
25
|
+
target_search_network: false,
|
|
26
|
+
target_content_network: true,
|
|
27
|
+
target_partner_search_network: false
|
|
28
|
+
};
|
|
29
|
+
}
|
|
22
30
|
if (input.start_date) campaign.start_date = input.start_date;
|
|
23
31
|
if (input.end_date) campaign.end_date = input.end_date;
|
|
24
32
|
switch (strategy) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface DoctorCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
status: "pass" | "fail" | "warn";
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorOptions {
|
|
8
|
+
configPath?: string;
|
|
9
|
+
credentialsPath?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDoctor(opts?: DoctorOptions): Promise<DoctorCheck[]>;
|
|
12
|
+
export declare function run(argv?: string[]): Promise<number>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "fs";
|
|
3
|
+
import { credentialsFilePath } from "./credentials.js";
|
|
4
|
+
import { resolveDefaultConfigPath } from "./install-cli.js";
|
|
5
|
+
async function runDoctor(opts = {}) {
|
|
6
|
+
const cfgPath = opts.configPath ?? resolveDefaultConfigPath();
|
|
7
|
+
const credsPath = opts.credentialsPath ?? credentialsFilePath;
|
|
8
|
+
const checks = [];
|
|
9
|
+
checks.push(checkNodeVersion());
|
|
10
|
+
const cfgExists = existsSync(cfgPath);
|
|
11
|
+
checks.push({
|
|
12
|
+
name: "claude_desktop_config exists",
|
|
13
|
+
status: cfgExists ? "pass" : "fail",
|
|
14
|
+
detail: cfgExists ? cfgPath : `Not found at ${cfgPath}. Run: npx mcp-google-ads-install`
|
|
15
|
+
});
|
|
16
|
+
let cfg = null;
|
|
17
|
+
let cfgParseOk = false;
|
|
18
|
+
if (cfgExists) {
|
|
19
|
+
try {
|
|
20
|
+
cfg = JSON.parse(readFileSync(cfgPath, "utf8") || "{}");
|
|
21
|
+
cfgParseOk = true;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
cfgParseOk = false;
|
|
24
|
+
}
|
|
25
|
+
checks.push({
|
|
26
|
+
name: "claude_desktop_config is valid JSON",
|
|
27
|
+
status: cfgParseOk ? "pass" : "fail",
|
|
28
|
+
detail: cfgParseOk ? "parsed ok" : `${cfgPath} is not valid JSON. Fix syntax or delete the file and re-run mcp-google-ads-install.`
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
checks.push({
|
|
32
|
+
name: "claude_desktop_config is valid JSON",
|
|
33
|
+
status: "fail",
|
|
34
|
+
detail: "config file missing \u2014 cannot check JSON validity"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const mcpServers = cfgParseOk && cfg && typeof cfg === "object" ? cfg.mcpServers ?? {} : {};
|
|
38
|
+
const registered = Boolean(mcpServers["google-ads"]);
|
|
39
|
+
checks.push({
|
|
40
|
+
name: "google-ads registered in claude_desktop_config",
|
|
41
|
+
status: registered ? "pass" : "fail",
|
|
42
|
+
detail: registered ? "entry present under mcpServers.google-ads" : "not registered. Run: npx mcp-google-ads-install"
|
|
43
|
+
});
|
|
44
|
+
const credsExist = existsSync(credsPath);
|
|
45
|
+
checks.push({
|
|
46
|
+
name: "credentials file exists",
|
|
47
|
+
status: credsExist ? "pass" : "fail",
|
|
48
|
+
detail: credsExist ? credsPath : `Not found at ${credsPath}. Run: npx mcp-google-ads-auth`
|
|
49
|
+
});
|
|
50
|
+
let creds = {};
|
|
51
|
+
if (credsExist) {
|
|
52
|
+
try {
|
|
53
|
+
creds = JSON.parse(readFileSync(credsPath, "utf8"));
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const hasRefresh = typeof creds.refresh_token === "string" && creds.refresh_token.length > 0;
|
|
58
|
+
const hasCustomerId = typeof creds.customer_id === "string" && creds.customer_id.length > 0;
|
|
59
|
+
checks.push({
|
|
60
|
+
name: "credentials file has refresh_token and customer_id",
|
|
61
|
+
status: hasRefresh && hasCustomerId ? "pass" : credsExist ? "fail" : "warn",
|
|
62
|
+
detail: credsExist ? hasRefresh && hasCustomerId ? "both fields present" : "missing required fields. Re-run: npx mcp-google-ads-auth" : "credentials file missing \u2014 cannot check"
|
|
63
|
+
});
|
|
64
|
+
const customerId = typeof creds.customer_id === "string" ? creds.customer_id : "";
|
|
65
|
+
const mccId = typeof creds.mcc_customer_id === "string" ? creds.mcc_customer_id : "";
|
|
66
|
+
const isMccTerminal = customerId.length > 0 && customerId === mccId;
|
|
67
|
+
checks.push({
|
|
68
|
+
name: "customer_id is a leaf account, not an MCC",
|
|
69
|
+
status: !hasCustomerId ? "warn" : isMccTerminal ? "warn" : "pass",
|
|
70
|
+
detail: !hasCustomerId ? "customer_id missing \u2014 cannot check" : isMccTerminal ? `customer_id (${customerId}) equals mcc_customer_id \u2014 this is a Manager account. Most tools need a leaf. Re-run: npx mcp-google-ads-auth and pick a client under the MCC.` : `leaf account (customer_id=${customerId}${mccId ? `, under MCC ${mccId}` : ", direct access"})`
|
|
71
|
+
});
|
|
72
|
+
return checks;
|
|
73
|
+
}
|
|
74
|
+
function checkNodeVersion() {
|
|
75
|
+
const match = process.version.match(/^v(\d+)\./);
|
|
76
|
+
const major = match ? parseInt(match[1], 10) : 0;
|
|
77
|
+
return {
|
|
78
|
+
name: "node version >= 18",
|
|
79
|
+
status: major >= 18 ? "pass" : "fail",
|
|
80
|
+
detail: major >= 18 ? process.version : `${process.version} is too old. Install Node 18+ from https://nodejs.org/`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function renderChecks(checks) {
|
|
84
|
+
const icon = (s) => s === "pass" ? "\u2713" : s === "warn" ? "\u26A0" : "\u2717";
|
|
85
|
+
const lines = ["", "mcp-google-ads \u2014 diagnostic", ""];
|
|
86
|
+
for (const c of checks) {
|
|
87
|
+
lines.push(` ${icon(c.status)} ${c.name}`);
|
|
88
|
+
lines.push(` ${c.detail}`);
|
|
89
|
+
}
|
|
90
|
+
const failed = checks.filter((c) => c.status === "fail").length;
|
|
91
|
+
const warned = checks.filter((c) => c.status === "warn").length;
|
|
92
|
+
lines.push("");
|
|
93
|
+
if (failed === 0 && warned === 0) {
|
|
94
|
+
lines.push("All checks passed. If Claude Desktop still shows the MCP as disconnected,");
|
|
95
|
+
lines.push("fully quit Claude Desktop (Cmd+Q) and reopen it.");
|
|
96
|
+
} else {
|
|
97
|
+
lines.push(`${failed} failure${failed === 1 ? "" : "s"}, ${warned} warning${warned === 1 ? "" : "s"}. See details above.`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
102
|
+
function parseArgs(argv) {
|
|
103
|
+
const out = { help: false };
|
|
104
|
+
for (let i = 0; i < argv.length; i++) {
|
|
105
|
+
const a = argv[i];
|
|
106
|
+
if (a === "--help" || a === "-h") out.help = true;
|
|
107
|
+
else if (a === "--config" && argv[i + 1]) out.configPath = argv[++i];
|
|
108
|
+
else if (a === "--credentials" && argv[i + 1]) out.credentialsPath = argv[++i];
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
async function run(argv = process.argv.slice(2)) {
|
|
113
|
+
const args = parseArgs(argv);
|
|
114
|
+
if (args.help) {
|
|
115
|
+
process.stderr.write("mcp-google-ads-doctor \u2014 diagnose Claude Desktop + Google Ads MCP setup\n");
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
const checks = await runDoctor(args);
|
|
119
|
+
process.stdout.write(renderChecks(checks));
|
|
120
|
+
return checks.some((c) => c.status === "fail") ? 1 : 0;
|
|
121
|
+
}
|
|
122
|
+
import { fileURLToPath } from "url";
|
|
123
|
+
import { realpathSync } from "fs";
|
|
124
|
+
function isMainModule() {
|
|
125
|
+
if (!process.argv[1]) return false;
|
|
126
|
+
try {
|
|
127
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (isMainModule()) {
|
|
133
|
+
run().then((code) => process.exit(code)).catch((err) => {
|
|
134
|
+
process.stderr.write(`
|
|
135
|
+
\u274C ${err instanceof Error ? err.message : String(err)}
|
|
136
|
+
`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
run,
|
|
142
|
+
runDoctor
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=doctor-cli.js.map
|
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");
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
interface InstallOptions {
|
|
3
|
+
configPath?: string;
|
|
4
|
+
customerId?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function resolveDefaultConfigPath(): string;
|
|
7
|
+
export declare function installIntoConfig(opts?: InstallOptions): {
|
|
8
|
+
path: string;
|
|
9
|
+
existed: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function run(argv?: string[]): Promise<void>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir, platform } from "os";
|
|
4
|
+
import { dirname, join } from "path";
|
|
5
|
+
function resolveDefaultConfigPath() {
|
|
6
|
+
const home = homedir();
|
|
7
|
+
switch (platform()) {
|
|
8
|
+
case "darwin":
|
|
9
|
+
return join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
10
|
+
case "win32":
|
|
11
|
+
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json");
|
|
12
|
+
default:
|
|
13
|
+
return join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function installIntoConfig(opts = {}) {
|
|
17
|
+
const configPath = opts.configPath ?? resolveDefaultConfigPath();
|
|
18
|
+
const existed = existsSync(configPath);
|
|
19
|
+
let config = {};
|
|
20
|
+
if (existed) {
|
|
21
|
+
const raw = readFileSync(configPath, "utf8");
|
|
22
|
+
if (raw.trim().length > 0) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
26
|
+
config = parsed;
|
|
27
|
+
}
|
|
28
|
+
} catch {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Claude Desktop config at ${configPath} is not valid JSON. Refusing to overwrite. Open the file, fix the syntax (or delete it to start fresh), and re-run.`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
const normalizedCustomerId = opts.customerId?.replace(/-/g, "");
|
|
38
|
+
const entry = {
|
|
39
|
+
command: "npx",
|
|
40
|
+
args: ["-y", "mcp-google-ads@latest"]
|
|
41
|
+
};
|
|
42
|
+
if (normalizedCustomerId) {
|
|
43
|
+
entry.env = { GOOGLE_ADS_CUSTOMER_ID: normalizedCustomerId };
|
|
44
|
+
}
|
|
45
|
+
const nextConfig = {
|
|
46
|
+
...config,
|
|
47
|
+
mcpServers: {
|
|
48
|
+
...config.mcpServers ?? {},
|
|
49
|
+
"google-ads": entry
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
writeFileSync(configPath, JSON.stringify(nextConfig, null, 2) + "\n", { mode: 420 });
|
|
53
|
+
return { path: configPath, existed };
|
|
54
|
+
}
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const out = { help: false };
|
|
57
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
if (a === "--help" || a === "-h") out.help = true;
|
|
60
|
+
else if (a === "--customer-id" && argv[i + 1]) out.customerId = argv[++i];
|
|
61
|
+
else if (a === "--config" && argv[i + 1]) out.configPath = argv[++i];
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function printHelp() {
|
|
66
|
+
process.stderr.write(
|
|
67
|
+
[
|
|
68
|
+
"mcp-google-ads-install \u2014 add mcp-google-ads to Claude Desktop config",
|
|
69
|
+
"",
|
|
70
|
+
"Usage:",
|
|
71
|
+
" npx mcp-google-ads-install",
|
|
72
|
+
" npx mcp-google-ads-install --customer-id 745-851-7309",
|
|
73
|
+
"",
|
|
74
|
+
"Options:",
|
|
75
|
+
" --customer-id <id> Pin the customer ID in the config (skips auth picker later)",
|
|
76
|
+
" --config <path> Override the default Claude Desktop config path",
|
|
77
|
+
" -h, --help Show this help",
|
|
78
|
+
"",
|
|
79
|
+
`Default config path: ${resolveDefaultConfigPath()}`,
|
|
80
|
+
""
|
|
81
|
+
].join("\n")
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
async function run(argv = process.argv.slice(2)) {
|
|
85
|
+
const args = parseArgs(argv);
|
|
86
|
+
if (args.help) {
|
|
87
|
+
printHelp();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const result = installIntoConfig(args);
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
[
|
|
93
|
+
"",
|
|
94
|
+
`\u2713 ${result.existed ? "Updated" : "Created"}: ${result.path}`,
|
|
95
|
+
"",
|
|
96
|
+
"Next steps:",
|
|
97
|
+
" 1. Fully quit Claude Desktop (Cmd+Q on macOS, not just close the window).",
|
|
98
|
+
" 2. Reopen Claude Desktop.",
|
|
99
|
+
" 3. If you haven't authenticated yet, run: npx mcp-google-ads-auth",
|
|
100
|
+
""
|
|
101
|
+
].join("\n")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
import { fileURLToPath } from "url";
|
|
105
|
+
import { realpathSync } from "fs";
|
|
106
|
+
function isMainModule() {
|
|
107
|
+
if (!process.argv[1]) return false;
|
|
108
|
+
try {
|
|
109
|
+
return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (isMainModule()) {
|
|
115
|
+
run().catch((err) => {
|
|
116
|
+
process.stderr.write(`
|
|
117
|
+
\u274C ${err instanceof Error ? err.message : String(err)}
|
|
118
|
+
`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
export {
|
|
123
|
+
installIntoConfig,
|
|
124
|
+
resolveDefaultConfigPath,
|
|
125
|
+
run
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=install-cli.js.map
|
|
@@ -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,12 +1,14 @@
|
|
|
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",
|
|
9
|
-
"mcp-google-ads-auth": "dist/auth-cli.js"
|
|
9
|
+
"mcp-google-ads-auth": "dist/auth-cli.js",
|
|
10
|
+
"mcp-google-ads-install": "dist/install-cli.js",
|
|
11
|
+
"mcp-google-ads-doctor": "dist/doctor-cli.js"
|
|
10
12
|
},
|
|
11
13
|
"types": "dist/index.d.ts",
|
|
12
14
|
"type": "module",
|
|
@@ -18,6 +20,14 @@
|
|
|
18
20
|
"./auth-cli": {
|
|
19
21
|
"import": "./dist/auth-cli.js",
|
|
20
22
|
"types": "./dist/auth-cli.d.ts"
|
|
23
|
+
},
|
|
24
|
+
"./install-cli": {
|
|
25
|
+
"import": "./dist/install-cli.js",
|
|
26
|
+
"types": "./dist/install-cli.d.ts"
|
|
27
|
+
},
|
|
28
|
+
"./doctor-cli": {
|
|
29
|
+
"import": "./dist/doctor-cli.js",
|
|
30
|
+
"types": "./dist/doctor-cli.d.ts"
|
|
21
31
|
}
|
|
22
32
|
},
|
|
23
33
|
"files": [
|