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 +1 -1
- package/README.md +2 -0
- package/dist/build-info.json +1 -1
- package/dist/index.js +105 -10
- package/dist/resilience.js +4 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
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:
|
|
1245
|
-
budget_amount_micros:
|
|
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 (
|
|
1369
|
+
if (hasCampaignIds) {
|
|
1342
1370
|
results.campaigns = await adsManager.enableCampaigns(customerId, args.campaign_ids);
|
|
1343
1371
|
}
|
|
1344
|
-
if (
|
|
1372
|
+
if (hasAdGroupIds) {
|
|
1345
1373
|
results.adGroups = await adsManager.enableAdGroups(customerId, args.ad_group_ids);
|
|
1346
1374
|
}
|
|
1347
|
-
if (
|
|
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 (
|
|
1404
|
+
if (hasCampaignIds) {
|
|
1365
1405
|
results.campaigns = await adsManager.pauseCampaigns(customerId, args.campaign_ids);
|
|
1366
1406
|
}
|
|
1367
|
-
if (
|
|
1407
|
+
if (hasAdGroupIds) {
|
|
1368
1408
|
results.adGroups = await adsManager.pauseAdGroups(customerId, args.ad_group_ids);
|
|
1369
1409
|
}
|
|
1370
|
-
if (
|
|
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(
|
|
1861
|
+
text: JSON.stringify(safeErrorResponse, null, 2),
|
|
1767
1862
|
}],
|
|
1768
1863
|
};
|
|
1769
1864
|
}
|
package/dist/resilience.js
CHANGED
|
@@ -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.
|
|
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": {
|