@cashtokenai/meta-mcp-server 1.0.0
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/LICENSE +21 -0
- package/README.md +294 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +60 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +28 -0
- package/dist/context.d.ts +9 -0
- package/dist/context.js +8 -0
- package/dist/errors.d.ts +41 -0
- package/dist/errors.js +120 -0
- package/dist/helpers/cache.d.ts +6 -0
- package/dist/helpers/cache.js +28 -0
- package/dist/helpers/format.d.ts +17 -0
- package/dist/helpers/format.js +28 -0
- package/dist/helpers/graph-client.d.ts +90 -0
- package/dist/helpers/graph-client.js +219 -0
- package/dist/helpers/schema.d.ts +30 -0
- package/dist/helpers/schema.js +69 -0
- package/dist/helpers/validate.d.ts +26 -0
- package/dist/helpers/validate.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +36 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.js +18 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +34 -0
- package/dist/tools/ads/get-account.d.ts +29 -0
- package/dist/tools/ads/get-account.js +45 -0
- package/dist/tools/ads/get-creative.d.ts +29 -0
- package/dist/tools/ads/get-creative.js +37 -0
- package/dist/tools/ads/get-insights.d.ts +129 -0
- package/dist/tools/ads/get-insights.js +153 -0
- package/dist/tools/ads/list-accounts.d.ts +54 -0
- package/dist/tools/ads/list-accounts.js +59 -0
- package/dist/tools/ads/list-ads.d.ts +53 -0
- package/dist/tools/ads/list-ads.js +59 -0
- package/dist/tools/ads/list-adsets.d.ts +49 -0
- package/dist/tools/ads/list-adsets.js +54 -0
- package/dist/tools/ads/list-campaigns.d.ts +45 -0
- package/dist/tools/ads/list-campaigns.js +64 -0
- package/dist/tools/ads/list-custom-audiences.d.ts +41 -0
- package/dist/tools/ads/list-custom-audiences.js +41 -0
- package/dist/tools/business/list-assets.d.ts +37 -0
- package/dist/tools/business/list-assets.js +136 -0
- package/dist/tools/business/list-businesses.d.ts +37 -0
- package/dist/tools/business/list-businesses.js +81 -0
- package/dist/tools/business/list-system-users.d.ts +41 -0
- package/dist/tools/business/list-system-users.js +73 -0
- package/dist/tools/catalog/get-diagnostics.d.ts +29 -0
- package/dist/tools/catalog/get-diagnostics.js +26 -0
- package/dist/tools/catalog/list-products.d.ts +45 -0
- package/dist/tools/catalog/list-products.js +49 -0
- package/dist/tools/catalog/list.d.ts +54 -0
- package/dist/tools/catalog/list.js +48 -0
- package/dist/tools/instagram/get-account.d.ts +29 -0
- package/dist/tools/instagram/get-account.js +34 -0
- package/dist/tools/instagram/get-audience-demographics.d.ts +45 -0
- package/dist/tools/instagram/get-audience-demographics.js +58 -0
- package/dist/tools/instagram/get-media-insights.d.ts +29 -0
- package/dist/tools/instagram/get-media-insights.js +49 -0
- package/dist/tools/instagram/list-accounts.d.ts +33 -0
- package/dist/tools/instagram/list-accounts.js +63 -0
- package/dist/tools/instagram/list-media.d.ts +41 -0
- package/dist/tools/instagram/list-media.js +42 -0
- package/dist/tools/meta/graph-read.d.ts +33 -0
- package/dist/tools/meta/graph-read.js +71 -0
- package/dist/tools/overview/business-overview.d.ts +49 -0
- package/dist/tools/overview/business-overview.js +235 -0
- package/dist/tools/overview/content-report.d.ts +57 -0
- package/dist/tools/overview/content-report.js +344 -0
- package/dist/tools/overview/latest-posts-summary.d.ts +41 -0
- package/dist/tools/overview/latest-posts-summary.js +189 -0
- package/dist/tools/pages/get-insights.d.ts +41 -0
- package/dist/tools/pages/get-insights.js +49 -0
- package/dist/tools/pages/get-post-insights.d.ts +33 -0
- package/dist/tools/pages/get-post-insights.js +47 -0
- package/dist/tools/pages/get.d.ts +29 -0
- package/dist/tools/pages/get.js +50 -0
- package/dist/tools/pages/list-posts.d.ts +53 -0
- package/dist/tools/pages/list-posts.js +62 -0
- package/dist/tools/pages/list-reviews.d.ts +41 -0
- package/dist/tools/pages/list-reviews.js +37 -0
- package/dist/tools/pages/list-videos.d.ts +41 -0
- package/dist/tools/pages/list-videos.js +40 -0
- package/dist/tools/pages/list.d.ts +41 -0
- package/dist/tools/pages/list.js +39 -0
- package/dist/tools/pixels/get-stats.d.ts +41 -0
- package/dist/tools/pixels/get-stats.js +34 -0
- package/dist/tools/pixels/list.d.ts +41 -0
- package/dist/tools/pixels/list.js +41 -0
- package/dist/tools/register.d.ts +3 -0
- package/dist/tools/register.js +95 -0
- package/dist/tools/shared.d.ts +45 -0
- package/dist/tools/shared.js +137 -0
- package/dist/tools/token/health.d.ts +17 -0
- package/dist/tools/token/health.js +65 -0
- package/dist/tools/token/inspect.d.ts +26 -0
- package/dist/tools/token/inspect.js +88 -0
- package/dist/tools/whatsapp/get-analytics.d.ts +57 -0
- package/dist/tools/whatsapp/get-analytics.js +66 -0
- package/dist/tools/whatsapp/list-phone-numbers.d.ts +41 -0
- package/dist/tools/whatsapp/list-phone-numbers.js +35 -0
- package/dist/tools/whatsapp/list-templates.d.ts +45 -0
- package/dist/tools/whatsapp/list-templates.js +44 -0
- package/dist/tools/whatsapp/list-wabas.d.ts +54 -0
- package/dist/tools/whatsapp/list-wabas.js +48 -0
- package/dist/types/meta.d.ts +46 -0
- package/dist/types/meta.js +1 -0
- package/package.json +77 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolContext } from "../../context.js";
|
|
3
|
+
export declare const inputSchema: z.ZodObject<{
|
|
4
|
+
include_instagram: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
include_facebook: z.ZodDefault<z.ZodBoolean>;
|
|
6
|
+
max_pages: z.ZodDefault<z.ZodNumber>;
|
|
7
|
+
include_post_insights: z.ZodDefault<z.ZodBoolean>;
|
|
8
|
+
include_media_insights: z.ZodDefault<z.ZodBoolean>;
|
|
9
|
+
}, "strict", z.ZodTypeAny, {
|
|
10
|
+
max_pages: number;
|
|
11
|
+
include_instagram: boolean;
|
|
12
|
+
include_facebook: boolean;
|
|
13
|
+
include_post_insights: boolean;
|
|
14
|
+
include_media_insights: boolean;
|
|
15
|
+
}, {
|
|
16
|
+
max_pages?: number | undefined;
|
|
17
|
+
include_instagram?: boolean | undefined;
|
|
18
|
+
include_facebook?: boolean | undefined;
|
|
19
|
+
include_post_insights?: boolean | undefined;
|
|
20
|
+
include_media_insights?: boolean | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
export type Input = z.infer<typeof inputSchema>;
|
|
23
|
+
export declare const definition: {
|
|
24
|
+
readonly name: "meta_latest_posts_summary";
|
|
25
|
+
readonly title: "Latest post performance across every social account";
|
|
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
|
+
readonly inputSchema: {
|
|
28
|
+
include_instagram: z.ZodDefault<z.ZodBoolean>;
|
|
29
|
+
include_facebook: z.ZodDefault<z.ZodBoolean>;
|
|
30
|
+
max_pages: z.ZodDefault<z.ZodNumber>;
|
|
31
|
+
include_post_insights: z.ZodDefault<z.ZodBoolean>;
|
|
32
|
+
include_media_insights: z.ZodDefault<z.ZodBoolean>;
|
|
33
|
+
};
|
|
34
|
+
readonly annotations: {
|
|
35
|
+
readonly readOnlyHint: true;
|
|
36
|
+
readonly destructiveHint: false;
|
|
37
|
+
readonly idempotentHint: true;
|
|
38
|
+
readonly openWorldHint: true;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export declare function handler(input: Input, ctx: ToolContext): Promise<import("../../helpers/format.js").ToolTextResult>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MetaError } from "../../errors.js";
|
|
3
|
+
import { jsonBlock, toolResult } from "../../helpers/format.js";
|
|
4
|
+
export const inputSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
include_instagram: z
|
|
7
|
+
.boolean()
|
|
8
|
+
.default(true)
|
|
9
|
+
.describe("Walk linked Instagram Business accounts via assigned Pages and include their latest media."),
|
|
10
|
+
include_facebook: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.default(true)
|
|
13
|
+
.describe("Walk assigned Facebook Pages and include their latest post."),
|
|
14
|
+
max_pages: z
|
|
15
|
+
.number()
|
|
16
|
+
.int()
|
|
17
|
+
.min(1)
|
|
18
|
+
.max(20)
|
|
19
|
+
.default(10)
|
|
20
|
+
.describe("Cap on Pages walked. Each Page is one extra Graph call to fetch latest post + insights."),
|
|
21
|
+
include_post_insights: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.default(true)
|
|
24
|
+
.describe("Include per-post insights (impressions, reach, engaged users). Disable to halve the Graph calls."),
|
|
25
|
+
include_media_insights: z
|
|
26
|
+
.boolean()
|
|
27
|
+
.default(true)
|
|
28
|
+
.describe("Include per-IG-media insights (reach, views, likes). Disable if your app lacks instagram_manage_insights Standard Access."),
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
export const definition = {
|
|
32
|
+
name: "meta_latest_posts_summary",
|
|
33
|
+
title: "Latest post performance across every social account",
|
|
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.`,
|
|
44
|
+
inputSchema: inputSchema.shape,
|
|
45
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
46
|
+
};
|
|
47
|
+
export async function handler(input, ctx) {
|
|
48
|
+
const started = Date.now();
|
|
49
|
+
const entries = [];
|
|
50
|
+
const errors = {};
|
|
51
|
+
// Step 1: list assigned Pages (used by both Facebook and Instagram paths).
|
|
52
|
+
let pages = [];
|
|
53
|
+
try {
|
|
54
|
+
const pagesResp = await ctx.graph.get({
|
|
55
|
+
path: "me/assigned_pages",
|
|
56
|
+
params: { fields: "id,name", limit: input.max_pages },
|
|
57
|
+
});
|
|
58
|
+
pages = Array.isArray(pagesResp.data) ? pagesResp.data : [];
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
const e = err instanceof MetaError ? err : new MetaError(err.message);
|
|
62
|
+
errors["assigned_pages"] = { error: e.message, hint: e.hint };
|
|
63
|
+
}
|
|
64
|
+
// Step 2: for each Page, fetch latest published post + insights (in parallel).
|
|
65
|
+
if (input.include_facebook && pages.length > 0) {
|
|
66
|
+
const fbResults = await Promise.all(pages.map(async (p) => {
|
|
67
|
+
try {
|
|
68
|
+
const pageToken = await ctx.graph.getPageAccessToken(p.id);
|
|
69
|
+
const postsPage = await ctx.graph.get({
|
|
70
|
+
path: `${p.id}/published_posts`,
|
|
71
|
+
params: {
|
|
72
|
+
fields: "id,message,created_time,permalink_url,status_type,attachments{media_type,title,url},reactions.summary(true).limit(0),shares,comments.summary(true).limit(0)",
|
|
73
|
+
limit: 1,
|
|
74
|
+
},
|
|
75
|
+
accessTokenOverride: pageToken,
|
|
76
|
+
});
|
|
77
|
+
const latest = postsPage.data?.[0] ?? null;
|
|
78
|
+
let insights = null;
|
|
79
|
+
if (latest && input.include_post_insights) {
|
|
80
|
+
try {
|
|
81
|
+
const insResp = await ctx.graph.get({
|
|
82
|
+
path: `${latest.id}/insights`,
|
|
83
|
+
params: {
|
|
84
|
+
metric: "post_impressions,post_impressions_unique,post_engaged_users,post_clicks,post_reactions_by_type_total",
|
|
85
|
+
},
|
|
86
|
+
accessTokenOverride: pageToken,
|
|
87
|
+
});
|
|
88
|
+
insights = { data: insResp.data ?? [] };
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
const e = err instanceof MetaError ? err : new MetaError(err.message);
|
|
92
|
+
insights = { error: e.message, hint: e.hint };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
platform: "facebook",
|
|
97
|
+
page_id: p.id,
|
|
98
|
+
page_name: p.name,
|
|
99
|
+
latest_post: latest,
|
|
100
|
+
insights,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const e = err instanceof MetaError ? err : new MetaError(err.message);
|
|
105
|
+
return {
|
|
106
|
+
platform: "facebook",
|
|
107
|
+
page_id: p.id,
|
|
108
|
+
page_name: p.name,
|
|
109
|
+
latest_post: null,
|
|
110
|
+
insights: { error: e.message, hint: e.hint },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}));
|
|
114
|
+
entries.push(...fbResults);
|
|
115
|
+
}
|
|
116
|
+
// Step 3: resolve IG via each Page, then fetch latest media + insights.
|
|
117
|
+
if (input.include_instagram && pages.length > 0) {
|
|
118
|
+
const igLinks = await Promise.all(pages.map(async (p) => {
|
|
119
|
+
try {
|
|
120
|
+
const d = await ctx.graph.get({
|
|
121
|
+
path: p.id,
|
|
122
|
+
params: { fields: "id,name,instagram_business_account{id,username,name}" },
|
|
123
|
+
});
|
|
124
|
+
return d.instagram_business_account
|
|
125
|
+
? { ig: d.instagram_business_account, sourcePage: p }
|
|
126
|
+
: null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
const igAccounts = igLinks.filter((x) => x !== null);
|
|
133
|
+
const igResults = await Promise.all(igAccounts.map(async (link) => {
|
|
134
|
+
try {
|
|
135
|
+
const mediaPage = await ctx.graph.get({
|
|
136
|
+
path: `${link.ig.id}/media`,
|
|
137
|
+
params: {
|
|
138
|
+
fields: "id,media_type,media_product_type,caption,media_url,thumbnail_url,permalink,timestamp,like_count,comments_count",
|
|
139
|
+
limit: 1,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const latest = mediaPage.data?.[0] ?? null;
|
|
143
|
+
let insights = null;
|
|
144
|
+
if (latest && input.include_media_insights) {
|
|
145
|
+
try {
|
|
146
|
+
const insResp = await ctx.graph.get({
|
|
147
|
+
path: `${latest.id}/insights`,
|
|
148
|
+
params: { metric: "reach,views,likes,comments,shares,saved,total_interactions" },
|
|
149
|
+
});
|
|
150
|
+
insights = { data: insResp.data ?? [] };
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
const e = err instanceof MetaError ? err : new MetaError(err.message);
|
|
154
|
+
insights = { error: e.message, hint: e.hint };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
platform: "instagram",
|
|
159
|
+
ig_user_id: link.ig.id,
|
|
160
|
+
ig_username: link.ig.username,
|
|
161
|
+
page_id: link.sourcePage.id,
|
|
162
|
+
page_name: link.sourcePage.name,
|
|
163
|
+
latest_post: latest,
|
|
164
|
+
insights,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const e = err instanceof MetaError ? err : new MetaError(err.message);
|
|
169
|
+
return {
|
|
170
|
+
platform: "instagram",
|
|
171
|
+
ig_user_id: link.ig.id,
|
|
172
|
+
ig_username: link.ig.username,
|
|
173
|
+
latest_post: null,
|
|
174
|
+
insights: { error: e.message, hint: e.hint },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}));
|
|
178
|
+
entries.push(...igResults);
|
|
179
|
+
}
|
|
180
|
+
const structured = {
|
|
181
|
+
generated_at: new Date().toISOString(),
|
|
182
|
+
latency_ms: Date.now() - started,
|
|
183
|
+
facebook_count: entries.filter((e) => e.platform === "facebook").length,
|
|
184
|
+
instagram_count: entries.filter((e) => e.platform === "instagram").length,
|
|
185
|
+
entries,
|
|
186
|
+
errors: Object.keys(errors).length > 0 ? errors : null,
|
|
187
|
+
};
|
|
188
|
+
return toolResult(structured, jsonBlock(structured));
|
|
189
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { ToolContext } from "../../context.js";
|
|
3
|
+
export declare const inputSchema: z.ZodObject<{
|
|
4
|
+
page_id: z.ZodString;
|
|
5
|
+
metrics: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
6
|
+
period: z.ZodDefault<z.ZodEnum<["day", "week", "days_28", "lifetime", "total_over_range"]>>;
|
|
7
|
+
since: z.ZodOptional<z.ZodString>;
|
|
8
|
+
until: z.ZodOptional<z.ZodString>;
|
|
9
|
+
}, "strict", z.ZodTypeAny, {
|
|
10
|
+
page_id: string;
|
|
11
|
+
metrics: string[];
|
|
12
|
+
period: "day" | "week" | "days_28" | "lifetime" | "total_over_range";
|
|
13
|
+
since?: string | undefined;
|
|
14
|
+
until?: string | undefined;
|
|
15
|
+
}, {
|
|
16
|
+
page_id: string;
|
|
17
|
+
since?: string | undefined;
|
|
18
|
+
until?: string | undefined;
|
|
19
|
+
metrics?: string[] | undefined;
|
|
20
|
+
period?: "day" | "week" | "days_28" | "lifetime" | "total_over_range" | undefined;
|
|
21
|
+
}>;
|
|
22
|
+
export type Input = z.infer<typeof inputSchema>;
|
|
23
|
+
export declare const definition: {
|
|
24
|
+
readonly name: "meta_page_get_insights";
|
|
25
|
+
readonly title: "Get Page-level insights";
|
|
26
|
+
readonly description: "Reads Page-level metrics: impressions, reach, engagements, fan growth, page views, video views. Requires 'read_insights' scope. Pick the right period per metric — many accept only 'day' or 'days_28'.";
|
|
27
|
+
readonly inputSchema: {
|
|
28
|
+
page_id: z.ZodString;
|
|
29
|
+
metrics: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
30
|
+
period: z.ZodDefault<z.ZodEnum<["day", "week", "days_28", "lifetime", "total_over_range"]>>;
|
|
31
|
+
since: z.ZodOptional<z.ZodString>;
|
|
32
|
+
until: z.ZodOptional<z.ZodString>;
|
|
33
|
+
};
|
|
34
|
+
readonly annotations: {
|
|
35
|
+
readonly readOnlyHint: true;
|
|
36
|
+
readonly destructiveHint: false;
|
|
37
|
+
readonly idempotentHint: true;
|
|
38
|
+
readonly openWorldHint: true;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export declare function handler(input: Input, ctx: ToolContext): Promise<import("../../helpers/format.js").ToolTextResult>;
|