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.
@@ -1 +1 @@
1
- {"sha":"5a28978","builtAt":"2026-04-09T20:25:53.245Z"}
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
- logger.info({ sha: buildInfo.sha, builtAt: buildInfo.builtAt }, "Build fingerprint");
17
+ console.error(`[build] SHA: ${buildInfo.sha} (${buildInfo.builtAt})`);
16
18
  }
17
19
  catch {
18
- // build-info.json not present (dev mode)
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.log(`${__cliPkg.name} v${__cliPkg.version}\n`);
24
- console.log(`Usage: ${__cliPkg.name} [options]\n`);
25
- console.log("MCP server communicating via stdio. Configure in your .mcp.json.\n");
26
- console.log("Options:");
27
- console.log(" --help, -h Show this help message");
28
- console.log(" --version, -v Show version number");
29
- console.log(`\nDocumentation: https://github.com/mharnett/mcp-google-ads`);
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.log(__cliPkg.version);
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: "mcp-google-ads",
1155
- version: "1.0.0",
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"));
@@ -1,4 +1,4 @@
1
- import { retry, circuitBreaker, wrap, handleAll, timeout, TimeoutStrategy, ExponentialBackoff, ConsecutiveBreaker, } from "cockatiel";
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
- const jsonStr = JSON.stringify(data);
25
- const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
26
- if (sizeBytes > MAX_RESPONSE_SIZE) {
27
- logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context }, `Response exceeds size limit, truncating`);
28
- // If it's an array, truncate
29
- if (Array.isArray(data)) {
30
- const truncated = data.slice(0, Math.max(1, Math.floor(data.length * 0.5)));
31
- return truncated;
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
- // If it's an object with items/results, truncate those
34
- if (typeof data === "object" && data !== null) {
35
- const obj = data;
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
- return obj;
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 data;
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(handleAll, {
85
+ const retryPolicy = retry(isTransient, {
55
86
  maxAttempts: 3,
56
87
  backoff,
57
88
  });
58
- const circuitBreakerPolicy = circuitBreaker(handleAll, {
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.6",
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": {