mobile-growth-mcp 2.3.4 → 2.3.5
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/index.js +100 -871
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -164,715 +164,8 @@ function registerFetchedPrompts(server2, apiKey2, prompts) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
// src/tools/meta-campaigns.ts
|
|
168
|
-
import { z as z2 } from "zod";
|
|
169
|
-
|
|
170
|
-
// src/meta/client.ts
|
|
171
|
-
var META_API_VERSION = "v21.0";
|
|
172
|
-
var META_BASE_URL = `https://graph.facebook.com/${META_API_VERSION}`;
|
|
173
|
-
var THROTTLE_WARN_THRESHOLD = 75;
|
|
174
|
-
function getMetaAccessToken() {
|
|
175
|
-
const token = process.env.META_ACCESS_TOKEN;
|
|
176
|
-
if (!token) {
|
|
177
|
-
throw new Error(
|
|
178
|
-
'Missing META_ACCESS_TOKEN. To fix, add it to your MCP config:\n \u2022 Claude Code / Cursor: add "META_ACCESS_TOKEN": "..." to the "env" block in .mcp.json\n \u2022 Claude Desktop: add "META_ACCESS_TOKEN": "..." to the "env" block in claude_desktop_config.json\n \u2022 CLI: add META_ACCESS_TOKEN=... to a .env file in your working directory\nThen restart your MCP client.'
|
|
179
|
-
);
|
|
180
|
-
}
|
|
181
|
-
return token;
|
|
182
|
-
}
|
|
183
|
-
function isMetaApiError(body) {
|
|
184
|
-
return typeof body === "object" && body !== null && "error" in body && typeof body.error?.message === "string";
|
|
185
|
-
}
|
|
186
|
-
function formatMetaError(err) {
|
|
187
|
-
const code = err.code;
|
|
188
|
-
const sub = err.error_subcode;
|
|
189
|
-
if (code === 190) {
|
|
190
|
-
return `Authentication error: Access token is invalid or expired. Generate a new token in Meta Business Suite. (code ${code})`;
|
|
191
|
-
}
|
|
192
|
-
if (code === 4 || code === 17 || code >= 8e4 && code <= 80099) {
|
|
193
|
-
return `Rate limit hit: ${err.message}. Wait a few minutes before retrying. (code ${code}${sub ? `/${sub}` : ""})`;
|
|
194
|
-
}
|
|
195
|
-
if (code === 100) {
|
|
196
|
-
return `Invalid parameter: ${err.message} (code ${code}${sub ? `/${sub}` : ""})`;
|
|
197
|
-
}
|
|
198
|
-
return `Meta API error: ${err.message} (code ${code}${sub ? `/${sub}` : ""})`;
|
|
199
|
-
}
|
|
200
|
-
function parseThrottleHeader(headers) {
|
|
201
|
-
const raw = headers.get("x-fb-ads-insights-throttle");
|
|
202
|
-
if (!raw) return void 0;
|
|
203
|
-
try {
|
|
204
|
-
const info = JSON.parse(raw);
|
|
205
|
-
const maxUtil = Math.max(info.app_id_util_pct, info.acc_id_util_pct);
|
|
206
|
-
const warning = maxUtil > THROTTLE_WARN_THRESHOLD ? `\u26A0 Meta API utilization at ${maxUtil}% \u2014 approaching rate limit. Slow down requests.` : void 0;
|
|
207
|
-
return { throttle: info, warning };
|
|
208
|
-
} catch {
|
|
209
|
-
return void 0;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async function metaApiGet(options) {
|
|
213
|
-
const token = getMetaAccessToken();
|
|
214
|
-
const url = new URL(`${META_BASE_URL}${options.path}`);
|
|
215
|
-
if (options.params) {
|
|
216
|
-
for (const [key, value] of Object.entries(options.params)) {
|
|
217
|
-
url.searchParams.set(key, value);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
const response = await fetch(url.toString(), {
|
|
221
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
222
|
-
});
|
|
223
|
-
const body = await response.json();
|
|
224
|
-
if (!response.ok || isMetaApiError(body)) {
|
|
225
|
-
if (isMetaApiError(body)) {
|
|
226
|
-
throw new Error(formatMetaError(body.error));
|
|
227
|
-
}
|
|
228
|
-
throw new Error(`Meta API returned ${response.status}: ${JSON.stringify(body)}`);
|
|
229
|
-
}
|
|
230
|
-
const throttleInfo = parseThrottleHeader(response.headers);
|
|
231
|
-
return {
|
|
232
|
-
data: body,
|
|
233
|
-
throttle: throttleInfo?.throttle,
|
|
234
|
-
warning: throttleInfo?.warning
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function activeFilter() {
|
|
238
|
-
return JSON.stringify([
|
|
239
|
-
{
|
|
240
|
-
field: "ad.effective_status",
|
|
241
|
-
operator: "IN",
|
|
242
|
-
value: ["ACTIVE"]
|
|
243
|
-
}
|
|
244
|
-
]);
|
|
245
|
-
}
|
|
246
|
-
function getActionValue(actions, actionType) {
|
|
247
|
-
if (!actions) return 0;
|
|
248
|
-
const action = actions.find((a) => a.action_type === actionType);
|
|
249
|
-
return action ? parseFloat(action.value) : 0;
|
|
250
|
-
}
|
|
251
|
-
function getCostPerAction(costPerAction, actionType) {
|
|
252
|
-
if (!costPerAction) return null;
|
|
253
|
-
const action = costPerAction.find((a) => a.action_type === actionType);
|
|
254
|
-
return action ? parseFloat(action.value) : null;
|
|
255
|
-
}
|
|
256
|
-
var DEFAULT_DATE_PRESET = "last_7d";
|
|
257
|
-
var CAMPAIGN_DEFAULT_FIELDS = "id,name,status,effective_status,objective,bid_strategy,daily_budget,lifetime_budget,buying_type,special_ad_categories";
|
|
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
|
-
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
|
-
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";
|
|
263
|
-
|
|
264
|
-
// src/tools/meta-campaigns.ts
|
|
265
|
-
function registerGetMetaCampaigns(server2) {
|
|
266
|
-
server2.tool(
|
|
267
|
-
"get_meta_campaigns",
|
|
268
|
-
"List campaigns from a Meta ad account. Defaults to active campaigns with lean field set. Returns first page only \u2014 use 'after' cursor for next page.",
|
|
269
|
-
{
|
|
270
|
-
ad_account_id: z2.string().describe("Meta ad account ID (e.g. act_123456789)"),
|
|
271
|
-
fields: z2.string().optional().describe(
|
|
272
|
-
`Comma-separated fields. Default: ${CAMPAIGN_DEFAULT_FIELDS}`
|
|
273
|
-
),
|
|
274
|
-
effective_status: z2.array(z2.string()).optional().describe(
|
|
275
|
-
'Filter by status. Default: ["ACTIVE"]. Use ["ACTIVE","PAUSED"] for all non-deleted.'
|
|
276
|
-
),
|
|
277
|
-
limit: z2.number().min(1).max(100).optional().describe("Results per page (default 50, max 100)"),
|
|
278
|
-
after: z2.string().optional().describe("Pagination cursor from previous response")
|
|
279
|
-
},
|
|
280
|
-
async ({ ad_account_id, fields, effective_status, limit, after }) => {
|
|
281
|
-
try {
|
|
282
|
-
const params = {
|
|
283
|
-
fields: fields ?? CAMPAIGN_DEFAULT_FIELDS,
|
|
284
|
-
limit: String(limit ?? 50)
|
|
285
|
-
};
|
|
286
|
-
if (effective_status) {
|
|
287
|
-
params.filtering = JSON.stringify([
|
|
288
|
-
{
|
|
289
|
-
field: "effective_status",
|
|
290
|
-
operator: "IN",
|
|
291
|
-
value: effective_status
|
|
292
|
-
}
|
|
293
|
-
]);
|
|
294
|
-
} else {
|
|
295
|
-
params.filtering = activeFilter();
|
|
296
|
-
}
|
|
297
|
-
if (after) {
|
|
298
|
-
params.after = after;
|
|
299
|
-
}
|
|
300
|
-
const result = await metaApiGet({
|
|
301
|
-
path: `/${ad_account_id}/campaigns`,
|
|
302
|
-
params
|
|
303
|
-
});
|
|
304
|
-
const campaigns = result.data.data;
|
|
305
|
-
const nextCursor = result.data.paging?.cursors?.after;
|
|
306
|
-
let text = `Found ${campaigns.length} campaigns:
|
|
307
|
-
|
|
308
|
-
`;
|
|
309
|
-
for (const c of campaigns) {
|
|
310
|
-
text += `- **${c.name}** (${c.id})
|
|
311
|
-
Status: ${c.effective_status} | Objective: ${c.objective}` + (c.bid_strategy ? ` | Bid: ${c.bid_strategy}` : "") + (c.daily_budget ? ` | Daily: $${(parseInt(c.daily_budget) / 100).toFixed(2)}` : "") + (c.lifetime_budget ? ` | Lifetime: $${(parseInt(c.lifetime_budget) / 100).toFixed(2)}` : "") + "\n";
|
|
312
|
-
}
|
|
313
|
-
if (nextCursor) {
|
|
314
|
-
text += `
|
|
315
|
-
_More results available. Pass \`after: "${nextCursor}"\` for next page._`;
|
|
316
|
-
}
|
|
317
|
-
if (result.warning) {
|
|
318
|
-
text = `${result.warning}
|
|
319
|
-
|
|
320
|
-
${text}`;
|
|
321
|
-
}
|
|
322
|
-
return { content: [{ type: "text", text }] };
|
|
323
|
-
} catch (err) {
|
|
324
|
-
return {
|
|
325
|
-
content: [
|
|
326
|
-
{
|
|
327
|
-
type: "text",
|
|
328
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
329
|
-
}
|
|
330
|
-
],
|
|
331
|
-
isError: true
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// src/tools/meta-adsets.ts
|
|
339
|
-
import { z as z3 } from "zod";
|
|
340
|
-
function registerGetMetaAdSets(server2) {
|
|
341
|
-
server2.tool(
|
|
342
|
-
"get_meta_adsets",
|
|
343
|
-
"List ad sets from a Meta ad account, optionally scoped to a campaign. Defaults to active ad sets with lean field set.",
|
|
344
|
-
{
|
|
345
|
-
ad_account_id: z3.string().describe("Meta ad account ID (e.g. act_123456789)"),
|
|
346
|
-
campaign_id: z3.string().optional().describe(
|
|
347
|
-
"Scope to a specific campaign ID. If provided, fetches ad sets under that campaign."
|
|
348
|
-
),
|
|
349
|
-
fields: z3.string().optional().describe(`Comma-separated fields. Default: ${ADSET_DEFAULT_FIELDS}`),
|
|
350
|
-
effective_status: z3.array(z3.string()).optional().describe('Filter by status. Default: ["ACTIVE"]'),
|
|
351
|
-
limit: z3.number().min(1).max(100).optional().describe("Results per page (default 50, max 100)"),
|
|
352
|
-
after: z3.string().optional().describe("Pagination cursor from previous response")
|
|
353
|
-
},
|
|
354
|
-
async ({
|
|
355
|
-
ad_account_id,
|
|
356
|
-
campaign_id,
|
|
357
|
-
fields,
|
|
358
|
-
effective_status,
|
|
359
|
-
limit,
|
|
360
|
-
after
|
|
361
|
-
}) => {
|
|
362
|
-
try {
|
|
363
|
-
const params = {
|
|
364
|
-
fields: fields ?? ADSET_DEFAULT_FIELDS,
|
|
365
|
-
limit: String(limit ?? 50)
|
|
366
|
-
};
|
|
367
|
-
if (effective_status) {
|
|
368
|
-
params.filtering = JSON.stringify([
|
|
369
|
-
{
|
|
370
|
-
field: "effective_status",
|
|
371
|
-
operator: "IN",
|
|
372
|
-
value: effective_status
|
|
373
|
-
}
|
|
374
|
-
]);
|
|
375
|
-
} else {
|
|
376
|
-
params.filtering = activeFilter();
|
|
377
|
-
}
|
|
378
|
-
if (after) {
|
|
379
|
-
params.after = after;
|
|
380
|
-
}
|
|
381
|
-
const parentPath = campaign_id ? `/${campaign_id}/adsets` : `/${ad_account_id}/adsets`;
|
|
382
|
-
const result = await metaApiGet({
|
|
383
|
-
path: parentPath,
|
|
384
|
-
params
|
|
385
|
-
});
|
|
386
|
-
const adsets = result.data.data;
|
|
387
|
-
const nextCursor = result.data.paging?.cursors?.after;
|
|
388
|
-
let text = `Found ${adsets.length} ad sets:
|
|
389
|
-
|
|
390
|
-
`;
|
|
391
|
-
for (const a of adsets) {
|
|
392
|
-
text += `- **${a.name}** (${a.id})
|
|
393
|
-
Campaign: ${a.campaign_id} | Status: ${a.effective_status}` + (a.optimization_goal ? ` | Goal: ${a.optimization_goal}` : "") + (a.bid_strategy ? ` | Bid: ${a.bid_strategy}` : "") + (a.daily_budget ? ` | Daily: $${(parseInt(a.daily_budget) / 100).toFixed(2)}` : "") + "\n";
|
|
394
|
-
}
|
|
395
|
-
if (nextCursor) {
|
|
396
|
-
text += `
|
|
397
|
-
_More results available. Pass \`after: "${nextCursor}"\` for next page._`;
|
|
398
|
-
}
|
|
399
|
-
if (result.warning) {
|
|
400
|
-
text = `${result.warning}
|
|
401
|
-
|
|
402
|
-
${text}`;
|
|
403
|
-
}
|
|
404
|
-
return { content: [{ type: "text", text }] };
|
|
405
|
-
} catch (err) {
|
|
406
|
-
return {
|
|
407
|
-
content: [
|
|
408
|
-
{
|
|
409
|
-
type: "text",
|
|
410
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
411
|
-
}
|
|
412
|
-
],
|
|
413
|
-
isError: true
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// src/tools/meta-ads.ts
|
|
421
|
-
import { z as z4 } from "zod";
|
|
422
|
-
function registerGetMetaAds(server2) {
|
|
423
|
-
server2.tool(
|
|
424
|
-
"get_meta_ads",
|
|
425
|
-
"List ads from a Meta ad account, optionally scoped to a campaign or ad set. Defaults to active ads with lean field set.",
|
|
426
|
-
{
|
|
427
|
-
ad_account_id: z4.string().describe("Meta ad account ID (e.g. act_123456789)"),
|
|
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."),
|
|
430
|
-
fields: z4.string().optional().describe(`Comma-separated fields. Default: ${AD_DEFAULT_FIELDS}`),
|
|
431
|
-
effective_status: z4.array(z4.string()).optional().describe('Filter by status. Default: ["ACTIVE"]'),
|
|
432
|
-
limit: z4.number().min(1).max(100).optional().describe("Results per page (default 50, max 100)"),
|
|
433
|
-
after: z4.string().optional().describe("Pagination cursor from previous response")
|
|
434
|
-
},
|
|
435
|
-
async ({ ad_account_id, campaign_id, adset_id, fields, effective_status, limit, after }) => {
|
|
436
|
-
try {
|
|
437
|
-
const params = {
|
|
438
|
-
fields: fields ?? AD_DEFAULT_FIELDS,
|
|
439
|
-
limit: String(limit ?? 50)
|
|
440
|
-
};
|
|
441
|
-
if (effective_status) {
|
|
442
|
-
params.filtering = JSON.stringify([
|
|
443
|
-
{
|
|
444
|
-
field: "effective_status",
|
|
445
|
-
operator: "IN",
|
|
446
|
-
value: effective_status
|
|
447
|
-
}
|
|
448
|
-
]);
|
|
449
|
-
} else {
|
|
450
|
-
params.filtering = activeFilter();
|
|
451
|
-
}
|
|
452
|
-
if (after) {
|
|
453
|
-
params.after = after;
|
|
454
|
-
}
|
|
455
|
-
const parentPath = adset_id ? `/${adset_id}/ads` : campaign_id ? `/${campaign_id}/ads` : `/${ad_account_id}/ads`;
|
|
456
|
-
const result = await metaApiGet({
|
|
457
|
-
path: parentPath,
|
|
458
|
-
params
|
|
459
|
-
});
|
|
460
|
-
const ads = result.data.data;
|
|
461
|
-
const nextCursor = result.data.paging?.cursors?.after;
|
|
462
|
-
let text = `Found ${ads.length} ads:
|
|
463
|
-
|
|
464
|
-
`;
|
|
465
|
-
for (const ad of ads) {
|
|
466
|
-
text += `- **${ad.name}** (${ad.id})
|
|
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";
|
|
468
|
-
}
|
|
469
|
-
if (nextCursor) {
|
|
470
|
-
text += `
|
|
471
|
-
_More results available. Pass \`after: "${nextCursor}"\` for next page._`;
|
|
472
|
-
}
|
|
473
|
-
if (result.warning) {
|
|
474
|
-
text = `${result.warning}
|
|
475
|
-
|
|
476
|
-
${text}`;
|
|
477
|
-
}
|
|
478
|
-
return { content: [{ type: "text", text }] };
|
|
479
|
-
} catch (err) {
|
|
480
|
-
return {
|
|
481
|
-
content: [
|
|
482
|
-
{
|
|
483
|
-
type: "text",
|
|
484
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
485
|
-
}
|
|
486
|
-
],
|
|
487
|
-
isError: true
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// src/tools/meta-insights.ts
|
|
495
|
-
import { z as z5 } from "zod";
|
|
496
|
-
function registerGetMetaInsights(server2) {
|
|
497
|
-
server2.tool(
|
|
498
|
-
"get_meta_insights",
|
|
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.",
|
|
500
|
-
{
|
|
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)"),
|
|
504
|
-
level: z5.enum(["account", "campaign", "adset", "ad"]).optional().describe("Aggregation level (default: campaign)"),
|
|
505
|
-
fields: z5.string().optional().describe(
|
|
506
|
-
`Comma-separated fields. Default includes level-appropriate name fields + ${INSIGHT_DEFAULT_FIELDS}`
|
|
507
|
-
),
|
|
508
|
-
date_preset: z5.string().optional().describe(`Date preset (default: ${DEFAULT_DATE_PRESET})`),
|
|
509
|
-
time_range: z5.object({
|
|
510
|
-
since: z5.string().describe("Start date YYYY-MM-DD"),
|
|
511
|
-
until: z5.string().describe("End date YYYY-MM-DD")
|
|
512
|
-
}).optional().describe("Custom date range. Overrides date_preset if provided."),
|
|
513
|
-
time_increment: z5.string().optional().describe(
|
|
514
|
-
'Time granularity: "1" for daily, "7" for weekly, "monthly", or "all_days" (default: aggregated)'
|
|
515
|
-
),
|
|
516
|
-
breakdowns: z5.string().optional().describe(
|
|
517
|
-
"Comma-separated breakdowns (e.g. age,gender or publisher_platform,platform_position)"
|
|
518
|
-
),
|
|
519
|
-
filtering: z5.string().optional().describe(
|
|
520
|
-
'JSON filtering array. Default: active only. Pass "[]" to include all statuses.'
|
|
521
|
-
),
|
|
522
|
-
conversion_event: z5.string().optional().describe(
|
|
523
|
-
"Action type for CPA calculation (default: mobile_app_install)"
|
|
524
|
-
),
|
|
525
|
-
sort: z5.string().optional().describe(
|
|
526
|
-
'Sort field (e.g. "spend_descending", "impressions_descending")'
|
|
527
|
-
),
|
|
528
|
-
limit: z5.number().min(1).max(500).optional().describe("Results per page (default 50, max 500)"),
|
|
529
|
-
after: z5.string().optional().describe("Pagination cursor from previous response")
|
|
530
|
-
},
|
|
531
|
-
async ({
|
|
532
|
-
ad_account_id,
|
|
533
|
-
campaign_id,
|
|
534
|
-
adset_id,
|
|
535
|
-
level,
|
|
536
|
-
fields,
|
|
537
|
-
date_preset,
|
|
538
|
-
time_range,
|
|
539
|
-
time_increment,
|
|
540
|
-
breakdowns,
|
|
541
|
-
filtering,
|
|
542
|
-
conversion_event,
|
|
543
|
-
sort,
|
|
544
|
-
limit,
|
|
545
|
-
after
|
|
546
|
-
}) => {
|
|
547
|
-
try {
|
|
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
|
-
}
|
|
560
|
-
const params = {
|
|
561
|
-
fields: effectiveFields,
|
|
562
|
-
level: effectiveLevel,
|
|
563
|
-
limit: String(limit ?? 50)
|
|
564
|
-
};
|
|
565
|
-
if (time_range) {
|
|
566
|
-
params.time_range = JSON.stringify(time_range);
|
|
567
|
-
} else {
|
|
568
|
-
params.date_preset = date_preset ?? DEFAULT_DATE_PRESET;
|
|
569
|
-
}
|
|
570
|
-
if (time_increment) {
|
|
571
|
-
params.time_increment = time_increment;
|
|
572
|
-
}
|
|
573
|
-
if (breakdowns) {
|
|
574
|
-
params.breakdowns = breakdowns;
|
|
575
|
-
}
|
|
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
|
-
});
|
|
590
|
-
}
|
|
591
|
-
params.filtering = JSON.stringify(baseFilters);
|
|
592
|
-
if (sort) {
|
|
593
|
-
params.sort = sort;
|
|
594
|
-
}
|
|
595
|
-
if (after) {
|
|
596
|
-
params.after = after;
|
|
597
|
-
}
|
|
598
|
-
const result = await metaApiGet({
|
|
599
|
-
path: `/${ad_account_id}/insights`,
|
|
600
|
-
params
|
|
601
|
-
});
|
|
602
|
-
const rows = result.data.data;
|
|
603
|
-
const nextCursor = result.data.paging?.cursors?.after;
|
|
604
|
-
if (!rows || rows.length === 0) {
|
|
605
|
-
return {
|
|
606
|
-
content: [
|
|
607
|
-
{
|
|
608
|
-
type: "text",
|
|
609
|
-
text: "No insight data returned for the given parameters. Try a broader date range or check that active campaigns exist."
|
|
610
|
-
}
|
|
611
|
-
]
|
|
612
|
-
};
|
|
613
|
-
}
|
|
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";
|
|
618
|
-
for (const row of rows) {
|
|
619
|
-
const spend = parseFloat(row.spend || "0");
|
|
620
|
-
const impressions = parseInt(row.impressions || "0");
|
|
621
|
-
const conversions = getActionValue(row.actions, convEvent);
|
|
622
|
-
const cpa = getCostPerAction(row.cost_per_action_type, convEvent);
|
|
623
|
-
const label = row.ad_name ?? row.adset_name ?? row.campaign_name ?? row.campaign_id ?? "Account";
|
|
624
|
-
const breakdownParts = [];
|
|
625
|
-
if (row.age) breakdownParts.push(`Age: ${row.age}`);
|
|
626
|
-
if (row.gender) breakdownParts.push(`Gender: ${row.gender}`);
|
|
627
|
-
if (row.publisher_platform)
|
|
628
|
-
breakdownParts.push(`Platform: ${row.publisher_platform}`);
|
|
629
|
-
if (row.platform_position)
|
|
630
|
-
breakdownParts.push(`Position: ${row.platform_position}`);
|
|
631
|
-
if (row.country) breakdownParts.push(`Country: ${row.country}`);
|
|
632
|
-
const breakdownStr = breakdownParts.length > 0 ? ` (${breakdownParts.join(", ")})` : "";
|
|
633
|
-
const dateStr = time_increment && row.date_start !== row.date_stop ? ` [${row.date_start} \u2192 ${row.date_stop}]` : time_increment ? ` [${row.date_start}]` : "";
|
|
634
|
-
text += `**${label}**${breakdownStr}${dateStr}
|
|
635
|
-
Spend: $${spend.toFixed(2)} | Impressions: ${impressions.toLocaleString()}` + (row.clicks ? ` | Clicks: ${row.clicks}` : "") + (row.ctr ? ` | CTR: ${row.ctr}%` : "") + (row.cpm ? ` | CPM: $${row.cpm}` : "") + (row.frequency ? ` | Freq: ${row.frequency}` : "") + (conversions > 0 ? ` | ${convEvent}: ${conversions}` : "") + (cpa !== null ? ` | CPA: $${cpa.toFixed(2)}` : "") + "\n\n";
|
|
636
|
-
}
|
|
637
|
-
if (nextCursor) {
|
|
638
|
-
text += `_More results available. Pass \`after: "${nextCursor}"\` for next page._
|
|
639
|
-
`;
|
|
640
|
-
}
|
|
641
|
-
if (result.warning) {
|
|
642
|
-
text = `${result.warning}
|
|
643
|
-
|
|
644
|
-
${text}`;
|
|
645
|
-
}
|
|
646
|
-
return { content: [{ type: "text", text }] };
|
|
647
|
-
} catch (err) {
|
|
648
|
-
return {
|
|
649
|
-
content: [
|
|
650
|
-
{
|
|
651
|
-
type: "text",
|
|
652
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
653
|
-
}
|
|
654
|
-
],
|
|
655
|
-
isError: true
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// src/tools/meta-ad-fatigue.ts
|
|
663
|
-
import { z as z6 } from "zod";
|
|
664
|
-
function registerGetMetaAdFatigue(server2) {
|
|
665
|
-
server2.tool(
|
|
666
|
-
"get_meta_ad_fatigue",
|
|
667
|
-
"Detect creative fatigue in active ads. Analyzes frequency, CTR decline, and CPA trends over the last 7 days at daily granularity. Grounded in knowledge base insights: wk-tw-001 (diagnostic patterns), ds-pt-003 (frequency thresholds), vs-nt-002 (degraded creative rules).",
|
|
668
|
-
{
|
|
669
|
-
ad_account_id: z6.string().describe("Meta ad account ID (e.g. act_123456789)"),
|
|
670
|
-
campaign_id: z6.string().optional().describe("Scope to a specific campaign"),
|
|
671
|
-
conversion_event: z6.string().optional().describe("Action type for CPA (default: mobile_app_install)"),
|
|
672
|
-
frequency_warning: z6.number().optional().describe("Frequency threshold for warning (default: 3)"),
|
|
673
|
-
frequency_critical: z6.number().optional().describe("Frequency threshold for critical (default: 5)"),
|
|
674
|
-
ctr_decline_threshold: z6.number().optional().describe(
|
|
675
|
-
"CTR decline % from peak to flag fatigue (default: 30)"
|
|
676
|
-
)
|
|
677
|
-
},
|
|
678
|
-
async ({
|
|
679
|
-
ad_account_id,
|
|
680
|
-
campaign_id,
|
|
681
|
-
conversion_event,
|
|
682
|
-
frequency_warning,
|
|
683
|
-
frequency_critical,
|
|
684
|
-
ctr_decline_threshold
|
|
685
|
-
}) => {
|
|
686
|
-
try {
|
|
687
|
-
const convEvent = conversion_event ?? "mobile_app_install";
|
|
688
|
-
const freqWarn = frequency_warning ?? 3;
|
|
689
|
-
const freqCrit = frequency_critical ?? 5;
|
|
690
|
-
const ctrThreshold = ctr_decline_threshold ?? 30;
|
|
691
|
-
const filtering = JSON.parse(activeFilter());
|
|
692
|
-
if (campaign_id) {
|
|
693
|
-
filtering.push({
|
|
694
|
-
field: "campaign.id",
|
|
695
|
-
operator: "EQUAL",
|
|
696
|
-
value: campaign_id
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
const result = await metaApiGet({
|
|
700
|
-
path: `/${ad_account_id}/insights`,
|
|
701
|
-
params: {
|
|
702
|
-
level: "ad",
|
|
703
|
-
time_increment: "1",
|
|
704
|
-
fields: "ad_id,ad_name,spend,impressions,clicks,ctr,cpm,frequency,actions,cost_per_action_type",
|
|
705
|
-
date_preset: "last_7d",
|
|
706
|
-
filtering: JSON.stringify(filtering),
|
|
707
|
-
limit: "500"
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
const rows = result.data.data;
|
|
711
|
-
if (!rows || rows.length === 0) {
|
|
712
|
-
return {
|
|
713
|
-
content: [
|
|
714
|
-
{
|
|
715
|
-
type: "text",
|
|
716
|
-
text: "No active ad data found for the last 7 days."
|
|
717
|
-
}
|
|
718
|
-
]
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
const adMap = /* @__PURE__ */ new Map();
|
|
722
|
-
for (const row of rows) {
|
|
723
|
-
const adId = row.ad_id;
|
|
724
|
-
if (!adMap.has(adId)) {
|
|
725
|
-
adMap.set(adId, {
|
|
726
|
-
ad_id: adId,
|
|
727
|
-
ad_name: row.ad_name ?? adId,
|
|
728
|
-
days: []
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
adMap.get(adId).days.push({
|
|
732
|
-
date: row.date_start,
|
|
733
|
-
spend: parseFloat(row.spend || "0"),
|
|
734
|
-
impressions: parseInt(row.impressions || "0"),
|
|
735
|
-
clicks: parseInt(row.clicks || "0"),
|
|
736
|
-
ctr: parseFloat(row.ctr || "0"),
|
|
737
|
-
cpm: parseFloat(row.cpm || "0"),
|
|
738
|
-
frequency: parseFloat(row.frequency || "0"),
|
|
739
|
-
conversions: getActionValue(row.actions, convEvent),
|
|
740
|
-
cpa: getCostPerAction(row.cost_per_action_type, convEvent)
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
const results = [];
|
|
744
|
-
let totalSpend = 0;
|
|
745
|
-
for (const ad of adMap.values()) {
|
|
746
|
-
ad.days.sort(
|
|
747
|
-
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
|
|
748
|
-
);
|
|
749
|
-
const adSpend = ad.days.reduce((s, d) => s + d.spend, 0);
|
|
750
|
-
totalSpend += adSpend;
|
|
751
|
-
const avgFreq = ad.days.reduce((s, d) => s + d.frequency, 0) / ad.days.length;
|
|
752
|
-
const peakCtr = Math.max(...ad.days.map((d) => d.ctr));
|
|
753
|
-
const last3 = ad.days.slice(-3);
|
|
754
|
-
const recentCtr = last3.reduce((s, d) => s + d.ctr, 0) / last3.length;
|
|
755
|
-
const ctrDecline = peakCtr > 0 ? (peakCtr - recentCtr) / peakCtr * 100 : 0;
|
|
756
|
-
const mid = Math.floor(ad.days.length / 2);
|
|
757
|
-
const earlyDays = ad.days.slice(0, Math.max(mid, 1));
|
|
758
|
-
const lateDays = ad.days.slice(mid);
|
|
759
|
-
const earlyConv = earlyDays.reduce((s, d) => s + d.conversions, 0);
|
|
760
|
-
const earlySpend = earlyDays.reduce((s, d) => s + d.spend, 0);
|
|
761
|
-
const earlyCpa = earlyConv > 0 ? earlySpend / earlyConv : null;
|
|
762
|
-
const lateConv = lateDays.reduce((s, d) => s + d.conversions, 0);
|
|
763
|
-
const lateSpend = lateDays.reduce((s, d) => s + d.spend, 0);
|
|
764
|
-
const recentCpa = lateConv > 0 ? lateSpend / lateConv : null;
|
|
765
|
-
const cpaChange = earlyCpa !== null && recentCpa !== null && earlyCpa > 0 ? (recentCpa - earlyCpa) / earlyCpa * 100 : null;
|
|
766
|
-
let status2 = "HEALTHY";
|
|
767
|
-
let diagnosis = "Metrics stable \u2014 no fatigue detected.";
|
|
768
|
-
const highFreq = avgFreq >= freqCrit;
|
|
769
|
-
const medFreq = avgFreq >= freqWarn;
|
|
770
|
-
const ctrDeclining = ctrDecline >= ctrThreshold;
|
|
771
|
-
const cpaRising = cpaChange !== null && cpaChange > 20;
|
|
772
|
-
if (highFreq && cpaRising) {
|
|
773
|
-
status2 = "FATIGUED";
|
|
774
|
-
diagnosis = `Audience saturation: frequency ${avgFreq.toFixed(1)} + CPA rising ${cpaChange.toFixed(0)}% [wk-tw-001 #1, ds-pt-003]`;
|
|
775
|
-
} else if (ctrDeclining && cpaRising) {
|
|
776
|
-
status2 = "FATIGUED";
|
|
777
|
-
diagnosis = `Creative fatigue: CTR declined ${ctrDecline.toFixed(0)}% from peak + CPA rising ${cpaChange.toFixed(0)}% [wk-tw-001 #4]`;
|
|
778
|
-
} else if (highFreq) {
|
|
779
|
-
status2 = "WARNING";
|
|
780
|
-
diagnosis = `High frequency (${avgFreq.toFixed(1)}) \u2014 approaching saturation [ds-pt-003]. CPA ${cpaRising ? "rising" : "stable"}.`;
|
|
781
|
-
} else if (ctrDeclining) {
|
|
782
|
-
status2 = "WARNING";
|
|
783
|
-
diagnosis = `CTR declining ${ctrDecline.toFixed(0)}% from peak \u2014 early fatigue signal [wk-tw-001 #4].`;
|
|
784
|
-
} else if (medFreq && cpaRising) {
|
|
785
|
-
status2 = "WARNING";
|
|
786
|
-
diagnosis = `Frequency ${avgFreq.toFixed(1)} + CPA trending up ${cpaChange.toFixed(0)}% \u2014 monitor closely [wk-tw-001 #1].`;
|
|
787
|
-
}
|
|
788
|
-
results.push({
|
|
789
|
-
ad_id: ad.ad_id,
|
|
790
|
-
ad_name: ad.ad_name,
|
|
791
|
-
total_spend: adSpend,
|
|
792
|
-
avg_frequency: avgFreq,
|
|
793
|
-
peak_ctr: peakCtr,
|
|
794
|
-
recent_ctr: recentCtr,
|
|
795
|
-
ctr_decline_pct: ctrDecline,
|
|
796
|
-
early_cpa: earlyCpa,
|
|
797
|
-
recent_cpa: recentCpa,
|
|
798
|
-
cpa_change_pct: cpaChange,
|
|
799
|
-
status: status2,
|
|
800
|
-
diagnosis
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
const statusOrder = { FATIGUED: 0, WARNING: 1, HEALTHY: 2 };
|
|
804
|
-
results.sort(
|
|
805
|
-
(a, b) => statusOrder[a.status] - statusOrder[b.status] || b.total_spend - a.total_spend
|
|
806
|
-
);
|
|
807
|
-
const fatigued = results.filter((r) => r.status === "FATIGUED");
|
|
808
|
-
const warning = results.filter((r) => r.status === "WARNING");
|
|
809
|
-
const healthy = results.filter((r) => r.status === "HEALTHY");
|
|
810
|
-
const fatiguedSpend = fatigued.reduce((s, r) => s + r.total_spend, 0);
|
|
811
|
-
const spendConcentration = totalSpend > 0 ? fatiguedSpend / totalSpend * 100 : 0;
|
|
812
|
-
let text = `# Ad Fatigue Report
|
|
813
|
-
|
|
814
|
-
`;
|
|
815
|
-
text += `**${results.length} ads analyzed** | ${fatigued.length} fatigued | ${warning.length} warning | ${healthy.length} healthy
|
|
816
|
-
`;
|
|
817
|
-
text += `**Total spend:** $${totalSpend.toFixed(2)} | **Fatigued ad spend:** $${fatiguedSpend.toFixed(2)} (${spendConcentration.toFixed(0)}%)
|
|
818
|
-
|
|
819
|
-
`;
|
|
820
|
-
if (spendConcentration > 50) {
|
|
821
|
-
text += `\u26A0 **Over 50% of spend is going to fatigued ads.** Urgent creative rotation needed.
|
|
822
|
-
|
|
823
|
-
`;
|
|
824
|
-
}
|
|
825
|
-
text += `| Status | Ad | Spend | Freq | CTR (peak\u2192recent) | CPA Trend | Diagnosis |
|
|
826
|
-
`;
|
|
827
|
-
text += `|--------|-----|-------|------|-------------------|-----------|------------|
|
|
828
|
-
`;
|
|
829
|
-
for (const r of results) {
|
|
830
|
-
const icon = r.status === "FATIGUED" ? "\u{1F534}" : r.status === "WARNING" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
831
|
-
text += `| ${icon} ${r.status} | ${r.ad_name.slice(0, 30)} | $${r.total_spend.toFixed(0)} | ${r.avg_frequency.toFixed(1)} | ${r.peak_ctr.toFixed(2)}%\u2192${r.recent_ctr.toFixed(2)}% (${r.ctr_decline_pct > 0 ? "-" : ""}${r.ctr_decline_pct.toFixed(0)}%) | ${r.cpa_change_pct !== null ? `${r.cpa_change_pct > 0 ? "+" : ""}${r.cpa_change_pct.toFixed(0)}%` : "N/A"} | ${r.diagnosis} |
|
|
832
|
-
`;
|
|
833
|
-
}
|
|
834
|
-
text += `
|
|
835
|
-
## Recommendations
|
|
836
|
-
|
|
837
|
-
`;
|
|
838
|
-
if (fatigued.length > 0) {
|
|
839
|
-
text += `### Fatigued Ads (${fatigued.length})
|
|
840
|
-
- **Rotate with genuinely new creative concepts**, not variations. Meta's Andromeda requires at least 25% visual difference to treat it as a new ad [lp-pt-001, oh-li-001].
|
|
841
|
-
- **Change the hook** (first 3 seconds) to access new audience segments [oh-li-005].
|
|
842
|
-
- **Do NOT re-test degraded creatives** \u2014 exhaust all other angles first. Wait 3-6 months before revisiting [vs-nt-002].
|
|
843
|
-
- Meta will auto-shift spend away from degrading creatives [vs-nt-001] \u2014 don't panic-pause, but do prepare replacements.
|
|
844
|
-
|
|
845
|
-
`;
|
|
846
|
-
}
|
|
847
|
-
if (healthy.length > 0) {
|
|
848
|
-
text += `### Healthy Ads (${healthy.length})
|
|
849
|
-
- Let Meta manage allocation. Don't force spend to low-spend winners \u2014 Meta has determined scaling them would degrade performance [ds-pt-004].
|
|
850
|
-
|
|
851
|
-
`;
|
|
852
|
-
}
|
|
853
|
-
if (result.warning) {
|
|
854
|
-
text = `${result.warning}
|
|
855
|
-
|
|
856
|
-
${text}`;
|
|
857
|
-
}
|
|
858
|
-
return { content: [{ type: "text", text }] };
|
|
859
|
-
} catch (err) {
|
|
860
|
-
return {
|
|
861
|
-
content: [
|
|
862
|
-
{
|
|
863
|
-
type: "text",
|
|
864
|
-
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
865
|
-
}
|
|
866
|
-
],
|
|
867
|
-
isError: true
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
|
|
874
167
|
// src/tools/google-campaigns.ts
|
|
875
|
-
import { z as
|
|
168
|
+
import { z as z2 } from "zod";
|
|
876
169
|
|
|
877
170
|
// src/google/oauth.ts
|
|
878
171
|
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -1126,10 +419,10 @@ function registerGetGoogleAdsCampaigns(server2) {
|
|
|
1126
419
|
"get_google_ads_campaigns",
|
|
1127
420
|
"List Google App Campaigns with status, bid strategy, budgets, and app info. Campaign naming conventions encode dimensions (app, event, country, OS) \u2014 surface the raw name for parsing. Requires Google Ads credentials \u2014 run `npx mobile-growth-mcp auth google` to set up.",
|
|
1128
421
|
{
|
|
1129
|
-
customer_id:
|
|
1130
|
-
status:
|
|
1131
|
-
channel_sub_type:
|
|
1132
|
-
limit:
|
|
422
|
+
customer_id: z2.string().describe("Google Ads customer ID (e.g. 123-456-7890 or 1234567890)"),
|
|
423
|
+
status: z2.array(z2.enum(["ENABLED", "PAUSED", "REMOVED"])).optional().describe('Filter by campaign status. Default: ["ENABLED"]'),
|
|
424
|
+
channel_sub_type: z2.array(z2.enum(["APP_CAMPAIGN", "APP_CAMPAIGN_FOR_ENGAGEMENT"])).optional().describe('Filter by sub-type. Default: ["APP_CAMPAIGN", "APP_CAMPAIGN_FOR_ENGAGEMENT"]. Set to include all app campaign types'),
|
|
425
|
+
limit: z2.number().min(1).max(100).optional().describe("Max campaigns to return (default 50)")
|
|
1133
426
|
},
|
|
1134
427
|
async ({ customer_id, status: status2, channel_sub_type, limit }) => {
|
|
1135
428
|
try {
|
|
@@ -1210,16 +503,16 @@ function registerGetGoogleAdsCampaigns(server2) {
|
|
|
1210
503
|
}
|
|
1211
504
|
|
|
1212
505
|
// src/tools/google-ad-groups.ts
|
|
1213
|
-
import { z as
|
|
506
|
+
import { z as z3 } from "zod";
|
|
1214
507
|
function registerGetGoogleAdsAdGroups(server2) {
|
|
1215
508
|
server2.tool(
|
|
1216
509
|
"get_google_ad_groups",
|
|
1217
510
|
"List ad groups within Google App Campaigns. In UAC, ad groups represent creative themes (e.g. 'at_home_workouts'). The algorithm allocates spend across ad groups based on which themes resonate \u2014 observe spend distribution to identify winning messaging angles. To scale, add new ad groups with different text-asset strategies rather than aggressively increasing bids (ab-pt-006).",
|
|
1218
511
|
{
|
|
1219
|
-
customer_id:
|
|
1220
|
-
campaign_id:
|
|
1221
|
-
status:
|
|
1222
|
-
limit:
|
|
512
|
+
customer_id: z3.string().describe("Google Ads customer ID (e.g. 123-456-7890 or 1234567890)"),
|
|
513
|
+
campaign_id: z3.string().optional().describe("Scope to a specific campaign. If omitted, returns ad groups across all app campaigns"),
|
|
514
|
+
status: z3.array(z3.enum(["ENABLED", "PAUSED", "REMOVED"])).optional().describe('Filter by ad group status. Default: ["ENABLED"]'),
|
|
515
|
+
limit: z3.number().min(1).max(100).optional().describe("Max results to return (default 50)")
|
|
1223
516
|
},
|
|
1224
517
|
async ({ customer_id, campaign_id, status: status2, limit }) => {
|
|
1225
518
|
try {
|
|
@@ -1290,7 +583,7 @@ function registerGetGoogleAdsAdGroups(server2) {
|
|
|
1290
583
|
}
|
|
1291
584
|
|
|
1292
585
|
// src/tools/google-assets.ts
|
|
1293
|
-
import { z as
|
|
586
|
+
import { z as z4 } from "zod";
|
|
1294
587
|
var SLOT_LIMITS = {
|
|
1295
588
|
HEADLINE: 5,
|
|
1296
589
|
DESCRIPTION: 5,
|
|
@@ -1309,12 +602,12 @@ function registerGetGoogleAdsAssets(server2) {
|
|
|
1309
602
|
"get_google_assets",
|
|
1310
603
|
"List creative assets linked to a campaign or ad group with metadata, performance labels, and slot utilization audit. Google allows up to 5 headlines, 5 descriptions, 20 images, 20 videos, 20 HTML5 per ad group. Missing asset types mean missing inventory channels. Google's built-in asset ratings (Low/Good/Best) measure scalability, not conversion value \u2014 evaluate by your own CPA/ROAS (ab-pt-008).",
|
|
1311
604
|
{
|
|
1312
|
-
customer_id:
|
|
1313
|
-
campaign_id:
|
|
1314
|
-
ad_group_id:
|
|
1315
|
-
asset_type:
|
|
1316
|
-
include_slot_audit:
|
|
1317
|
-
limit:
|
|
605
|
+
customer_id: z4.string().describe("Google Ads customer ID"),
|
|
606
|
+
campaign_id: z4.string().optional().describe("Scope to a specific campaign"),
|
|
607
|
+
ad_group_id: z4.string().optional().describe("Scope to a specific ad group"),
|
|
608
|
+
asset_type: z4.array(z4.enum(["IMAGE", "YOUTUBE_VIDEO", "TEXT", "MEDIA_BUNDLE"])).optional().describe("Filter by asset type"),
|
|
609
|
+
include_slot_audit: z4.boolean().optional().describe("Include slot utilization audit (default true)"),
|
|
610
|
+
limit: z4.number().min(1).max(500).optional().describe("Max results to return (default 50)")
|
|
1318
611
|
},
|
|
1319
612
|
async ({ customer_id, campaign_id, ad_group_id, asset_type, include_slot_audit, limit }) => {
|
|
1320
613
|
try {
|
|
@@ -1498,7 +791,7 @@ function registerGetGoogleAdsAssets(server2) {
|
|
|
1498
791
|
}
|
|
1499
792
|
|
|
1500
793
|
// src/tools/google-insights.ts
|
|
1501
|
-
import { z as
|
|
794
|
+
import { z as z5 } from "zod";
|
|
1502
795
|
function parseNum(v) {
|
|
1503
796
|
if (v === void 0 || v === null) return 0;
|
|
1504
797
|
if (typeof v === "number") return v;
|
|
@@ -1555,19 +848,19 @@ function registerGetGoogleAdsInsights(server2) {
|
|
|
1555
848
|
"get_google_insights",
|
|
1556
849
|
"Pull performance metrics from Google Ads 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 (ab-pt-005). Supports campaign, ad_group, asset, and account levels.",
|
|
1557
850
|
{
|
|
1558
|
-
customer_id:
|
|
1559
|
-
level:
|
|
1560
|
-
campaign_id:
|
|
1561
|
-
ad_group_id:
|
|
1562
|
-
breakdown:
|
|
1563
|
-
date_range:
|
|
1564
|
-
start_date:
|
|
1565
|
-
end_date:
|
|
851
|
+
customer_id: z5.string().describe("Google Ads customer ID"),
|
|
852
|
+
level: z5.enum(["account", "campaign", "ad_group", "asset"]).optional().describe("Aggregation level (default: campaign)"),
|
|
853
|
+
campaign_id: z5.string().optional().describe("Scope to specific campaign"),
|
|
854
|
+
ad_group_id: z5.string().optional().describe("Scope to specific ad group"),
|
|
855
|
+
breakdown: z5.enum(["network", "device"]).optional().describe("Segmentation dimension. Only one at a time (GAQL restriction)"),
|
|
856
|
+
date_range: z5.object({
|
|
857
|
+
start_date: z5.string().describe("YYYY-MM-DD"),
|
|
858
|
+
end_date: z5.string().describe("YYYY-MM-DD")
|
|
1566
859
|
}).optional().describe("Custom date range. Overrides date_preset"),
|
|
1567
|
-
date_preset:
|
|
1568
|
-
time_increment:
|
|
1569
|
-
sort:
|
|
1570
|
-
limit:
|
|
860
|
+
date_preset: z5.enum(["LAST_7_DAYS", "LAST_14_DAYS", "LAST_30_DAYS", "THIS_MONTH", "LAST_MONTH"]).optional().describe("Predefined date range (default: LAST_7_DAYS)"),
|
|
861
|
+
time_increment: z5.enum(["daily", "weekly", "monthly", "summary"]).optional().describe("Time granularity (default: summary)"),
|
|
862
|
+
sort: z5.enum(["cost_desc", "conversions_desc", "impressions_desc", "ctr_desc"]).optional().describe("Sort order (default: cost_desc)"),
|
|
863
|
+
limit: z5.number().min(1).max(500).optional().describe("Max results (default 50)")
|
|
1571
864
|
},
|
|
1572
865
|
async ({ customer_id, level, campaign_id, ad_group_id, breakdown, date_range, date_preset, time_increment, sort, limit }) => {
|
|
1573
866
|
try {
|
|
@@ -1745,7 +1038,7 @@ function registerGetGoogleAdsInsights(server2) {
|
|
|
1745
1038
|
}
|
|
1746
1039
|
|
|
1747
1040
|
// src/tools/google-network-mix.ts
|
|
1748
|
-
import { z as
|
|
1041
|
+
import { z as z6 } from "zod";
|
|
1749
1042
|
function parseNum2(v) {
|
|
1750
1043
|
if (v === void 0 || v === null) return 0;
|
|
1751
1044
|
if (typeof v === "number") return v;
|
|
@@ -1757,13 +1050,13 @@ function registerGetGoogleAdsNetworkMix(server2) {
|
|
|
1757
1050
|
"get_google_network_mix",
|
|
1758
1051
|
"Analyze traffic distribution across Google's ad networks (Search, Display/AdMob, YouTube) over time. Computes spend share % per network per day and flags significant shifts. Traffic shifts signal performance problems \u2014 a sudden shift to Display/MGDN tanks CPA. Google optimizes for Google's revenue alongside yours; use this to detect shifts to low-quality inventory (ab-pt-005, ab-pt-017).",
|
|
1759
1052
|
{
|
|
1760
|
-
customer_id:
|
|
1761
|
-
campaign_id:
|
|
1762
|
-
date_range:
|
|
1763
|
-
start_date:
|
|
1764
|
-
end_date:
|
|
1053
|
+
customer_id: z6.string().describe("Google Ads customer ID"),
|
|
1054
|
+
campaign_id: z6.string().optional().describe("Scope to one campaign. If omitted, aggregates across all app campaigns"),
|
|
1055
|
+
date_range: z6.object({
|
|
1056
|
+
start_date: z6.string().describe("YYYY-MM-DD"),
|
|
1057
|
+
end_date: z6.string().describe("YYYY-MM-DD")
|
|
1765
1058
|
}).optional().describe("Custom date range. Default: last 14 days"),
|
|
1766
|
-
shift_threshold_pct:
|
|
1059
|
+
shift_threshold_pct: z6.number().optional().describe("Flag networks whose spend share changed by more than this % (default 10)")
|
|
1767
1060
|
},
|
|
1768
1061
|
async ({ customer_id, campaign_id, date_range, shift_threshold_pct }) => {
|
|
1769
1062
|
try {
|
|
@@ -1930,7 +1223,7 @@ function registerGetGoogleAdsNetworkMix(server2) {
|
|
|
1930
1223
|
}
|
|
1931
1224
|
|
|
1932
1225
|
// src/tools/google-asset-fatigue.ts
|
|
1933
|
-
import { z as
|
|
1226
|
+
import { z as z7 } from "zod";
|
|
1934
1227
|
function parseNum3(v) {
|
|
1935
1228
|
if (v === void 0 || v === null) return 0;
|
|
1936
1229
|
if (typeof v === "number") return v;
|
|
@@ -1970,13 +1263,13 @@ function registerGetGoogleAdsAssetFatigue(server2) {
|
|
|
1970
1263
|
"get_google_asset_fatigue",
|
|
1971
1264
|
"Detect creative asset fatigue in Google UAC campaigns by analyzing per-asset impression trends, CTR decline, and CPA deterioration. Also checks asset age against Google's 2-week learning minimum and 2-3 month refresh cadence. Google doesn't expose per-asset frequency \u2014 uses impression volume decay as the primary fatigue signal. Never remove assets within first 2 weeks (goog-pdf-018). Google's performance label measures scalability, not fatigue (ab-pt-008).",
|
|
1972
1265
|
{
|
|
1973
|
-
customer_id:
|
|
1974
|
-
campaign_id:
|
|
1975
|
-
ad_group_id:
|
|
1976
|
-
lookback_days:
|
|
1977
|
-
ctr_decline_threshold_pct:
|
|
1978
|
-
impression_decay_threshold_pct:
|
|
1979
|
-
asset_type:
|
|
1266
|
+
customer_id: z7.string().describe("Google Ads customer ID"),
|
|
1267
|
+
campaign_id: z7.string().describe("Campaign to analyze"),
|
|
1268
|
+
ad_group_id: z7.string().optional().describe("Scope to specific ad group"),
|
|
1269
|
+
lookback_days: z7.number().min(7).max(90).optional().describe("Days of daily data to analyze (default 14)"),
|
|
1270
|
+
ctr_decline_threshold_pct: z7.number().optional().describe("CTR decline % from peak to flag fatigue (default 30)"),
|
|
1271
|
+
impression_decay_threshold_pct: z7.number().optional().describe("Impression volume drop % from peak to flag (default 50)"),
|
|
1272
|
+
asset_type: z7.array(z7.enum(["IMAGE", "YOUTUBE_VIDEO", "TEXT"])).optional().describe("Filter by asset type")
|
|
1980
1273
|
},
|
|
1981
1274
|
async ({ customer_id, campaign_id, ad_group_id, lookback_days, ctr_decline_threshold_pct, impression_decay_threshold_pct, asset_type }) => {
|
|
1982
1275
|
try {
|
|
@@ -2228,7 +1521,7 @@ Sources: goog-pdf-018, ab-pt-008, goog-pdf-019, ab-pt-007
|
|
|
2228
1521
|
}
|
|
2229
1522
|
|
|
2230
1523
|
// src/tools/google-upload-assets.ts
|
|
2231
|
-
import { z as
|
|
1524
|
+
import { z as z8 } from "zod";
|
|
2232
1525
|
import { readFile } from "fs/promises";
|
|
2233
1526
|
import { basename, resolve } from "path";
|
|
2234
1527
|
async function fetchAsBase64(url) {
|
|
@@ -2251,25 +1544,25 @@ function registerUploadGoogleImageAssets(server2) {
|
|
|
2251
1544
|
"upload_google_image_assets",
|
|
2252
1545
|
"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
1546
|
{
|
|
2254
|
-
customer_id:
|
|
2255
|
-
images:
|
|
2256
|
-
|
|
2257
|
-
source:
|
|
1547
|
+
customer_id: z8.string().describe("Google Ads customer ID"),
|
|
1548
|
+
images: z8.array(
|
|
1549
|
+
z8.object({
|
|
1550
|
+
source: z8.string().describe(
|
|
2258
1551
|
"Image URL (https://...) or local file path (/path/to/image.png)"
|
|
2259
1552
|
),
|
|
2260
|
-
name:
|
|
1553
|
+
name: z8.string().optional().describe(
|
|
2261
1554
|
"Asset name in Google Ads (default: filename from source)"
|
|
2262
1555
|
)
|
|
2263
1556
|
})
|
|
2264
1557
|
).min(1).max(50).describe("Array of images to upload (max 50 per call)"),
|
|
2265
|
-
campaign_id:
|
|
1558
|
+
campaign_id: z8.string().optional().describe(
|
|
2266
1559
|
"Link uploaded assets to this campaign (creates CampaignAsset links)"
|
|
2267
1560
|
),
|
|
2268
|
-
ad_group_id:
|
|
1561
|
+
ad_group_id: z8.string().optional().describe(
|
|
2269
1562
|
"Link uploaded assets to this ad group (creates AdGroupAsset links). Takes priority over campaign_id."
|
|
2270
1563
|
),
|
|
2271
|
-
field_type:
|
|
2272
|
-
dry_run:
|
|
1564
|
+
field_type: z8.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)"),
|
|
1565
|
+
dry_run: z8.boolean().optional().describe("Preview what would be uploaded without making changes")
|
|
2273
1566
|
},
|
|
2274
1567
|
async ({
|
|
2275
1568
|
customer_id,
|
|
@@ -2393,7 +1686,7 @@ Assets exist in the account and can be linked manually.`;
|
|
|
2393
1686
|
function registerConnectionStatus(server2, status2) {
|
|
2394
1687
|
server2.tool(
|
|
2395
1688
|
"connection_status",
|
|
2396
|
-
"Check the connection status of the knowledge base and
|
|
1689
|
+
"Check the connection status of the knowledge base and Google Ads API. Call this if tools seem missing or you get unexpected errors.",
|
|
2397
1690
|
{},
|
|
2398
1691
|
async () => {
|
|
2399
1692
|
const lines = ["# Connection Status", ""];
|
|
@@ -2417,26 +1710,11 @@ function registerConnectionStatus(server2, status2) {
|
|
|
2417
1710
|
);
|
|
2418
1711
|
}
|
|
2419
1712
|
lines.push("");
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
} else {
|
|
2426
|
-
lines.push(
|
|
2427
|
-
"## Meta Marketing API: Not Connected (Optional)",
|
|
2428
|
-
"- KB, suggestions, and private insights work without it",
|
|
2429
|
-
"- Connect Meta to unlock live campaign data and reports",
|
|
2430
|
-
"",
|
|
2431
|
-
"### How to connect",
|
|
2432
|
-
"Provide your Meta access token using one of these methods:",
|
|
2433
|
-
'1. MCP config: add `"META_ACCESS_TOKEN": "..."` to the `"env"` block in `.mcp.json` (Claude Code/Cursor) or `claude_desktop_config.json` (Claude Desktop)',
|
|
2434
|
-
"2. CLI argument: add `--meta-token=...` to the args array",
|
|
2435
|
-
"3. `.env` file: add `META_ACCESS_TOKEN=...` to a `.env` file in your working directory",
|
|
2436
|
-
"",
|
|
2437
|
-
"Then restart your MCP client."
|
|
2438
|
-
);
|
|
2439
|
-
}
|
|
1713
|
+
lines.push(
|
|
1714
|
+
"## Meta Ads Data: Use Meta's Official AI Connector",
|
|
1715
|
+
"- This MCP no longer ships Meta API tools (avoids unofficial-API risk).",
|
|
1716
|
+
"- For Meta data, install Meta's official Meta Ads MCP / AI connector and let the analytical skills here interpret what it returns."
|
|
1717
|
+
);
|
|
2440
1718
|
lines.push("");
|
|
2441
1719
|
if (status2.google.configured) {
|
|
2442
1720
|
lines.push(
|
|
@@ -2523,7 +1801,9 @@ var TOPICS = [
|
|
|
2523
1801
|
"competitive_analysis",
|
|
2524
1802
|
"ad_copy",
|
|
2525
1803
|
"conversion_rate",
|
|
2526
|
-
"strategy"
|
|
1804
|
+
"strategy",
|
|
1805
|
+
"psychology",
|
|
1806
|
+
"compliance"
|
|
2527
1807
|
];
|
|
2528
1808
|
var APPLIES_TO = [
|
|
2529
1809
|
"subscription_apps",
|
|
@@ -2583,15 +1863,17 @@ function registerVocabularyResource(server2) {
|
|
|
2583
1863
|
}
|
|
2584
1864
|
|
|
2585
1865
|
// src/resources/instructions.ts
|
|
2586
|
-
var INSTRUCTIONS = `# Mobile Growth MCP \u2014 Knowledge Base +
|
|
1866
|
+
var INSTRUCTIONS = `# Mobile Growth MCP \u2014 Knowledge Base + Analysis Skills
|
|
2587
1867
|
|
|
2588
1868
|
## Welcome
|
|
2589
1869
|
|
|
2590
|
-
You're connected to the Mobile Growth knowledge base \u2014 curated expert insights on mobile advertising, campaign optimization, and subscription app growth \u2014 plus
|
|
1870
|
+
You're connected to the Mobile Growth knowledge base \u2014 curated expert insights on mobile advertising, campaign optimization, and subscription app growth \u2014 plus Google Ads API integration and a library of analytical skills.
|
|
2591
1871
|
|
|
2592
1872
|
**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.
|
|
2593
1873
|
|
|
2594
|
-
> **
|
|
1874
|
+
> **For Meta Ads data**, use Meta's official Meta Ads MCP / AI connector. This MCP intentionally does **not** ship Meta API tools \u2014 instead, the analytical skills here (ad-fatigue-report, weekly-performance, audit-meta-account, etc.) interpret data fetched from Meta's official connector or pasted from CSV exports.
|
|
1875
|
+
>
|
|
1876
|
+
> Connecting Google Ads is optional. The knowledge base, community suggestions, and private insights all work with just your API key.
|
|
2595
1877
|
|
|
2596
1878
|
Quick examples:
|
|
2597
1879
|
- "subscription app creative fatigue signals"
|
|
@@ -2602,7 +1884,7 @@ Quick examples:
|
|
|
2602
1884
|
If you can't find what you need, call \`submit_feedback\` to report the gap \u2014 it helps us improve the knowledge base.
|
|
2603
1885
|
|
|
2604
1886
|
## What This Is
|
|
2605
|
-
A curated knowledge base of mobile advertising insights +
|
|
1887
|
+
A curated knowledge base of mobile advertising insights + Google Ads API integration + analytical skills (Meta + Google) that interpret data from the platforms' own MCPs/CSV exports.
|
|
2606
1888
|
|
|
2607
1889
|
---
|
|
2608
1890
|
|
|
@@ -2645,52 +1927,20 @@ Save knowledge that is private to your API key. Immediately searchable but only
|
|
|
2645
1927
|
- Same full schema as suggest_insight
|
|
2646
1928
|
- No admin approval needed \u2014 saved instantly
|
|
2647
1929
|
|
|
1930
|
+
### suggest_skill
|
|
1931
|
+
Propose a new skill (repeatable workflow) for the knowledge base. **Use this when a user describes a workflow they want automated** \u2014 recurring audits, analyses, or procedures they run regularly.
|
|
1932
|
+
- **Before calling**: draft the full skill .md yourself. Canonical format: "## What It Does", "## When to Use This" (bullet triggers), "## What It Needs" (data sources + exact CSV export instructions), "## Procedure" (numbered steps with tool calls or CSV column specs), "## Output".
|
|
1933
|
+
- Pass the draft as content_md. Only use user_description without a draft if the user wants to submit a rough idea for the admin to develop.
|
|
1934
|
+
- Provide when_to_use phrases and data_sources (meta_api / google_ads_api / csv / manual_input)
|
|
1935
|
+
- Submissions go to admin review before becoming live
|
|
1936
|
+
|
|
2648
1937
|
---
|
|
2649
1938
|
|
|
2650
|
-
## Meta
|
|
2651
|
-
|
|
2652
|
-
**
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
### get_meta_campaigns
|
|
2657
|
-
List campaigns from a Meta ad account. Defaults to active campaigns.
|
|
2658
|
-
- **ad_account_id** (required): e.g. "act_123456789"
|
|
2659
|
-
- **fields, effective_status, limit, after** (optional)
|
|
2660
|
-
|
|
2661
|
-
### get_meta_adsets
|
|
2662
|
-
List ad sets, optionally scoped to a campaign.
|
|
2663
|
-
- **ad_account_id** (required)
|
|
2664
|
-
- **campaign_id** (optional): Scope to specific campaign
|
|
2665
|
-
- **fields, effective_status, limit, after** (optional)
|
|
2666
|
-
|
|
2667
|
-
### get_meta_ads
|
|
2668
|
-
List ads, optionally scoped to an ad set.
|
|
2669
|
-
- **ad_account_id** (required)
|
|
2670
|
-
- **adset_id** (optional): Scope to specific ad set
|
|
2671
|
-
- **fields, effective_status, limit, after** (optional)
|
|
2672
|
-
|
|
2673
|
-
### get_meta_insights
|
|
2674
|
-
Pull performance insights with configurable level, breakdowns, date range. Ad-level queries auto-include ad_id, ad_name, adset_id, adset_name.
|
|
2675
|
-
- **ad_account_id** (required)
|
|
2676
|
-
- **campaign_id** (optional): Scope to a specific campaign
|
|
2677
|
-
- **adset_id** (optional): Scope to a specific ad set
|
|
2678
|
-
- **level** (optional): account, campaign, adset, ad (default: campaign)
|
|
2679
|
-
- **date_preset** (optional): default last_7d
|
|
2680
|
-
- **time_range** (optional): {since, until} for custom dates
|
|
2681
|
-
- **time_increment** (optional): "1" for daily, "7" for weekly
|
|
2682
|
-
- **breakdowns** (optional): e.g. "age,gender" or "publisher_platform,platform_position"
|
|
2683
|
-
- **conversion_event** (optional): default "mobile_app_install"
|
|
2684
|
-
- **fields, filtering, sort, limit, after** (optional)
|
|
2685
|
-
|
|
2686
|
-
### get_meta_ad_fatigue
|
|
2687
|
-
Built-in report: detect creative fatigue via frequency, CTR decline, CPA trends.
|
|
2688
|
-
- **ad_account_id** (required)
|
|
2689
|
-
- **campaign_id** (optional): Scope to specific campaign
|
|
2690
|
-
- **conversion_event** (optional): default "mobile_app_install"
|
|
2691
|
-
- **frequency_warning** (optional): default 3
|
|
2692
|
-
- **frequency_critical** (optional): default 5
|
|
2693
|
-
- **ctr_decline_threshold** (optional): default 30%
|
|
1939
|
+
## Meta Ads Data \u2014 Use Meta's Official AI Connector
|
|
1940
|
+
|
|
1941
|
+
This MCP **does not ship Meta API tools**. Use Meta's official Meta Ads MCP / AI connector to fetch campaign data, performance metrics, audiences, and catalog operations. Then run the analytical skills below to interpret what comes back.
|
|
1942
|
+
|
|
1943
|
+
This separation keeps users out of unofficial-API risk while preserving the analytical value (KB-grounded methodology) that this MCP provides.
|
|
2694
1944
|
|
|
2695
1945
|
---
|
|
2696
1946
|
|
|
@@ -2757,19 +2007,19 @@ Detect creative asset fatigue by analyzing per-asset impression trends, CTR decl
|
|
|
2757
2007
|
|
|
2758
2008
|
## Reports (MCP Prompts)
|
|
2759
2009
|
|
|
2760
|
-
Pre-built
|
|
2010
|
+
Pre-built analytical workflows. Each skill expects either Meta MCP\u2013sourced data (use Meta's official AI connector) or CSV exports from Ads Manager. Select a prompt and provide your \`ad_account_id\` to run:
|
|
2761
2011
|
|
|
2762
|
-
| Prompt | What it does |
|
|
2763
|
-
|
|
2764
|
-
| ad-fatigue-report | Detect creative fatigue with daily granularity |
|
|
2765
|
-
| weekly-performance | Week-over-week health comparison with diagnosis |
|
|
2766
|
-
| creative-performance | Categorize ads by health status |
|
|
2767
|
-
| audience-composition | Age
|
|
2768
|
-
| architecture-review | Campaign structure evaluation |
|
|
2769
|
-
| audit-meta-account | Comprehensive account audit |
|
|
2770
|
-
| campaign-comparison | Side-by-side campaign comparison |
|
|
2771
|
-
| placement-audit | Detailed placement audit with examples |
|
|
2772
|
-
| attribution-analysis | Conversion quality validation |
|
|
2012
|
+
| Prompt | What it does |
|
|
2013
|
+
|--------|-------------|
|
|
2014
|
+
| ad-fatigue-report | Detect creative fatigue with daily granularity |
|
|
2015
|
+
| weekly-performance | Week-over-week health comparison with diagnosis |
|
|
2016
|
+
| creative-performance | Categorize ads by health status |
|
|
2017
|
+
| audience-composition | Age \xD7 gender heatmap with CPA analysis |
|
|
2018
|
+
| architecture-review | Campaign structure evaluation |
|
|
2019
|
+
| audit-meta-account | Comprehensive account audit |
|
|
2020
|
+
| campaign-comparison | Side-by-side campaign comparison |
|
|
2021
|
+
| placement-audit | Detailed placement audit with examples |
|
|
2022
|
+
| attribution-analysis | Conversion quality validation |
|
|
2773
2023
|
|
|
2774
2024
|
---
|
|
2775
2025
|
|
|
@@ -2797,9 +2047,9 @@ When your response draws on knowledge base results, **always attribute visibly**
|
|
|
2797
2047
|
5. Use \`get_google_asset_fatigue\` on specific campaigns to detect creative decay
|
|
2798
2048
|
|
|
2799
2049
|
### For Meta analysis:
|
|
2800
|
-
1.
|
|
2801
|
-
2.
|
|
2802
|
-
3. For custom analysis, use
|
|
2050
|
+
1. Use Meta's official Meta Ads MCP / AI connector to fetch the data
|
|
2051
|
+
2. Run a report (MCP prompt) here to interpret it against the knowledge base
|
|
2052
|
+
3. For custom analysis, ask the user to paste a CSV export and use the skill's Option B path
|
|
2803
2053
|
|
|
2804
2054
|
### General:
|
|
2805
2055
|
- Always \`search_insights\` before making recommendations \u2014 ground advice in expert knowledge
|
|
@@ -2823,16 +2073,9 @@ function buildStatusSection(status2) {
|
|
|
2823
2073
|
" - Fix: provide your API key via `--api-key=me_...` CLI arg, `API_KEY` env var, or `.env` file"
|
|
2824
2074
|
);
|
|
2825
2075
|
}
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
lines.push(
|
|
2830
|
-
"- **Meta Marketing API**: Not connected (optional \u2014 KB works without it)"
|
|
2831
|
-
);
|
|
2832
|
-
lines.push(
|
|
2833
|
-
" - To connect: provide your token via `--meta-token=...` CLI arg, `META_ACCESS_TOKEN` env var, or `.env` file"
|
|
2834
|
-
);
|
|
2835
|
-
}
|
|
2076
|
+
lines.push(
|
|
2077
|
+
"- **Meta Ads Data**: Use Meta's official Meta Ads MCP / AI connector \u2014 this MCP no longer ships Meta API tools"
|
|
2078
|
+
);
|
|
2836
2079
|
if (status2.google.configured) {
|
|
2837
2080
|
lines.push("- **Google Ads API**: Configured");
|
|
2838
2081
|
} else {
|
|
@@ -2919,9 +2162,6 @@ function resolve2(envName, cliName) {
|
|
|
2919
2162
|
function resolveApiKey() {
|
|
2920
2163
|
return resolve2("API_KEY", "api-key");
|
|
2921
2164
|
}
|
|
2922
|
-
function resolveMetaToken() {
|
|
2923
|
-
return resolve2("META_ACCESS_TOKEN", "meta-token");
|
|
2924
|
-
}
|
|
2925
2165
|
function resolveGoogleAdsConfig() {
|
|
2926
2166
|
const devToken = resolve2("GOOGLE_ADS_DEVELOPER_TOKEN", "google-dev-token");
|
|
2927
2167
|
const clientId = resolve2("GOOGLE_ADS_CLIENT_ID", "google-client-id");
|
|
@@ -3218,10 +2458,8 @@ Error: ${err instanceof Error ? err.message : String(err)}`
|
|
|
3218
2458
|
// src/index.ts
|
|
3219
2459
|
await maybeRunAuthCommand();
|
|
3220
2460
|
var apiKeyResult = resolveApiKey();
|
|
3221
|
-
var metaTokenResult = resolveMetaToken();
|
|
3222
2461
|
var googleAdsResult = resolveGoogleAdsConfig();
|
|
3223
2462
|
if (apiKeyResult.value) process.env.API_KEY = apiKeyResult.value;
|
|
3224
|
-
if (metaTokenResult.value) process.env.META_ACCESS_TOKEN = metaTokenResult.value;
|
|
3225
2463
|
if (googleAdsResult.developerToken)
|
|
3226
2464
|
process.env.GOOGLE_ADS_DEVELOPER_TOKEN = googleAdsResult.developerToken;
|
|
3227
2465
|
if (googleAdsResult.clientId)
|
|
@@ -3236,9 +2474,6 @@ var apiKey = apiKeyResult.value;
|
|
|
3236
2474
|
console.error(
|
|
3237
2475
|
apiKey ? `API key: ${apiKeyResult.source}` : "API key: not configured \u2014 KB tools will not be available"
|
|
3238
2476
|
);
|
|
3239
|
-
console.error(
|
|
3240
|
-
metaTokenResult.value ? `Meta token: ${metaTokenResult.source}` : "Meta token: not configured (optional \u2014 KB works without it)"
|
|
3241
|
-
);
|
|
3242
2477
|
console.error(
|
|
3243
2478
|
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`
|
|
3244
2479
|
);
|
|
@@ -3248,7 +2483,6 @@ var server = new McpServer({
|
|
|
3248
2483
|
});
|
|
3249
2484
|
var status = {
|
|
3250
2485
|
kb: { connected: false, toolCount: 0, promptCount: 0 },
|
|
3251
|
-
meta: { tokenConfigured: !!metaTokenResult.value },
|
|
3252
2486
|
google: {
|
|
3253
2487
|
configured: googleAdsResult.configured,
|
|
3254
2488
|
missing: googleAdsResult.missing
|
|
@@ -3271,11 +2505,6 @@ if (apiKey) {
|
|
|
3271
2505
|
} else {
|
|
3272
2506
|
status.kb.error = "API_KEY not configured";
|
|
3273
2507
|
}
|
|
3274
|
-
registerGetMetaCampaigns(server);
|
|
3275
|
-
registerGetMetaAdSets(server);
|
|
3276
|
-
registerGetMetaAds(server);
|
|
3277
|
-
registerGetMetaInsights(server);
|
|
3278
|
-
registerGetMetaAdFatigue(server);
|
|
3279
2508
|
registerGetGoogleAdsCampaigns(server);
|
|
3280
2509
|
registerGetGoogleAdsAdGroups(server);
|
|
3281
2510
|
registerGetGoogleAdsAssets(server);
|
package/package.json
CHANGED