@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
package/dist/config.d.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  export interface Config {
2
2
  accessToken: string;
3
3
  appSecret: string | undefined;
4
+ /**
5
+ * Optional secondary "insights app" credentials. Meta's app-type rules
6
+ * forbid combining the Marketing-API use case with the Instagram-content /
7
+ * Pages-everything use cases on a single app. When insights endpoints
8
+ * (IG media/audience insights, Page/post insights) live on a *different*
9
+ * Meta app than the primary (ads) app, set these so those specific calls
10
+ * route through the second app's token + secret. Leave unset to use the
11
+ * primary token for everything (single-app deployments).
12
+ */
13
+ insightsAccessToken: string | undefined;
14
+ insightsAppSecret: string | undefined;
4
15
  apiVersion: string;
5
16
  httpTimeoutMs: number;
6
17
  cacheTtlSeconds: number;
@@ -11,5 +22,18 @@ export interface Config {
11
22
  allowedIgUserIds: Set<string> | null;
12
23
  logLevel: string;
13
24
  }
25
+ /**
26
+ * Credentials for a single Meta app: an access token plus the app secret used
27
+ * to compute the request's `appsecret_proof`. Returned by `insightsCreds()`.
28
+ */
29
+ export interface AppCreds {
30
+ token: string;
31
+ appSecret: string | undefined;
32
+ }
33
+ /**
34
+ * Returns the secondary insights-app credentials when configured, otherwise
35
+ * null. Insight tools call this; null means "fall back to the primary token".
36
+ */
37
+ export declare function insightsCreds(config: Config): AppCreds | null;
14
38
  export declare function loadConfig(): Config;
15
39
  export declare function assertAllowed(kind: "business" | "ad_account" | "page" | "ig_user", id: string, config: Config): void;
package/dist/config.js CHANGED
@@ -1,4 +1,16 @@
1
1
  import { DEFAULT_API_VERSION } from "./constants.js";
2
+ /**
3
+ * Returns the secondary insights-app credentials when configured, otherwise
4
+ * null. Insight tools call this; null means "fall back to the primary token".
5
+ */
6
+ export function insightsCreds(config) {
7
+ if (!config.insightsAccessToken)
8
+ return null;
9
+ return {
10
+ token: config.insightsAccessToken,
11
+ appSecret: config.insightsAppSecret,
12
+ };
13
+ }
2
14
  function parseAllowlist(raw) {
3
15
  if (!raw || !raw.trim())
4
16
  return null;
@@ -22,6 +34,8 @@ export function loadConfig() {
22
34
  return {
23
35
  accessToken,
24
36
  appSecret: process.env.META_APP_SECRET?.trim() || undefined,
37
+ insightsAccessToken: process.env.META_INSIGHTS_ACCESS_TOKEN?.trim() || undefined,
38
+ insightsAppSecret: process.env.META_INSIGHTS_APP_SECRET?.trim() || undefined,
25
39
  apiVersion: process.env.META_API_VERSION?.trim() || DEFAULT_API_VERSION,
26
40
  httpTimeoutMs: parseNumber(process.env.META_HTTP_TIMEOUT_MS, 30_000),
27
41
  cacheTtlSeconds: parseNumber(process.env.META_CACHE_TTL_SECONDS, 120),
@@ -4,7 +4,7 @@ export declare const CHARACTER_LIMIT = 25000;
4
4
  export declare const DEFAULT_PAGE_LIMIT = 25;
5
5
  export declare const MAX_PAGE_LIMIT = 500;
6
6
  export declare const SERVER_NAME = "meta-business-manager-mcp-server";
7
- export declare const SERVER_VERSION = "0.1.8";
7
+ export declare const SERVER_VERSION = "0.1.11";
8
8
  export declare const META_ERROR_CODES: {
9
9
  readonly UNKNOWN: 1;
10
10
  readonly SERVICE_TEMPORARILY_UNAVAILABLE: 2;
package/dist/constants.js CHANGED
@@ -4,7 +4,7 @@ export const CHARACTER_LIMIT = 25_000;
4
4
  export const DEFAULT_PAGE_LIMIT = 25;
5
5
  export const MAX_PAGE_LIMIT = 500;
6
6
  export const SERVER_NAME = "meta-business-manager-mcp-server";
7
- export const SERVER_VERSION = "0.1.8";
7
+ export const SERVER_VERSION = "0.1.11";
8
8
  export const META_ERROR_CODES = {
9
9
  UNKNOWN: 1,
10
10
  SERVICE_TEMPORARILY_UNAVAILABLE: 2,
@@ -19,6 +19,13 @@ export interface GraphGetOptions {
19
19
  * binding to the app remains intact.
20
20
  */
21
21
  accessTokenOverride?: string;
22
+ /**
23
+ * Override the app secret used to compute `appsecret_proof` for this call.
24
+ * Required when `accessTokenOverride` is a token issued by a *different*
25
+ * Meta app (e.g. the secondary "insights app"), because the proof must be
26
+ * HMAC(token, that app's secret). Defaults to the primary app secret.
27
+ */
28
+ appSecretOverride?: string;
22
29
  }
23
30
  export interface GraphPagedResponse<T = unknown> {
24
31
  data: T[];
@@ -60,7 +67,10 @@ export declare class GraphClient {
60
67
  * Requires the configured system user to be assigned to the Page with
61
68
  * at least the "View performance" or "Analyze Page" task.
62
69
  */
63
- getPageAccessToken(pageId: string): Promise<string>;
70
+ getPageAccessToken(pageId: string, creds?: {
71
+ token: string;
72
+ appSecret: string | undefined;
73
+ }): Promise<string>;
64
74
  get<T = GraphPagedResponse>(opts: GraphGetOptions): Promise<T>;
65
75
  /**
66
76
  * Follow `paging.next` cursors and concatenate `data` arrays up to maxPages.
@@ -51,10 +51,13 @@ export class GraphClient {
51
51
  * Requires the configured system user to be assigned to the Page with
52
52
  * at least the "View performance" or "Analyze Page" task.
53
53
  */
54
- async getPageAccessToken(pageId) {
54
+ async getPageAccessToken(pageId, creds) {
55
55
  if (!pageId)
56
56
  throw new MetaError("getPageAccessToken: pageId is required");
57
- const cached = this.pageTokens.get(pageId);
57
+ // Cache key namespaces by the issuing app so a Page token derived from the
58
+ // primary app and one derived from the secondary insights app never collide.
59
+ const cacheKey = creds ? `insights:${pageId}` : pageId;
60
+ const cached = this.pageTokens.get(cacheKey);
58
61
  if (cached)
59
62
  return cached;
60
63
  const resp = await this.get({
@@ -63,20 +66,22 @@ export class GraphClient {
63
66
  // Don't cache via the URL+params LRU because the value is sensitive and
64
67
  // we want to keep the per-page Map as the single source of truth.
65
68
  noCache: true,
69
+ accessTokenOverride: creds?.token,
70
+ appSecretOverride: creds?.appSecret,
66
71
  });
67
72
  if (!resp.access_token) {
68
73
  throw new MetaError(`Page ${pageId} did not return an access_token. The system user may not be assigned to this Page or lacks the required task.`, {
69
74
  hint: "In Business Settings → System Users → AI_Insights_Reader → Add Assets, assign this Page with at least 'View performance' or 'Analyze Page' tasks.",
70
75
  });
71
76
  }
72
- this.pageTokens.set(pageId, resp.access_token);
77
+ this.pageTokens.set(cacheKey, resp.access_token);
73
78
  return resp.access_token;
74
79
  }
75
80
  async get(opts) {
76
81
  const version = opts.apiVersion ?? this.config.apiVersion;
77
82
  const path = opts.path.replace(/^\/+/, "");
78
83
  const url = `/${version}/${path}`;
79
- const params = this.buildParams(opts.params, opts.accessTokenOverride);
84
+ const params = this.buildParams(opts.params, opts.accessTokenOverride, opts.appSecretOverride);
80
85
  const cacheKey = this.cacheKeyFor(url, params);
81
86
  if (!opts.noCache) {
82
87
  const cached = this.cache.get(cacheKey);
@@ -106,11 +111,12 @@ export class GraphClient {
106
111
  collected.push(...page.data);
107
112
  nextAfter = page.paging?.cursors?.after;
108
113
  if (page.paging?.next && pages < cap) {
109
- // Propagate the original accessTokenOverride to subsequent pages —
110
- // optsFromNextUrl strips access_token from the URL but the override
111
- // (if any) must continue to be applied.
114
+ // Propagate the original token + app-secret overrides to subsequent
115
+ // pages — optsFromNextUrl strips credentials from the URL but the
116
+ // overrides (if any) must continue to be applied.
112
117
  const next = this.optsFromNextUrl(page.paging.next);
113
118
  next.accessTokenOverride = opts.accessTokenOverride;
119
+ next.appSecretOverride = opts.appSecretOverride;
114
120
  currentOpts = next;
115
121
  }
116
122
  else {
@@ -132,7 +138,7 @@ export class GraphClient {
132
138
  }
133
139
  return { path, params, apiVersion: version };
134
140
  }
135
- buildParams(params, accessTokenOverride) {
141
+ buildParams(params, accessTokenOverride, appSecretOverride) {
136
142
  const out = {};
137
143
  for (const [k, v] of Object.entries(params ?? {})) {
138
144
  if (v === undefined || v === null)
@@ -140,19 +146,21 @@ export class GraphClient {
140
146
  out[k] = v;
141
147
  }
142
148
  const token = accessTokenOverride ?? this.config.accessToken;
149
+ // The app secret must match the app that ISSUED the token. For a
150
+ // secondary-app token, the caller passes appSecretOverride; otherwise we
151
+ // fall back to the primary app secret.
152
+ const appSecret = appSecretOverride ?? this.config.appSecret;
143
153
  out.access_token = token;
144
- if (this.config.appSecret) {
145
- out.appsecret_proof = this.appsecretProof(token);
154
+ if (appSecret) {
155
+ out.appsecret_proof = this.appsecretProof(token, appSecret);
146
156
  }
147
157
  return out;
148
158
  }
149
- appsecretProof(token) {
159
+ appsecretProof(token, appSecret) {
150
160
  // appsecret_proof = HMAC-SHA256(access_token, app_secret).
151
- // Recompute against whichever token is in use (system-user OR Page).
152
- return crypto
153
- .createHmac("sha256", this.config.appSecret)
154
- .update(token ?? this.config.accessToken)
155
- .digest("hex");
161
+ // Recompute against whichever (token, app-secret) pair is in use
162
+ // primary system-user token, Page token, or a secondary-app token.
163
+ return crypto.createHmac("sha256", appSecret).update(token).digest("hex");
156
164
  }
157
165
  cacheKeyFor(url, params) {
158
166
  const { access_token: _at, appsecret_proof: _ap, ...rest } = params;
package/dist/index.js CHANGED
File without changes
package/dist/server.js CHANGED
@@ -2,11 +2,31 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { createContext } from "./context.js";
3
3
  import { SERVER_NAME, SERVER_VERSION } from "./constants.js";
4
4
  import { registerTools } from "./tools/register.js";
5
+ /**
6
+ * Server-level routing guidance surfaced to MCP clients via the SDK's
7
+ * `instructions` field. Gateways that inject server instructions into the
8
+ * model's system prompt use this to pick the right tool without the user
9
+ * having to fine-tune their phrasing. Keep it short, imperative, and focused
10
+ * on the question-classes users actually ask.
11
+ */
12
+ const SERVER_INSTRUCTIONS = `This server exposes READ-ONLY Meta Business Manager analytics (Facebook Pages, Instagram, ads, pixels, catalog, WhatsApp).
13
+
14
+ TOOL ROUTING — pick the broadest matching tool and CALL IT. Never tell the user to rephrase, and never ask for IDs the server can discover itself:
15
+
16
+ - "How is my marketing doing / give me an overview / snapshot" → meta_business_overview (no business_id needed; it auto-discovers).
17
+ - "How did my LATEST / most recent post do" → meta_latest_posts_summary.
18
+ - "How many posts in the last week / this month, by type, performance comparison, content report, trend" → meta_content_report (takes a date window).
19
+ - "Audience demographics / age / gender / country / city breakdown" → meta_ig_get_audience_demographics.
20
+ - "Ad spend / campaign performance / CTR / CPC / ROAS / which ad/campaign" → meta_ads_get_insights (level=account|campaign|adset|ad, with breakdowns).
21
+ - "What assets / pages / accounts do I have" → meta_business_list_assets.
22
+ - Unsure which asset ID to use → call a discovery/report tool first (they auto-discover); do NOT guess placeholder IDs.
23
+
24
+ If a tool returns a (#10)/(#200) permission error, relay its 'hint' to the user verbatim — it names the exact Meta App Review or scope action needed. Prefer the report/overview tools (one call) over chaining several low-level tools.`;
5
25
  export function buildServer(config, logger) {
6
26
  const server = new McpServer({
7
27
  name: SERVER_NAME,
8
28
  version: SERVER_VERSION,
9
- });
29
+ }, { instructions: SERVER_INSTRUCTIONS });
10
30
  const ctx = createContext(config, logger);
11
31
  const toolNames = registerTools(server, ctx);
12
32
  logger.info({ tools: toolNames, count: toolNames.length }, "tools registered");
@@ -82,7 +82,7 @@ export type Input = z.infer<typeof inputSchema>;
82
82
  export declare const definition: {
83
83
  readonly name: "meta_ads_get_insights";
84
84
  readonly title: "Get Marketing API insights (the workhorse)";
85
- readonly description: "Fetch Marketing API insights for any ad object: ad account ('act_<id>'), campaign, ad set, or ad.\n\nReturns spend, impressions, clicks, CPC/CPM/CTR, reach, frequency, actions (conversions), and video metrics — with optional demographic / placement / device breakdowns.\n\nTips for the AI consuming this:\n- Default date_preset is last_30d. Override via date_preset or time_range.\n- Set level to 'ad' and breakdowns=['publisher_platform','platform_position'] to see which placements convert.\n- Use action_breakdowns=['action_type'] to split purchases vs. add_to_cart vs. lead.\n- If you get code 613 or 17, you hit rate limits — narrow the date range or drop breakdowns.\n\nRequires 'ads_read' token scope.";
85
+ readonly description: "Fetch Marketing API insights for any ad object: ad account ('act_<id>'), campaign, ad set, or ad.\n\nUSE FOR ad/paid questions: \"how much did I spend\", \"ad spend last 7/30 days\", \"campaign performance\", \"which campaign/ad performed best\", \"CTR / CPC / CPM / ROAS / cost per result\", \"conversions from ads\", \"ad performance comparison\", \"spend by placement / age / gender / country\". This is PAID-ads analytics. For organic post/content performance use meta_content_report; for the latest organic post use meta_latest_posts_summary.\n\nReturns spend, impressions, clicks, CPC/CPM/CTR, reach, frequency, actions (conversions), and video metrics — with optional demographic / placement / device breakdowns.\n\nTips for the AI consuming this:\n- Default date_preset is last_30d. Override via date_preset or time_range.\n- Set level to 'ad' and breakdowns=['publisher_platform','platform_position'] to see which placements convert.\n- Use action_breakdowns=['action_type'] to split purchases vs. add_to_cart vs. lead.\n- If you get code 613 or 17, you hit rate limits — narrow the date range or drop breakdowns.\n\nRequires 'ads_read' token scope.";
86
86
  readonly inputSchema: {
87
87
  limit: z.ZodDefault<z.ZodNumber>;
88
88
  after: z.ZodOptional<z.ZodString>;
@@ -101,16 +101,18 @@ export const inputSchema = z
101
101
  export const definition = {
102
102
  name: "meta_ads_get_insights",
103
103
  title: "Get Marketing API insights (the workhorse)",
104
- description: `Fetch Marketing API insights for any ad object: ad account ('act_<id>'), campaign, ad set, or ad.
105
-
106
- Returns spend, impressions, clicks, CPC/CPM/CTR, reach, frequency, actions (conversions), and video metrics with optional demographic / placement / device breakdowns.
107
-
108
- Tips for the AI consuming this:
109
- - Default date_preset is last_30d. Override via date_preset or time_range.
110
- - Set level to 'ad' and breakdowns=['publisher_platform','platform_position'] to see which placements convert.
111
- - Use action_breakdowns=['action_type'] to split purchases vs. add_to_cart vs. lead.
112
- - If you get code 613 or 17, you hit rate limits — narrow the date range or drop breakdowns.
113
-
104
+ description: `Fetch Marketing API insights for any ad object: ad account ('act_<id>'), campaign, ad set, or ad.
105
+
106
+ USE FOR ad/paid questions: "how much did I spend", "ad spend last 7/30 days", "campaign performance", "which campaign/ad performed best", "CTR / CPC / CPM / ROAS / cost per result", "conversions from ads", "ad performance comparison", "spend by placement / age / gender / country". This is PAID-ads analytics. For organic post/content performance use meta_content_report; for the latest organic post use meta_latest_posts_summary.
107
+
108
+ Returns spend, impressions, clicks, CPC/CPM/CTR, reach, frequency, actions (conversions), and video metrics — with optional demographic / placement / device breakdowns.
109
+
110
+ Tips for the AI consuming this:
111
+ - Default date_preset is last_30d. Override via date_preset or time_range.
112
+ - Set level to 'ad' and breakdowns=['publisher_platform','platform_position'] to see which placements convert.
113
+ - Use action_breakdowns=['action_type'] to split purchases vs. add_to_cart vs. lead.
114
+ - If you get code 613 or 17, you hit rate limits — narrow the date range or drop breakdowns.
115
+
114
116
  Requires 'ads_read' token scope.`,
115
117
  inputSchema: inputSchema.shape,
116
118
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
@@ -31,10 +31,10 @@ export const inputSchema = z
31
31
  export const definition = {
32
32
  name: "meta_ads_list_accounts",
33
33
  title: "List ad accounts under a business",
34
- description: `Lists ad accounts the business owns (owned_ad_accounts) and/or has access to (client_ad_accounts).
35
-
36
- Needs token scope 'ads_read' and the system user must have 'View performance' task on each account.
37
-
34
+ description: `Lists ad accounts the business owns (owned_ad_accounts) and/or has access to (client_ad_accounts).
35
+
36
+ Needs token scope 'ads_read' and the system user must have 'View performance' task on each account.
37
+
38
38
  Returns account_id (numeric), currency, timezone, status, balance, spend cap. Use 'act_<account_id>' when feeding downstream tools like meta_ads_get_insights.`,
39
39
  inputSchema: inputSchema.shape,
40
40
  annotations: {
@@ -34,16 +34,16 @@ export const inputSchema = z
34
34
  export const definition = {
35
35
  name: "meta_business_list_assets",
36
36
  title: "List assets assigned to a Business Manager",
37
- description: `For a given business_id, enumerates the business assets the token can see in one call:
38
- - Ad accounts (owned + optionally client)
39
- - Facebook Pages (owned + optionally client)
40
- - Instagram Business accounts
41
- - Meta Pixels
42
- - Product catalogs
43
- - WhatsApp Business accounts (WABAs)
44
-
45
- Each section is fetched as a separate Graph call. Failures on individual sections do not abort the others — the response reports per-section success/error so the assistant can work around partial failures.
46
-
37
+ description: `For a given business_id, enumerates the business assets the token can see in one call:
38
+ - Ad accounts (owned + optionally client)
39
+ - Facebook Pages (owned + optionally client)
40
+ - Instagram Business accounts
41
+ - Meta Pixels
42
+ - Product catalogs
43
+ - WhatsApp Business accounts (WABAs)
44
+
45
+ Each section is fetched as a separate Graph call. Failures on individual sections do not abort the others — the response reports per-section success/error so the assistant can work around partial failures.
46
+
47
47
  Use this to discover IDs for downstream insight tools.`,
48
48
  inputSchema: inputSchema.shape,
49
49
  annotations: {
@@ -23,10 +23,10 @@ export const inputSchema = z
23
23
  export const definition = {
24
24
  name: "meta_business_list",
25
25
  title: "List Meta businesses visible to the token",
26
- description: `Lists every Business Manager the configured access token can see — via GET /me/businesses.
27
-
28
- Typical usage: call this once at the start of a session to discover available business IDs, then pass the chosen ID to meta_business_list_assets / meta_business_list_system_users.
29
-
26
+ description: `Lists every Business Manager the configured access token can see — via GET /me/businesses.
27
+
28
+ Typical usage: call this once at the start of a session to discover available business IDs, then pass the chosen ID to meta_business_list_assets / meta_business_list_system_users.
29
+
30
30
  Respects META_ALLOWED_BUSINESS_IDS: if set, filters the response to that allowlist.`,
31
31
  inputSchema: inputSchema.shape,
32
32
  annotations: {
@@ -16,10 +16,10 @@ export const inputSchema = z
16
16
  export const definition = {
17
17
  name: "meta_business_list_system_users",
18
18
  title: "List system users attached to a business",
19
- description: `Lists the system-user (server/software) identities under a Business Manager.
20
-
21
- System users are non-human identities used for API access. This tool helps diagnose which identity owns the token in use (cross-reference with meta_token_inspect).
22
-
19
+ description: `Lists the system-user (server/software) identities under a Business Manager.
20
+
21
+ System users are non-human identities used for API access. This tool helps diagnose which identity owns the token in use (cross-reference with meta_token_inspect).
22
+
23
23
  Reads /{business_id}/system_users.`,
24
24
  inputSchema: inputSchema.shape,
25
25
  annotations: {
@@ -5,16 +5,19 @@ export declare const inputSchema: z.ZodObject<{
5
5
  metric: z.ZodDefault<z.ZodEnum<["follower_demographics", "engaged_audience_demographics", "reached_audience_demographics"]>>;
6
6
  breakdown: z.ZodDefault<z.ZodEnum<["age", "gender", "country", "city", "audience_city"]>>;
7
7
  metric_type: z.ZodDefault<z.ZodEnum<["total_value", "time_series"]>>;
8
+ period: z.ZodDefault<z.ZodEnum<["lifetime", "day"]>>;
8
9
  timeframe: z.ZodDefault<z.ZodEnum<["last_14_days", "last_30_days", "last_90_days", "prev_month", "this_month", "this_week"]>>;
9
10
  }, "strict", z.ZodTypeAny, {
10
11
  ig_user_id: string;
11
12
  metric: "follower_demographics" | "engaged_audience_demographics" | "reached_audience_demographics";
13
+ period: "day" | "lifetime";
12
14
  breakdown: "age" | "gender" | "country" | "city" | "audience_city";
13
15
  metric_type: "total_value" | "time_series";
14
16
  timeframe: "this_month" | "last_14_days" | "last_30_days" | "last_90_days" | "prev_month" | "this_week";
15
17
  }, {
16
18
  ig_user_id: string;
17
19
  metric?: "follower_demographics" | "engaged_audience_demographics" | "reached_audience_demographics" | undefined;
20
+ period?: "day" | "lifetime" | undefined;
18
21
  breakdown?: "age" | "gender" | "country" | "city" | "audience_city" | undefined;
19
22
  metric_type?: "total_value" | "time_series" | undefined;
20
23
  timeframe?: "this_month" | "last_14_days" | "last_30_days" | "last_90_days" | "prev_month" | "this_week" | undefined;
@@ -23,12 +26,13 @@ export type Input = z.infer<typeof inputSchema>;
23
26
  export declare const definition: {
24
27
  readonly name: "meta_ig_get_audience_demographics";
25
28
  readonly title: "Get IG audience demographics";
26
- readonly description: "Reads demographic breakdown of an IG Business account's followers, engaged audience, or reached audience. Use breakdown='age'|'gender'|'country'|'city' to slice.\n\n**Requirements (in order of frequency-of-failure):**\n1. Token has 'instagram_manage_insights' scope.\n2. The Hodusoft app must be approved for **Standard Access** on `instagram_manage_insights` via Meta App Review. In development tier, Meta returns `(#10) Application does not have permission for this action` on this endpoint specifically even if the scope is on the token.\n3. The IG Business account must have ≥ 100 followers (Meta hides smaller accounts' demographics for privacy).\n\nIf you see code (#10), it's almost always #2. Track the App Review request and surface the gap to the operator; this tool is functioning correctly when it returns that error.";
29
+ readonly description: "Reads demographic breakdown of an IG Business account's followers, engaged audience, or reached audience. Use breakdown='age'|'gender'|'country'|'city' to slice.\n\nUSE FOR: \"audience demographics\", \"age / gender / country / city breakdown of my followers\", \"who follows me\", \"audience profile\".\n\nRequirements:\n1. Token has 'instagram_manage_insights' scope (the insights app / META_INSIGHTS_* token).\n2. The IG Business account must have 100 followersbelow that Meta returns empty for privacy.\n3. Meta requires period='lifetime' for demographics metrics (sent automatically by this tool).\n\nIf you see '(#10) Application does not have permission', the configured app lacks instagram_manage_insights relay the hint. If you see '(#100) period is required', upgrade the server (fixed in v0.1.11+).";
27
30
  readonly inputSchema: {
28
31
  ig_user_id: z.ZodString;
29
32
  metric: z.ZodDefault<z.ZodEnum<["follower_demographics", "engaged_audience_demographics", "reached_audience_demographics"]>>;
30
33
  breakdown: z.ZodDefault<z.ZodEnum<["age", "gender", "country", "city", "audience_city"]>>;
31
34
  metric_type: z.ZodDefault<z.ZodEnum<["total_value", "time_series"]>>;
35
+ period: z.ZodDefault<z.ZodEnum<["lifetime", "day"]>>;
32
36
  timeframe: z.ZodDefault<z.ZodEnum<["last_14_days", "last_30_days", "last_90_days", "prev_month", "this_month", "this_week"]>>;
33
37
  };
34
38
  readonly annotations: {
@@ -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 { runGet } from "../shared.js";
4
+ import { runGetViaInsightsApp } from "../shared.js";
5
5
  const METRIC_TYPE = z.enum(["total_value", "time_series"]);
6
6
  const BREAKDOWN = z.enum(["age", "gender", "country", "city", "audience_city"]);
7
7
  export const inputSchema = z
@@ -17,6 +17,10 @@ export const inputSchema = z
17
17
  .describe("Which audience cohort."),
18
18
  breakdown: BREAKDOWN.default("age").describe("Demographic dimension."),
19
19
  metric_type: METRIC_TYPE.default("total_value"),
20
+ period: z
21
+ .enum(["lifetime", "day"])
22
+ .default("lifetime")
23
+ .describe("Required by Meta for demographics metrics — almost always 'lifetime'. Omitting it triggers '(#100) The parameter period is required'."),
20
24
  timeframe: z
21
25
  .enum(["last_14_days", "last_30_days", "last_90_days", "prev_month", "this_month", "this_week"])
22
26
  .default("last_30_days")
@@ -26,23 +30,26 @@ export const inputSchema = z
26
30
  export const definition = {
27
31
  name: "meta_ig_get_audience_demographics",
28
32
  title: "Get IG audience demographics",
29
- description: `Reads demographic breakdown of an IG Business account's followers, engaged audience, or reached audience. Use breakdown='age'|'gender'|'country'|'city' to slice.
30
-
31
- **Requirements (in order of frequency-of-failure):**
32
- 1. Token has 'instagram_manage_insights' scope.
33
- 2. The Hodusoft app must be approved for **Standard Access** on \`instagram_manage_insights\` via Meta App Review. In development tier, Meta returns \`(#10) Application does not have permission for this action\` on this endpoint specifically — even if the scope is on the token.
34
- 3. The IG Business account must have ≥ 100 followers (Meta hides smaller accounts' demographics for privacy).
35
-
36
- If you see code (#10), it's almost always #2. Track the App Review request and surface the gap to the operator; this tool is functioning correctly when it returns that error.`,
33
+ description: `Reads demographic breakdown of an IG Business account's followers, engaged audience, or reached audience. Use breakdown='age'|'gender'|'country'|'city' to slice.
34
+
35
+ USE FOR: "audience demographics", "age / gender / country / city breakdown of my followers", "who follows me", "audience profile".
36
+
37
+ Requirements:
38
+ 1. Token has 'instagram_manage_insights' scope (the insights app / META_INSIGHTS_* token).
39
+ 2. The IG Business account must have ≥ 100 followers — below that Meta returns empty for privacy.
40
+ 3. Meta requires period='lifetime' for demographics metrics (sent automatically by this tool).
41
+
42
+ If you see '(#10) Application does not have permission', the configured app lacks instagram_manage_insights — relay the hint. If you see '(#100) period is required', upgrade the server (fixed in v0.1.11+).`,
37
43
  inputSchema: inputSchema.shape,
38
44
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
39
45
  };
40
46
  export async function handler(input, ctx) {
41
47
  assertAllowed("ig_user", input.ig_user_id, ctx.config);
42
- return runGet(ctx, {
48
+ return runGetViaInsightsApp(ctx, {
43
49
  path: `${input.ig_user_id}/insights`,
44
50
  params: {
45
51
  metric: input.metric,
52
+ period: input.period,
46
53
  breakdown: input.breakdown,
47
54
  metric_type: input.metric_type,
48
55
  timeframe: input.timeframe,
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { metaIdSchema } from "../../helpers/schema.js";
3
- import { runGet } from "../shared.js";
3
+ import { runGetViaInsightsApp } from "../shared.js";
4
4
  export const inputSchema = z
5
5
  .object({
6
6
  media_id: metaIdSchema.describe("IG media ID."),
@@ -25,24 +25,24 @@ export const inputSchema = z
25
25
  export const definition = {
26
26
  name: "meta_ig_get_media_insights",
27
27
  title: "Get Instagram media insights",
28
- description: `Per-media metrics: reach, views, saves, likes, comments, shares, total_interactions, profile_visits, follows.
29
-
30
- **Requirements (in order of frequency-of-failure):**
31
- 1. Token has 'instagram_manage_insights' scope.
32
- 2. **The configured app must be approved for Standard Access on \`instagram_manage_insights\` via Meta App Review.** In development tier, Meta returns \`(#10) Application does not have permission for this action\` for the IG insights endpoints — even if the scope is on the token. This applies to media insights AND audience demographics. Track the App Review request and surface this to the operator; this tool is functioning correctly when it returns that error.
33
- 3. The IG account must be Business or Creator (not personal).
34
-
35
- **Metric / media-type compatibility (post v22 deprecations):**
36
- - IMAGE / CAROUSEL_ALBUM: reach, saved, likes, comments, shares, total_interactions, profile_visits
37
- - VIDEO / REELS: views, reach, likes, comments, shares, saved, total_interactions ('impressions' is deprecated, use 'views')
38
- - STORY: views, reach, replies, navigation, profile_visits, follows (impressions is deprecated; use views)
39
-
28
+ description: `Per-media metrics: reach, views, saves, likes, comments, shares, total_interactions, profile_visits, follows.
29
+
30
+ **Requirements (in order of frequency-of-failure):**
31
+ 1. Token has 'instagram_manage_insights' scope.
32
+ 2. **The configured app must be approved for Standard Access on \`instagram_manage_insights\` via Meta App Review.** In development tier, Meta returns \`(#10) Application does not have permission for this action\` for the IG insights endpoints — even if the scope is on the token. This applies to media insights AND audience demographics. Track the App Review request and surface this to the operator; this tool is functioning correctly when it returns that error.
33
+ 3. The IG account must be Business or Creator (not personal).
34
+
35
+ **Metric / media-type compatibility (post v22 deprecations):**
36
+ - IMAGE / CAROUSEL_ALBUM: reach, saved, likes, comments, shares, total_interactions, profile_visits
37
+ - VIDEO / REELS: views, reach, likes, comments, shares, saved, total_interactions ('impressions' is deprecated, use 'views')
38
+ - STORY: views, reach, replies, navigation, profile_visits, follows (impressions is deprecated; use views)
39
+
40
40
  If you see code (#100) "metric not supported for media type", narrow the metrics list to the matching subset above. If you see code (#10), it's almost always App Review (#2 above).`,
41
41
  inputSchema: inputSchema.shape,
42
42
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
43
43
  };
44
44
  export async function handler(input, ctx) {
45
- return runGet(ctx, {
45
+ return runGetViaInsightsApp(ctx, {
46
46
  path: `${input.media_id}/insights`,
47
47
  params: { metric: input.metrics.join(",") },
48
48
  });
@@ -10,8 +10,8 @@ export const inputSchema = z
10
10
  export const definition = {
11
11
  name: "meta_ig_list_accounts",
12
12
  title: "List Instagram Business accounts reachable via Pages",
13
- description: `Walks the Pages visible to the token (/me/assigned_pages) and, for each, resolves the linked Instagram Business account via 'instagram_business_account'. This is the modern discovery path — the /owned_instagram_accounts business edge is unreliable.
14
-
13
+ description: `Walks the Pages visible to the token (/me/assigned_pages) and, for each, resolves the linked Instagram Business account via 'instagram_business_account'. This is the modern discovery path — the /owned_instagram_accounts business edge is unreliable.
14
+
15
15
  Requires 'instagram_basic' scope on the token and 'Analyze Instagram account' task on each IG.`,
16
16
  inputSchema: inputSchema.shape,
17
17
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
@@ -23,13 +23,13 @@ const FORBIDDEN_PARAMS = new Set(["access_token", "appsecret_proof"]);
23
23
  export const definition = {
24
24
  name: "meta_graph_read",
25
25
  title: "Arbitrary read-only Graph API call",
26
- description: `Escape-hatch for arbitrary GET requests against the Meta Graph API.
27
-
28
- This tool exists so the assistant can reach fields and endpoints that do not yet have a dedicated tool wrapper — without ever sending a write. The underlying HTTP client is hard-wired to GET only; POST/PUT/DELETE are refused before the request leaves the process.
29
-
30
- Rules:
31
- - Do NOT include 'access_token' or 'appsecret_proof' in params — they are added automatically.
32
- - Do NOT prefix the path with the API version.
26
+ description: `Escape-hatch for arbitrary GET requests against the Meta Graph API.
27
+
28
+ This tool exists so the assistant can reach fields and endpoints that do not yet have a dedicated tool wrapper — without ever sending a write. The underlying HTTP client is hard-wired to GET only; POST/PUT/DELETE are refused before the request leaves the process.
29
+
30
+ Rules:
31
+ - Do NOT include 'access_token' or 'appsecret_proof' in params — they are added automatically.
32
+ - Do NOT prefix the path with the API version.
33
33
  - Prefer dedicated tools (meta_business_list_assets, meta_ads_get_insights, …) when they exist. Use this as a last resort.`,
34
34
  inputSchema: inputSchema.shape,
35
35
  annotations: {
@@ -21,16 +21,16 @@ export const inputSchema = z
21
21
  export const definition = {
22
22
  name: "meta_business_overview",
23
23
  title: "Eagle's-eye snapshot of a business",
24
- description: `One-call consolidated read across the whole Meta surface for a business:
25
-
26
- - Identity (system user + token app)
27
- - Assigned Pages + each Page's high-level insights (impressions, engagement, fans) for the requested window
28
- - Instagram Business accounts linked to those Pages (followers, media_count)
29
- - Owned + client ad accounts with balance, spend cap, amount spent, and last-window insights (spend, impressions, clicks, CTR, CPC, reach)
30
- - Pixels with last_fired_time (if include_pixels)
31
- - Catalogs with product counts (if include_catalogs)
32
- - WhatsApp Business Accounts + phone numbers (if include_whatsapp)
33
-
24
+ description: `One-call consolidated read across the whole Meta surface for a business:
25
+
26
+ - Identity (system user + token app)
27
+ - Assigned Pages + each Page's high-level insights (impressions, engagement, fans) for the requested window
28
+ - Instagram Business accounts linked to those Pages (followers, media_count)
29
+ - Owned + client ad accounts with balance, spend cap, amount spent, and last-window insights (spend, impressions, clicks, CTR, CPC, reach)
30
+ - Pixels with last_fired_time (if include_pixels)
31
+ - Catalogs with product counts (if include_catalogs)
32
+ - WhatsApp Business Accounts + phone numbers (if include_whatsapp)
33
+
34
34
  Each section has its own error isolation — one failing asset does not kill the report. Ideal as the opening call for any AI-driven marketing insights conversation.`,
35
35
  inputSchema: inputSchema.shape,
36
36
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },