mcp-google-ads 1.0.14 → 1.0.16

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":"1c7a8b2","builtAt":"2026-04-09T23:02:14.294Z"}
1
+ {"sha":"9b25077","builtAt":"2026-04-09T23:18:47.469Z"}
package/dist/index.js CHANGED
@@ -22,7 +22,13 @@ catch {
22
22
  }
23
23
  // Version safety: warn if running a deprecated or dangerously old version
24
24
  const __minimumSafeVersion = "1.0.5"; // minimum version with GAQL sanitization
25
- if (__cliPkg.version < __minimumSafeVersion) {
25
+ const __semverLt = (a, b) => { const pa = a.split(".").map(Number), pb = b.split(".").map(Number); for (let i = 0; i < 3; i++) {
26
+ if ((pa[i] || 0) < (pb[i] || 0))
27
+ return true;
28
+ if ((pa[i] || 0) > (pb[i] || 0))
29
+ return false;
30
+ } return false; };
31
+ if (__semverLt(__cliPkg.version, __minimumSafeVersion)) {
26
32
  console.error(`[WARNING] Running deprecated version ${__cliPkg.version}. Minimum safe version is ${__minimumSafeVersion}. Please upgrade.`);
27
33
  }
28
34
  // CLI flags
@@ -733,13 +739,13 @@ class GoogleAdsManager {
733
739
  AND ad_group_criterion.status != 'REMOVED'
734
740
  `;
735
741
  if (options.keywordTextContains) {
736
- query += ` AND ad_group_criterion.keyword.text LIKE '%${options.keywordTextContains}%'`;
742
+ query += ` AND ad_group_criterion.keyword.text LIKE '%${escapeGaqlString(options.keywordTextContains)}%'`;
737
743
  }
738
744
  if (options.campaignIds && options.campaignIds.length > 0) {
739
745
  query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
740
746
  }
741
747
  if (options.adGroupIds && options.adGroupIds.length > 0) {
742
- query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
748
+ query += ` AND ad_group.id IN (${options.adGroupIds.map(sanitizeNumericId).join(",")})`;
743
749
  }
744
750
  query += ` ORDER BY metrics.cost_micros DESC`;
745
751
  const result = await withResilience(() => customer.query(query), "getKeywordPerformance");
@@ -773,13 +779,13 @@ class GoogleAdsManager {
773
779
  AND ad_group_criterion.status != 'REMOVED'
774
780
  `;
775
781
  if (options.keywordTextContains) {
776
- query += ` AND ad_group_criterion.keyword.text LIKE '%${options.keywordTextContains}%'`;
782
+ query += ` AND ad_group_criterion.keyword.text LIKE '%${escapeGaqlString(options.keywordTextContains)}%'`;
777
783
  }
778
784
  if (options.campaignIds && options.campaignIds.length > 0) {
779
785
  query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
780
786
  }
781
787
  if (options.adGroupIds && options.adGroupIds.length > 0) {
782
- query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
788
+ query += ` AND ad_group.id IN (${options.adGroupIds.map(sanitizeNumericId).join(",")})`;
783
789
  }
784
790
  query += ` ORDER BY ad_group_criterion.keyword.text, segments.conversion_action_name`;
785
791
  const result = await withResilience(() => customer.query(query), "getKeywordPerformanceWithConversions");
@@ -811,13 +817,13 @@ class GoogleAdsManager {
811
817
  WHERE segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'
812
818
  `;
813
819
  if (options.searchTermContains) {
814
- query += ` AND search_term_view.search_term LIKE '%${options.searchTermContains}%'`;
820
+ query += ` AND search_term_view.search_term LIKE '%${escapeGaqlString(options.searchTermContains)}%'`;
815
821
  }
816
822
  if (options.campaignIds && options.campaignIds.length > 0) {
817
823
  query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
818
824
  }
819
825
  if (options.adGroupIds && options.adGroupIds.length > 0) {
820
- query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
826
+ query += ` AND ad_group.id IN (${options.adGroupIds.map(sanitizeNumericId).join(",")})`;
821
827
  }
822
828
  query += ` ORDER BY metrics.impressions DESC`;
823
829
  const result = await withResilience(() => customer.query(query), "getSearchTermReport");
@@ -847,13 +853,13 @@ class GoogleAdsManager {
847
853
  WHERE segments.date BETWEEN '${options.startDate}' AND '${options.endDate}'
848
854
  `;
849
855
  if (options.searchTermContains) {
850
- query += ` AND search_term_view.search_term LIKE '%${options.searchTermContains}%'`;
856
+ query += ` AND search_term_view.search_term LIKE '%${escapeGaqlString(options.searchTermContains)}%'`;
851
857
  }
852
858
  if (options.campaignIds && options.campaignIds.length > 0) {
853
859
  query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
854
860
  }
855
861
  if (options.adGroupIds && options.adGroupIds.length > 0) {
856
- query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
862
+ query += ` AND ad_group.id IN (${options.adGroupIds.map(sanitizeNumericId).join(",")})`;
857
863
  }
858
864
  query += ` ORDER BY search_term_view.search_term, segments.conversion_action_name`;
859
865
  const result = await withResilience(() => customer.query(query), "getSearchTermReportWithConversions");
@@ -898,7 +904,7 @@ class GoogleAdsManager {
898
904
  query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
899
905
  }
900
906
  if (options.adGroupIds && options.adGroupIds.length > 0) {
901
- query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
907
+ query += ` AND ad_group.id IN (${options.adGroupIds.map(sanitizeNumericId).join(",")})`;
902
908
  }
903
909
  query += ` ORDER BY metrics.impressions DESC`;
904
910
  const result = await withResilience(() => customer.query(query), "getAdPerformance");
@@ -941,7 +947,7 @@ class GoogleAdsManager {
941
947
  query += ` AND campaign.id IN (${options.campaignIds.map(sanitizeNumericId).join(",")})`;
942
948
  }
943
949
  if (options.adGroupIds && options.adGroupIds.length > 0) {
944
- query += ` AND ad_group.id IN (${options.adGroupIds.join(",")})`;
950
+ query += ` AND ad_group.id IN (${options.adGroupIds.map(sanitizeNumericId).join(",")})`;
945
951
  }
946
952
  query += ` ORDER BY ad_group_ad.ad.id, segments.conversion_action_name`;
947
953
  const result = await withResilience(() => customer.query(query), "getAdPerformanceWithConversions");
@@ -1240,9 +1246,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1240
1246
  }
1241
1247
  case "google_ads_create_campaign": {
1242
1248
  const customerId = args?.customer_id || "";
1249
+ const daily_budget = args?.daily_budget;
1250
+ // Budget validation: reject $0 and negative budgets
1251
+ if (daily_budget !== undefined && daily_budget <= 0) {
1252
+ return {
1253
+ content: [{
1254
+ type: "text",
1255
+ text: JSON.stringify({ error: "daily_budget must be positive (in dollars, e.g., 10 = $10/day)" }, null, 2),
1256
+ }],
1257
+ };
1258
+ }
1259
+ // Campaign name sanitization: strip HTML tags
1260
+ const rawName = args?.name;
1261
+ const sanitizedName = rawName.replace(/<[^>]*>/g, "");
1262
+ if (sanitizedName !== rawName) {
1263
+ console.error(`[warning] Stripped HTML from campaign name: "${rawName}" -> "${sanitizedName}"`);
1264
+ }
1243
1265
  const result = await adsManager.createCampaign(customerId, {
1244
- name: args?.name,
1245
- budget_amount_micros: args?.daily_budget * 1000000,
1266
+ name: sanitizedName,
1267
+ budget_amount_micros: Math.round(daily_budget * 1000000),
1246
1268
  });
1247
1269
  return {
1248
1270
  content: [{
@@ -1260,7 +1282,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1260
1282
  const result = await adsManager.createAdGroup(customerId, {
1261
1283
  name: args?.name,
1262
1284
  campaign_id: args?.campaign_id,
1263
- cpc_bid_micros: args?.cpc_bid ? args.cpc_bid * 1000000 : undefined,
1285
+ cpc_bid_micros: args?.cpc_bid ? Math.round(args.cpc_bid * 1000000) : undefined,
1264
1286
  });
1265
1287
  return {
1266
1288
  content: [{
@@ -1337,14 +1359,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1337
1359
  }
1338
1360
  case "google_ads_enable_items": {
1339
1361
  const customerId = args?.customer_id || "";
1362
+ // Validate at least one ID array is provided and non-empty
1363
+ const hasCampaignIds = args?.campaign_ids && args.campaign_ids.length > 0;
1364
+ const hasAdGroupIds = args?.ad_group_ids && args.ad_group_ids.length > 0;
1365
+ const hasAdIds = args?.ad_ids && args.ad_ids.length > 0;
1366
+ if (!hasCampaignIds && !hasAdGroupIds && !hasAdIds) {
1367
+ return {
1368
+ content: [{
1369
+ type: "text",
1370
+ text: JSON.stringify({ error: "No item IDs provided. Specify at least one campaign, ad group, or ad ID." }, null, 2),
1371
+ }],
1372
+ };
1373
+ }
1340
1374
  const results = {};
1341
- if (args?.campaign_ids) {
1375
+ if (hasCampaignIds) {
1342
1376
  results.campaigns = await adsManager.enableCampaigns(customerId, args.campaign_ids);
1343
1377
  }
1344
- if (args?.ad_group_ids) {
1378
+ if (hasAdGroupIds) {
1345
1379
  results.adGroups = await adsManager.enableAdGroups(customerId, args.ad_group_ids);
1346
1380
  }
1347
- if (args?.ad_ids) {
1381
+ if (hasAdIds) {
1348
1382
  results.ads = await adsManager.enableAds(customerId, args.ad_ids);
1349
1383
  }
1350
1384
  return {
@@ -1360,14 +1394,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1360
1394
  }
1361
1395
  case "google_ads_pause_items": {
1362
1396
  const customerId = args?.customer_id || "";
1397
+ // Validate at least one ID array is provided and non-empty -- nothing to pause otherwise
1398
+ const hasCampaignIds = args?.campaign_ids && args.campaign_ids.length > 0;
1399
+ const hasAdGroupIds = args?.ad_group_ids && args.ad_group_ids.length > 0;
1400
+ const hasAdIds = args?.ad_ids && args.ad_ids.length > 0;
1401
+ if (!hasCampaignIds && !hasAdGroupIds && !hasAdIds) {
1402
+ return {
1403
+ content: [{
1404
+ type: "text",
1405
+ text: JSON.stringify({ error: "No item IDs provided. Specify at least one campaign, ad group, or keyword ID." }, null, 2),
1406
+ }],
1407
+ };
1408
+ }
1363
1409
  const results = {};
1364
- if (args?.campaign_ids) {
1410
+ if (hasCampaignIds) {
1365
1411
  results.campaigns = await adsManager.pauseCampaigns(customerId, args.campaign_ids);
1366
1412
  }
1367
- if (args?.ad_group_ids) {
1413
+ if (hasAdGroupIds) {
1368
1414
  results.adGroups = await adsManager.pauseAdGroups(customerId, args.ad_group_ids);
1369
1415
  }
1370
- if (args?.ad_ids) {
1416
+ if (hasAdIds) {
1371
1417
  results.ads = await adsManager.pauseAds(customerId, args.ad_ids);
1372
1418
  }
1373
1419
  return {
@@ -1555,6 +1601,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1555
1601
  // REPORTING HANDLERS
1556
1602
  // ============================================
1557
1603
  case "google_ads_keyword_performance": {
1604
+ // Future date validation
1605
+ const today_kp = new Date().toISOString().slice(0, 10);
1606
+ if (args?.start_date && args.start_date > today_kp) {
1607
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1608
+ }
1558
1609
  const customerId = args?.customer_id || "";
1559
1610
  const result = await adsManager.getKeywordPerformance(customerId, {
1560
1611
  startDate: args?.start_date,
@@ -1571,6 +1622,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1571
1622
  };
1572
1623
  }
1573
1624
  case "google_ads_keyword_performance_by_conversion": {
1625
+ const today_kpbc = new Date().toISOString().slice(0, 10);
1626
+ if (args?.start_date && args.start_date > today_kpbc) {
1627
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1628
+ }
1574
1629
  const customerId = args?.customer_id || "";
1575
1630
  const result = await adsManager.getKeywordPerformanceWithConversions(customerId, {
1576
1631
  startDate: args?.start_date,
@@ -1587,6 +1642,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1587
1642
  };
1588
1643
  }
1589
1644
  case "google_ads_search_term_report": {
1645
+ const today_str = new Date().toISOString().slice(0, 10);
1646
+ if (args?.start_date && args.start_date > today_str) {
1647
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1648
+ }
1590
1649
  const customerId = args?.customer_id || "";
1591
1650
  const result = await adsManager.getSearchTermReport(customerId, {
1592
1651
  startDate: args?.start_date,
@@ -1604,6 +1663,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1604
1663
  };
1605
1664
  }
1606
1665
  case "google_ads_search_term_report_by_conversion": {
1666
+ const today_strbc = new Date().toISOString().slice(0, 10);
1667
+ if (args?.start_date && args.start_date > today_strbc) {
1668
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1669
+ }
1607
1670
  const customerId = args?.customer_id || "";
1608
1671
  const result = await adsManager.getSearchTermReportWithConversions(customerId, {
1609
1672
  startDate: args?.start_date,
@@ -1621,6 +1684,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1621
1684
  };
1622
1685
  }
1623
1686
  case "google_ads_ad_performance": {
1687
+ const today_ap = new Date().toISOString().slice(0, 10);
1688
+ if (args?.start_date && args.start_date > today_ap) {
1689
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1690
+ }
1624
1691
  const customerId = args?.customer_id || "";
1625
1692
  const result = await adsManager.getAdPerformance(customerId, {
1626
1693
  startDate: args?.start_date,
@@ -1636,6 +1703,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1636
1703
  };
1637
1704
  }
1638
1705
  case "google_ads_ad_performance_by_conversion": {
1706
+ const today_apbc = new Date().toISOString().slice(0, 10);
1707
+ if (args?.start_date && args.start_date > today_apbc) {
1708
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1709
+ }
1639
1710
  const customerId = args?.customer_id || "";
1640
1711
  const result = await adsManager.getAdPerformanceWithConversions(customerId, {
1641
1712
  startDate: args?.start_date,
@@ -1696,6 +1767,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1696
1767
  const campaignId = args?.campaign_id;
1697
1768
  const dailyBudget = args?.daily_budget;
1698
1769
  const createNew = args?.create_new_budget || false;
1770
+ // Budget validation: reject $0 and negative budgets
1771
+ if (dailyBudget <= 0) {
1772
+ return {
1773
+ content: [{
1774
+ type: "text",
1775
+ text: JSON.stringify({ error: "Budget must be positive (in dollars, e.g., 10 = $10/day)" }, null, 2),
1776
+ }],
1777
+ };
1778
+ }
1699
1779
  const result = await adsManager.updateCampaignBudget(customerId, campaignId, dailyBudget, createNew);
1700
1780
  return {
1701
1781
  content: [{
@@ -1710,6 +1790,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1710
1790
  case "google_ads_gaql_query": {
1711
1791
  const customerId = args?.customer_id || "";
1712
1792
  const query = args?.query;
1793
+ // Block mutation statements -- GAQL query tool is read-only
1794
+ const upperQuery = query.toUpperCase().trim();
1795
+ if (upperQuery.startsWith("INSERT") || upperQuery.startsWith("UPDATE") || upperQuery.startsWith("DELETE") || upperQuery.startsWith("CREATE") || upperQuery.startsWith("DROP")) {
1796
+ return {
1797
+ content: [{
1798
+ type: "text",
1799
+ text: JSON.stringify({ error: "GAQL query tool is read-only. Mutation statements (INSERT, UPDATE, DELETE) are not allowed. Use the dedicated create/update tools instead." }, null, 2),
1800
+ }],
1801
+ };
1802
+ }
1713
1803
  const result = await adsManager.executeGaql(customerId, query);
1714
1804
  return {
1715
1805
  content: [{
@@ -1721,6 +1811,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1721
1811
  case "google_ads_keyword_volume": {
1722
1812
  const customerId = args?.customer_id || "";
1723
1813
  const keywords = args?.keywords;
1814
+ // Enforce max 20 keywords per request
1815
+ if (keywords.length > 20) {
1816
+ return {
1817
+ content: [{
1818
+ type: "text",
1819
+ text: JSON.stringify({ error: `Too many keywords (${keywords.length}). Maximum is 20 per request. Split into multiple calls.` }, null, 2),
1820
+ }],
1821
+ };
1822
+ }
1724
1823
  const geoTargetConstants = args?.geo_target_constants;
1725
1824
  const language = args?.language;
1726
1825
  const result = await adsManager.keywordVolume(customerId, keywords, geoTargetConstants, language);
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.14",
4
+ "version": "1.0.16",
5
5
  "description": "MCP server for Google Ads API with MCC support, 34 tools for campaign management, reporting, and optimization. Safe by default -- all changes created PAUSED.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {