mcp-google-ads 1.0.4 → 1.0.6

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":"8e7eb9a","builtAt":"2026-04-09T19:36:52.493Z"}
1
+ {"sha":"5a28978","builtAt":"2026-04-09T20:25:53.245Z"}
package/dist/index.js CHANGED
@@ -80,6 +80,17 @@ function getClientFromWorkingDir(config, cwd) {
80
80
  return null;
81
81
  }
82
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
+ // ============================================
83
94
  // TYPED ERRORS & VALIDATION (extracted to errors.ts)
84
95
  // ============================================
85
96
  import { GoogleAdsAuthError, GoogleAdsRateLimitError, GoogleAdsServiceError, validateCredentials, classifyError, } from "./errors.js";
@@ -193,7 +204,7 @@ class GoogleAdsManager {
193
204
  WHERE ad_group.status != 'REMOVED'
194
205
  `;
195
206
  if (campaignId) {
196
- query += ` AND campaign.id = ${campaignId}`;
207
+ query += ` AND campaign.id = ${sanitizeNumericId(campaignId)}`;
197
208
  }
198
209
  query += ` ORDER BY campaign.name, ad_group.name`;
199
210
  const result = await withResilience(() => customer.query(query), "listAdGroups");
@@ -219,10 +230,10 @@ class GoogleAdsManager {
219
230
  WHERE ad_group_ad.status != 'REMOVED'
220
231
  `;
221
232
  if (options.campaignId) {
222
- query += ` AND campaign.id = ${options.campaignId}`;
233
+ query += ` AND campaign.id = ${sanitizeNumericId(options.campaignId)}`;
223
234
  }
224
235
  if (options.adGroupId) {
225
- query += ` AND ad_group.id = ${options.adGroupId}`;
236
+ query += ` AND ad_group.id = ${sanitizeNumericId(options.adGroupId)}`;
226
237
  }
227
238
  query += ` ORDER BY campaign.name, ad_group.name`;
228
239
  const result = await withResilience(() => customer.query(query), "listAds");
@@ -297,7 +308,7 @@ class GoogleAdsManager {
297
308
  const existing = await withResilience(() => customer.query(`
298
309
  SELECT label.resource_name, label.name
299
310
  FROM label
300
- WHERE label.name = '${labelName}'
311
+ WHERE label.name = '${escapeGaqlString(labelName)}'
301
312
  AND label.status = 'ENABLED'
302
313
  `), "ensureLabelExists.query");
303
314
  if (existing.length > 0) {
@@ -308,7 +319,7 @@ class GoogleAdsManager {
308
319
  if (result.existing) {
309
320
  // Race condition: re-query
310
321
  const requery = await withResilience(() => customer.query(`
311
- 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'
312
323
  `), "ensureLabelExists.requery");
313
324
  return requery[0].label.resource_name;
314
325
  }
@@ -711,7 +722,7 @@ class GoogleAdsManager {
711
722
  query += ` AND ad_group_criterion.keyword.text LIKE '%${options.keywordTextContains}%'`;
712
723
  }
713
724
  if (options.campaignIds && options.campaignIds.length > 0) {
714
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
725
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
715
726
  }
716
727
  if (options.adGroupIds && options.adGroupIds.length > 0) {
717
728
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -751,7 +762,7 @@ class GoogleAdsManager {
751
762
  query += ` AND ad_group_criterion.keyword.text LIKE '%${options.keywordTextContains}%'`;
752
763
  }
753
764
  if (options.campaignIds && options.campaignIds.length > 0) {
754
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
765
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
755
766
  }
756
767
  if (options.adGroupIds && options.adGroupIds.length > 0) {
757
768
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -789,7 +800,7 @@ class GoogleAdsManager {
789
800
  query += ` AND search_term_view.search_term LIKE '%${options.searchTermContains}%'`;
790
801
  }
791
802
  if (options.campaignIds && options.campaignIds.length > 0) {
792
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
803
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
793
804
  }
794
805
  if (options.adGroupIds && options.adGroupIds.length > 0) {
795
806
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -825,7 +836,7 @@ class GoogleAdsManager {
825
836
  query += ` AND search_term_view.search_term LIKE '%${options.searchTermContains}%'`;
826
837
  }
827
838
  if (options.campaignIds && options.campaignIds.length > 0) {
828
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
839
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
829
840
  }
830
841
  if (options.adGroupIds && options.adGroupIds.length > 0) {
831
842
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -870,7 +881,7 @@ class GoogleAdsManager {
870
881
  AND ad_group_ad.status != 'REMOVED'
871
882
  `;
872
883
  if (options.campaignIds && options.campaignIds.length > 0) {
873
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
884
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
874
885
  }
875
886
  if (options.adGroupIds && options.adGroupIds.length > 0) {
876
887
  query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
@@ -913,7 +924,7 @@ class GoogleAdsManager {
913
924
  AND ad_group_ad.status != 'REMOVED'
914
925
  `;
915
926
  if (options.campaignIds && options.campaignIds.length > 0) {
916
- query += ` AND campaign.id IN (${options.campaignIds.join(",")})`;
927
+ query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
917
928
  }
918
929
  if (options.adGroupIds && options.adGroupIds.length > 0) {
919
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.4",
4
+ "version": "1.0.6",
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": {