mcp-google-ads 1.0.5 → 1.0.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # MCP Google Ads Server
2
2
 
3
- An MCP (Model Context Protocol) server for the Google Ads API with built-in safeguards for review before changes go live. Production-proven with MCC (Manager Account) support, 35 tools for campaign management, reporting, and optimization.
3
+ An MCP (Model Context Protocol) server for the Google Ads API with built-in safeguards for review before changes go live. Production-proven with MCC (Manager Account) support, 34 tools for campaign management, reporting, and optimization.
4
4
 
5
5
  ## Features
6
6
 
@@ -133,7 +133,7 @@ Restart Claude Code.
133
133
  5. Claude enables (requires your approval prompt)
134
134
  ```
135
135
 
136
- ### Available Tools (35)
136
+ ### Available Tools (34)
137
137
 
138
138
  #### Context & Discovery
139
139
  | Tool | Description |
@@ -1 +1 @@
1
- {"sha":"82b0bbb","builtAt":"2026-04-09T19:59:25.007Z"}
1
+ {"sha":"3490598","builtAt":"2026-04-09T21:18:29.192Z"}
package/dist/index.js CHANGED
@@ -233,7 +233,7 @@ class GoogleAdsManager {
233
233
  query += ` AND campaign.id = ${sanitizeNumericId(options.campaignId)}`;
234
234
  }
235
235
  if (options.adGroupId) {
236
- query += ` AND ad_group.id = ${options.adGroupId}`;
236
+ query += ` AND ad_group.id = ${sanitizeNumericId(options.adGroupId)}`;
237
237
  }
238
238
  query += ` ORDER BY campaign.name, ad_group.name`;
239
239
  const result = await withResilience(() => customer.query(query), "listAds");
@@ -1151,8 +1151,8 @@ class GoogleAdsManager {
1151
1151
  const config = loadConfig();
1152
1152
  const adsManager = new GoogleAdsManager(config);
1153
1153
  const server = new Server({
1154
- name: "mcp-google-ads",
1155
- version: "1.0.0",
1154
+ name: __cliPkg.name,
1155
+ version: __cliPkg.version,
1156
1156
  }, {
1157
1157
  capabilities: {
1158
1158
  tools: {},
@@ -1806,4 +1806,12 @@ async function main() {
1806
1806
  await server.connect(transport);
1807
1807
  logger.info("MCP Google Ads server running");
1808
1808
  }
1809
+ process.on("SIGTERM", () => {
1810
+ console.error("[shutdown] SIGTERM received, exiting");
1811
+ process.exit(0);
1812
+ });
1813
+ process.on("SIGINT", () => {
1814
+ console.error("[shutdown] SIGINT received, exiting");
1815
+ process.exit(0);
1816
+ });
1809
1817
  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
@@ -21,27 +21,39 @@ export const logger = pino({
21
21
  // ============================================
22
22
  const MAX_RESPONSE_SIZE = 200_000; // 200KB
23
23
  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;
24
+ let current = data;
25
+ for (let pass = 0; pass < 10; pass++) {
26
+ const jsonStr = JSON.stringify(current);
27
+ const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
28
+ if (sizeBytes <= MAX_RESPONSE_SIZE)
29
+ return current;
30
+ logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
31
+ if (Array.isArray(current)) {
32
+ current = current.slice(0, Math.max(1, Math.floor(current.length * 0.5)));
33
+ continue;
32
34
  }
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])) {
35
+ if (typeof current === "object" && current !== null) {
36
+ const obj = current;
37
+ let truncated = false;
38
+ for (const key of ["items", "results", "data", "rows", "tags", "triggers", "variables"]) {
39
+ if (Array.isArray(obj[key]) && obj[key].length > 1) {
38
40
  obj[key] = obj[key].slice(0, Math.max(1, Math.floor(obj[key].length * 0.5)));
39
- return obj;
41
+ if ("count" in obj)
42
+ obj.count = obj[key].length;
43
+ if ("row_count" in obj)
44
+ obj.row_count = obj[key].length;
45
+ obj.truncated = true;
46
+ truncated = true;
47
+ break;
40
48
  }
41
49
  }
50
+ if (truncated)
51
+ continue;
42
52
  }
53
+ // Can't truncate further (not an array/object with known keys)
54
+ break;
43
55
  }
44
- return data;
56
+ return current;
45
57
  }
46
58
  // ============================================
47
59
  // RETRY + CIRCUIT BREAKER + TIMEOUT
@@ -50,12 +62,28 @@ const backoff = new ExponentialBackoff({
50
62
  initialDelay: 100,
51
63
  maxDelay: 5_000,
52
64
  });
65
+ const isTransient = handleWhen((err) => {
66
+ const msg = (err?.message || "").toLowerCase();
67
+ const code = err?.code || err?.status;
68
+ // Don't retry auth errors
69
+ if (code === 401 || code === 403 || code === 7 || code === 16)
70
+ return false;
71
+ if (msg.includes("unauthenticated") || msg.includes("permission_denied") || msg.includes("invalid_grant"))
72
+ return false;
73
+ // Don't retry client errors (except rate limits)
74
+ if (code === 429 || msg.includes("rate"))
75
+ return true;
76
+ if (code >= 400 && code < 500)
77
+ return false;
78
+ // Retry everything else (5xx, timeouts, network errors)
79
+ return true;
80
+ });
53
81
  // Individual policies
54
- const retryPolicy = retry(handleAll, {
82
+ const retryPolicy = retry(isTransient, {
55
83
  maxAttempts: 3,
56
84
  backoff,
57
85
  });
58
- const circuitBreakerPolicy = circuitBreaker(handleAll, {
86
+ const circuitBreakerPolicy = circuitBreaker(isTransient, {
59
87
  halfOpenAfter: 60_000, // 60s to attempt recovery
60
88
  breaker: new ConsecutiveBreaker(5), // Open after 5 consecutive failures
61
89
  });
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.5",
4
+ "version": "1.0.7",
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": {