mcp-google-ads 1.0.13 → 1.0.15

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 drak-marketing
3
+ Copyright (c) 2026 drak-marketing
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -54,6 +54,8 @@ npm install
54
54
  npm run build
55
55
  ```
56
56
 
57
+ **Security:** Never share your `.mcp.json` file or commit it to git -- it may contain API credentials. Add `.mcp.json` to your `.gitignore`.
58
+
57
59
  ### 3. Configure
58
60
 
59
61
  ```bash
@@ -1 +1 @@
1
- {"sha":"00a842b","builtAt":"2026-04-09T22:23:35.553Z"}
1
+ {"sha":"d46fcf9","builtAt":"2026-04-09T23:11:30.041Z"}
package/dist/index.js CHANGED
@@ -1240,9 +1240,25 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1240
1240
  }
1241
1241
  case "google_ads_create_campaign": {
1242
1242
  const customerId = args?.customer_id || "";
1243
+ const daily_budget = args?.daily_budget;
1244
+ // Budget validation: reject $0 and negative budgets
1245
+ if (daily_budget !== undefined && daily_budget <= 0) {
1246
+ return {
1247
+ content: [{
1248
+ type: "text",
1249
+ text: JSON.stringify({ error: "daily_budget must be positive (in dollars, e.g., 10 = $10/day)" }, null, 2),
1250
+ }],
1251
+ };
1252
+ }
1253
+ // Campaign name sanitization: strip HTML tags
1254
+ const rawName = args?.name;
1255
+ const sanitizedName = rawName.replace(/<[^>]*>/g, "");
1256
+ if (sanitizedName !== rawName) {
1257
+ console.error(`[warning] Stripped HTML from campaign name: "${rawName}" -> "${sanitizedName}"`);
1258
+ }
1243
1259
  const result = await adsManager.createCampaign(customerId, {
1244
- name: args?.name,
1245
- budget_amount_micros: args?.daily_budget * 1000000,
1260
+ name: sanitizedName,
1261
+ budget_amount_micros: Math.round(daily_budget * 1000000),
1246
1262
  });
1247
1263
  return {
1248
1264
  content: [{
@@ -1260,7 +1276,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1260
1276
  const result = await adsManager.createAdGroup(customerId, {
1261
1277
  name: args?.name,
1262
1278
  campaign_id: args?.campaign_id,
1263
- cpc_bid_micros: args?.cpc_bid ? args.cpc_bid * 1000000 : undefined,
1279
+ cpc_bid_micros: args?.cpc_bid ? Math.round(args.cpc_bid * 1000000) : undefined,
1264
1280
  });
1265
1281
  return {
1266
1282
  content: [{
@@ -1337,14 +1353,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1337
1353
  }
1338
1354
  case "google_ads_enable_items": {
1339
1355
  const customerId = args?.customer_id || "";
1356
+ // Validate at least one ID array is provided and non-empty
1357
+ const hasCampaignIds = args?.campaign_ids && args.campaign_ids.length > 0;
1358
+ const hasAdGroupIds = args?.ad_group_ids && args.ad_group_ids.length > 0;
1359
+ const hasAdIds = args?.ad_ids && args.ad_ids.length > 0;
1360
+ if (!hasCampaignIds && !hasAdGroupIds && !hasAdIds) {
1361
+ return {
1362
+ content: [{
1363
+ type: "text",
1364
+ text: JSON.stringify({ error: "No item IDs provided. Specify at least one campaign, ad group, or ad ID." }, null, 2),
1365
+ }],
1366
+ };
1367
+ }
1340
1368
  const results = {};
1341
- if (args?.campaign_ids) {
1369
+ if (hasCampaignIds) {
1342
1370
  results.campaigns = await adsManager.enableCampaigns(customerId, args.campaign_ids);
1343
1371
  }
1344
- if (args?.ad_group_ids) {
1372
+ if (hasAdGroupIds) {
1345
1373
  results.adGroups = await adsManager.enableAdGroups(customerId, args.ad_group_ids);
1346
1374
  }
1347
- if (args?.ad_ids) {
1375
+ if (hasAdIds) {
1348
1376
  results.ads = await adsManager.enableAds(customerId, args.ad_ids);
1349
1377
  }
1350
1378
  return {
@@ -1360,14 +1388,26 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1360
1388
  }
1361
1389
  case "google_ads_pause_items": {
1362
1390
  const customerId = args?.customer_id || "";
1391
+ // Validate at least one ID array is provided and non-empty -- nothing to pause otherwise
1392
+ const hasCampaignIds = args?.campaign_ids && args.campaign_ids.length > 0;
1393
+ const hasAdGroupIds = args?.ad_group_ids && args.ad_group_ids.length > 0;
1394
+ const hasAdIds = args?.ad_ids && args.ad_ids.length > 0;
1395
+ if (!hasCampaignIds && !hasAdGroupIds && !hasAdIds) {
1396
+ return {
1397
+ content: [{
1398
+ type: "text",
1399
+ text: JSON.stringify({ error: "No item IDs provided. Specify at least one campaign, ad group, or keyword ID." }, null, 2),
1400
+ }],
1401
+ };
1402
+ }
1363
1403
  const results = {};
1364
- if (args?.campaign_ids) {
1404
+ if (hasCampaignIds) {
1365
1405
  results.campaigns = await adsManager.pauseCampaigns(customerId, args.campaign_ids);
1366
1406
  }
1367
- if (args?.ad_group_ids) {
1407
+ if (hasAdGroupIds) {
1368
1408
  results.adGroups = await adsManager.pauseAdGroups(customerId, args.ad_group_ids);
1369
1409
  }
1370
- if (args?.ad_ids) {
1410
+ if (hasAdIds) {
1371
1411
  results.ads = await adsManager.pauseAds(customerId, args.ad_ids);
1372
1412
  }
1373
1413
  return {
@@ -1555,6 +1595,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1555
1595
  // REPORTING HANDLERS
1556
1596
  // ============================================
1557
1597
  case "google_ads_keyword_performance": {
1598
+ // Future date validation
1599
+ const today_kp = new Date().toISOString().slice(0, 10);
1600
+ if (args?.start_date && args.start_date > today_kp) {
1601
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1602
+ }
1558
1603
  const customerId = args?.customer_id || "";
1559
1604
  const result = await adsManager.getKeywordPerformance(customerId, {
1560
1605
  startDate: args?.start_date,
@@ -1571,6 +1616,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1571
1616
  };
1572
1617
  }
1573
1618
  case "google_ads_keyword_performance_by_conversion": {
1619
+ const today_kpbc = new Date().toISOString().slice(0, 10);
1620
+ if (args?.start_date && args.start_date > today_kpbc) {
1621
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1622
+ }
1574
1623
  const customerId = args?.customer_id || "";
1575
1624
  const result = await adsManager.getKeywordPerformanceWithConversions(customerId, {
1576
1625
  startDate: args?.start_date,
@@ -1587,6 +1636,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1587
1636
  };
1588
1637
  }
1589
1638
  case "google_ads_search_term_report": {
1639
+ const today_str = new Date().toISOString().slice(0, 10);
1640
+ if (args?.start_date && args.start_date > today_str) {
1641
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1642
+ }
1590
1643
  const customerId = args?.customer_id || "";
1591
1644
  const result = await adsManager.getSearchTermReport(customerId, {
1592
1645
  startDate: args?.start_date,
@@ -1604,6 +1657,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1604
1657
  };
1605
1658
  }
1606
1659
  case "google_ads_search_term_report_by_conversion": {
1660
+ const today_strbc = new Date().toISOString().slice(0, 10);
1661
+ if (args?.start_date && args.start_date > today_strbc) {
1662
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1663
+ }
1607
1664
  const customerId = args?.customer_id || "";
1608
1665
  const result = await adsManager.getSearchTermReportWithConversions(customerId, {
1609
1666
  startDate: args?.start_date,
@@ -1621,6 +1678,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1621
1678
  };
1622
1679
  }
1623
1680
  case "google_ads_ad_performance": {
1681
+ const today_ap = new Date().toISOString().slice(0, 10);
1682
+ if (args?.start_date && args.start_date > today_ap) {
1683
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1684
+ }
1624
1685
  const customerId = args?.customer_id || "";
1625
1686
  const result = await adsManager.getAdPerformance(customerId, {
1626
1687
  startDate: args?.start_date,
@@ -1636,6 +1697,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1636
1697
  };
1637
1698
  }
1638
1699
  case "google_ads_ad_performance_by_conversion": {
1700
+ const today_apbc = new Date().toISOString().slice(0, 10);
1701
+ if (args?.start_date && args.start_date > today_apbc) {
1702
+ return { content: [{ type: "text", text: JSON.stringify({ error: `start_date "${args.start_date}" is in the future. Reports only cover historical data.` }, null, 2) }] };
1703
+ }
1639
1704
  const customerId = args?.customer_id || "";
1640
1705
  const result = await adsManager.getAdPerformanceWithConversions(customerId, {
1641
1706
  startDate: args?.start_date,
@@ -1696,6 +1761,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1696
1761
  const campaignId = args?.campaign_id;
1697
1762
  const dailyBudget = args?.daily_budget;
1698
1763
  const createNew = args?.create_new_budget || false;
1764
+ // Budget validation: reject $0 and negative budgets
1765
+ if (dailyBudget <= 0) {
1766
+ return {
1767
+ content: [{
1768
+ type: "text",
1769
+ text: JSON.stringify({ error: "Budget must be positive (in dollars, e.g., 10 = $10/day)" }, null, 2),
1770
+ }],
1771
+ };
1772
+ }
1699
1773
  const result = await adsManager.updateCampaignBudget(customerId, campaignId, dailyBudget, createNew);
1700
1774
  return {
1701
1775
  content: [{
@@ -1710,6 +1784,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1710
1784
  case "google_ads_gaql_query": {
1711
1785
  const customerId = args?.customer_id || "";
1712
1786
  const query = args?.query;
1787
+ // Block mutation statements -- GAQL query tool is read-only
1788
+ const upperQuery = query.toUpperCase().trim();
1789
+ if (upperQuery.startsWith("INSERT") || upperQuery.startsWith("UPDATE") || upperQuery.startsWith("DELETE") || upperQuery.startsWith("CREATE") || upperQuery.startsWith("DROP")) {
1790
+ return {
1791
+ content: [{
1792
+ type: "text",
1793
+ 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),
1794
+ }],
1795
+ };
1796
+ }
1713
1797
  const result = await adsManager.executeGaql(customerId, query);
1714
1798
  return {
1715
1799
  content: [{
@@ -1721,6 +1805,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1721
1805
  case "google_ads_keyword_volume": {
1722
1806
  const customerId = args?.customer_id || "";
1723
1807
  const keywords = args?.keywords;
1808
+ // Enforce max 20 keywords per request
1809
+ if (keywords.length > 20) {
1810
+ return {
1811
+ content: [{
1812
+ type: "text",
1813
+ text: JSON.stringify({ error: `Too many keywords (${keywords.length}). Maximum is 20 per request. Split into multiple calls.` }, null, 2),
1814
+ }],
1815
+ };
1816
+ }
1724
1817
  const geoTargetConstants = args?.geo_target_constants;
1725
1818
  const language = args?.language;
1726
1819
  const result = await adsManager.keywordVolume(customerId, keywords, geoTargetConstants, language);
@@ -1759,11 +1852,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
1759
1852
  else {
1760
1853
  response.details = rawError.errors || rawError.stack;
1761
1854
  }
1855
+ // Size-limit error responses through safeResponse to prevent oversized payloads
1856
+ const safeErrorResponse = safeResponse(response, "error");
1762
1857
  return {
1763
1858
  isError: true,
1764
1859
  content: [{
1765
1860
  type: "text",
1766
- text: JSON.stringify(response, null, 2),
1861
+ text: JSON.stringify(safeErrorResponse, null, 2),
1767
1862
  }],
1768
1863
  };
1769
1864
  }
@@ -31,6 +31,10 @@ export function safeResponse(data, context) {
31
31
  const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
32
32
  if (sizeBytes <= MAX_RESPONSE_SIZE)
33
33
  return current;
34
+ // Deep clone on first truncation pass to avoid mutating the original object
35
+ if (pass === 0 && typeof current === "object" && current !== null) {
36
+ current = JSON.parse(JSON.stringify(current));
37
+ }
34
38
  logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
35
39
  if (Array.isArray(current)) {
36
40
  current = current.slice(0, Math.max(1, Math.floor(current.length * 0.5)));
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.13",
4
+ "version": "1.0.15",
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": {