@praise25/meta-mcp-server 0.1.8 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +292 -292
  3. package/dist/config.d.ts +24 -0
  4. package/dist/config.js +14 -0
  5. package/dist/constants.d.ts +1 -1
  6. package/dist/constants.js +1 -1
  7. package/dist/helpers/graph-client.d.ts +11 -1
  8. package/dist/helpers/graph-client.js +24 -16
  9. package/dist/index.js +0 -0
  10. package/dist/server.js +21 -1
  11. package/dist/tools/ads/get-insights.d.ts +1 -1
  12. package/dist/tools/ads/get-insights.js +12 -10
  13. package/dist/tools/ads/list-accounts.js +4 -4
  14. package/dist/tools/business/list-assets.js +10 -10
  15. package/dist/tools/business/list-businesses.js +4 -4
  16. package/dist/tools/business/list-system-users.js +4 -4
  17. package/dist/tools/instagram/get-audience-demographics.d.ts +5 -1
  18. package/dist/tools/instagram/get-audience-demographics.js +17 -10
  19. package/dist/tools/instagram/get-media-insights.js +14 -14
  20. package/dist/tools/instagram/list-accounts.js +2 -2
  21. package/dist/tools/meta/graph-read.js +7 -7
  22. package/dist/tools/overview/business-overview.js +10 -10
  23. package/dist/tools/overview/content-report.d.ts +57 -0
  24. package/dist/tools/overview/content-report.js +344 -0
  25. package/dist/tools/overview/latest-posts-summary.d.ts +1 -1
  26. package/dist/tools/overview/latest-posts-summary.js +10 -16
  27. package/dist/tools/pages/get-insights.js +2 -2
  28. package/dist/tools/pages/get-post-insights.js +4 -4
  29. package/dist/tools/pages/list-posts.d.ts +1 -1
  30. package/dist/tools/pages/list-posts.js +3 -1
  31. package/dist/tools/register.js +3 -2
  32. package/dist/tools/shared.d.ts +19 -4
  33. package/dist/tools/shared.js +56 -0
  34. package/dist/tools/token/health.js +8 -6
  35. package/dist/tools/token/inspect.js +11 -11
  36. package/dist/tools/whatsapp/get-analytics.js +2 -2
  37. package/package.json +77 -77
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ import type { ToolContext } from "../../context.js";
3
+ export declare const inputSchema: z.ZodObject<{
4
+ date_preset: z.ZodDefault<z.ZodEnum<["last_7d", "last_14d", "last_28d", "last_30d", "last_90d"]>>;
5
+ since: z.ZodOptional<z.ZodString>;
6
+ until: z.ZodOptional<z.ZodString>;
7
+ include_facebook: z.ZodDefault<z.ZodBoolean>;
8
+ include_instagram: z.ZodDefault<z.ZodBoolean>;
9
+ include_insights: z.ZodDefault<z.ZodBoolean>;
10
+ insights_top_n: z.ZodDefault<z.ZodNumber>;
11
+ max_pages: z.ZodDefault<z.ZodNumber>;
12
+ max_posts_per_account: z.ZodDefault<z.ZodNumber>;
13
+ }, "strict", z.ZodTypeAny, {
14
+ date_preset: "last_7d" | "last_14d" | "last_28d" | "last_30d" | "last_90d";
15
+ max_pages: number;
16
+ include_instagram: boolean;
17
+ include_facebook: boolean;
18
+ include_insights: boolean;
19
+ insights_top_n: number;
20
+ max_posts_per_account: number;
21
+ since?: string | undefined;
22
+ until?: string | undefined;
23
+ }, {
24
+ since?: string | undefined;
25
+ until?: string | undefined;
26
+ date_preset?: "last_7d" | "last_14d" | "last_28d" | "last_30d" | "last_90d" | undefined;
27
+ max_pages?: number | undefined;
28
+ include_instagram?: boolean | undefined;
29
+ include_facebook?: boolean | undefined;
30
+ include_insights?: boolean | undefined;
31
+ insights_top_n?: number | undefined;
32
+ max_posts_per_account?: number | undefined;
33
+ }>;
34
+ export type Input = z.infer<typeof inputSchema>;
35
+ export declare const definition: {
36
+ readonly name: "meta_content_report";
37
+ readonly title: "Content report: how many posts over a period, by type, with engagement & comparison";
38
+ readonly description: "One-call content-performance report over a TIME WINDOW (default last 7 days / this week) across all Facebook Pages and Instagram accounts.\n\nUSE FOR questions like:\n- \"How many posts have gone up in the last week / 7 days / this month?\"\n- \"Classify my posts by type (photo / video / reel / carousel).\"\n- \"Compare Facebook vs Instagram content performance.\"\n- \"Which post type drives the most engagement?\"\n- \"Give me a weekly content report / performance comparison.\"\n\nNOT FOR: a single most-recent post → use meta_latest_posts_summary. A whole-business snapshot (ads + pages + pixels) → use meta_business_overview. Audience age/gender/country demographics → use meta_ig_get_audience_demographics (account-level, not per-post).\n\nReturns, in one call:\n- Total post count in the window, per platform (Facebook / Instagram)\n- Breakdown by post type (photo, video, reel, carousel, link, status) with count + total/avg engagement per type\n- Top posts by engagement\n- A Facebook-vs-Instagram comparison block ready for the AI to narrate\n- Optional per-post reach/views (include_insights=true)\n\nEngagement counts (likes, comments, shares, reactions) are ALWAYS included — they come free with the post list. Reach/views/impressions need include_insights=true (one extra call per post, capped).\n\nWalks /me/assigned_pages → Page published_posts in the window, and the linked Instagram accounts → media filtered to the window. Per-section error isolation: one failing account never blanks the report.";
39
+ readonly inputSchema: {
40
+ date_preset: z.ZodDefault<z.ZodEnum<["last_7d", "last_14d", "last_28d", "last_30d", "last_90d"]>>;
41
+ since: z.ZodOptional<z.ZodString>;
42
+ until: z.ZodOptional<z.ZodString>;
43
+ include_facebook: z.ZodDefault<z.ZodBoolean>;
44
+ include_instagram: z.ZodDefault<z.ZodBoolean>;
45
+ include_insights: z.ZodDefault<z.ZodBoolean>;
46
+ insights_top_n: z.ZodDefault<z.ZodNumber>;
47
+ max_pages: z.ZodDefault<z.ZodNumber>;
48
+ max_posts_per_account: z.ZodDefault<z.ZodNumber>;
49
+ };
50
+ readonly annotations: {
51
+ readonly readOnlyHint: true;
52
+ readonly destructiveHint: false;
53
+ readonly idempotentHint: true;
54
+ readonly openWorldHint: true;
55
+ };
56
+ };
57
+ export declare function handler(input: Input, ctx: ToolContext): Promise<import("../../helpers/format.js").ToolTextResult>;
@@ -0,0 +1,344 @@
1
+ import { z } from "zod";
2
+ import { insightsCreds } from "../../config.js";
3
+ import { MetaError } from "../../errors.js";
4
+ import { jsonBlock, toolResult } from "../../helpers/format.js";
5
+ /**
6
+ * Window presets → number of days back from now. Kept friendly so the AI can
7
+ * pass the same vocabulary users type ("last 7 days", "this week").
8
+ */
9
+ const WINDOW_DAYS = {
10
+ last_7d: 7,
11
+ last_14d: 14,
12
+ last_28d: 28,
13
+ last_30d: 30,
14
+ last_90d: 90,
15
+ };
16
+ export const inputSchema = z
17
+ .object({
18
+ date_preset: z
19
+ .enum(["last_7d", "last_14d", "last_28d", "last_30d", "last_90d"])
20
+ .default("last_7d")
21
+ .describe("Time window for the report. 'last_7d' = the last 7 days / this week (default). Overridden by since/until if those are supplied."),
22
+ since: z
23
+ .string()
24
+ .optional()
25
+ .describe("Explicit window start, ISO date e.g. '2026-05-01'. Overrides date_preset."),
26
+ until: z
27
+ .string()
28
+ .optional()
29
+ .describe("Explicit window end, ISO date e.g. '2026-05-31'. Defaults to now."),
30
+ include_facebook: z.boolean().default(true).describe("Include Facebook Page posts."),
31
+ include_instagram: z.boolean().default(true).describe("Include Instagram media."),
32
+ include_insights: z
33
+ .boolean()
34
+ .default(false)
35
+ .describe("Also fetch per-post reach/views/impressions for the top posts (capped by insights_top_n). Off by default because it costs one Graph call per post; engagement counts (likes/comments/shares/reactions) are ALWAYS included for free regardless."),
36
+ insights_top_n: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(25)
41
+ .default(5)
42
+ .describe("When include_insights=true, how many top-engagement posts per platform to fetch insights for."),
43
+ max_pages: z.number().int().min(1).max(20).default(10).describe("Cap on Pages walked."),
44
+ max_posts_per_account: z
45
+ .number()
46
+ .int()
47
+ .min(1)
48
+ .max(200)
49
+ .default(100)
50
+ .describe("Cap on posts fetched per account before classifying."),
51
+ })
52
+ .strict();
53
+ export const definition = {
54
+ name: "meta_content_report",
55
+ title: "Content report: how many posts over a period, by type, with engagement & comparison",
56
+ description: `One-call content-performance report over a TIME WINDOW (default last 7 days / this week) across all Facebook Pages and Instagram accounts.
57
+
58
+ USE FOR questions like:
59
+ - "How many posts have gone up in the last week / 7 days / this month?"
60
+ - "Classify my posts by type (photo / video / reel / carousel)."
61
+ - "Compare Facebook vs Instagram content performance."
62
+ - "Which post type drives the most engagement?"
63
+ - "Give me a weekly content report / performance comparison."
64
+
65
+ NOT FOR: a single most-recent post → use meta_latest_posts_summary. A whole-business snapshot (ads + pages + pixels) → use meta_business_overview. Audience age/gender/country demographics → use meta_ig_get_audience_demographics (account-level, not per-post).
66
+
67
+ Returns, in one call:
68
+ - Total post count in the window, per platform (Facebook / Instagram)
69
+ - Breakdown by post type (photo, video, reel, carousel, link, status) with count + total/avg engagement per type
70
+ - Top posts by engagement
71
+ - A Facebook-vs-Instagram comparison block ready for the AI to narrate
72
+ - Optional per-post reach/views (include_insights=true)
73
+
74
+ Engagement counts (likes, comments, shares, reactions) are ALWAYS included — they come free with the post list. Reach/views/impressions need include_insights=true (one extra call per post, capped).
75
+
76
+ Walks /me/assigned_pages → Page published_posts in the window, and the linked Instagram accounts → media filtered to the window. Per-section error isolation: one failing account never blanks the report.`,
77
+ inputSchema: inputSchema.shape,
78
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
79
+ };
80
+ /** Normalize a Facebook post into a type + engagement total. */
81
+ function classifyFacebook(post) {
82
+ const statusType = String(post.status_type ?? "");
83
+ const attachments = post.attachments;
84
+ const mediaType = attachments?.data?.[0]?.media_type ?? "";
85
+ let type = "status";
86
+ if (mediaType === "photo" || statusType === "added_photos")
87
+ type = "photo";
88
+ else if (mediaType === "video" || statusType === "added_video")
89
+ type = "video";
90
+ else if (mediaType === "album")
91
+ type = "carousel";
92
+ else if (mediaType === "link" || statusType === "shared_story")
93
+ type = "link";
94
+ else if (statusType === "mobile_status_update")
95
+ type = "status";
96
+ const reactions = post.reactions?.summary?.total_count ?? 0;
97
+ const comments = post.comments?.summary?.total_count ?? 0;
98
+ const shares = post.shares?.count ?? 0;
99
+ const breakdown = { reactions, comments, shares };
100
+ return { type, engagement: reactions + comments + shares, breakdown };
101
+ }
102
+ /** Normalize an Instagram media into a type + engagement total. */
103
+ function classifyInstagram(media) {
104
+ const mediaType = String(media.media_type ?? "");
105
+ const productType = String(media.media_product_type ?? "");
106
+ let type = "image";
107
+ if (productType === "REELS")
108
+ type = "reel";
109
+ else if (mediaType === "VIDEO")
110
+ type = "video";
111
+ else if (mediaType === "CAROUSEL_ALBUM")
112
+ type = "carousel";
113
+ else if (mediaType === "IMAGE")
114
+ type = "image";
115
+ const likes = Number(media.like_count ?? 0);
116
+ const comments = Number(media.comments_count ?? 0);
117
+ const breakdown = { likes, comments };
118
+ return { type, engagement: likes + comments, breakdown };
119
+ }
120
+ function aggregate(posts) {
121
+ const byType = {};
122
+ let totalEngagement = 0;
123
+ for (const p of posts) {
124
+ (byType[p.type] ??= { count: 0, engagement: 0 });
125
+ byType[p.type].count += 1;
126
+ byType[p.type].engagement += p.engagement;
127
+ totalEngagement += p.engagement;
128
+ }
129
+ const byTypeOut = Object.fromEntries(Object.entries(byType).map(([t, v]) => [
130
+ t,
131
+ { count: v.count, total_engagement: v.engagement, avg_engagement: Math.round((v.engagement / v.count) * 10) / 10 },
132
+ ]));
133
+ return {
134
+ total_posts: posts.length,
135
+ total_engagement: totalEngagement,
136
+ avg_engagement_per_post: posts.length ? Math.round((totalEngagement / posts.length) * 10) / 10 : 0,
137
+ by_type: byTypeOut,
138
+ };
139
+ }
140
+ export async function handler(input, ctx) {
141
+ const started = Date.now();
142
+ const now = new Date();
143
+ const untilDate = input.until ? new Date(input.until) : now;
144
+ const sinceDate = input.since
145
+ ? new Date(input.since)
146
+ : new Date(untilDate.getTime() - (WINDOW_DAYS[input.date_preset] ?? 7) * 86_400_000);
147
+ const sinceUnix = Math.floor(sinceDate.getTime() / 1000);
148
+ const untilUnix = Math.floor(untilDate.getTime() / 1000);
149
+ const igCreds = insightsCreds(ctx.config);
150
+ const igOverride = igCreds ? { accessTokenOverride: igCreds.token, appSecretOverride: igCreds.appSecret } : {};
151
+ const posts = [];
152
+ const errors = {};
153
+ // Discover Pages.
154
+ let pages = [];
155
+ try {
156
+ const resp = await ctx.graph.get({
157
+ path: "me/assigned_pages",
158
+ params: { fields: "id,name", limit: input.max_pages },
159
+ });
160
+ pages = Array.isArray(resp.data) ? resp.data : [];
161
+ }
162
+ catch (err) {
163
+ const e = err instanceof MetaError ? err : new MetaError(err.message);
164
+ errors["assigned_pages"] = { error: e.message, hint: e.hint };
165
+ }
166
+ // Facebook posts in window.
167
+ if (input.include_facebook) {
168
+ for (const p of pages) {
169
+ try {
170
+ const pageToken = await ctx.graph.getPageAccessToken(p.id);
171
+ const { data } = await ctx.graph.getAllPages({
172
+ path: `${p.id}/published_posts`,
173
+ params: {
174
+ fields: "id,message,created_time,permalink_url,status_type,attachments{media_type},reactions.summary(true).limit(0),comments.summary(true).limit(0),shares",
175
+ since: String(sinceUnix),
176
+ until: String(untilUnix),
177
+ limit: 50,
178
+ },
179
+ accessTokenOverride: pageToken,
180
+ }, Math.ceil(input.max_posts_per_account / 50));
181
+ for (const post of data.slice(0, input.max_posts_per_account)) {
182
+ const c = classifyFacebook(post);
183
+ posts.push({
184
+ platform: "facebook",
185
+ owner: p.name ?? p.id,
186
+ owner_id: p.id,
187
+ post_id: String(post.id),
188
+ type: c.type,
189
+ created: String(post.created_time ?? ""),
190
+ permalink: post.permalink_url,
191
+ caption: post.message?.slice(0, 140),
192
+ engagement: c.engagement,
193
+ engagement_breakdown: c.breakdown,
194
+ });
195
+ }
196
+ }
197
+ catch (err) {
198
+ const e = err instanceof MetaError ? err : new MetaError(err.message);
199
+ errors[`fb:${p.id}`] = { error: e.message, hint: e.hint };
200
+ }
201
+ }
202
+ }
203
+ // Instagram media in window (no native since/until — paginate then filter by timestamp).
204
+ if (input.include_instagram) {
205
+ for (const p of pages) {
206
+ let ig;
207
+ try {
208
+ const d = await ctx.graph.get({
209
+ path: p.id,
210
+ params: { fields: "instagram_business_account{id,username}" },
211
+ ...igOverride,
212
+ });
213
+ ig = d.instagram_business_account;
214
+ }
215
+ catch {
216
+ /* page may have no IG; skip silently */
217
+ }
218
+ if (!ig)
219
+ continue;
220
+ try {
221
+ const { data } = await ctx.graph.getAllPages({
222
+ path: `${ig.id}/media`,
223
+ params: {
224
+ fields: "id,media_type,media_product_type,caption,permalink,timestamp,like_count,comments_count",
225
+ limit: 50,
226
+ },
227
+ ...igOverride,
228
+ }, Math.ceil(input.max_posts_per_account / 50));
229
+ for (const media of data) {
230
+ const ts = Math.floor(new Date(String(media.timestamp)).getTime() / 1000);
231
+ if (ts < sinceUnix || ts > untilUnix)
232
+ continue; // window filter
233
+ const c = classifyInstagram(media);
234
+ posts.push({
235
+ platform: "instagram",
236
+ owner: ig.username ? `@${ig.username}` : ig.id,
237
+ owner_id: ig.id,
238
+ post_id: String(media.id),
239
+ type: c.type,
240
+ created: String(media.timestamp ?? ""),
241
+ permalink: media.permalink,
242
+ caption: media.caption?.slice(0, 140),
243
+ engagement: c.engagement,
244
+ engagement_breakdown: c.breakdown,
245
+ });
246
+ }
247
+ }
248
+ catch (err) {
249
+ const e = err instanceof MetaError ? err : new MetaError(err.message);
250
+ errors[`ig:${ig.id}`] = { error: e.message, hint: e.hint };
251
+ }
252
+ }
253
+ }
254
+ // Optional per-post insights for the top-N by engagement per platform.
255
+ if (input.include_insights && posts.length > 0) {
256
+ const topByPlatform = (plat) => posts
257
+ .filter((p) => p.platform === plat)
258
+ .sort((a, b) => b.engagement - a.engagement)
259
+ .slice(0, input.insights_top_n);
260
+ const targets = [...topByPlatform("facebook"), ...topByPlatform("instagram")];
261
+ await Promise.all(targets.map(async (p) => {
262
+ try {
263
+ if (p.platform === "instagram") {
264
+ const r = await ctx.graph.get({
265
+ path: `${p.post_id}/insights`,
266
+ params: { metric: "reach,views,total_interactions,saved,shares" },
267
+ ...igOverride,
268
+ });
269
+ p.insights = { data: r.data ?? [] };
270
+ }
271
+ else {
272
+ const parentPageId = p.owner_id;
273
+ const creds = insightsCreds(ctx.config);
274
+ const pageToken = await ctx.graph.getPageAccessToken(parentPageId, creds ? { token: creds.token, appSecret: creds.appSecret } : undefined);
275
+ // Meta deprecated the post_impressions* / post_engaged_users family
276
+ // on the post /insights edge (v22+), and rejects the WHOLE call if
277
+ // any single metric is invalid for the post type. So try
278
+ // post-type-appropriate candidate sets in order and keep the first
279
+ // that succeeds; the per-post engagement counts already display
280
+ // regardless, so this is best-effort enrichment.
281
+ const candidates = p.type === "video"
282
+ ? [["post_video_views", "post_clicks"], ["post_video_views"], ["post_clicks"]]
283
+ : [["post_clicks", "post_reactions_by_type_total"], ["post_clicks"]];
284
+ let captured = false;
285
+ let lastErr = "no metric set succeeded";
286
+ for (const metrics of candidates) {
287
+ try {
288
+ const r = await ctx.graph.get({
289
+ path: `${p.post_id}/insights`,
290
+ params: { metric: metrics.join(",") },
291
+ accessTokenOverride: pageToken,
292
+ appSecretOverride: creds?.appSecret,
293
+ });
294
+ p.insights = { data: r.data ?? [] };
295
+ captured = true;
296
+ break;
297
+ }
298
+ catch (err) {
299
+ lastErr = err instanceof MetaError ? err.message : err.message;
300
+ }
301
+ }
302
+ if (!captured) {
303
+ p.insights = {
304
+ error: lastErr,
305
+ note: "Facebook post-level reach via Insights API is best-effort; engagement counts above are authoritative.",
306
+ };
307
+ }
308
+ }
309
+ }
310
+ catch (err) {
311
+ const e = err instanceof MetaError ? err : new MetaError(err.message);
312
+ p.insights = { error: e.message };
313
+ }
314
+ }));
315
+ }
316
+ const fbPosts = posts.filter((p) => p.platform === "facebook");
317
+ const igPosts = posts.filter((p) => p.platform === "instagram");
318
+ const topOverall = [...posts].sort((a, b) => b.engagement - a.engagement).slice(0, 10);
319
+ const structured = {
320
+ window: {
321
+ preset: input.since || input.until ? "custom" : input.date_preset,
322
+ since: sinceDate.toISOString(),
323
+ until: untilDate.toISOString(),
324
+ },
325
+ generated_at: now.toISOString(),
326
+ latency_ms: Date.now() - started,
327
+ totals: {
328
+ all_posts: posts.length,
329
+ facebook: aggregate(fbPosts),
330
+ instagram: aggregate(igPosts),
331
+ },
332
+ comparison: {
333
+ facebook_posts: fbPosts.length,
334
+ instagram_posts: igPosts.length,
335
+ facebook_total_engagement: fbPosts.reduce((s, p) => s + p.engagement, 0),
336
+ instagram_total_engagement: igPosts.reduce((s, p) => s + p.engagement, 0),
337
+ note: "Engagement = reactions+comments+shares (FB) / likes+comments (IG). For audience age/gender/country demographics, call meta_ig_get_audience_demographics (account-level).",
338
+ },
339
+ top_posts: topOverall,
340
+ posts,
341
+ errors: Object.keys(errors).length ? errors : null,
342
+ };
343
+ return toolResult(structured, jsonBlock(structured));
344
+ }
@@ -23,7 +23,7 @@ export type Input = z.infer<typeof inputSchema>;
23
23
  export declare const definition: {
24
24
  readonly name: "meta_latest_posts_summary";
25
25
  readonly title: "Latest post performance across every social account";
26
- readonly description: "Workflow tool that answers \"how did my latest post on each social account perform?\" in a single call.\n\nInternally walks:\n1. /me/assigned_pages every Facebook Page assigned to this token\n2. For each Page: latest published post + (optionally) its insights (impressions, reach, engaged_users, clicks)\n3. For each Page: linked instagram_business_account\n4. For each IG: latest media + (optionally) its insights (reach, views, likes, comments, shares, saved, total_interactions)\n\nReturns a unified array of '{platform, owner, latest_post, insights}' entries, plus a per-section error map so a single failure doesn't blank the report.\n\n**Use this as the FIRST tool call** when the user asks anything like:\n- \"How are my social media accounts performing?\"\n- \"What's the engagement on my latest posts?\"\n- \"How did my most recent content do?\"\n\nIt removes the need to discover Page / IG IDs separately, and avoids the N+1 sequence of meta_page_list meta_page_list_posts meta_page_get_post_insights × pages × posts.";
26
+ readonly description: "Single-call summary of the SINGLE most-recent post on each Facebook Page and Instagram account, with its engagement + insights.\n\nUSE FOR (latest / most-recent only):\n- \"How did my latest post perform?\"\n- \"What's the engagement on my most recent post on each account?\"\n- \"Show me the newest post on each platform.\"\n\nNOT FOR: a date range or \"how many posts this week/month\" or content comparison over a period use meta_content_report (it takes a time window). A whole-business snapshot incl. ads/pixels use meta_business_overview. Audience age/gender/country use meta_ig_get_audience_demographics.\n\nInternally walks /me/assigned_pages each Page's latest published post + optional insights each linked Instagram account's latest media + optional insights. Auto-discovers all Page/IG IDs the caller never supplies them. Per-section error isolation so one failure doesn't blank the result.";
27
27
  readonly inputSchema: {
28
28
  include_instagram: z.ZodDefault<z.ZodBoolean>;
29
29
  include_facebook: z.ZodDefault<z.ZodBoolean>;
@@ -31,22 +31,16 @@ export const inputSchema = z
31
31
  export const definition = {
32
32
  name: "meta_latest_posts_summary",
33
33
  title: "Latest post performance across every social account",
34
- description: `Workflow tool that answers "how did my latest post on each social account perform?" in a single call.
35
-
36
- Internally walks:
37
- 1. /me/assigned_pages every Facebook Page assigned to this token
38
- 2. For each Page: latest published post + (optionally) its insights (impressions, reach, engaged_users, clicks)
39
- 3. For each Page: linked instagram_business_account
40
- 4. For each IG: latest media + (optionally) its insights (reach, views, likes, comments, shares, saved, total_interactions)
41
-
42
- Returns a unified array of '{platform, owner, latest_post, insights}' entries, plus a per-section error map so a single failure doesn't blank the report.
43
-
44
- **Use this as the FIRST tool call** when the user asks anything like:
45
- - "How are my social media accounts performing?"
46
- - "What's the engagement on my latest posts?"
47
- - "How did my most recent content do?"
48
-
49
- It removes the need to discover Page / IG IDs separately, and avoids the N+1 sequence of meta_page_list → meta_page_list_posts → meta_page_get_post_insights × pages × posts.`,
34
+ description: `Single-call summary of the SINGLE most-recent post on each Facebook Page and Instagram account, with its engagement + insights.
35
+
36
+ USE FOR (latest / most-recent only):
37
+ - "How did my latest post perform?"
38
+ - "What's the engagement on my most recent post on each account?"
39
+ - "Show me the newest post on each platform."
40
+
41
+ NOT FOR: a date range or "how many posts this week/month" or content comparison over a period → use meta_content_report (it takes a time window). A whole-business snapshot incl. ads/pixels → use meta_business_overview. Audience age/gender/country → use meta_ig_get_audience_demographics.
42
+
43
+ Internally walks /me/assigned_pages → each Page's latest published post + optional insights → each linked Instagram account's latest media + optional insights. Auto-discovers all Page/IG IDs — the caller never supplies them. Per-section error isolation so one failure doesn't blank the result.`,
50
44
  inputSchema: inputSchema.shape,
51
45
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
52
46
  };
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { assertAllowed } from "../../config.js";
3
3
  import { metaIdSchema } from "../../helpers/schema.js";
4
- import { runGetAsPage } from "../shared.js";
4
+ import { runGetAsPageViaInsightsApp } from "../shared.js";
5
5
  const PERIOD = z.enum(["day", "week", "days_28", "lifetime", "total_over_range"]);
6
6
  export const inputSchema = z
7
7
  .object({
@@ -37,7 +37,7 @@ export const definition = {
37
37
  };
38
38
  export async function handler(input, ctx) {
39
39
  assertAllowed("page", input.page_id, ctx.config);
40
- return runGetAsPage(ctx, input.page_id, {
40
+ return runGetAsPageViaInsightsApp(ctx, input.page_id, {
41
41
  path: `${input.page_id}/insights`,
42
42
  params: {
43
43
  metric: input.metrics.join(","),
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { metaIdSchema } from "../../helpers/schema.js";
3
- import { errorResult, runGetAsPage } from "../shared.js";
3
+ import { errorResult, runGetAsPageViaInsightsApp } from "../shared.js";
4
4
  export const inputSchema = z
5
5
  .object({
6
6
  post_id: metaIdSchema.describe("Post ID. Meta uses the '{page_id}_{post_id}' format — pass it in full so the server can resolve the parent Page's access token automatically."),
@@ -27,8 +27,8 @@ export const inputSchema = z
27
27
  export const definition = {
28
28
  name: "meta_page_get_post_insights",
29
29
  title: "Get insights for a single Page post",
30
- description: `Reads impressions, unique reach, paid vs. organic split, engaged users, click counts, reactions-by-type, and video view metrics for a single post.
31
-
30
+ description: `Reads impressions, unique reach, paid vs. organic split, engaged users, click counts, reactions-by-type, and video view metrics for a single post.
31
+
32
32
  Uses the parent Page's access token (auto-resolved from the '{page_id}_{post_id}' prefix; pass page_id explicitly if your post_id isn't in that form). Requires the system user to be assigned to the Page with 'View performance' or 'Analyze Page' tasks, plus 'read_insights' scope on the token.`,
33
33
  inputSchema: inputSchema.shape,
34
34
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
@@ -40,7 +40,7 @@ export async function handler(input, ctx) {
40
40
  if (!/^\d+$/.test(parentPageId)) {
41
41
  return errorResult(new Error(`Could not infer parent Page ID from post_id '${input.post_id}'. Pass page_id explicitly.`));
42
42
  }
43
- return runGetAsPage(ctx, parentPageId, {
43
+ return runGetAsPageViaInsightsApp(ctx, parentPageId, {
44
44
  path: `${input.post_id}/insights`,
45
45
  params: { metric: input.metrics.join(",") },
46
46
  });
@@ -32,7 +32,7 @@ export type Input = z.infer<typeof inputSchema>;
32
32
  export declare const definition: {
33
33
  readonly name: "meta_page_list_posts";
34
34
  readonly title: "List Page posts";
35
- readonly description: "Lists Page posts with attached counts (reactions, comments, shares). Use since/until for a date window. The ID returned for each post is the one meta_page_get_post_insights expects.";
35
+ readonly description: "Lists raw Facebook Page posts for ONE page, with reaction/comment/share counts. Supports since/until for a date window and returns post IDs for meta_page_get_post_insights.\n\nUSE FOR: drilling into a single specific Page's post list, or when you already have the page_id. For a cross-account content report (\"how many posts this week across all my accounts, by type, with comparison\") prefer meta_content_report — it walks every Page + Instagram account and classifies/aggregates automatically, no page_id needed.";
36
36
  readonly inputSchema: {
37
37
  limit: z.ZodDefault<z.ZodNumber>;
38
38
  after: z.ZodOptional<z.ZodString>;
@@ -39,7 +39,9 @@ export const inputSchema = z
39
39
  export const definition = {
40
40
  name: "meta_page_list_posts",
41
41
  title: "List Page posts",
42
- description: `Lists Page posts with attached counts (reactions, comments, shares). Use since/until for a date window. The ID returned for each post is the one meta_page_get_post_insights expects.`,
42
+ description: `Lists raw Facebook Page posts for ONE page, with reaction/comment/share counts. Supports since/until for a date window and returns post IDs for meta_page_get_post_insights.
43
+
44
+ USE FOR: drilling into a single specific Page's post list, or when you already have the page_id. For a cross-account content report ("how many posts this week across all my accounts, by type, with comparison") prefer meta_content_report — it walks every Page + Instagram account and classifies/aggregates automatically, no page_id needed.`,
43
45
  inputSchema: inputSchema.shape,
44
46
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
45
47
  };
@@ -46,6 +46,7 @@ import * as whatsappGetAnalytics from "./whatsapp/get-analytics.js";
46
46
  // Overview
47
47
  import * as businessOverview from "./overview/business-overview.js";
48
48
  import * as latestPostsSummary from "./overview/latest-posts-summary.js";
49
+ import * as contentReport from "./overview/content-report.js";
49
50
  const TOOLS = [
50
51
  // Token + meta (3)
51
52
  tokenInspect, tokenHealth, graphRead,
@@ -65,8 +66,8 @@ const TOOLS = [
65
66
  catalogList, catalogListProducts, catalogGetDiagnostics,
66
67
  // WhatsApp (4)
67
68
  whatsappListWabas, whatsappListPhoneNumbers, whatsappListTemplates, whatsappGetAnalytics,
68
- // Overview (2)
69
- businessOverview, latestPostsSummary,
69
+ // Overview (3)
70
+ businessOverview, latestPostsSummary, contentReport,
70
71
  ].map((m) => m);
71
72
  export function registerTools(server, ctx) {
72
73
  const names = [];
@@ -1,7 +1,3 @@
1
- /**
2
- * Shared helpers used across domain tools — keeps each tool file focused on
3
- * its endpoint instead of re-implementing boilerplate.
4
- */
5
1
  import type { ToolContext } from "../context.js";
6
2
  import { type ToolTextResult } from "../helpers/format.js";
7
3
  import type { GraphGetOptions } from "../helpers/graph-client.js";
@@ -17,6 +13,14 @@ export interface ListRunOpts {
17
13
  export declare function runList<T>(ctx: ToolContext, opts: GraphGetOptions, pag: ListRunOpts, extra?: Record<string, unknown>): Promise<ToolTextResult>;
18
14
  /** Run a single read and return a tool result. */
19
15
  export declare function runGet<T>(ctx: ToolContext, opts: GraphGetOptions, extra?: Record<string, unknown>): Promise<ToolTextResult>;
16
+ /**
17
+ * Like runGet, but routes through the secondary insights-app token + secret
18
+ * when one is configured (META_INSIGHTS_ACCESS_TOKEN). Use for direct
19
+ * `/{id}/insights` reads that require a permission (e.g.
20
+ * instagram_manage_insights) the primary ads app cannot host. Falls back to
21
+ * the primary token transparently when no insights app is configured.
22
+ */
23
+ export declare function runGetViaInsightsApp<T>(ctx: ToolContext, opts: GraphGetOptions, extra?: Record<string, unknown>): Promise<ToolTextResult>;
20
24
  export declare function errorResult(err: unknown): ToolTextResult;
21
25
  /**
22
26
  * Resolves a Page access token for `pageId`, then runs `runList` with it as
@@ -28,3 +32,14 @@ export declare function errorResult(err: unknown): ToolTextResult;
28
32
  export declare function runListAsPage<T>(ctx: ToolContext, pageId: string, opts: GraphGetOptions, pag: ListRunOpts, extra?: Record<string, unknown>): Promise<ToolTextResult>;
29
33
  /** Same as runGet but resolves and uses the Page access token first. */
30
34
  export declare function runGetAsPage<T>(ctx: ToolContext, pageId: string, opts: GraphGetOptions, extra?: Record<string, unknown>): Promise<ToolTextResult>;
35
+ /**
36
+ * Page-insights variant of runGetAsPage. When a secondary insights app is
37
+ * configured (the app holding the Pages "read_insights" use case), the Page
38
+ * access token is derived from the *insights* app's token and the call's
39
+ * appsecret_proof uses the insights app secret. Falls back to the primary
40
+ * app's Page token when no insights app is configured.
41
+ *
42
+ * Use for /{page_id}/insights and /{post_id}/insights, which need
43
+ * `read_insights` — a permission the Marketing-API primary app cannot host.
44
+ */
45
+ export declare function runGetAsPageViaInsightsApp<T>(ctx: ToolContext, pageId: string, opts: GraphGetOptions, extra?: Record<string, unknown>): Promise<ToolTextResult>;