mcp-google-ads 1.0.6 → 1.0.8
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/build-info.json +1 -1
- package/dist/index.js +25 -13
- package/dist/resilience.js +50 -19
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
1
|
+
{"sha":"bdd46f2","builtAt":"2026-04-09T21:28:07.536Z"}
|
package/dist/index.js
CHANGED
|
@@ -8,29 +8,30 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
|
|
|
8
8
|
import { tools } from "./tools.js";
|
|
9
9
|
import { GoogleAdsApi, enums } from "google-ads-api";
|
|
10
10
|
import { readFileSync, existsSync } from "fs";
|
|
11
|
+
// CLI package info
|
|
12
|
+
const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
|
|
11
13
|
// Log build fingerprint at startup
|
|
12
14
|
try {
|
|
13
15
|
const __buildInfoDir = dirname(new URL(import.meta.url).pathname);
|
|
14
16
|
const buildInfo = JSON.parse(readFileSync(join(__buildInfoDir, "build-info.json"), "utf-8"));
|
|
15
|
-
|
|
17
|
+
console.error(`[build] SHA: ${buildInfo.sha} (${buildInfo.builtAt})`);
|
|
16
18
|
}
|
|
17
19
|
catch {
|
|
18
|
-
|
|
20
|
+
console.error(`[build] ${__cliPkg.name}@${__cliPkg.version} (dev mode)`);
|
|
19
21
|
}
|
|
20
22
|
// CLI flags
|
|
21
|
-
const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
|
|
22
23
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
23
|
-
console.
|
|
24
|
-
console.
|
|
25
|
-
console.
|
|
26
|
-
console.
|
|
27
|
-
console.
|
|
28
|
-
console.
|
|
29
|
-
console.
|
|
24
|
+
console.error(`${__cliPkg.name} v${__cliPkg.version}\n`);
|
|
25
|
+
console.error(`Usage: ${__cliPkg.name} [options]\n`);
|
|
26
|
+
console.error("MCP server communicating via stdio. Configure in your .mcp.json.\n");
|
|
27
|
+
console.error("Options:");
|
|
28
|
+
console.error(" --help, -h Show this help message");
|
|
29
|
+
console.error(" --version, -v Show version number");
|
|
30
|
+
console.error(`\nDocumentation: https://github.com/mharnett/mcp-google-ads`);
|
|
30
31
|
process.exit(0);
|
|
31
32
|
}
|
|
32
33
|
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
33
|
-
console.
|
|
34
|
+
console.error(__cliPkg.version);
|
|
34
35
|
process.exit(0);
|
|
35
36
|
}
|
|
36
37
|
function loadConfig() {
|
|
@@ -1151,8 +1152,8 @@ class GoogleAdsManager {
|
|
|
1151
1152
|
const config = loadConfig();
|
|
1152
1153
|
const adsManager = new GoogleAdsManager(config);
|
|
1153
1154
|
const server = new Server({
|
|
1154
|
-
name:
|
|
1155
|
-
version:
|
|
1155
|
+
name: __cliPkg.name,
|
|
1156
|
+
version: __cliPkg.version,
|
|
1156
1157
|
}, {
|
|
1157
1158
|
capabilities: {
|
|
1158
1159
|
tools: {},
|
|
@@ -1806,4 +1807,15 @@ async function main() {
|
|
|
1806
1807
|
await server.connect(transport);
|
|
1807
1808
|
logger.info("MCP Google Ads server running");
|
|
1808
1809
|
}
|
|
1810
|
+
process.on("SIGTERM", () => {
|
|
1811
|
+
console.error("[shutdown] SIGTERM received, exiting");
|
|
1812
|
+
process.exit(0);
|
|
1813
|
+
});
|
|
1814
|
+
process.on("SIGINT", () => {
|
|
1815
|
+
console.error("[shutdown] SIGINT received, exiting");
|
|
1816
|
+
process.exit(0);
|
|
1817
|
+
});
|
|
1818
|
+
process.on("SIGPIPE", () => {
|
|
1819
|
+
// Client disconnected -- expected during shutdown
|
|
1820
|
+
});
|
|
1809
1821
|
main().catch((err) => logger.error({ error: err.message, stack: err.stack }, "Fatal startup error"));
|
package/dist/resilience.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retry, circuitBreaker, wrap,
|
|
1
|
+
import { retry, circuitBreaker, wrap, handleWhen, timeout, TimeoutStrategy, ExponentialBackoff, ConsecutiveBreaker, } from "cockatiel";
|
|
2
2
|
import pino from "pino";
|
|
3
3
|
// ============================================
|
|
4
4
|
// LOGGER
|
|
@@ -12,36 +12,51 @@ export const logger = pino({
|
|
|
12
12
|
colorize: true,
|
|
13
13
|
singleLine: true,
|
|
14
14
|
translateTime: "SYS:standard",
|
|
15
|
+
destination: 2, // stderr -- stdout is reserved for MCP JSON-RPC
|
|
15
16
|
},
|
|
16
17
|
},
|
|
17
18
|
}),
|
|
18
|
-
}
|
|
19
|
+
},
|
|
20
|
+
// When no transport (test mode), write to stderr directly
|
|
21
|
+
process.env.NODE_ENV === "test" ? pino.destination(2) : undefined);
|
|
19
22
|
// ============================================
|
|
20
23
|
// SAFE RESPONSE (Response Size Limiting)
|
|
21
24
|
// ============================================
|
|
22
25
|
const MAX_RESPONSE_SIZE = 200_000; // 200KB
|
|
23
26
|
export function safeResponse(data, context) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
let current = data;
|
|
28
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
29
|
+
const jsonStr = JSON.stringify(current);
|
|
30
|
+
const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
|
|
31
|
+
if (sizeBytes <= MAX_RESPONSE_SIZE)
|
|
32
|
+
return current;
|
|
33
|
+
logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
|
|
34
|
+
if (Array.isArray(current)) {
|
|
35
|
+
current = current.slice(0, Math.max(1, Math.floor(current.length * 0.5)));
|
|
36
|
+
continue;
|
|
32
37
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
for (const key of ["items", "results", "data", "rows"]) {
|
|
37
|
-
if (Array.isArray(obj[key])) {
|
|
38
|
+
if (typeof current === "object" && current !== null) {
|
|
39
|
+
const obj = current;
|
|
40
|
+
let truncated = false;
|
|
41
|
+
for (const key of ["items", "results", "data", "rows", "tags", "triggers", "variables"]) {
|
|
42
|
+
if (Array.isArray(obj[key]) && obj[key].length > 1) {
|
|
38
43
|
obj[key] = obj[key].slice(0, Math.max(1, Math.floor(obj[key].length * 0.5)));
|
|
39
|
-
|
|
44
|
+
if ("count" in obj)
|
|
45
|
+
obj.count = obj[key].length;
|
|
46
|
+
if ("row_count" in obj)
|
|
47
|
+
obj.row_count = obj[key].length;
|
|
48
|
+
obj.truncated = true;
|
|
49
|
+
truncated = true;
|
|
50
|
+
break;
|
|
40
51
|
}
|
|
41
52
|
}
|
|
53
|
+
if (truncated)
|
|
54
|
+
continue;
|
|
42
55
|
}
|
|
56
|
+
// Can't truncate further (not an array/object with known keys)
|
|
57
|
+
break;
|
|
43
58
|
}
|
|
44
|
-
return
|
|
59
|
+
return current;
|
|
45
60
|
}
|
|
46
61
|
// ============================================
|
|
47
62
|
// RETRY + CIRCUIT BREAKER + TIMEOUT
|
|
@@ -50,12 +65,28 @@ const backoff = new ExponentialBackoff({
|
|
|
50
65
|
initialDelay: 100,
|
|
51
66
|
maxDelay: 5_000,
|
|
52
67
|
});
|
|
68
|
+
const isTransient = handleWhen((err) => {
|
|
69
|
+
const msg = (err?.message || "").toLowerCase();
|
|
70
|
+
const code = err?.code || err?.status;
|
|
71
|
+
// Don't retry auth errors
|
|
72
|
+
if (code === 401 || code === 403 || code === 7 || code === 16)
|
|
73
|
+
return false;
|
|
74
|
+
if (msg.includes("unauthenticated") || msg.includes("permission_denied") || msg.includes("invalid_grant"))
|
|
75
|
+
return false;
|
|
76
|
+
// Don't retry client errors (except rate limits)
|
|
77
|
+
if (code === 429 || msg.includes("rate"))
|
|
78
|
+
return true;
|
|
79
|
+
if (code >= 400 && code < 500)
|
|
80
|
+
return false;
|
|
81
|
+
// Retry everything else (5xx, timeouts, network errors)
|
|
82
|
+
return true;
|
|
83
|
+
});
|
|
53
84
|
// Individual policies
|
|
54
|
-
const retryPolicy = retry(
|
|
85
|
+
const retryPolicy = retry(isTransient, {
|
|
55
86
|
maxAttempts: 3,
|
|
56
87
|
backoff,
|
|
57
88
|
});
|
|
58
|
-
const circuitBreakerPolicy = circuitBreaker(
|
|
89
|
+
const circuitBreakerPolicy = circuitBreaker(isTransient, {
|
|
59
90
|
halfOpenAfter: 60_000, // 60s to attempt recovery
|
|
60
91
|
breaker: new ConsecutiveBreaker(5), // Open after 5 consecutive failures
|
|
61
92
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-ads",
|
|
3
3
|
"mcpName": "io.github.mharnett/google-ads",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"description": "MCP server for Google Ads API with MCC support, 35 tools for campaign management, reporting, and optimization. Safe by default — all changes created PAUSED.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|