mcp-google-ads 1.0.3 → 1.0.5

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":"a78df84","builtAt":"2026-04-09T18:53:13.812Z"}
1
+ {"sha":"82b0bbb","builtAt":"2026-04-09T19:59:25.007Z"}
package/dist/index.js CHANGED
@@ -17,6 +17,22 @@ try {
17
17
  catch {
18
18
  // build-info.json not present (dev mode)
19
19
  }
20
+ // CLI flags
21
+ const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
22
+ 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`);
30
+ process.exit(0);
31
+ }
32
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
33
+ console.log(__cliPkg.version);
34
+ process.exit(0);
35
+ }
20
36
  function loadConfig() {
21
37
  // Try config.json first (for multi-client setups)
22
38
  const configPath = join(dirname(new URL(import.meta.url).pathname), "..", "config.json");
@@ -64,6 +80,17 @@ function getClientFromWorkingDir(config, cwd) {
64
80
  return null;
65
81
  }
66
82
  // ============================================
83
+ // GAQL SANITIZATION
84
+ // ============================================
85
+ /** Strip non-numeric characters from IDs used in GAQL WHERE clauses. */
86
+ function sanitizeNumericId(id) {
87
+ return id.replace(/[^0-9]/g, "");
88
+ }
89
+ /** Escape single quotes in strings used in GAQL WHERE clauses. */
90
+ function escapeGaqlString(s) {
91
+ return s.replace(/'/g, "\\'");
92
+ }
93
+ // ============================================
67
94
  // TYPED ERRORS & VALIDATION (extracted to errors.ts)
68
95
  // ============================================
69
96
  import { GoogleAdsAuthError, GoogleAdsRateLimitError, GoogleAdsServiceError, validateCredentials, classifyError, } from "./errors.js";
@@ -177,7 +204,7 @@ class GoogleAdsManager {
177
204
  WHERE ad_group.status != 'REMOVED'
178
205
  `;
179
206
  if (campaignId) {
180
- query += ` AND campaign.id = ${campaignId}`;
207
+ query += ` AND campaign.id = ${sanitizeNumericId(campaignId)}`;
181
208
  }
182
209
  query += ` ORDER BY campaign.name, ad_group.name`;
183
210
  const result = await withResilience(() => customer.query(query), "listAdGroups");
@@ -203,7 +230,7 @@ class GoogleAdsManager {
203
230
  WHERE ad_group_ad.status != 'REMOVED'
204
231
  `;
205
232
  if (options.campaignId) {
206
- query += ` AND campaign.id = ${options.campaignId}`;
233
+ query += ` AND campaign.id = ${sanitizeNumericId(options.campaignId)}`;
207
234
  }
208
235
  if (options.adGroupId) {
209
236
  query += ` AND ad_group.id = ${options.adGroupId}`;
@@ -281,7 +308,7 @@ class GoogleAdsManager {
281
308
  const existing = await withResilience(() => customer.query(`
282
309
  SELECT label.resource_name, label.name
283
310
  FROM label
284
- WHERE label.name = '${labelName}'
311
+ WHERE label.name = '${escapeGaqlString(labelName)}'
285
312
  AND label.status = 'ENABLED'
286
313
  `), "ensureLabelExists.query");
287
314
  if (existing.length > 0) {
@@ -292,7 +319,7 @@ class GoogleAdsManager {
292
319
  if (result.existing) {
293
320
  // Race condition: re-query
294
321
  const requery = await withResilience(() => customer.query(`
295
- SELECT label.resource_name FROM label WHERE label.name = '${labelName}' AND label.status = 'ENABLED'
322
+ SELECT label.resource_name FROM label WHERE label.name = '${escapeGaqlString(labelName)}' AND label.status = 'ENABLED'
296
323
  `), "ensureLabelExists.requery");
297
324
  return requery[0].label.resource_name;
298
325
  }
@@ -695,7 +722,7 @@ class GoogleAdsManager {
695
722
  query += ` AND ad_group_criterion.keyword.text LIKE '%${options.keywordTextContains}%'`;
696
723
  }
697
724
  if (options.campaignIds && options.campaignIds.length > 0) {
698
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
725
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
699
726
  }
700
727
  if (options.adGroupIds && options.adGroupIds.length > 0) {
701
728
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -735,7 +762,7 @@ class GoogleAdsManager {
735
762
  query += ` AND ad_group_criterion.keyword.text LIKE '%${options.keywordTextContains}%'`;
736
763
  }
737
764
  if (options.campaignIds && options.campaignIds.length > 0) {
738
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
765
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
739
766
  }
740
767
  if (options.adGroupIds && options.adGroupIds.length > 0) {
741
768
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -773,7 +800,7 @@ class GoogleAdsManager {
773
800
  query += ` AND search_term_view.search_term LIKE '%${options.searchTermContains}%'`;
774
801
  }
775
802
  if (options.campaignIds && options.campaignIds.length > 0) {
776
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
803
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
777
804
  }
778
805
  if (options.adGroupIds && options.adGroupIds.length > 0) {
779
806
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -809,7 +836,7 @@ class GoogleAdsManager {
809
836
  query += ` AND search_term_view.search_term LIKE '%${options.searchTermContains}%'`;
810
837
  }
811
838
  if (options.campaignIds && options.campaignIds.length > 0) {
812
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
839
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
813
840
  }
814
841
  if (options.adGroupIds && options.adGroupIds.length > 0) {
815
842
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -854,7 +881,7 @@ class GoogleAdsManager {
854
881
  AND ad_group_ad.status != 'REMOVED'
855
882
  `;
856
883
  if (options.campaignIds && options.campaignIds.length > 0) {
857
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
884
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
858
885
  }
859
886
  if (options.adGroupIds && options.adGroupIds.length > 0) {
860
887
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -897,7 +924,7 @@ class GoogleAdsManager {
897
924
  AND ad_group_ad.status != 'REMOVED'
898
925
  `;
899
926
  if (options.campaignIds && options.campaignIds.length > 0) {
900
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
927
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
901
928
  }
902
929
  if (options.adGroupIds && options.adGroupIds.length > 0) {
903
930
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
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.3",
4
+ "version": "1.0.5",
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": {