mobile-growth-mcp 2.2.4 → 2.3.4

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.
Files changed (2) hide show
  1. package/dist/index.js +248 -29
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -258,6 +258,8 @@ var CAMPAIGN_DEFAULT_FIELDS = "id,name,status,effective_status,objective,bid_str
258
258
  var ADSET_DEFAULT_FIELDS = "id,name,status,effective_status,campaign_id,optimization_goal,billing_event,bid_strategy,bid_amount,daily_budget,lifetime_budget,targeting,promoted_object";
259
259
  var AD_DEFAULT_FIELDS = "id,name,status,effective_status,adset_id,campaign_id,creative{id,title,body,image_url,video_id,call_to_action_type}";
260
260
  var INSIGHT_DEFAULT_FIELDS = "campaign_id,campaign_name,spend,impressions,clicks,ctr,cpm,cpc,actions,cost_per_action_type";
261
+ var INSIGHT_ADSET_FIELDS = "adset_id,adset_name";
262
+ var INSIGHT_AD_FIELDS = "adset_id,adset_name,ad_id,ad_name";
261
263
 
262
264
  // src/tools/meta-campaigns.ts
263
265
  function registerGetMetaCampaigns(server2) {
@@ -420,16 +422,17 @@ import { z as z4 } from "zod";
420
422
  function registerGetMetaAds(server2) {
421
423
  server2.tool(
422
424
  "get_meta_ads",
423
- "List ads from a Meta ad account, optionally scoped to an ad set. Defaults to active ads with lean field set.",
425
+ "List ads from a Meta ad account, optionally scoped to a campaign or ad set. Defaults to active ads with lean field set.",
424
426
  {
425
427
  ad_account_id: z4.string().describe("Meta ad account ID (e.g. act_123456789)"),
426
- adset_id: z4.string().optional().describe("Scope to a specific ad set ID."),
428
+ campaign_id: z4.string().optional().describe("Scope to a specific campaign ID (e.g. 23851234567890)"),
429
+ adset_id: z4.string().optional().describe("Scope to a specific ad set ID. Takes priority over campaign_id."),
427
430
  fields: z4.string().optional().describe(`Comma-separated fields. Default: ${AD_DEFAULT_FIELDS}`),
428
431
  effective_status: z4.array(z4.string()).optional().describe('Filter by status. Default: ["ACTIVE"]'),
429
432
  limit: z4.number().min(1).max(100).optional().describe("Results per page (default 50, max 100)"),
430
433
  after: z4.string().optional().describe("Pagination cursor from previous response")
431
434
  },
432
- async ({ ad_account_id, adset_id, fields, effective_status, limit, after }) => {
435
+ async ({ ad_account_id, campaign_id, adset_id, fields, effective_status, limit, after }) => {
433
436
  try {
434
437
  const params = {
435
438
  fields: fields ?? AD_DEFAULT_FIELDS,
@@ -449,7 +452,7 @@ function registerGetMetaAds(server2) {
449
452
  if (after) {
450
453
  params.after = after;
451
454
  }
452
- const parentPath = adset_id ? `/${adset_id}/ads` : `/${ad_account_id}/ads`;
455
+ const parentPath = adset_id ? `/${adset_id}/ads` : campaign_id ? `/${campaign_id}/ads` : `/${ad_account_id}/ads`;
453
456
  const result = await metaApiGet({
454
457
  path: parentPath,
455
458
  params
@@ -461,7 +464,7 @@ function registerGetMetaAds(server2) {
461
464
  `;
462
465
  for (const ad of ads) {
463
466
  text += `- **${ad.name}** (${ad.id})
464
- Ad Set: ${ad.adset_id} | Status: ${ad.effective_status}` + (ad.creative?.call_to_action_type ? ` | CTA: ${ad.creative.call_to_action_type}` : "") + (ad.creative?.video_id ? " | Format: Video" : "") + (ad.creative?.image_url ? " | Format: Image" : "") + "\n";
467
+ Campaign: ${ad.campaign_id} | Ad Set: ${ad.adset_id} | Status: ${ad.effective_status}` + (ad.creative?.call_to_action_type ? ` | CTA: ${ad.creative.call_to_action_type}` : "") + (ad.creative?.video_id ? " | Format: Video" : "") + (ad.creative?.image_url ? " | Format: Image" : "") + "\n";
465
468
  }
466
469
  if (nextCursor) {
467
470
  text += `
@@ -493,12 +496,14 @@ import { z as z5 } from "zod";
493
496
  function registerGetMetaInsights(server2) {
494
497
  server2.tool(
495
498
  "get_meta_insights",
496
- "Pull performance insights from a Meta ad account with configurable level, breakdowns, and date range. Default: campaign-level, last 7 days, active only. Conversion event is configurable (default: mobile_app_install).",
499
+ "Pull performance insights from a Meta ad account with configurable level, breakdowns, and date range. Default: campaign-level, last 7 days, active only. Conversion event is configurable (default: mobile_app_install). Use campaign_id or adset_id to scope results.",
497
500
  {
498
501
  ad_account_id: z5.string().describe("Meta ad account ID (e.g. act_123456789)"),
502
+ campaign_id: z5.string().optional().describe("Scope to a specific campaign ID (e.g. 23851234567890)"),
503
+ adset_id: z5.string().optional().describe("Scope to a specific ad set ID (e.g. 23851234567891)"),
499
504
  level: z5.enum(["account", "campaign", "adset", "ad"]).optional().describe("Aggregation level (default: campaign)"),
500
505
  fields: z5.string().optional().describe(
501
- `Comma-separated fields. Default: ${INSIGHT_DEFAULT_FIELDS}`
506
+ `Comma-separated fields. Default includes level-appropriate name fields + ${INSIGHT_DEFAULT_FIELDS}`
502
507
  ),
503
508
  date_preset: z5.string().optional().describe(`Date preset (default: ${DEFAULT_DATE_PRESET})`),
504
509
  time_range: z5.object({
@@ -525,6 +530,8 @@ function registerGetMetaInsights(server2) {
525
530
  },
526
531
  async ({
527
532
  ad_account_id,
533
+ campaign_id,
534
+ adset_id,
528
535
  level,
529
536
  fields,
530
537
  date_preset,
@@ -539,9 +546,20 @@ function registerGetMetaInsights(server2) {
539
546
  }) => {
540
547
  try {
541
548
  const convEvent = conversion_event ?? "mobile_app_install";
549
+ const effectiveLevel = level ?? "campaign";
550
+ let effectiveFields = fields;
551
+ if (!effectiveFields) {
552
+ if (effectiveLevel === "ad") {
553
+ effectiveFields = `${INSIGHT_AD_FIELDS},${INSIGHT_DEFAULT_FIELDS}`;
554
+ } else if (effectiveLevel === "adset") {
555
+ effectiveFields = `${INSIGHT_ADSET_FIELDS},${INSIGHT_DEFAULT_FIELDS}`;
556
+ } else {
557
+ effectiveFields = INSIGHT_DEFAULT_FIELDS;
558
+ }
559
+ }
542
560
  const params = {
543
- fields: fields ?? INSIGHT_DEFAULT_FIELDS,
544
- level: level ?? "campaign",
561
+ fields: effectiveFields,
562
+ level: effectiveLevel,
545
563
  limit: String(limit ?? 50)
546
564
  };
547
565
  if (time_range) {
@@ -555,11 +573,22 @@ function registerGetMetaInsights(server2) {
555
573
  if (breakdowns) {
556
574
  params.breakdowns = breakdowns;
557
575
  }
558
- if (filtering !== void 0) {
559
- params.filtering = filtering === "[]" ? "[]" : filtering;
560
- } else {
561
- params.filtering = activeFilter();
576
+ const baseFilters = filtering !== void 0 ? filtering === "[]" ? [] : JSON.parse(filtering) : JSON.parse(activeFilter());
577
+ if (campaign_id) {
578
+ baseFilters.push({
579
+ field: "campaign.id",
580
+ operator: "IN",
581
+ value: [campaign_id]
582
+ });
583
+ }
584
+ if (adset_id) {
585
+ baseFilters.push({
586
+ field: "adset.id",
587
+ operator: "IN",
588
+ value: [adset_id]
589
+ });
562
590
  }
591
+ params.filtering = JSON.stringify(baseFilters);
563
592
  if (sort) {
564
593
  params.sort = sort;
565
594
  }
@@ -582,9 +611,10 @@ function registerGetMetaInsights(server2) {
582
611
  ]
583
612
  };
584
613
  }
585
- let text = `**${rows.length} rows** | Level: ${level ?? "campaign"} | Event: ${convEvent}
586
-
587
- `;
614
+ let text = `**${rows.length} rows** | Level: ${effectiveLevel} | Event: ${convEvent}`;
615
+ if (campaign_id) text += ` | Campaign: ${campaign_id}`;
616
+ if (adset_id) text += ` | Ad Set: ${adset_id}`;
617
+ text += "\n\n";
588
618
  for (const row of rows) {
589
619
  const spend = parseFloat(row.spend || "0");
590
620
  const impressions = parseInt(row.impressions || "0");
@@ -944,6 +974,30 @@ function formatGoogleAdsError(err) {
944
974
  }
945
975
  return `Google Ads API error (${code}): ${err.message}`;
946
976
  }
977
+ async function googleAdsMutate(customerId, operations) {
978
+ const auth = getGoogleAdsAuth();
979
+ const normalizedId = normalizeCustomerId(customerId);
980
+ const headers = await auth.getHeaders(normalizedId);
981
+ const url = `${BASE_URL}/customers/${normalizedId}/googleAds:mutate`;
982
+ const response = await fetch(url, {
983
+ method: "POST",
984
+ headers: {
985
+ ...headers,
986
+ "Content-Type": "application/json"
987
+ },
988
+ body: JSON.stringify({ mutateOperations: operations })
989
+ });
990
+ const body = await response.json();
991
+ if (!response.ok) {
992
+ if (isGoogleAdsError(body)) {
993
+ throw new Error(formatGoogleAdsError(body.error));
994
+ }
995
+ throw new Error(
996
+ `Google Ads API mutate returned ${response.status}: ${JSON.stringify(body)}`
997
+ );
998
+ }
999
+ return body;
1000
+ }
947
1001
  async function googleAdsQuery(customerId, query) {
948
1002
  const auth = getGoogleAdsAuth();
949
1003
  const normalizedId = normalizeCustomerId(customerId);
@@ -2173,6 +2227,168 @@ Sources: goog-pdf-018, ab-pt-008, goog-pdf-019, ab-pt-007
2173
2227
  );
2174
2228
  }
2175
2229
 
2230
+ // src/tools/google-upload-assets.ts
2231
+ import { z as z13 } from "zod";
2232
+ import { readFile } from "fs/promises";
2233
+ import { basename, resolve } from "path";
2234
+ async function fetchAsBase64(url) {
2235
+ if (url.startsWith("data:")) {
2236
+ const commaIdx = url.indexOf(",");
2237
+ if (commaIdx === -1) throw new Error("Invalid data URI");
2238
+ return url.slice(commaIdx + 1);
2239
+ }
2240
+ const res = await fetch(url);
2241
+ if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
2242
+ const buf = Buffer.from(await res.arrayBuffer());
2243
+ return buf.toString("base64");
2244
+ }
2245
+ async function readFileAsBase64(filePath) {
2246
+ const buf = await readFile(resolve(filePath));
2247
+ return buf.toString("base64");
2248
+ }
2249
+ function registerUploadGoogleImageAssets(server2) {
2250
+ server2.tool(
2251
+ "upload_google_image_assets",
2252
+ "Upload image assets to a Google Ads account. Accepts URLs or local file paths. Each image becomes an Asset resource in the account. Optionally link assets to a campaign or ad group after upload. Supports batch upload (up to 50 images per call).",
2253
+ {
2254
+ customer_id: z13.string().describe("Google Ads customer ID"),
2255
+ images: z13.array(
2256
+ z13.object({
2257
+ source: z13.string().describe(
2258
+ "Image URL (https://...) or local file path (/path/to/image.png)"
2259
+ ),
2260
+ name: z13.string().optional().describe(
2261
+ "Asset name in Google Ads (default: filename from source)"
2262
+ )
2263
+ })
2264
+ ).min(1).max(50).describe("Array of images to upload (max 50 per call)"),
2265
+ campaign_id: z13.string().optional().describe(
2266
+ "Link uploaded assets to this campaign (creates CampaignAsset links)"
2267
+ ),
2268
+ ad_group_id: z13.string().optional().describe(
2269
+ "Link uploaded assets to this ad group (creates AdGroupAsset links). Takes priority over campaign_id."
2270
+ ),
2271
+ field_type: z13.enum(["IMAGE", "LANDSCAPE_LOGO", "LOGO", "MARKETING_IMAGE", "SQUARE_MARKETING_IMAGE", "PORTRAIT_MARKETING_IMAGE"]).optional().describe("Asset field type when linking to campaign/ad group (default: IMAGE)"),
2272
+ dry_run: z13.boolean().optional().describe("Preview what would be uploaded without making changes")
2273
+ },
2274
+ async ({
2275
+ customer_id,
2276
+ images,
2277
+ campaign_id,
2278
+ ad_group_id,
2279
+ field_type,
2280
+ dry_run
2281
+ }) => {
2282
+ try {
2283
+ const normalizedId = normalizeCustomerId(customer_id);
2284
+ const effectiveFieldType = field_type ?? "IMAGE";
2285
+ if (dry_run) {
2286
+ let text2 = `**Dry run** \u2014 would upload ${images.length} image(s) to customer ${customer_id}
2287
+
2288
+ `;
2289
+ for (const img of images) {
2290
+ const name = img.name ?? basename(img.source).replace(/\?.*$/, "") ?? "unnamed";
2291
+ text2 += `- ${name} \u2190 ${img.source}
2292
+ `;
2293
+ }
2294
+ if (ad_group_id) {
2295
+ text2 += `
2296
+ Would link to ad group ${ad_group_id} as ${effectiveFieldType}`;
2297
+ } else if (campaign_id) {
2298
+ text2 += `
2299
+ Would link to campaign ${campaign_id} as ${effectiveFieldType}`;
2300
+ }
2301
+ return { content: [{ type: "text", text: text2 }] };
2302
+ }
2303
+ const assetOps = [];
2304
+ const assetNames = [];
2305
+ for (const img of images) {
2306
+ const name = img.name ?? basename(img.source).replace(/\?.*$/, "") ?? "unnamed";
2307
+ assetNames.push(name);
2308
+ const isUrl = img.source.startsWith("http://") || img.source.startsWith("https://") || img.source.startsWith("data:");
2309
+ const data = isUrl ? await fetchAsBase64(img.source) : await readFileAsBase64(img.source);
2310
+ assetOps.push({
2311
+ assetOperation: {
2312
+ create: {
2313
+ name,
2314
+ type: "IMAGE",
2315
+ imageAsset: { data }
2316
+ }
2317
+ }
2318
+ });
2319
+ }
2320
+ const assetResult = await googleAdsMutate(normalizedId, assetOps);
2321
+ const createdResourceNames = assetResult.mutateOperationResponses.map(
2322
+ (r) => r.assetResult?.resourceName ?? ""
2323
+ );
2324
+ const successCount = createdResourceNames.filter(Boolean).length;
2325
+ let text = `**Uploaded ${successCount}/${images.length} image assets**
2326
+
2327
+ `;
2328
+ for (let i = 0; i < images.length; i++) {
2329
+ const rn = createdResourceNames[i];
2330
+ if (rn) {
2331
+ text += `\u2713 ${assetNames[i]} \u2192 ${rn}
2332
+ `;
2333
+ } else {
2334
+ text += `\u2717 ${assetNames[i]} \u2014 failed
2335
+ `;
2336
+ }
2337
+ }
2338
+ if (successCount > 0 && (ad_group_id || campaign_id)) {
2339
+ const linkOps = [];
2340
+ for (const resourceName of createdResourceNames) {
2341
+ if (!resourceName) continue;
2342
+ if (ad_group_id) {
2343
+ linkOps.push({
2344
+ adGroupAssetOperation: {
2345
+ create: {
2346
+ adGroup: `customers/${normalizedId}/adGroups/${ad_group_id}`,
2347
+ asset: resourceName,
2348
+ fieldType: effectiveFieldType
2349
+ }
2350
+ }
2351
+ });
2352
+ } else if (campaign_id) {
2353
+ linkOps.push({
2354
+ campaignAssetOperation: {
2355
+ create: {
2356
+ campaign: `customers/${normalizedId}/campaigns/${campaign_id}`,
2357
+ asset: resourceName,
2358
+ fieldType: effectiveFieldType
2359
+ }
2360
+ }
2361
+ });
2362
+ }
2363
+ }
2364
+ try {
2365
+ await googleAdsMutate(normalizedId, linkOps);
2366
+ const linkTarget = ad_group_id ? `ad group ${ad_group_id}` : `campaign ${campaign_id}`;
2367
+ text += `
2368
+ \u2713 Linked ${successCount} assets to ${linkTarget} as ${effectiveFieldType}`;
2369
+ } catch (linkErr) {
2370
+ text += `
2371
+ \u26A0\uFE0F Assets uploaded but linking failed: ${linkErr instanceof Error ? linkErr.message : String(linkErr)}`;
2372
+ text += `
2373
+ Assets exist in the account and can be linked manually.`;
2374
+ }
2375
+ }
2376
+ return { content: [{ type: "text", text }] };
2377
+ } catch (err) {
2378
+ return {
2379
+ content: [
2380
+ {
2381
+ type: "text",
2382
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
2383
+ }
2384
+ ],
2385
+ isError: true
2386
+ };
2387
+ }
2388
+ }
2389
+ );
2390
+ }
2391
+
2176
2392
  // src/tools/connection-status.ts
2177
2393
  function registerConnectionStatus(server2, status2) {
2178
2394
  server2.tool(
@@ -2455,8 +2671,10 @@ List ads, optionally scoped to an ad set.
2455
2671
  - **fields, effective_status, limit, after** (optional)
2456
2672
 
2457
2673
  ### get_meta_insights
2458
- Pull performance insights with configurable level, breakdowns, date range.
2674
+ Pull performance insights with configurable level, breakdowns, date range. Ad-level queries auto-include ad_id, ad_name, adset_id, adset_name.
2459
2675
  - **ad_account_id** (required)
2676
+ - **campaign_id** (optional): Scope to a specific campaign
2677
+ - **adset_id** (optional): Scope to a specific ad set
2460
2678
  - **level** (optional): account, campaign, adset, ad (default: campaign)
2461
2679
  - **date_preset** (optional): default last_7d
2462
2680
  - **time_range** (optional): {since, until} for custom dates
@@ -2689,7 +2907,7 @@ function getDotEnv() {
2689
2907
  }
2690
2908
  return dotEnvCache;
2691
2909
  }
2692
- function resolve(envName, cliName) {
2910
+ function resolve2(envName, cliName) {
2693
2911
  const cli = getCliArg(cliName);
2694
2912
  if (cli) return { value: cli, source: `--${cliName} argument` };
2695
2913
  const env = process.env[envName]?.trim();
@@ -2699,17 +2917,17 @@ function resolve(envName, cliName) {
2699
2917
  return { value: void 0, source: "not configured" };
2700
2918
  }
2701
2919
  function resolveApiKey() {
2702
- return resolve("API_KEY", "api-key");
2920
+ return resolve2("API_KEY", "api-key");
2703
2921
  }
2704
2922
  function resolveMetaToken() {
2705
- return resolve("META_ACCESS_TOKEN", "meta-token");
2923
+ return resolve2("META_ACCESS_TOKEN", "meta-token");
2706
2924
  }
2707
2925
  function resolveGoogleAdsConfig() {
2708
- const devToken = resolve("GOOGLE_ADS_DEVELOPER_TOKEN", "google-dev-token");
2709
- const clientId = resolve("GOOGLE_ADS_CLIENT_ID", "google-client-id");
2710
- const clientSecret = resolve("GOOGLE_ADS_CLIENT_SECRET", "google-client-secret");
2711
- const refreshToken = resolve("GOOGLE_ADS_REFRESH_TOKEN", "google-refresh-token");
2712
- const loginCustomerId = resolve("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "google-login-customer-id");
2926
+ const devToken = resolve2("GOOGLE_ADS_DEVELOPER_TOKEN", "google-dev-token");
2927
+ const clientId = resolve2("GOOGLE_ADS_CLIENT_ID", "google-client-id");
2928
+ const clientSecret = resolve2("GOOGLE_ADS_CLIENT_SECRET", "google-client-secret");
2929
+ const refreshToken = resolve2("GOOGLE_ADS_REFRESH_TOKEN", "google-refresh-token");
2930
+ const loginCustomerId = resolve2("GOOGLE_ADS_LOGIN_CUSTOMER_ID", "google-login-customer-id");
2713
2931
  const required = [
2714
2932
  { name: "GOOGLE_ADS_DEVELOPER_TOKEN", result: devToken },
2715
2933
  { name: "GOOGLE_ADS_CLIENT_ID", result: clientId },
@@ -2752,8 +2970,8 @@ async function maybeRunAuthCommand() {
2752
2970
  process.exit(0);
2753
2971
  }
2754
2972
  function prompt(rl, question) {
2755
- return new Promise((resolve2) => {
2756
- rl.question(question, (answer) => resolve2(answer.trim()));
2973
+ return new Promise((resolve3) => {
2974
+ rl.question(question, (answer) => resolve3(answer.trim()));
2757
2975
  });
2758
2976
  }
2759
2977
  function openBrowser(url) {
@@ -2841,7 +3059,7 @@ function saveToEnv(vars) {
2841
3059
  }
2842
3060
  }
2843
3061
  async function waitForOAuthCallback(clientId, clientSecret) {
2844
- return new Promise((resolve2, reject) => {
3062
+ return new Promise((resolve3, reject) => {
2845
3063
  const server2 = createServer(
2846
3064
  async (req, res) => {
2847
3065
  const url = new URL(req.url ?? "/", `http://localhost:${OAUTH_PORT}`);
@@ -2896,7 +3114,7 @@ async function waitForOAuthCallback(clientId, clientSecret) {
2896
3114
  "<html><body><h2>Success!</h2><p>You can close this tab and return to the terminal.</p></body></html>"
2897
3115
  );
2898
3116
  server2.close();
2899
- resolve2({ refreshToken: tokenData.refresh_token });
3117
+ resolve3({ refreshToken: tokenData.refresh_token });
2900
3118
  } catch (err) {
2901
3119
  res.writeHead(500, { "Content-Type": "text/html" });
2902
3120
  res.end(
@@ -3064,6 +3282,7 @@ registerGetGoogleAdsAssets(server);
3064
3282
  registerGetGoogleAdsInsights(server);
3065
3283
  registerGetGoogleAdsNetworkMix(server);
3066
3284
  registerGetGoogleAdsAssetFatigue(server);
3285
+ registerUploadGoogleImageAssets(server);
3067
3286
  registerConnectionStatus(server, status);
3068
3287
  registerVocabularyResource(server);
3069
3288
  registerInstructionsResource(server, status);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-growth-mcp",
3
- "version": "2.2.4",
3
+ "version": "2.3.4",
4
4
  "description": "MCP server for mobile growth & UA knowledge base — campaign optimization, creative strategy, and subscription app insights",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",