hermoso 0.1.187 → 0.1.189

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 (3) hide show
  1. package/README.md +5 -0
  2. package/mcp/tools.mjs +23 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -94,6 +94,11 @@ Claude in Chrome and clients like it complete it unattended today. Everything el
94
94
  and `upgrade_plan` mint a ready-to-pay link for more credits or a bigger plan, and `billing_status` reads the
95
95
  balance any time.
96
96
 
97
+ **An agent with its own payment credential can pay with no human at all.** `POST /api/billing/machine-payment`
98
+ with `{"packId": "pack-1k"}` answers HTTP 402 carrying a `WWW-Authenticate: Payment` challenge (Stripe, through the
99
+ Machine Payments Protocol); pay the challenge and retry, and the same credit pack lands on the same balance.
100
+ `GET /api/billing/config` lists the packs under `machinePayments`. Same packs, same prices, no per-call billing.
101
+
97
102
  **The agentic path takes a paid plan.** Any of them. The free plan is there for a person signing up at
98
103
  [app.hermoso.ai](https://app.hermoso.ai), and asking for it here returns a refusal that says so. Nothing is
99
104
  created until the payment completes, so an unpaid signup leaves no account behind and charges nothing.
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 Meta connected with an Instagram Business account linked to the Page.',
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
 
@@ -5828,6 +5837,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
5828
5837
  platform: z.enum(['MESSENGER', 'INSTAGRAM']).optional().describe('omit to read both — Messenger and Instagram Direct are separate inboxes'),
5829
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".'),
5830
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.'),
5831
5841
  pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
5832
5842
  },
5833
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() },
@@ -5848,6 +5858,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
5848
5858
  conversationId: z.string().describe('from list_meta_conversations'),
5849
5859
  platform: z.enum(['MESSENGER', 'INSTAGRAM']).optional().describe('so the Instagram 20-message ceiling can be stated when it applies'),
5850
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.'),
5851
5862
  pageId: z.string().optional(),
5852
5863
  },
5853
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() },
@@ -5866,6 +5877,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
5866
5877
  text: z.string().describe('the reply'),
5867
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'),
5868
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.'),
5869
5881
  pageId: z.string().optional(),
5870
5882
  },
5871
5883
  outputSchema: { messageId: z.string().optional(), recipientId: z.string().optional(), windowState: z.string().optional(), summary: z.string().optional() },
@@ -17219,6 +17231,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17219
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.",
17220
17232
  inputSchema: {
17221
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.'),
17222
17235
  pageId: z.string().optional().describe('which connected Page — omit when the brand has only one'),
17223
17236
  limit: z.number().optional().describe('how many posts (default 25, max 100)'),
17224
17237
  cursor: z.string().optional().describe('paging cursor returned by a previous call'),
@@ -17227,7 +17240,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
17227
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() },
17228
17241
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
17229
17242
  }, wrap(async (a) => {
17230
- 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' } : {}) });
17231
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}` : ''}`);
17232
17245
  if (!rows.length) return ok(`No posts on ${d.account || d.target}${d.note ? ` (${d.note})` : ''}.`, d);
17233
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.187",
3
+ "version": "0.1.189",
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",