mobile-growth-mcp 2.2.1 → 2.2.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 +207 -64
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ async function jsonRpcRequest(apiKey2, method, params) {
23
23
  Accept: "application/json, text/event-stream"
24
24
  },
25
25
  body: JSON.stringify(body),
26
- signal: AbortSignal.timeout(15e3)
26
+ signal: AbortSignal.timeout(3e4)
27
27
  });
28
28
  if (!res.ok) {
29
29
  const text = await res.text();
@@ -39,19 +39,34 @@ async function fetchRemoteTools(apiKey2) {
39
39
  return resp.result?.tools ?? [];
40
40
  }
41
41
  async function callRemoteTool(apiKey2, name, args) {
42
- const resp = await jsonRpcRequest(apiKey2, "tools/call", {
43
- name,
44
- arguments: args
45
- });
46
- if (resp.error) {
47
- return {
48
- content: [{ type: "text", text: `Remote error: ${resp.error.message}` }],
49
- isError: true
50
- };
42
+ const maxAttempts = 2;
43
+ let lastError;
44
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
45
+ try {
46
+ const resp = await jsonRpcRequest(apiKey2, "tools/call", {
47
+ name,
48
+ arguments: args
49
+ });
50
+ if (resp.error) {
51
+ return {
52
+ content: [{ type: "text", text: `Remote error: ${resp.error.message}` }],
53
+ isError: true
54
+ };
55
+ }
56
+ return {
57
+ content: resp.result?.content ?? [{ type: "text", text: "No content returned" }],
58
+ isError: resp.result?.isError
59
+ };
60
+ } catch (err) {
61
+ lastError = err;
62
+ const isRetryable = lastError.name === "AbortError" || lastError.name === "TimeoutError" || lastError.message?.includes("fetch failed");
63
+ if (!isRetryable || attempt === maxAttempts) break;
64
+ await new Promise((r) => setTimeout(r, 2e3));
65
+ }
51
66
  }
52
67
  return {
53
- content: resp.result?.content ?? [{ type: "text", text: "No content returned" }],
54
- isError: resp.result?.isError
68
+ content: [{ type: "text", text: `Remote call failed after retry: ${lastError?.message ?? "unknown error"}` }],
69
+ isError: true
55
70
  };
56
71
  }
57
72
  function jsonSchemaToZodShape(inputSchema) {
@@ -222,7 +237,7 @@ async function metaApiGet(options) {
222
237
  function activeFilter() {
223
238
  return JSON.stringify([
224
239
  {
225
- field: "effective_status",
240
+ field: "ad.effective_status",
226
241
  operator: "IN",
227
242
  value: ["ACTIVE"]
228
243
  }
@@ -958,6 +973,23 @@ async function googleAdsQuery(customerId, query) {
958
973
  }
959
974
 
960
975
  // src/google/format.ts
976
+ async function resolveCampaignId(customerId, value) {
977
+ if (/^\d+$/.test(value)) return value;
978
+ const query = `SELECT campaign.id, campaign.name FROM campaign WHERE campaign.name = '${value.replace(/'/g, "\\'")}' LIMIT 1`;
979
+ const chunks = await googleAdsQuery(customerId, query);
980
+ const rows = chunks.flatMap((c) => c.results ?? []);
981
+ if (rows.length === 0) throw new Error(`Campaign not found by name: "${value}"`);
982
+ return rows[0].campaign.id;
983
+ }
984
+ async function resolveAdGroupId(customerId, value, campaignId) {
985
+ if (/^\d+$/.test(value)) return value;
986
+ let query = `SELECT ad_group.id, ad_group.name FROM ad_group WHERE ad_group.name = '${value.replace(/'/g, "\\'")}' LIMIT 1`;
987
+ if (campaignId) query = query.replace("LIMIT 1", `AND campaign.id = ${campaignId} LIMIT 1`);
988
+ const chunks = await googleAdsQuery(customerId, query);
989
+ const rows = chunks.flatMap((c) => c.results ?? []);
990
+ if (rows.length === 0) throw new Error(`Ad group not found by name: "${value}"`);
991
+ return rows[0].adGroup.id;
992
+ }
961
993
  function formatMicros(micros) {
962
994
  if (!micros) return "\u2014";
963
995
  const val = parseInt(micros, 10);
@@ -1091,7 +1123,7 @@ function registerGetGoogleAdsCampaigns(server2) {
1091
1123
  const header = `## Google Campaigns (${rows.length} found)
1092
1124
 
1093
1125
  `;
1094
- const tableHeader = "| Campaign | Status | Type | Bid Strategy | Target | Daily Budget | App |\n|---|---|---|---|---|---|---|\n";
1126
+ const tableHeader = "| ID | Campaign | Status | Type | Bid Strategy | Target | Daily Budget | App |\n|---|---|---|---|---|---|---|---|\n";
1095
1127
  let tableRows = "";
1096
1128
  for (const row of rows) {
1097
1129
  const c = row.campaign;
@@ -1102,7 +1134,7 @@ function registerGetGoogleAdsCampaigns(server2) {
1102
1134
  const appId = c.appCampaignSetting?.appId ?? "";
1103
1135
  const store = appStoreLabel(c.appCampaignSetting?.appStore);
1104
1136
  const appDisplay = appId ? `${appId} (${store})` : "\u2014";
1105
- tableRows += `| ${c.name} | ${c.status} | ${channelSubTypeLabel(c.advertisingChannelSubType)} | ${c.biddingStrategyType ?? "\u2014"} | ${formatTarget(row)} | ${budgetDisplay} | ${appDisplay} |
1137
+ tableRows += `| ${c.id} | ${c.name} | ${c.status} | ${channelSubTypeLabel(c.advertisingChannelSubType)} | ${c.biddingStrategyType ?? "\u2014"} | ${formatTarget(row)} | ${budgetDisplay} | ${appDisplay} |
1106
1138
  `;
1107
1139
  }
1108
1140
  return {
@@ -1143,8 +1175,9 @@ function registerGetGoogleAdsAdGroups(server2) {
1143
1175
  let whereClause = `WHERE campaign.advertising_channel_sub_type IN ('APP_CAMPAIGN', 'APP_CAMPAIGN_FOR_ENGAGEMENT')
1144
1176
  AND ad_group.status IN (${statusList})`;
1145
1177
  if (campaign_id) {
1178
+ const resolvedCampaignId = await resolveCampaignId(customer_id, campaign_id);
1146
1179
  whereClause += `
1147
- AND campaign.id = ${campaign_id}`;
1180
+ AND campaign.id = ${resolvedCampaignId}`;
1148
1181
  }
1149
1182
  const query = `
1150
1183
  SELECT
@@ -1175,13 +1208,13 @@ function registerGetGoogleAdsAdGroups(server2) {
1175
1208
  const header = `## Ad Groups${campaignName} (${rows.length} found)
1176
1209
 
1177
1210
  `;
1178
- const tableHeader = "| Ad Group | Status | Type | Campaign |\n|---|---|---|---|\n";
1211
+ const tableHeader = "| Ad Group ID | Ad Group | Status | Type | Campaign ID | Campaign |\n|---|---|---|---|---|---|\n";
1179
1212
  let tableRows = "";
1180
1213
  for (const row of rows) {
1181
1214
  const ag = row.adGroup;
1182
1215
  const c = row.campaign;
1183
1216
  if (!ag) continue;
1184
- tableRows += `| ${ag.name} | ${ag.status ?? "\u2014"} | ${ag.type ?? "\u2014"} | ${c?.name ?? "\u2014"} |
1217
+ tableRows += `| ${ag.id} | ${ag.name} | ${ag.status ?? "\u2014"} | ${ag.type ?? "\u2014"} | ${c?.id ?? "\u2014"} | ${c?.name ?? "\u2014"} |
1185
1218
  `;
1186
1219
  }
1187
1220
  return {
@@ -1236,8 +1269,14 @@ function registerGetGoogleAdsAssets(server2) {
1236
1269
  const conditions = [
1237
1270
  "campaign.status = 'ENABLED'"
1238
1271
  ];
1239
- if (campaign_id) conditions.push(`campaign.id = ${campaign_id}`);
1240
- if (ad_group_id) conditions.push(`ad_group.id = ${ad_group_id}`);
1272
+ if (campaign_id) {
1273
+ const resolvedCampaignId = await resolveCampaignId(customer_id, campaign_id);
1274
+ conditions.push(`campaign.id = ${resolvedCampaignId}`);
1275
+ }
1276
+ if (ad_group_id) {
1277
+ const resolvedAdGroupId = await resolveAdGroupId(customer_id, ad_group_id, campaign_id);
1278
+ conditions.push(`ad_group.id = ${resolvedAdGroupId}`);
1279
+ }
1241
1280
  if (asset_type?.length) {
1242
1281
  const typeList = asset_type.map((t) => `'${t}'`).join(", ");
1243
1282
  conditions.push(`asset.type IN (${typeList})`);
@@ -1531,8 +1570,14 @@ function registerGetGoogleAdsInsights(server2) {
1531
1570
  "campaign.status = 'ENABLED'",
1532
1571
  `segments.date BETWEEN '${startDate}' AND '${endDate}'`
1533
1572
  ];
1534
- if (campaign_id) conditions.push(`campaign.id = ${campaign_id}`);
1535
- if (ad_group_id) conditions.push(`ad_group.id = ${ad_group_id}`);
1573
+ if (campaign_id) {
1574
+ const resolvedCampaignId = await resolveCampaignId(customer_id, campaign_id);
1575
+ conditions.push(`campaign.id = ${resolvedCampaignId}`);
1576
+ }
1577
+ if (ad_group_id) {
1578
+ const resolvedAdGroupId = await resolveAdGroupId(customer_id, ad_group_id, campaign_id);
1579
+ conditions.push(`ad_group.id = ${resolvedAdGroupId}`);
1580
+ }
1536
1581
  const orderBy = `ORDER BY ${sortKeyForEnum(sortField)} DESC`;
1537
1582
  const query = `
1538
1583
  SELECT ${selectFields.join(", ")}
@@ -1677,7 +1722,10 @@ function registerGetGoogleAdsNetworkMix(server2) {
1677
1722
  "campaign.status = 'ENABLED'",
1678
1723
  `segments.date BETWEEN '${startDate}' AND '${endDate}'`
1679
1724
  ];
1680
- if (campaign_id) conditions.push(`campaign.id = ${campaign_id}`);
1725
+ if (campaign_id) {
1726
+ const resolvedCampaignId = await resolveCampaignId(customer_id, campaign_id);
1727
+ conditions.push(`campaign.id = ${resolvedCampaignId}`);
1728
+ }
1681
1729
  const query = `
1682
1730
  SELECT
1683
1731
  campaign.id,
@@ -1887,11 +1935,15 @@ function registerGetGoogleAdsAssetFatigue(server2) {
1887
1935
  startDate.setDate(startDate.getDate() - lookback + 1);
1888
1936
  const ageStartDate = new Date(endDate);
1889
1937
  ageStartDate.setDate(ageStartDate.getDate() - 89);
1938
+ const resolvedCampaignId = await resolveCampaignId(customer_id, campaign_id);
1890
1939
  const conditions = [
1891
- `campaign.id = ${campaign_id}`,
1940
+ `campaign.id = ${resolvedCampaignId}`,
1892
1941
  `segments.date BETWEEN '${dateFmt(startDate)}' AND '${dateFmt(endDate)}'`
1893
1942
  ];
1894
- if (ad_group_id) conditions.push(`ad_group.id = ${ad_group_id}`);
1943
+ if (ad_group_id) {
1944
+ const resolvedAdGroupId = await resolveAdGroupId(customer_id, ad_group_id, resolvedCampaignId);
1945
+ conditions.push(`ad_group.id = ${resolvedAdGroupId}`);
1946
+ }
1895
1947
  if (asset_type?.length) {
1896
1948
  const typeList = asset_type.map((t) => `'${t}'`).join(", ");
1897
1949
  conditions.push(`asset.type IN (${typeList})`);
@@ -2156,10 +2208,11 @@ function registerConnectionStatus(server2, status2) {
2156
2208
  );
2157
2209
  } else {
2158
2210
  lines.push(
2159
- "## Meta Marketing API: Not Configured",
2160
- "- Meta tools will return an error when called",
2211
+ "## Meta Marketing API: Not Connected (Optional)",
2212
+ "- KB, suggestions, and private insights work without it",
2213
+ "- Connect Meta to unlock live campaign data and reports",
2161
2214
  "",
2162
- "### How to fix",
2215
+ "### How to connect",
2163
2216
  "Provide your Meta access token using one of these methods:",
2164
2217
  '1. MCP config: add `"META_ACCESS_TOKEN": "..."` to the `"env"` block in `.mcp.json` (Claude Code/Cursor) or `claude_desktop_config.json` (Claude Desktop)',
2165
2218
  "2. CLI argument: add `--meta-token=...` to the args array",
@@ -2176,11 +2229,11 @@ function registerConnectionStatus(server2, status2) {
2176
2229
  );
2177
2230
  } else {
2178
2231
  lines.push(
2179
- "## Google Ads API: Not Configured",
2180
- "- Google Ads tools will return an error when called",
2181
- `- Missing: ${status2.google.missing.join(", ")}`,
2232
+ "## Google Ads API: Not Connected (Optional)",
2233
+ "- KB, suggestions, and private insights work without it",
2234
+ "- Connect Google Ads to unlock campaign data and network analysis",
2182
2235
  "",
2183
- "### How to fix",
2236
+ "### How to connect",
2184
2237
  "Option 1 \u2014 Interactive setup (recommended):",
2185
2238
  "```",
2186
2239
  "npx mobile-growth-mcp auth google",
@@ -2314,35 +2367,40 @@ function registerVocabularyResource(server2) {
2314
2367
  }
2315
2368
 
2316
2369
  // src/resources/instructions.ts
2317
- var INSTRUCTIONS = `# Mobile Growth MCP \u2014 Knowledge Base + Meta Ad Tools
2370
+ var INSTRUCTIONS = `# Mobile Growth MCP \u2014 Knowledge Base + Ad Platform Tools
2318
2371
 
2319
2372
  ## Welcome
2320
2373
 
2321
- You're connected to the Mobile Growth knowledge base \u2014 curated expert insights on mobile advertising, campaign optimization, and subscription app growth.
2374
+ You're connected to the Mobile Growth knowledge base \u2014 curated expert insights on mobile advertising, campaign optimization, and subscription app growth \u2014 plus direct Meta and Google Ads API integration.
2322
2375
 
2323
2376
  **The knowledge base is always on.** Use \`search_insights\` freely \u2014 before making recommendations, when diagnosing issues, or exploring strategies. The more specific your query, the better the results.
2324
2377
 
2378
+ > **Note:** Connecting Meta or Google Ads is optional. The knowledge base, community suggestions, and private insights all work with just your API key. Add Meta or Google Ads credentials later if you want live campaign data and reports.
2379
+
2325
2380
  Quick examples:
2326
2381
  - "subscription app creative fatigue signals"
2327
2382
  - "Meta CBO vs ABO tradeoffs for subscription apps"
2383
+ - "Google UAC network shift detection"
2328
2384
  - "iOS attribution strategies post-ATT"
2329
2385
 
2330
2386
  If you can't find what you need, call \`submit_feedback\` to report the gap \u2014 it helps us improve the knowledge base.
2331
2387
 
2332
2388
  ## What This Is
2333
- A curated knowledge base of mobile advertising insights + direct Meta Marketing API integration. Query expert knowledge, pull live campaign data, and run pre-built reports \u2014 all from your LLM.
2389
+ A curated knowledge base of mobile advertising insights + direct Meta Marketing API and Google Ads API integration. Query expert knowledge, pull live campaign data, and run pre-built reports \u2014 all from your LLM.
2390
+
2391
+ ---
2334
2392
 
2335
2393
  ## Knowledge Base Tools
2336
2394
 
2337
2395
  ### search_insights
2338
- Semantic + keyword hybrid search across the knowledge base.
2396
+ Semantic + keyword hybrid search across the knowledge base. **Use this first** for any question about mobile advertising before searching the web.
2339
2397
  - **query** (required): Natural language search query
2340
2398
  - **topics** (optional): Filter by topic tags, e.g. ["creative_strategy", "scaling"]
2341
2399
  - **applies_to** (optional): Filter by applicability, e.g. ["subscription_apps", "ios"]
2342
2400
  - **limit** (optional): Max results, 1-30, default 10
2343
2401
 
2344
2402
  ### list_insights
2345
- Browse all insights with optional filtering. Returns titles and metadata.
2403
+ Browse all insights with optional filtering. Returns titles and metadata. Private insights are marked with a lock icon.
2346
2404
  - **topic** (optional): Filter by a single topic tag
2347
2405
  - **applies_to** (optional): Filter by a single applies_to value
2348
2406
 
@@ -2350,12 +2408,29 @@ Browse all insights with optional filtering. Returns titles and metadata.
2350
2408
  Fetch the full content of a specific insight by ID or slug.
2351
2409
  - **id** (required): Numeric ID or string slug (e.g. "mb-li-001")
2352
2410
 
2411
+ ### get_vocabulary_counts
2412
+ Returns counts of how many insights use each topic and applies_to tag. Lightweight way to explore what's in the KB.
2413
+
2353
2414
  ### submit_feedback
2354
- Report a gap in the knowledge base or a missing capability. Helps improve the product.
2355
- - **category** (required): missing_knowledge, missing_feature, search_quality, or other
2356
- - **summary** (required): What was needed but not available (anonymized \u2014 no account IDs or tokens)
2415
+ Report a gap in the knowledge base, a bug in any tool, or a missing capability.
2416
+ - **category** (required): missing_knowledge, missing_feature, search_quality, bug_report, or other
2417
+ - **summary** (required): What was needed or what went wrong (anonymized \u2014 no account IDs or tokens)
2357
2418
  - **search_queries_tried** (optional): Search queries that returned poor/no results
2358
2419
 
2420
+ ## Community Knowledge
2421
+
2422
+ ### suggest_insight \u2B50 DEFAULT for saving knowledge
2423
+ Submit knowledge for admin review. **This is the default tool when a user wants to save an insight** \u2014 it contributes to the shared knowledge base that benefits all users. Extract as much structured data as possible from the source material \u2014 full insight schema (title, insight text, source metadata, topics, actionable steps). Once approved, it's added to the shared knowledge base.
2424
+ - Use this when the user shares an article, post, or discussion with valuable mobile growth knowledge
2425
+ - Keep raw_excerpt concise (under 500 chars) for reliability
2426
+
2427
+ ### save_private_insight
2428
+ Save knowledge that is private to your API key. Immediately searchable but only visible to you. **Only use this instead of suggest_insight when** the content contains client-specific data, internal benchmarks, account metrics, or the user explicitly asks for private storage.
2429
+ - Same full schema as suggest_insight
2430
+ - No admin approval needed \u2014 saved instantly
2431
+
2432
+ ---
2433
+
2359
2434
  ## Meta Marketing API Tools
2360
2435
 
2361
2436
  **Requires META_ACCESS_TOKEN env var** \u2014 without it, these tools return a clear error. Knowledge base tools work with just API_KEY.
@@ -2399,33 +2474,87 @@ Built-in report: detect creative fatigue via frequency, CTR decline, CPA trends.
2399
2474
  - **frequency_critical** (optional): default 5
2400
2475
  - **ctr_decline_threshold** (optional): default 30%
2401
2476
 
2477
+ ---
2478
+
2402
2479
  ## Google Ads Tools
2403
2480
 
2404
- **Requires Google Ads credentials** \u2014 run \`npx mobile-growth-mcp auth google\` to set up interactively. This walks you through developer token, OAuth app, and authorization. Credentials are saved to \`.env\` and never leave the user's machine.
2481
+ **Requires Google Ads credentials** \u2014 run \`npx mobile-growth-mcp auth google\` to set up interactively. Credentials are saved to \`.env\` and never leave the user's machine.
2482
+
2483
+ All tools accept campaign and ad group IDs as either **numeric IDs** or **campaign/ad group names** \u2014 names are auto-resolved to numeric IDs internally.
2405
2484
 
2406
2485
  ### get_google_ads_campaigns
2407
- List campaigns from a Google Ads account with key metrics (last 7 days).
2486
+ List Google App Campaigns with status, bid strategy, budgets, and app info. Returns **numeric campaign IDs** needed by other tools.
2408
2487
  - **customer_id** (required): Google Ads customer ID (e.g. "123-456-7890")
2409
- - **status** (optional): Filter by status \u2014 ENABLED, PAUSED, or REMOVED (default: ENABLED)
2488
+ - **status** (optional): Filter by status \u2014 ENABLED, PAUSED, REMOVED (default: ["ENABLED"])
2489
+ - **channel_sub_type** (optional): Filter by campaign type \u2014 APP_CAMPAIGN (ACi), APP_CAMPAIGN_FOR_ENGAGEMENT (ACe)
2410
2490
  - **limit** (optional): Max campaigns to return (default 50)
2411
2491
 
2492
+ ### get_google_ad_groups
2493
+ List ad groups within Google App Campaigns. Returns **numeric ad group IDs** and campaign IDs. In UAC, ad groups represent creative themes \u2014 observe spend distribution to identify winning messaging angles.
2494
+ - **customer_id** (required): Google Ads customer ID
2495
+ - **campaign_id** (optional): Scope to a specific campaign (name or numeric ID)
2496
+ - **status** (optional): Filter by status (default: ["ENABLED"])
2497
+ - **limit** (optional): Max results (default 50)
2498
+
2499
+ ### get_google_insights
2500
+ Pull performance metrics with configurable level, breakdowns, date ranges, and time granularity. Use **network breakdown** to detect traffic shifts between Search, Display/AdMob, and YouTube \u2014 the #1 diagnostic lever for Google campaigns.
2501
+ - **customer_id** (required): Google Ads customer ID
2502
+ - **level** (optional): account, campaign, ad_group, asset (default: campaign)
2503
+ - **campaign_id** (optional): Scope to specific campaign (name or numeric ID)
2504
+ - **ad_group_id** (optional): Scope to specific ad group
2505
+ - **breakdown** (optional): network or device (one at a time \u2014 GAQL restriction)
2506
+ - **date_range** (optional): {start_date, end_date} in YYYY-MM-DD
2507
+ - **date_preset** (optional): LAST_7_DAYS, LAST_14_DAYS, LAST_30_DAYS, THIS_MONTH, LAST_MONTH
2508
+ - **time_increment** (optional): daily, weekly, monthly, summary (default: summary)
2509
+ - **sort** (optional): cost_desc, conversions_desc, impressions_desc, ctr_desc
2510
+ - **limit** (optional): Max results (default 50, max 500)
2511
+
2512
+ ### get_google_assets
2513
+ List creative assets with metadata, performance labels, and slot utilization audit. Checks headlines, descriptions, images, videos against Google's per-slot maximums.
2514
+ - **customer_id** (required): Google Ads customer ID
2515
+ - **campaign_id** (optional): Scope to a specific campaign (name or numeric ID)
2516
+ - **ad_group_id** (optional): Scope to a specific ad group
2517
+ - **asset_type** (optional): IMAGE, YOUTUBE_VIDEO, TEXT, MEDIA_BUNDLE
2518
+ - **include_slot_audit** (optional): default true
2519
+ - **limit** (optional): Max results (default 50)
2520
+
2521
+ ### get_google_network_mix
2522
+ Analyze traffic distribution across Google's ad networks (Search, Display/AdMob, YouTube) over time. Flags significant shifts in spend share \u2014 a sudden shift to Display/MGDN typically tanks CPA.
2523
+ - **customer_id** (required): Google Ads customer ID
2524
+ - **campaign_id** (optional): Scope to one campaign (name or numeric ID)
2525
+ - **date_range** (optional): {start_date, end_date} \u2014 default last 14 days
2526
+ - **shift_threshold_pct** (optional): Flag networks with spend share change > this % (default 10)
2527
+
2528
+ ### get_google_asset_fatigue
2529
+ Detect creative asset fatigue by analyzing per-asset impression trends, CTR decline, and CPA deterioration. Checks asset age against Google's 2-week learning minimum and 2-3 month refresh cadence.
2530
+ - **customer_id** (required): Google Ads customer ID
2531
+ - **campaign_id** (required): Campaign to analyze (name or numeric ID)
2532
+ - **ad_group_id** (optional): Scope to specific ad group
2533
+ - **lookback_days** (optional): Days of data to analyze, 7-90 (default 14)
2534
+ - **ctr_decline_threshold_pct** (optional): CTR decline % to flag (default 30)
2535
+ - **impression_decay_threshold_pct** (optional): Impression drop % to flag (default 50)
2536
+ - **asset_type** (optional): IMAGE, YOUTUBE_VIDEO, TEXT
2537
+
2538
+ ---
2539
+
2412
2540
  ## Reports (MCP Prompts)
2413
2541
 
2414
- Pre-built analysis workflows. Select a prompt and provide your ad_account_id to run:
2542
+ Pre-built analysis workflows for Meta accounts. Select a prompt and provide your ad_account_id to run:
2415
2543
 
2416
2544
  | Prompt | What it does | API calls |
2417
2545
  |--------|-------------|-----------|
2418
2546
  | ad-fatigue-report | Detect creative fatigue with daily granularity | 1 |
2419
2547
  | weekly-performance | Week-over-week health comparison with diagnosis | 2 |
2420
2548
  | creative-performance | Categorize ads by health status | 1 |
2421
- | placement-efficiency | Identify placement waste and savings | 1 per campaign |
2422
- | audience-composition | Age \xD7 gender heatmap with CPA analysis | 1-2 |
2549
+ | audience-composition | Age x gender heatmap with CPA analysis | 1-2 |
2423
2550
  | architecture-review | Campaign structure evaluation | 3 (no insights) |
2424
2551
  | audit-meta-account | Comprehensive account audit | 6+ |
2425
2552
  | campaign-comparison | Side-by-side campaign comparison | 3+ |
2426
2553
  | placement-audit | Detailed placement audit with examples | 1 per campaign |
2427
2554
  | attribution-analysis | Conversion quality validation | 2+ |
2428
2555
 
2556
+ ---
2557
+
2429
2558
  ## Resources
2430
2559
 
2431
2560
  ### vocabulary://tags
@@ -2435,17 +2564,31 @@ Lists all topic tags, applies_to tags, and platforms with counts.
2435
2564
 
2436
2565
  When your response draws on knowledge base results, **always attribute visibly** so the user knows the value came from the curated KB, not your general training data:
2437
2566
 
2438
- - **Tell the user** the information comes from the Mobile Growth knowledge base (e.g. "According to the Mobile Growth KB\u2026" or "The knowledge base recommends\u2026")
2439
- - **Cite source author + slug** for key claims (e.g. "\u2026(source: Eric Seufert, \`mb-li-001\`)")
2440
- - **When multiple insights support a recommendation**, mention the count (e.g. "3 insights in the KB agree that\u2026")
2441
- - **Distinguish KB-sourced advice from your own reasoning** \u2014 if you're adding your own analysis on top of KB results, make that clear (e.g. "The KB covers X; based on that, my suggestion is Y")
2442
-
2443
- ## Tips
2444
- - Start with \`list_insights\` to see what's in the knowledge base
2445
- - Use \`search_insights\` to find specific advice grounded in expert knowledge
2446
- - Meta tools default to safe parameters (last_7d, active-only) to avoid rate limits
2447
- - Reports reference specific knowledge base insight IDs \u2014 use \`get_insight\` to read the full context
2448
- - For custom date ranges, use time_range instead of date_preset
2567
+ - **Tell the user** the information comes from the Mobile Growth knowledge base (e.g. "According to the Mobile Growth KB..." or "The knowledge base recommends...")
2568
+ - **Cite source author + slug** for key claims (e.g. "...(source: Eric Seufert, \`mb-li-001\`)")
2569
+ - **When multiple insights support a recommendation**, mention the count (e.g. "3 insights in the KB agree that...")
2570
+ - **Distinguish KB-sourced advice from your own reasoning** \u2014 if you're adding your own analysis on top of KB results, make that clear
2571
+
2572
+ ## Workflow Tips
2573
+
2574
+ ### For Google Ads analysis:
2575
+ 1. Start with \`get_google_ads_campaigns\` to see campaigns and get numeric IDs
2576
+ 2. Use \`get_google_insights\` with network breakdown to check traffic distribution
2577
+ 3. Use \`get_google_network_mix\` if you suspect network shifts
2578
+ 4. Use \`get_google_assets\` to audit creative slot utilization
2579
+ 5. Use \`get_google_asset_fatigue\` on specific campaigns to detect creative decay
2580
+
2581
+ ### For Meta analysis:
2582
+ 1. Start with \`get_meta_campaigns\` to see account structure
2583
+ 2. Use reports (MCP prompts) for comprehensive analysis
2584
+ 3. For custom analysis, use \`get_meta_insights\` with breakdowns
2585
+
2586
+ ### General:
2587
+ - Always \`search_insights\` before making recommendations \u2014 ground advice in expert knowledge
2588
+ - Use \`get_insight\` to read full context when reports reference insight IDs
2589
+ - Use \`suggest_insight\` (default) when a user shares valuable knowledge from articles or discussions
2590
+ - Use \`save_private_insight\` only for client-specific data the user explicitly wants private
2591
+ - If a tool errors unexpectedly, call \`submit_feedback\` with category \`bug_report\`
2449
2592
  `;
2450
2593
  function buildStatusSection(status2) {
2451
2594
  if (!status2) return "";
@@ -2466,20 +2609,20 @@ function buildStatusSection(status2) {
2466
2609
  lines.push("- **Meta Marketing API**: Token configured");
2467
2610
  } else {
2468
2611
  lines.push(
2469
- "- **Meta Marketing API**: Token not configured \u2014 Meta tools will return errors"
2612
+ "- **Meta Marketing API**: Not connected (optional \u2014 KB works without it)"
2470
2613
  );
2471
2614
  lines.push(
2472
- " - Fix: provide your token via `--meta-token=...` CLI arg, `META_ACCESS_TOKEN` env var, or `.env` file"
2615
+ " - To connect: provide your token via `--meta-token=...` CLI arg, `META_ACCESS_TOKEN` env var, or `.env` file"
2473
2616
  );
2474
2617
  }
2475
2618
  if (status2.google.configured) {
2476
2619
  lines.push("- **Google Ads API**: Configured");
2477
2620
  } else {
2478
2621
  lines.push(
2479
- "- **Google Ads API**: Not configured \u2014 Google Ads tools will return errors"
2622
+ "- **Google Ads API**: Not connected (optional \u2014 KB works without it)"
2480
2623
  );
2481
2624
  lines.push(
2482
- " - Fix: run `npx mobile-growth-mcp auth google` to set up credentials"
2625
+ " - To connect: run `npx mobile-growth-mcp auth google` to set up credentials"
2483
2626
  );
2484
2627
  }
2485
2628
  return lines.join("\n");
@@ -2788,7 +2931,7 @@ async function runGoogleAuthFlow() {
2788
2931
  });
2789
2932
  try {
2790
2933
  console.log("Step 1: Developer token");
2791
- console.log(" (Found in Google Ads \u2192 Tools \u2192 API Center)\n");
2934
+ console.log(" (Found in Google Ads \u2192 Admin \u2192 API Center, or https://ads.google.com/aw/apicenter)\n");
2792
2935
  const developerToken = await prompt(rl, " Developer token: ");
2793
2936
  if (!developerToken) {
2794
2937
  console.error("\n Developer token is required.");
@@ -2876,10 +3019,10 @@ console.error(
2876
3019
  apiKey ? `API key: ${apiKeyResult.source}` : "API key: not configured \u2014 KB tools will not be available"
2877
3020
  );
2878
3021
  console.error(
2879
- metaTokenResult.value ? `Meta token: ${metaTokenResult.source}` : "Meta token: not configured \u2014 Meta tools will return errors when called"
3022
+ metaTokenResult.value ? `Meta token: ${metaTokenResult.source}` : "Meta token: not configured (optional \u2014 KB works without it)"
2880
3023
  );
2881
3024
  console.error(
2882
- googleAdsResult.configured ? `Google Ads: configured` : `Google Ads: not configured (missing: ${googleAdsResult.missing.join(", ")}) \u2014 run \`npx mobile-growth-mcp auth google\` to set up`
3025
+ googleAdsResult.configured ? `Google Ads: configured` : `Google Ads: not configured (optional \u2014 KB works without it). Run \`npx mobile-growth-mcp auth google\` to set up`
2883
3026
  );
2884
3027
  var server = new McpServer({
2885
3028
  name: "mobile-growth-mcp",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-growth-mcp",
3
- "version": "2.2.1",
3
+ "version": "2.2.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",