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.
- package/dist/build-info.json +1 -1
- package/dist/index.js +119 -20
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
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
|
-
|
|
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:
|
|
1245
|
-
budget_amount_micros:
|
|
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 (
|
|
1375
|
+
if (hasCampaignIds) {
|
|
1342
1376
|
results.campaigns = await adsManager.enableCampaigns(customerId, args.campaign_ids);
|
|
1343
1377
|
}
|
|
1344
|
-
if (
|
|
1378
|
+
if (hasAdGroupIds) {
|
|
1345
1379
|
results.adGroups = await adsManager.enableAdGroups(customerId, args.ad_group_ids);
|
|
1346
1380
|
}
|
|
1347
|
-
if (
|
|
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 (
|
|
1410
|
+
if (hasCampaignIds) {
|
|
1365
1411
|
results.campaigns = await adsManager.pauseCampaigns(customerId, args.campaign_ids);
|
|
1366
1412
|
}
|
|
1367
|
-
if (
|
|
1413
|
+
if (hasAdGroupIds) {
|
|
1368
1414
|
results.adGroups = await adsManager.pauseAdGroups(customerId, args.ad_group_ids);
|
|
1369
1415
|
}
|
|
1370
|
-
if (
|
|
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.
|
|
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": {
|