hermoso 0.1.186 → 0.1.188
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/mcp/tools.mjs +26 -12
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -201,6 +201,8 @@ export const MCP_INSTRUCTIONS = [
|
|
|
201
201
|
'• CREATE finished on-brand ads: render_ad, generate_image, generate_video, generate_avatar, make_template_ad, make_thumbnail, make_explainer, plan_ad, plan_variations; get_brand / draft_brand / update_brand; list_creators / save_creator; edit_video, dub_video, clip_video, reframe_video, upscale_video, stitch_video.',
|
|
202
202
|
'• RAW MODELS, prompt only: generate_image / generate_video with useBrand:false, generate_voice, generate_text, upload_file (any local or external file becomes a URL every publish, schedule and ad tool accepts).',
|
|
203
203
|
'• PUBLISH & SCHEDULE to the user\'s OWN accounts: post_to_meta (+Threads), post_to_x, post_to_linkedin, post_to_tiktok, post_to_youtube, post_to_pinterest, post_to_reddit, post_to_bluesky, post_to_telegram, post_to_google_business; schedule_post, list_scheduled, reschedule_post, cancel_scheduled; list_connectors, list_connector_accounts, set_connector_accounts.',
|
|
204
|
+
// INSTAGRAM_PATHS_NOTE — an inline copy of lib/instagram-paths.mjs (the twins ship without lib/); tools/instagram-paths-check.mjs asserts they are byte-equal.
|
|
205
|
+
'• INSTAGRAM, two connectors, one channel: TWO WAYS AN INSTAGRAM ACCOUNT CONNECTS, SAME FEATURES: through Meta (the account is linked to a Facebook Page and comes with that Page — this is also the only path with ads) or directly through the Instagram connector (the account signs in on instagram.com by itself, no Facebook Page or Meta login — right for people who run several Instagram accounts under different logins). Either way it is one `instagram` channel with publishing, media, post and account insights, comments and Instagram Direct DMs; a Page-linked account is chosen with pageId, a direct account (or one of several) with account = an @handle or id from list_connector_accounts("instagram"). "Not connected to Meta" never means "no Instagram" — check the Instagram connector too.',
|
|
204
206
|
'• ADS on eleven platforms, their accounts and their money: create_meta_campaign / _adset / _ad, create_google_ads_campaign / _ad_group / _ad and the TikTok, LinkedIn, Pinterest, Reddit, Microsoft and OpenAI equivalents; meta_insights, google_ads_report and the per-platform reports. Everything is created PAUSED and read back before it is described.',
|
|
205
207
|
// ── YOUR ROSTER IS NOT THE PRODUCT (2026-08-26) ──────────────────────────────────────────────────────────────
|
|
206
208
|
// The roster is scoped to the accounts this workspace has connected, because a tool for an unconnected provider
|
|
@@ -1549,6 +1551,7 @@ export const INBOX_SOURCES = [
|
|
|
1549
1551
|
// /conversation route already computes it as `replyTo`) and Bluesky wants the conversation (`convoId`). Both
|
|
1550
1552
|
// routes already return exactly that field at the top level, so nothing new is computed here.
|
|
1551
1553
|
{ source: 'meta_dm', provider: 'meta', read: '/api/meta/conversation', reply: '/api/meta/message', kind: 'dm', label: 'Meta direct message', dmReplyId: 'replyTo', needs: { arg: 'conversationId', param: 'conversationId', from: 'list_meta_conversations' } },
|
|
1554
|
+
{ source: 'instagram_dm', provider: 'instagram', read: '/api/meta/conversation', reply: '/api/meta/message', kind: 'dm', label: 'Instagram direct message', dmReplyId: 'replyTo', needs: { arg: 'conversationId', param: 'conversationId', from: 'list_meta_conversations' } }, // a DIRECT (Instagram Login) account's inbox — same routes, resolved through igDmCtx by `account` (2026-09-02)
|
|
1552
1555
|
{ source: 'meta_webhook', provider: 'meta', read: '/api/meta/webhooks/events', reply: null, kind: 'pushed', label: 'Pushed Meta event' },
|
|
1553
1556
|
{ source: 'threads_mention', provider: 'threads', read: '/api/threads/mentions', reply: '/api/threads/reply', kind: 'mention', label: 'Threads mention' },
|
|
1554
1557
|
{ source: 'youtube', provider: 'youtube', read: '/api/youtube/comments', reply: '/api/youtube/reply-comment', kind: 'comment', label: 'YouTube comment', needs: { arg: 'videoId', param: 'videoId', from: 'list_youtube_videos' } },
|
|
@@ -2475,7 +2478,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
2475
2478
|
description: "EVERYTHING PEOPLE SAID TO THIS BRAND, across every connected channel, in one list: Facebook and "
|
|
2476
2479
|
+ "Instagram comments, Threads replies and mentions, YouTube and Reddit comments, Google Business reviews, "
|
|
2477
2480
|
+ "Bluesky replies and mentions, and X mentions — plus DIRECT MESSAGES on Meta (Messenger and Instagram "
|
|
2478
|
-
+ "Direct), Bluesky, X and Telegram. Use this for 'what do I need to reply to', 'any new comments', 'any new DMs', 'how are people responding'. Each "
|
|
2481
|
+
+ "Direct), Instagram, Bluesky, X and Telegram. Use this for 'what do I need to reply to', 'any new comments', 'any new DMs', 'how are people responding'. Each "
|
|
2479
2482
|
+ "item carries a composite id you hand straight to reply_to_inbox_item. A channel that is not connected is "
|
|
2480
2483
|
+ "skipped silently; a channel that FAILS to read is named in `notes` rather than dropped, so a short list is "
|
|
2481
2484
|
+ "never mistaken for a quiet week. DMs are read one conversation at a time and fold to ONE item — the newest "
|
|
@@ -2585,7 +2588,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
2585
2588
|
// EACH VENDOR ANSWERS TO A DIFFERENT KIND OF THING and that is not a detail we can smooth over: Meta's Send
|
|
2586
2589
|
// API addresses a PERSON by page-scoped id, Bluesky's addresses a CONVERSATION. `dmReplyId` is what picked
|
|
2587
2590
|
// the right one out of each read route's own answer, so the id in hand is already the correct shape.
|
|
2588
|
-
else if (t.source === 'meta_dm') payload.recipientId = t.nativeId;
|
|
2591
|
+
else if (t.source === 'meta_dm' || t.source === 'instagram_dm') payload.recipientId = t.nativeId;
|
|
2589
2592
|
else if (t.source === 'bluesky_dm') payload.convoId = t.nativeId;
|
|
2590
2593
|
else if (t.source === 'x_dm') payload.conversationId = t.nativeId;
|
|
2591
2594
|
else if (t.source === 'telegram_dm') payload.chatId = t.nativeId;
|
|
@@ -3057,12 +3060,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3057
3060
|
inputSchema: {
|
|
3058
3061
|
postId: z.string().describe('post/media id returned by post_to_meta'),
|
|
3059
3062
|
target: z.enum(['facebook', 'instagram']).optional().describe('which metric set to ask for (default facebook)'),
|
|
3063
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
3060
3064
|
pageId: z.string().optional().describe('Page id — omit when only one Page is connected'),
|
|
3061
3065
|
},
|
|
3062
3066
|
outputSchema: { postId: z.string().optional(), metrics: z.array(z.any()).optional(), note: z.string().optional() },
|
|
3063
3067
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3064
3068
|
}, wrap(async (a) => {
|
|
3065
|
-
const d = await apiGet('/api/meta/post-insights', { postId: a.postId, target: a.target, pageId: a.pageId });
|
|
3069
|
+
const d = await apiGet('/api/meta/post-insights', { postId: a.postId, target: a.target, account: a.account, pageId: a.pageId });
|
|
3066
3070
|
return ok(`${d.target} post ${d.postId}:\n${(d.metrics || []).map(m => `• ${m.name}: ${m.value ?? '— (no value returned — MISSING, not zero)'}`).join('\n') || '(no metrics)'}${d.note ? `\n${d.note}` : ''}`, d);
|
|
3067
3071
|
}));
|
|
3068
3072
|
|
|
@@ -3070,7 +3074,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3070
3074
|
// read exactly ONE of Instagram's fourteen account metrics. No new scope, no reconnect — it was simply never built.
|
|
3071
3075
|
server.registerTool('instagram_insights', {
|
|
3072
3076
|
title: 'Instagram account insights + audience demographics',
|
|
3073
|
-
description: 'ACCOUNT-level performance for the brand’s connected Instagram Business account — views, reach, accounts engaged, total interactions, likes, comments, shares, saves, profile link taps, replies, reposts and follows/unfollows — plus the AUDIENCE DEMOGRAPHICS (follower_demographics and engaged_audience_demographics, broken down by age, city, country or gender), which is the read that says WHO the content reached rather than how many. Use meta_post_insights for one post and meta_page_insights for the Facebook Page. THERE IS NO "impressions": Meta deprecated it for every API version on 2025-04-21 and replaced it with "views" — an unknown metric is refused by name rather than quietly dropped. Instagram returns NO demographics for an account under 100 followers (or under 100 engagements in the window), and an absent block means exactly that, never an empty audience. Read-only, 0 credits. Needs
|
|
3077
|
+
description: 'ACCOUNT-level performance for the brand’s connected Instagram Business account — views, reach, accounts engaged, total interactions, likes, comments, shares, saves, profile link taps, replies, reposts and follows/unfollows — plus the AUDIENCE DEMOGRAPHICS (follower_demographics and engaged_audience_demographics, broken down by age, city, country or gender), which is the read that says WHO the content reached rather than how many. Use meta_post_insights for one post and meta_page_insights for the Facebook Page. THERE IS NO "impressions": Meta deprecated it for every API version on 2025-04-21 and replaced it with "views" — an unknown metric is refused by name rather than quietly dropped. Instagram returns NO demographics for an account under 100 followers (or under 100 engagements in the window), and an absent block means exactly that, never an empty audience. Read-only, 0 credits. Needs the Instagram account connected, either through Meta (an Instagram account linked to a Facebook Page) or on its own through the Instagram connector; when several are connected, name the one you mean with account.',
|
|
3074
3078
|
inputSchema: {
|
|
3075
3079
|
metrics: z.array(z.string()).optional().describe('account metrics (default: views, reach, accounts_engaged, total_interactions, likes, comments, shares, saves, profile_links_taps). Add follower_demographics or engaged_audience_demographics for the audience, which also needs a breakdown.'),
|
|
3076
3080
|
breakdown: z.array(z.string()).optional().describe('contact_button_type / follow_type / media_product_type for account metrics; age / city / country / gender for the demographic metrics (exactly one)'),
|
|
@@ -3078,12 +3082,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3078
3082
|
period: z.enum(['day', 'week', 'days_28']).optional().describe('aggregation for reach, the one time-series metric (default day)'),
|
|
3079
3083
|
since: z.string().optional().describe('YYYY-MM-DD window start'),
|
|
3080
3084
|
until: z.string().optional().describe('YYYY-MM-DD window end'),
|
|
3085
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
3081
3086
|
pageId: z.string().optional().describe('Facebook Page id the Instagram account is linked to — omit when only one Page is connected'),
|
|
3082
3087
|
},
|
|
3083
3088
|
outputSchema: { instagramId: z.string().optional(), profile: z.any().optional(), metrics: z.array(z.any()).optional(), demographics: z.array(z.any()).optional(), note: z.string().optional() },
|
|
3084
3089
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3085
3090
|
}, wrap(async (a) => {
|
|
3086
|
-
const d = await apiGet('/api/instagram/insights', { metrics: (a.metrics || []).join(','), breakdown: (a.breakdown || []).join(','), timeframe: a.timeframe, period: a.period, since: a.since, until: a.until, pageId: a.pageId });
|
|
3091
|
+
const d = await apiGet('/api/instagram/insights', { metrics: (a.metrics || []).join(','), breakdown: (a.breakdown || []).join(','), timeframe: a.timeframe, period: a.period, since: a.since, until: a.until, account: a.account, pageId: a.pageId });
|
|
3087
3092
|
const lines = (d.metrics || []).map(m => `• ${m.name}: ${m.value ?? '— (no value returned — MISSING, not zero)'}`);
|
|
3088
3093
|
const demo = (d.demographics || []).map(x => `${x.metric} by ${(x.breakdown || []).join('/')} (${x.timeframe}):\n${(x.rows || []).map(r => ` ${r.name}: ${r.breakdowns ? JSON.stringify(r.breakdowns).slice(0, 900) : (r.value ?? '—')}`).join('\n')}`);
|
|
3089
3094
|
return ok(`Instagram ${d.profile?.username ? '@' + d.profile.username : d.instagramId}${d.profile?.followers != null ? ` · ${d.profile.followers} followers` : ''}\n${lines.join('\n') || '(no account metrics)'}${demo.length ? `\n\n${demo.join('\n\n')}` : ''}\n${d.note || ''}`, d);
|
|
@@ -3094,12 +3099,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3094
3099
|
description: 'The connected Instagram Business account’s own recent media — id, caption, media type (feed / reel / story-era), permalink, timestamp, like and comment counts. This is where the media id every other Instagram tool needs comes from: resolve “my latest reel” yourself instead of asking the user for a link, then pass the id to meta_post_insights. Read-only, 0 credits.',
|
|
3095
3100
|
inputSchema: {
|
|
3096
3101
|
limit: z.number().optional().describe('how many (1–50, default 15)'),
|
|
3102
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
3097
3103
|
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
3098
3104
|
},
|
|
3099
3105
|
outputSchema: { instagramId: z.string().optional(), count: z.number().optional(), media: z.array(z.any()).optional() },
|
|
3100
3106
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3101
3107
|
}, wrap(async (a) => {
|
|
3102
|
-
const d = await apiGet('/api/instagram/media', { limit: a.limit, pageId: a.pageId });
|
|
3108
|
+
const d = await apiGet('/api/instagram/media', { limit: a.limit, account: a.account, pageId: a.pageId });
|
|
3103
3109
|
return ok(`${d.count} Instagram post(s):\n${(d.media || []).map(m => `• [${m.media_product_type || m.media_type}] ${String(m.caption || '(no caption)').replace(/\s+/g, ' ').slice(0, 90)} — ${m.like_count ?? '—'} likes, ${m.comments_count ?? '—'} comments · ${m.timestamp || ''}\n id ${m.id}${m.permalink ? ` · ${m.permalink}` : ''}`).join('\n') || ' (none)'}`, d);
|
|
3104
3110
|
}));
|
|
3105
3111
|
|
|
@@ -3127,6 +3133,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3127
3133
|
description: 'Read the comments under a Facebook Page post or Instagram media object — customer questions, objections and the exact language real people use about the product. Good raw material for ad copy, and the first step before replying or moderating. REPLIES: a reply is a comment ON a comment, and its id exists only under its PARENT — it is never returned by the post. Each row says how many replies it has; to read them (and to get the id reply_to_meta_comment / moderate_meta_comment need), call this tool again with postId set to that COMMENT id.',
|
|
3128
3134
|
inputSchema: {
|
|
3129
3135
|
postId: z.string().describe('post/media id — or a COMMENT id, which returns that comment\u2019s replies'),
|
|
3136
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
3130
3137
|
pageId: z.string().optional().describe('Page id — omit when only one Page is connected'),
|
|
3131
3138
|
limit: z.number().optional().describe('how many comments (1–50, default 25)'),
|
|
3132
3139
|
cursor: z.string().optional().describe('the cursor from a previous call. A post with more comments than one page comes back with hasMore + a truncationNote — counts or sentiment drawn from ONE page describe a sample, not the conversation.'),
|
|
@@ -3134,7 +3141,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3134
3141
|
outputSchema: { count: z.number().optional(), comments: z.array(z.any()).optional() },
|
|
3135
3142
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3136
3143
|
}, wrap(async (a) => {
|
|
3137
|
-
const d = await apiGet('/api/meta/comments', { postId: a.postId, pageId: a.pageId, limit: a.limit, cursor: a.cursor });
|
|
3144
|
+
const d = await apiGet('/api/meta/comments', { postId: a.postId, account: a.account, pageId: a.pageId, limit: a.limit, cursor: a.cursor });
|
|
3138
3145
|
// `hidden` is TRI-STATE: true / false / null = "could not tell" (the server returns null when NEITHER
|
|
3139
3146
|
// Facebook's `is_hidden` nor Instagram's `hidden` came back). Rendering null as blank would put the same
|
|
3140
3147
|
// silent "not hidden" back one layer up, which is exactly the defect the server-side table closed.
|
|
@@ -3175,12 +3182,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3175
3182
|
inputSchema: {
|
|
3176
3183
|
commentId: z.string().describe('comment id from list_meta_comments'),
|
|
3177
3184
|
message: z.string().describe('reply text'),
|
|
3185
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
3178
3186
|
pageId: z.string().optional().describe('Page id — omit when only one Page is connected'),
|
|
3179
3187
|
},
|
|
3180
3188
|
outputSchema: { ok: z.boolean().optional(), id: z.string().optional() },
|
|
3181
3189
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
3182
3190
|
}, wrap(async (a) => {
|
|
3183
|
-
const d = await apiPost('/api/meta/comment/reply', { commentId: a.commentId, message: a.message, pageId: a.pageId });
|
|
3191
|
+
const d = await apiPost('/api/meta/comment/reply', { commentId: a.commentId, message: a.message, account: a.account, pageId: a.pageId });
|
|
3184
3192
|
return ok(`Replied to comment ${a.commentId} (${d.id}).`, d);
|
|
3185
3193
|
}));
|
|
3186
3194
|
|
|
@@ -3191,12 +3199,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3191
3199
|
commentId: z.string().describe('comment id from list_meta_comments'),
|
|
3192
3200
|
action: z.enum(['hide', 'unhide', 'delete']).optional().describe('default hide'),
|
|
3193
3201
|
confirm: z.boolean().optional().describe('required (true) only for delete'),
|
|
3202
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
3194
3203
|
pageId: z.string().optional().describe('Page id — omit when only one Page is connected'),
|
|
3195
3204
|
},
|
|
3196
3205
|
outputSchema: { ok: z.boolean().optional(), action: z.string().optional() },
|
|
3197
3206
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
3198
3207
|
}, wrap(async (a) => {
|
|
3199
|
-
const d = await apiPost('/api/meta/comment/moderate', { commentId: a.commentId, action: a.action, confirm: a.confirm, pageId: a.pageId });
|
|
3208
|
+
const d = await apiPost('/api/meta/comment/moderate', { commentId: a.commentId, action: a.action, confirm: a.confirm, account: a.account, pageId: a.pageId });
|
|
3200
3209
|
return ok(`Comment ${d.commentId}: ${d.action}d.`, d);
|
|
3201
3210
|
}));
|
|
3202
3211
|
|
|
@@ -3660,6 +3669,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3660
3669
|
link: z.string().optional().describe('a URL to attach (FB text post only)'),
|
|
3661
3670
|
collaborators: z.array(z.string()).optional().describe('INSTAGRAM COLLAB \u2014 up to 3 Instagram usernames invited to CO-AUTHOR this post. Once one accepts, the post appears on THEIR profile too, with both handles in the header and the likes and comments shared \u2014 it is how a brand reaches a creator\u2019s audience without paying for placement, and it is the single most-asked thing a scheduler normally cannot do. Pass handles only ("hermosoai"), not profile links; a leading @ is fine. INSTAGRAM ONLY \u2014 Facebook and Threads have no collab post at all and are refused BY NAME rather than silently dropping the co-authors \u2014 and never on a Story. AN INVITE IS NOT A CO-POST: publishing SENDS a request the other account must accept in their Instagram notifications, and until they do the post is on this brand\u2019s profile ALONE; they may also decline, and Instagram sends no notification either way. So never report the post as live on both accounts \u2014 read the invite status back out of the reply, and use instagram_collaborators later to find out whether they accepted.'),
|
|
3662
3671
|
target: z.enum(['facebook', 'instagram', 'threads']).optional().describe('default facebook; instagram → the Page’s linked IG; threads → the brand’s connected Threads account'),
|
|
3672
|
+
account: z.string().optional().describe('WHICH Instagram account when target is instagram and the brand has several — Page-linked and Instagram Login accounts alike; an @username or id from list_connector_accounts("instagram"). Several and none named is refused by name; omit when there is one.'),
|
|
3663
3673
|
scheduleAt: z.string().optional().describe('FACEBOOK ONLY — schedule instead of posting now. ISO timestamp (2026-08-01T09:00:00Z) or unix seconds; must be 10 minutes to 30 days ahead. Facebook holds the post and publishes it at that time, so nothing has to stay running on our side. Instagram and Threads have NO scheduling in Meta’s API — passing this for them is refused rather than silently posted immediately.'),
|
|
3664
3674
|
locationId: z.string().optional().describe('Threads only — a place id from search_threads_locations, to geotag the post to a physical location (restaurant, storefront)'),
|
|
3665
3675
|
trialReel: z.enum(['MANUAL', 'SS_PERFORMANCE']).optional().describe('INSTAGRAM TRIAL REEL \u2014 publish this Reel to NON-FOLLOWERS ONLY at first, so a hook can be tested on a cold audience without spending it on the people who already follow the brand. Instagram then shows it to followers only if it graduates. MANUAL = the creator graduates it by hand in the Instagram app; SS_PERFORMANCE = Instagram graduates it automatically if it performs well. REELS ONLY and INSTAGRAM ONLY: an image, a carousel, a Facebook post or a Threads post is REFUSED BY NAME rather than quietly published as an ordinary post \u2014 a trial that silently goes to every follower is the exact opposite of what was asked for. Omit it for a normal Reel.'),
|
|
@@ -5827,6 +5837,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5827
5837
|
platform: z.enum(['MESSENGER', 'INSTAGRAM']).optional().describe('omit to read both — Messenger and Instagram Direct are separate inboxes'),
|
|
5828
5838
|
folder: z.enum(['inbox', 'other', 'page_done', 'spam', 'pending', 'archived']).optional().describe('narrow to ONE folder. Omit to read inbox + other, which is almost always what you want. "other" IS the message-requests folder. A folder Meta does not recognise is REFUSED rather than forwarded, because Meta answers an unknown folder with the DEFAULT inbox — so a plausible-looking spelling like "requests" would hand back the ordinary inbox and be reported as "no message requests".'),
|
|
5829
5839
|
limit: z.number().optional().describe('threads per surface (1–100, default 25)'),
|
|
5840
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). The way a DIRECT (Instagram Login) account is reached; omit for the Page-linked one.'),
|
|
5830
5841
|
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
5831
5842
|
},
|
|
5832
5843
|
outputSchema: { count: z.number().optional(), requests: z.number().optional(), folders: z.array(z.string()).optional(), conversations: z.array(z.any()).optional(), unreadable: z.array(z.any()).optional(), note: z.string().optional(), window: z.string().optional() },
|
|
@@ -5847,6 +5858,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5847
5858
|
conversationId: z.string().describe('from list_meta_conversations'),
|
|
5848
5859
|
platform: z.enum(['MESSENGER', 'INSTAGRAM']).optional().describe('so the Instagram 20-message ceiling can be stated when it applies'),
|
|
5849
5860
|
limit: z.number().optional().describe('messages (1–100, default 25)'),
|
|
5861
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). The way a DIRECT (Instagram Login) account is reached; omit for the Page-linked one.'),
|
|
5850
5862
|
pageId: z.string().optional(),
|
|
5851
5863
|
},
|
|
5852
5864
|
outputSchema: { conversationId: z.string().optional(), count: z.number().optional(), messages: z.array(z.any()).optional(), replyTo: z.string().optional(), windowState: z.string().optional(), hoursLeft: z.number().nullable().optional(), window: z.string().optional(), historyNote: z.string().optional() },
|
|
@@ -5865,6 +5877,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5865
5877
|
text: z.string().describe('the reply'),
|
|
5866
5878
|
conversationId: z.string().optional().describe('strongly recommended: it is what lets the 24-hour window be checked BEFORE sending rather than discovered by a refusal'),
|
|
5867
5879
|
platform: z.enum(['MESSENGER', 'INSTAGRAM']).optional(),
|
|
5880
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). The way a DIRECT (Instagram Login) account is reached; omit for the Page-linked one.'),
|
|
5868
5881
|
pageId: z.string().optional(),
|
|
5869
5882
|
},
|
|
5870
5883
|
outputSchema: { messageId: z.string().optional(), recipientId: z.string().optional(), windowState: z.string().optional(), summary: z.string().optional() },
|
|
@@ -15742,8 +15755,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
15742
15755
|
// channel (each was its own OAuth run). There is nothing to "share" or tick — every connected account is usable —
|
|
15743
15756
|
// so `write` is null and set_connector_accounts refuses by name; add one by connecting again in the app, remove one
|
|
15744
15757
|
// with disconnect_connector({provider, account}). The listing is what post_to_* and schedule_post's `account(s)` take.
|
|
15745
|
-
...Object.fromEntries(['tiktok', 'x', 'youtube', 'threads', 'bluesky', 'telegram', 'reddit', 'pinterest'].map((p) => [p, {
|
|
15746
|
-
label: p === 'x' ? 'X' : p === 'youtube' ? 'YouTube' : p === 'tiktok' ? 'TikTok' : p.charAt(0).toUpperCase() + p.slice(1), read: `/api/connectors/${p}/accounts`, multiAccount: true,
|
|
15758
|
+
...Object.fromEntries(['tiktok', 'x', 'youtube', 'threads', 'bluesky', 'telegram', 'reddit', 'pinterest', 'instagram'].map((p) => [p, {
|
|
15759
|
+
label: p === 'x' ? 'X' : p === 'youtube' ? 'YouTube' : p === 'tiktok' ? 'TikTok' : p.charAt(0).toUpperCase() + p.slice(1), read: `/api/connectors/${p}/accounts`, multiAccount: true, // instagram: Page-linked AND Instagram Login accounts, one list (2026-09-02)
|
|
15747
15760
|
identities: (d) => (d.accounts || []).map(a => ({ id: String(a.id), name: a.handle ? '@' + a.handle : (a.label || a.id), type: 'account', selected: true, detail: [a.label && a.handle && a.label !== a.handle ? a.label : '', a.live === false ? 'RECONNECT NEEDED' : '', d.primaryAccount === a.id ? 'primary' : ''].filter(Boolean).join(' · ') })),
|
|
15748
15761
|
write: null,
|
|
15749
15762
|
}])),
|
|
@@ -17218,6 +17231,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17218
17231
|
description: "List the connected Facebook Page's or Instagram account's OWN existing posts — id, caption, permalink, publish date and format. THIS IS THE TOOL THAT GETS YOU THE postId every other Meta read needs: meta_post_insights, list_meta_comments and manage_meta_post all require one, and until now the only way to have a postId was to have just published it yourself with post_to_meta. Use it for \"how did our last few posts do\", to find a post the user describes loosely, or before backfill_posts. Pass target:'instagram' for the linked IG account (Stories are excluded — Meta's media edge does not return them); Facebook hides unpublished drafts unless you ask for them. Only ever reads a Page the brand has connected. Read-only, 0 credits.",
|
|
17219
17232
|
inputSchema: {
|
|
17220
17233
|
target: z.enum(['facebook', 'instagram']).optional().describe("default facebook; 'instagram' reads the Page's linked IG business account"),
|
|
17234
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"). Needed when several are linked, and the way an Instagram Login (standalone) account is reached; omit for the one Page-linked account.'),
|
|
17221
17235
|
pageId: z.string().optional().describe('which connected Page — omit when the brand has only one'),
|
|
17222
17236
|
limit: z.number().optional().describe('how many posts (default 25, max 100)'),
|
|
17223
17237
|
cursor: z.string().optional().describe('paging cursor returned by a previous call'),
|
|
@@ -17226,7 +17240,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17226
17240
|
outputSchema: { target: z.string().optional(), account: z.string().nullable().optional(), pageId: z.string().optional(), posts: z.array(z.any()).optional(), cursor: z.string().nullable().optional(), note: z.string().optional() },
|
|
17227
17241
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
17228
17242
|
}, wrap(async (a) => {
|
|
17229
|
-
const d = await apiGet('/api/meta/posts', { ...(a.target ? { target: a.target } : {}), ...(a.pageId ? { pageId: a.pageId } : {}), ...(a.limit ? { limit: a.limit } : {}), ...(a.cursor ? { cursor: a.cursor } : {}), ...(a.includeUnpublished ? { includeUnpublished: 'true' } : {}) });
|
|
17243
|
+
const d = await apiGet('/api/meta/posts', { ...(a.target ? { target: a.target } : {}), ...(a.account ? { account: a.account } : {}), ...(a.pageId ? { pageId: a.pageId } : {}), ...(a.limit ? { limit: a.limit } : {}), ...(a.cursor ? { cursor: a.cursor } : {}), ...(a.includeUnpublished ? { includeUnpublished: 'true' } : {}) });
|
|
17230
17244
|
const rows = (d.posts || []).map(p => `• ${String(p.caption || '(no caption)').replace(/\s+/g, ' ').slice(0, 80)} — ${p.id}${p.publishedAt ? ` · ${String(p.publishedAt).slice(0, 10)}` : ''} · ${p.mediaKind}${p.url ? ` ${p.url}` : ''}`);
|
|
17231
17245
|
if (!rows.length) return ok(`No posts on ${d.account || d.target}${d.note ? ` (${d.note})` : ''}.`, d);
|
|
17232
17246
|
return ok(`${rows.length} post(s) on ${d.account || d.target}:\n${rows.join('\n')}${d.note ? `\n${d.note}` : ''}${d.cursor ? `\nMore available — pass cursor:"${d.cursor}".` : ''}`, d);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.188",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI ad studio and marketing MCP server with 745 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
|
|
6
6
|
"type": "module",
|