mobile-growth-mcp 2.3.4 → 2.3.6
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 +136 -886
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -9,27 +9,48 @@ import { z } from "zod";
|
|
|
9
9
|
var EDGE_FUNCTION_URL = "https://iattgvzqiqrpzoqnrwfr.supabase.co/functions/v1/mcp";
|
|
10
10
|
var nextRequestId = 1;
|
|
11
11
|
async function jsonRpcRequest(apiKey2, method, params) {
|
|
12
|
+
const id = nextRequestId++;
|
|
12
13
|
const body = {
|
|
13
14
|
jsonrpc: "2.0",
|
|
14
15
|
method,
|
|
15
|
-
id
|
|
16
|
+
id
|
|
16
17
|
};
|
|
17
18
|
if (params) body.params = params;
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
const toolName = params?.name ?? "";
|
|
20
|
+
const label = toolName ? `${method}:${toolName}` : method;
|
|
21
|
+
const t0 = Date.now();
|
|
22
|
+
console.error(`[proxy] \u2192 #${id} ${label} start`);
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(EDGE_FUNCTION_URL, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"x-api-key": apiKey2,
|
|
29
|
+
Accept: "application/json"
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify(body),
|
|
32
|
+
signal: AbortSignal.timeout(15e3)
|
|
33
|
+
});
|
|
34
|
+
const headersMs = Date.now() - t0;
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
console.error(
|
|
38
|
+
`[proxy] \u2717 #${id} ${label} http ${res.status} after ${headersMs}ms`
|
|
39
|
+
);
|
|
40
|
+
throw new Error(`Edge Function error (${res.status}): ${text}`);
|
|
41
|
+
}
|
|
42
|
+
const json = await res.json();
|
|
43
|
+
console.error(
|
|
44
|
+
`[proxy] \u2190 #${id} ${label} ok in ${Date.now() - t0}ms (headers ${headersMs}ms)`
|
|
45
|
+
);
|
|
46
|
+
return json;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const e = err;
|
|
49
|
+
console.error(
|
|
50
|
+
`[proxy] \u2717 #${id} ${label} ${e.name}: ${e.message} after ${Date.now() - t0}ms`
|
|
51
|
+
);
|
|
52
|
+
throw err;
|
|
31
53
|
}
|
|
32
|
-
return await res.json();
|
|
33
54
|
}
|
|
34
55
|
async function fetchRemoteTools(apiKey2) {
|
|
35
56
|
const resp = await jsonRpcRequest(apiKey2, "tools/list");
|
|
@@ -164,715 +185,8 @@ function registerFetchedPrompts(server2, apiKey2, prompts) {
|
|
|
164
185
|
}
|
|
165
186
|
}
|
|
166
187
|
|
|
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
188
|
// src/tools/google-campaigns.ts
|
|
875
|
-
import { z as
|
|
189
|
+
import { z as z2 } from "zod";
|
|
876
190
|
|
|
877
191
|
// src/google/oauth.ts
|
|
878
192
|
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
@@ -1126,10 +440,10 @@ function registerGetGoogleAdsCampaigns(server2) {
|
|
|
1126
440
|
"get_google_ads_campaigns",
|
|
1127
441
|
"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
442
|
{
|
|
1129
|
-
customer_id:
|
|
1130
|
-
status:
|
|
1131
|
-
channel_sub_type:
|
|
1132
|
-
limit:
|
|
443
|
+
customer_id: z2.string().describe("Google Ads customer ID (e.g. 123-456-7890 or 1234567890)"),
|
|
444
|
+
status: z2.array(z2.enum(["ENABLED", "PAUSED", "REMOVED"])).optional().describe('Filter by campaign status. Default: ["ENABLED"]'),
|
|
445
|
+
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'),
|
|
446
|
+
limit: z2.number().min(1).max(100).optional().describe("Max campaigns to return (default 50)")
|
|
1133
447
|
},
|
|
1134
448
|
async ({ customer_id, status: status2, channel_sub_type, limit }) => {
|
|
1135
449
|
try {
|
|
@@ -1210,16 +524,16 @@ function registerGetGoogleAdsCampaigns(server2) {
|
|
|
1210
524
|
}
|
|
1211
525
|
|
|
1212
526
|
// src/tools/google-ad-groups.ts
|
|
1213
|
-
import { z as
|
|
527
|
+
import { z as z3 } from "zod";
|
|
1214
528
|
function registerGetGoogleAdsAdGroups(server2) {
|
|
1215
529
|
server2.tool(
|
|
1216
530
|
"get_google_ad_groups",
|
|
1217
531
|
"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
532
|
{
|
|
1219
|
-
customer_id:
|
|
1220
|
-
campaign_id:
|
|
1221
|
-
status:
|
|
1222
|
-
limit:
|
|
533
|
+
customer_id: z3.string().describe("Google Ads customer ID (e.g. 123-456-7890 or 1234567890)"),
|
|
534
|
+
campaign_id: z3.string().optional().describe("Scope to a specific campaign. If omitted, returns ad groups across all app campaigns"),
|
|
535
|
+
status: z3.array(z3.enum(["ENABLED", "PAUSED", "REMOVED"])).optional().describe('Filter by ad group status. Default: ["ENABLED"]'),
|
|
536
|
+
limit: z3.number().min(1).max(100).optional().describe("Max results to return (default 50)")
|
|
1223
537
|
},
|
|
1224
538
|
async ({ customer_id, campaign_id, status: status2, limit }) => {
|
|
1225
539
|
try {
|
|
@@ -1290,7 +604,7 @@ function registerGetGoogleAdsAdGroups(server2) {
|
|
|
1290
604
|
}
|
|
1291
605
|
|
|
1292
606
|
// src/tools/google-assets.ts
|
|
1293
|
-
import { z as
|
|
607
|
+
import { z as z4 } from "zod";
|
|
1294
608
|
var SLOT_LIMITS = {
|
|
1295
609
|
HEADLINE: 5,
|
|
1296
610
|
DESCRIPTION: 5,
|
|
@@ -1309,12 +623,12 @@ function registerGetGoogleAdsAssets(server2) {
|
|
|
1309
623
|
"get_google_assets",
|
|
1310
624
|
"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
625
|
{
|
|
1312
|
-
customer_id:
|
|
1313
|
-
campaign_id:
|
|
1314
|
-
ad_group_id:
|
|
1315
|
-
asset_type:
|
|
1316
|
-
include_slot_audit:
|
|
1317
|
-
limit:
|
|
626
|
+
customer_id: z4.string().describe("Google Ads customer ID"),
|
|
627
|
+
campaign_id: z4.string().optional().describe("Scope to a specific campaign"),
|
|
628
|
+
ad_group_id: z4.string().optional().describe("Scope to a specific ad group"),
|
|
629
|
+
asset_type: z4.array(z4.enum(["IMAGE", "YOUTUBE_VIDEO", "TEXT", "MEDIA_BUNDLE"])).optional().describe("Filter by asset type"),
|
|
630
|
+
include_slot_audit: z4.boolean().optional().describe("Include slot utilization audit (default true)"),
|
|
631
|
+
limit: z4.number().min(1).max(500).optional().describe("Max results to return (default 50)")
|
|
1318
632
|
},
|
|
1319
633
|
async ({ customer_id, campaign_id, ad_group_id, asset_type, include_slot_audit, limit }) => {
|
|
1320
634
|
try {
|
|
@@ -1498,7 +812,7 @@ function registerGetGoogleAdsAssets(server2) {
|
|
|
1498
812
|
}
|
|
1499
813
|
|
|
1500
814
|
// src/tools/google-insights.ts
|
|
1501
|
-
import { z as
|
|
815
|
+
import { z as z5 } from "zod";
|
|
1502
816
|
function parseNum(v) {
|
|
1503
817
|
if (v === void 0 || v === null) return 0;
|
|
1504
818
|
if (typeof v === "number") return v;
|
|
@@ -1555,19 +869,19 @@ function registerGetGoogleAdsInsights(server2) {
|
|
|
1555
869
|
"get_google_insights",
|
|
1556
870
|
"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
871
|
{
|
|
1558
|
-
customer_id:
|
|
1559
|
-
level:
|
|
1560
|
-
campaign_id:
|
|
1561
|
-
ad_group_id:
|
|
1562
|
-
breakdown:
|
|
1563
|
-
date_range:
|
|
1564
|
-
start_date:
|
|
1565
|
-
end_date:
|
|
872
|
+
customer_id: z5.string().describe("Google Ads customer ID"),
|
|
873
|
+
level: z5.enum(["account", "campaign", "ad_group", "asset"]).optional().describe("Aggregation level (default: campaign)"),
|
|
874
|
+
campaign_id: z5.string().optional().describe("Scope to specific campaign"),
|
|
875
|
+
ad_group_id: z5.string().optional().describe("Scope to specific ad group"),
|
|
876
|
+
breakdown: z5.enum(["network", "device"]).optional().describe("Segmentation dimension. Only one at a time (GAQL restriction)"),
|
|
877
|
+
date_range: z5.object({
|
|
878
|
+
start_date: z5.string().describe("YYYY-MM-DD"),
|
|
879
|
+
end_date: z5.string().describe("YYYY-MM-DD")
|
|
1566
880
|
}).optional().describe("Custom date range. Overrides date_preset"),
|
|
1567
|
-
date_preset:
|
|
1568
|
-
time_increment:
|
|
1569
|
-
sort:
|
|
1570
|
-
limit:
|
|
881
|
+
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)"),
|
|
882
|
+
time_increment: z5.enum(["daily", "weekly", "monthly", "summary"]).optional().describe("Time granularity (default: summary)"),
|
|
883
|
+
sort: z5.enum(["cost_desc", "conversions_desc", "impressions_desc", "ctr_desc"]).optional().describe("Sort order (default: cost_desc)"),
|
|
884
|
+
limit: z5.number().min(1).max(500).optional().describe("Max results (default 50)")
|
|
1571
885
|
},
|
|
1572
886
|
async ({ customer_id, level, campaign_id, ad_group_id, breakdown, date_range, date_preset, time_increment, sort, limit }) => {
|
|
1573
887
|
try {
|
|
@@ -1745,7 +1059,7 @@ function registerGetGoogleAdsInsights(server2) {
|
|
|
1745
1059
|
}
|
|
1746
1060
|
|
|
1747
1061
|
// src/tools/google-network-mix.ts
|
|
1748
|
-
import { z as
|
|
1062
|
+
import { z as z6 } from "zod";
|
|
1749
1063
|
function parseNum2(v) {
|
|
1750
1064
|
if (v === void 0 || v === null) return 0;
|
|
1751
1065
|
if (typeof v === "number") return v;
|
|
@@ -1757,13 +1071,13 @@ function registerGetGoogleAdsNetworkMix(server2) {
|
|
|
1757
1071
|
"get_google_network_mix",
|
|
1758
1072
|
"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
1073
|
{
|
|
1760
|
-
customer_id:
|
|
1761
|
-
campaign_id:
|
|
1762
|
-
date_range:
|
|
1763
|
-
start_date:
|
|
1764
|
-
end_date:
|
|
1074
|
+
customer_id: z6.string().describe("Google Ads customer ID"),
|
|
1075
|
+
campaign_id: z6.string().optional().describe("Scope to one campaign. If omitted, aggregates across all app campaigns"),
|
|
1076
|
+
date_range: z6.object({
|
|
1077
|
+
start_date: z6.string().describe("YYYY-MM-DD"),
|
|
1078
|
+
end_date: z6.string().describe("YYYY-MM-DD")
|
|
1765
1079
|
}).optional().describe("Custom date range. Default: last 14 days"),
|
|
1766
|
-
shift_threshold_pct:
|
|
1080
|
+
shift_threshold_pct: z6.number().optional().describe("Flag networks whose spend share changed by more than this % (default 10)")
|
|
1767
1081
|
},
|
|
1768
1082
|
async ({ customer_id, campaign_id, date_range, shift_threshold_pct }) => {
|
|
1769
1083
|
try {
|
|
@@ -1930,7 +1244,7 @@ function registerGetGoogleAdsNetworkMix(server2) {
|
|
|
1930
1244
|
}
|
|
1931
1245
|
|
|
1932
1246
|
// src/tools/google-asset-fatigue.ts
|
|
1933
|
-
import { z as
|
|
1247
|
+
import { z as z7 } from "zod";
|
|
1934
1248
|
function parseNum3(v) {
|
|
1935
1249
|
if (v === void 0 || v === null) return 0;
|
|
1936
1250
|
if (typeof v === "number") return v;
|
|
@@ -1970,13 +1284,13 @@ function registerGetGoogleAdsAssetFatigue(server2) {
|
|
|
1970
1284
|
"get_google_asset_fatigue",
|
|
1971
1285
|
"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
1286
|
{
|
|
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:
|
|
1287
|
+
customer_id: z7.string().describe("Google Ads customer ID"),
|
|
1288
|
+
campaign_id: z7.string().describe("Campaign to analyze"),
|
|
1289
|
+
ad_group_id: z7.string().optional().describe("Scope to specific ad group"),
|
|
1290
|
+
lookback_days: z7.number().min(7).max(90).optional().describe("Days of daily data to analyze (default 14)"),
|
|
1291
|
+
ctr_decline_threshold_pct: z7.number().optional().describe("CTR decline % from peak to flag fatigue (default 30)"),
|
|
1292
|
+
impression_decay_threshold_pct: z7.number().optional().describe("Impression volume drop % from peak to flag (default 50)"),
|
|
1293
|
+
asset_type: z7.array(z7.enum(["IMAGE", "YOUTUBE_VIDEO", "TEXT"])).optional().describe("Filter by asset type")
|
|
1980
1294
|
},
|
|
1981
1295
|
async ({ customer_id, campaign_id, ad_group_id, lookback_days, ctr_decline_threshold_pct, impression_decay_threshold_pct, asset_type }) => {
|
|
1982
1296
|
try {
|
|
@@ -2228,7 +1542,7 @@ Sources: goog-pdf-018, ab-pt-008, goog-pdf-019, ab-pt-007
|
|
|
2228
1542
|
}
|
|
2229
1543
|
|
|
2230
1544
|
// src/tools/google-upload-assets.ts
|
|
2231
|
-
import { z as
|
|
1545
|
+
import { z as z8 } from "zod";
|
|
2232
1546
|
import { readFile } from "fs/promises";
|
|
2233
1547
|
import { basename, resolve } from "path";
|
|
2234
1548
|
async function fetchAsBase64(url) {
|
|
@@ -2251,25 +1565,25 @@ function registerUploadGoogleImageAssets(server2) {
|
|
|
2251
1565
|
"upload_google_image_assets",
|
|
2252
1566
|
"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
1567
|
{
|
|
2254
|
-
customer_id:
|
|
2255
|
-
images:
|
|
2256
|
-
|
|
2257
|
-
source:
|
|
1568
|
+
customer_id: z8.string().describe("Google Ads customer ID"),
|
|
1569
|
+
images: z8.array(
|
|
1570
|
+
z8.object({
|
|
1571
|
+
source: z8.string().describe(
|
|
2258
1572
|
"Image URL (https://...) or local file path (/path/to/image.png)"
|
|
2259
1573
|
),
|
|
2260
|
-
name:
|
|
1574
|
+
name: z8.string().optional().describe(
|
|
2261
1575
|
"Asset name in Google Ads (default: filename from source)"
|
|
2262
1576
|
)
|
|
2263
1577
|
})
|
|
2264
1578
|
).min(1).max(50).describe("Array of images to upload (max 50 per call)"),
|
|
2265
|
-
campaign_id:
|
|
1579
|
+
campaign_id: z8.string().optional().describe(
|
|
2266
1580
|
"Link uploaded assets to this campaign (creates CampaignAsset links)"
|
|
2267
1581
|
),
|
|
2268
|
-
ad_group_id:
|
|
1582
|
+
ad_group_id: z8.string().optional().describe(
|
|
2269
1583
|
"Link uploaded assets to this ad group (creates AdGroupAsset links). Takes priority over campaign_id."
|
|
2270
1584
|
),
|
|
2271
|
-
field_type:
|
|
2272
|
-
dry_run:
|
|
1585
|
+
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)"),
|
|
1586
|
+
dry_run: z8.boolean().optional().describe("Preview what would be uploaded without making changes")
|
|
2273
1587
|
},
|
|
2274
1588
|
async ({
|
|
2275
1589
|
customer_id,
|
|
@@ -2393,7 +1707,7 @@ Assets exist in the account and can be linked manually.`;
|
|
|
2393
1707
|
function registerConnectionStatus(server2, status2) {
|
|
2394
1708
|
server2.tool(
|
|
2395
1709
|
"connection_status",
|
|
2396
|
-
"Check the connection status of the knowledge base and
|
|
1710
|
+
"Check the connection status of the knowledge base and Google Ads API. Call this if tools seem missing or you get unexpected errors.",
|
|
2397
1711
|
{},
|
|
2398
1712
|
async () => {
|
|
2399
1713
|
const lines = ["# Connection Status", ""];
|
|
@@ -2417,26 +1731,11 @@ function registerConnectionStatus(server2, status2) {
|
|
|
2417
1731
|
);
|
|
2418
1732
|
}
|
|
2419
1733
|
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
|
-
}
|
|
1734
|
+
lines.push(
|
|
1735
|
+
"## Meta Ads Data: Use Meta's Official AI Connector",
|
|
1736
|
+
"- This MCP no longer ships Meta API tools (avoids unofficial-API risk).",
|
|
1737
|
+
"- For Meta data, install Meta's official Meta Ads MCP / AI connector and let the analytical skills here interpret what it returns."
|
|
1738
|
+
);
|
|
2440
1739
|
lines.push("");
|
|
2441
1740
|
if (status2.google.configured) {
|
|
2442
1741
|
lines.push(
|
|
@@ -2523,7 +1822,9 @@ var TOPICS = [
|
|
|
2523
1822
|
"competitive_analysis",
|
|
2524
1823
|
"ad_copy",
|
|
2525
1824
|
"conversion_rate",
|
|
2526
|
-
"strategy"
|
|
1825
|
+
"strategy",
|
|
1826
|
+
"psychology",
|
|
1827
|
+
"compliance"
|
|
2527
1828
|
];
|
|
2528
1829
|
var APPLIES_TO = [
|
|
2529
1830
|
"subscription_apps",
|
|
@@ -2583,15 +1884,17 @@ function registerVocabularyResource(server2) {
|
|
|
2583
1884
|
}
|
|
2584
1885
|
|
|
2585
1886
|
// src/resources/instructions.ts
|
|
2586
|
-
var INSTRUCTIONS = `# Mobile Growth MCP \u2014 Knowledge Base +
|
|
1887
|
+
var INSTRUCTIONS = `# Mobile Growth MCP \u2014 Knowledge Base + Analysis Skills
|
|
2587
1888
|
|
|
2588
1889
|
## Welcome
|
|
2589
1890
|
|
|
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
|
|
1891
|
+
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
1892
|
|
|
2592
1893
|
**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
1894
|
|
|
2594
|
-
> **
|
|
1895
|
+
> **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.
|
|
1896
|
+
>
|
|
1897
|
+
> Connecting Google Ads is optional. The knowledge base, community suggestions, and private insights all work with just your API key.
|
|
2595
1898
|
|
|
2596
1899
|
Quick examples:
|
|
2597
1900
|
- "subscription app creative fatigue signals"
|
|
@@ -2602,7 +1905,7 @@ Quick examples:
|
|
|
2602
1905
|
If you can't find what you need, call \`submit_feedback\` to report the gap \u2014 it helps us improve the knowledge base.
|
|
2603
1906
|
|
|
2604
1907
|
## What This Is
|
|
2605
|
-
A curated knowledge base of mobile advertising insights +
|
|
1908
|
+
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
1909
|
|
|
2607
1910
|
---
|
|
2608
1911
|
|
|
@@ -2645,52 +1948,20 @@ Save knowledge that is private to your API key. Immediately searchable but only
|
|
|
2645
1948
|
- Same full schema as suggest_insight
|
|
2646
1949
|
- No admin approval needed \u2014 saved instantly
|
|
2647
1950
|
|
|
1951
|
+
### suggest_skill
|
|
1952
|
+
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.
|
|
1953
|
+
- **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".
|
|
1954
|
+
- 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.
|
|
1955
|
+
- Provide when_to_use phrases and data_sources (meta_api / google_ads_api / csv / manual_input)
|
|
1956
|
+
- Submissions go to admin review before becoming live
|
|
1957
|
+
|
|
2648
1958
|
---
|
|
2649
1959
|
|
|
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%
|
|
1960
|
+
## Meta Ads Data \u2014 Use Meta's Official AI Connector
|
|
1961
|
+
|
|
1962
|
+
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.
|
|
1963
|
+
|
|
1964
|
+
This separation keeps users out of unofficial-API risk while preserving the analytical value (KB-grounded methodology) that this MCP provides.
|
|
2694
1965
|
|
|
2695
1966
|
---
|
|
2696
1967
|
|
|
@@ -2757,19 +2028,19 @@ Detect creative asset fatigue by analyzing per-asset impression trends, CTR decl
|
|
|
2757
2028
|
|
|
2758
2029
|
## Reports (MCP Prompts)
|
|
2759
2030
|
|
|
2760
|
-
Pre-built
|
|
2031
|
+
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
2032
|
|
|
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 |
|
|
2033
|
+
| Prompt | What it does |
|
|
2034
|
+
|--------|-------------|
|
|
2035
|
+
| ad-fatigue-report | Detect creative fatigue with daily granularity |
|
|
2036
|
+
| weekly-performance | Week-over-week health comparison with diagnosis |
|
|
2037
|
+
| creative-performance | Categorize ads by health status |
|
|
2038
|
+
| audience-composition | Age \xD7 gender heatmap with CPA analysis |
|
|
2039
|
+
| architecture-review | Campaign structure evaluation |
|
|
2040
|
+
| audit-meta-account | Comprehensive account audit |
|
|
2041
|
+
| campaign-comparison | Side-by-side campaign comparison |
|
|
2042
|
+
| placement-audit | Detailed placement audit with examples |
|
|
2043
|
+
| attribution-analysis | Conversion quality validation |
|
|
2773
2044
|
|
|
2774
2045
|
---
|
|
2775
2046
|
|
|
@@ -2797,9 +2068,9 @@ When your response draws on knowledge base results, **always attribute visibly**
|
|
|
2797
2068
|
5. Use \`get_google_asset_fatigue\` on specific campaigns to detect creative decay
|
|
2798
2069
|
|
|
2799
2070
|
### For Meta analysis:
|
|
2800
|
-
1.
|
|
2801
|
-
2.
|
|
2802
|
-
3. For custom analysis, use
|
|
2071
|
+
1. Use Meta's official Meta Ads MCP / AI connector to fetch the data
|
|
2072
|
+
2. Run a report (MCP prompt) here to interpret it against the knowledge base
|
|
2073
|
+
3. For custom analysis, ask the user to paste a CSV export and use the skill's Option B path
|
|
2803
2074
|
|
|
2804
2075
|
### General:
|
|
2805
2076
|
- Always \`search_insights\` before making recommendations \u2014 ground advice in expert knowledge
|
|
@@ -2823,16 +2094,9 @@ function buildStatusSection(status2) {
|
|
|
2823
2094
|
" - Fix: provide your API key via `--api-key=me_...` CLI arg, `API_KEY` env var, or `.env` file"
|
|
2824
2095
|
);
|
|
2825
2096
|
}
|
|
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
|
-
}
|
|
2097
|
+
lines.push(
|
|
2098
|
+
"- **Meta Ads Data**: Use Meta's official Meta Ads MCP / AI connector \u2014 this MCP no longer ships Meta API tools"
|
|
2099
|
+
);
|
|
2836
2100
|
if (status2.google.configured) {
|
|
2837
2101
|
lines.push("- **Google Ads API**: Configured");
|
|
2838
2102
|
} else {
|
|
@@ -2919,9 +2183,6 @@ function resolve2(envName, cliName) {
|
|
|
2919
2183
|
function resolveApiKey() {
|
|
2920
2184
|
return resolve2("API_KEY", "api-key");
|
|
2921
2185
|
}
|
|
2922
|
-
function resolveMetaToken() {
|
|
2923
|
-
return resolve2("META_ACCESS_TOKEN", "meta-token");
|
|
2924
|
-
}
|
|
2925
2186
|
function resolveGoogleAdsConfig() {
|
|
2926
2187
|
const devToken = resolve2("GOOGLE_ADS_DEVELOPER_TOKEN", "google-dev-token");
|
|
2927
2188
|
const clientId = resolve2("GOOGLE_ADS_CLIENT_ID", "google-client-id");
|
|
@@ -3218,10 +2479,8 @@ Error: ${err instanceof Error ? err.message : String(err)}`
|
|
|
3218
2479
|
// src/index.ts
|
|
3219
2480
|
await maybeRunAuthCommand();
|
|
3220
2481
|
var apiKeyResult = resolveApiKey();
|
|
3221
|
-
var metaTokenResult = resolveMetaToken();
|
|
3222
2482
|
var googleAdsResult = resolveGoogleAdsConfig();
|
|
3223
2483
|
if (apiKeyResult.value) process.env.API_KEY = apiKeyResult.value;
|
|
3224
|
-
if (metaTokenResult.value) process.env.META_ACCESS_TOKEN = metaTokenResult.value;
|
|
3225
2484
|
if (googleAdsResult.developerToken)
|
|
3226
2485
|
process.env.GOOGLE_ADS_DEVELOPER_TOKEN = googleAdsResult.developerToken;
|
|
3227
2486
|
if (googleAdsResult.clientId)
|
|
@@ -3236,9 +2495,6 @@ var apiKey = apiKeyResult.value;
|
|
|
3236
2495
|
console.error(
|
|
3237
2496
|
apiKey ? `API key: ${apiKeyResult.source}` : "API key: not configured \u2014 KB tools will not be available"
|
|
3238
2497
|
);
|
|
3239
|
-
console.error(
|
|
3240
|
-
metaTokenResult.value ? `Meta token: ${metaTokenResult.source}` : "Meta token: not configured (optional \u2014 KB works without it)"
|
|
3241
|
-
);
|
|
3242
2498
|
console.error(
|
|
3243
2499
|
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
2500
|
);
|
|
@@ -3248,7 +2504,6 @@ var server = new McpServer({
|
|
|
3248
2504
|
});
|
|
3249
2505
|
var status = {
|
|
3250
2506
|
kb: { connected: false, toolCount: 0, promptCount: 0 },
|
|
3251
|
-
meta: { tokenConfigured: !!metaTokenResult.value },
|
|
3252
2507
|
google: {
|
|
3253
2508
|
configured: googleAdsResult.configured,
|
|
3254
2509
|
missing: googleAdsResult.missing
|
|
@@ -3271,11 +2526,6 @@ if (apiKey) {
|
|
|
3271
2526
|
} else {
|
|
3272
2527
|
status.kb.error = "API_KEY not configured";
|
|
3273
2528
|
}
|
|
3274
|
-
registerGetMetaCampaigns(server);
|
|
3275
|
-
registerGetMetaAdSets(server);
|
|
3276
|
-
registerGetMetaAds(server);
|
|
3277
|
-
registerGetMetaInsights(server);
|
|
3278
|
-
registerGetMetaAdFatigue(server);
|
|
3279
2529
|
registerGetGoogleAdsCampaigns(server);
|
|
3280
2530
|
registerGetGoogleAdsAdGroups(server);
|
|
3281
2531
|
registerGetGoogleAdsAssets(server);
|
package/package.json
CHANGED