hermoso 0.1.157 → 0.1.158
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 +27 -8
- package/package.json +1 -1
package/mcp/tools.mjs
CHANGED
|
@@ -3293,7 +3293,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3293
3293
|
|
|
3294
3294
|
server.registerTool('list_threads_profile_posts', {
|
|
3295
3295
|
title: 'Read a public Threads account\u2019s posts',
|
|
3296
|
-
description: 'The recent PUBLIC posts of any Threads account \u2014 the raw material for a teardown, for mine_angles, and for ad copy in the language the market actually uses. Use it after threads_profile, or on its own when you already know the handle, and then USE what it returns: draft the brand\u2019s next post or plan_ad off the angles you found. AN EMPTY LIST IS NEVER PROOF THEY HAVE NOT POSTED \u2014 Meta returns nothing for a private account, for any account under 100 followers, and for handles our app is not yet approved to read; the note says which of those applies. Read-only, 0 credits. Needs Threads connected.',
|
|
3296
|
+
description: 'The recent PUBLIC posts of any Threads account \u2014 the raw material for a teardown, for mine_angles, and for ad copy in the language the market actually uses. Use it after threads_profile, or on its own when you already know the handle, and then USE what it returns: draft the brand\u2019s next post or plan_ad off the angles you found. AN EMPTY LIST IS NEVER PROOF THEY HAVE NOT POSTED \u2014 Meta returns nothing for a private account, for any account under 100 followers, and for handles our app is not yet approved to read (the `threads_profile_discovery` permission); the note says which of those applies. Read-only, 0 credits. Needs Threads connected.',
|
|
3297
3297
|
inputSchema: {
|
|
3298
3298
|
username: z.string().describe('the Threads handle \u2014 "nike", "@nike", or a threads.net profile link'),
|
|
3299
3299
|
limit: z.number().optional().describe('how many posts (1\u201350, default 25)'),
|
|
@@ -3435,8 +3435,12 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3435
3435
|
description: 'Send ONE approved WhatsApp template to MANY recipients — a customer list, a segment, a launch announcement. TEMPLATES ONLY: WhatsApp accepts free-form text only inside the 24-hour customer-service window that opens when THAT person messages the business, so a free-form broadcast is a separate bet on a separate window per person and Meta refuses each closed one. META HAS NO BULK ENDPOINT, so this is a paced fan-out — one message per recipient, about 10 a second — which is why it runs as a QUEUED JOB and returns a job id rather than a result: nothing has been sent when the call returns. Poll it by calling this tool again with `jobId` (or use get_job), and read the PER-RECIPIENT outcome; a partial failure is reported as one, never as success. PER-RECIPIENT VARIABLES ARE THE POINT: pass recipients as objects { to, components } so each person gets their own {{1}}, {{2}} values in a single broadcast. Duplicates are removed (the same number twice is a second charge for one person and trips Meta’s 6-second per-recipient limit) and unusable numbers are REFUSED BY NAME rather than silently dropped — pass skipInvalid:true to send to the rest, and they are still named in the result. RUN IT WITH dryRun:true FIRST: that costs nothing, sends nothing, and reports the exact recipient count, the template’s real category, what Meta bills for it, and whether the list fits inside the business portfolio’s moving 24-hour messaging limit. ⚠ META DOES NOT DELIVER MARKETING MESSAGES TO WHATSAPP USERS IN THE UNITED STATES AT ALL (error 131049, platform-wide since 1 April 2025) — the sends are still ACCEPTED and given message ids, so a US marketing broadcast looks like a success and is not one; the dry run says how many recipients are on +1. META BILLS THE BUSINESS, not Hermoso credits: per message delivered, at Meta’s own per-country rates, marketing always charged and utility/authentication free inside an open service window. WhatsApp policy REQUIRES the recipient’s opt-in first and nothing in the API enforces it — Meta punishes it afterwards through quality rating. Confirm-gated in code: show the user the message, the template and the recipient count, get an explicit yes, then call again with confirm:true AND confirmCount set to the exact number of distinct recipients. ACCEPTED IS NOT DELIVERED — never report a broadcast as delivered. 0 Hermoso credits.',
|
|
3436
3436
|
inputSchema: {
|
|
3437
3437
|
jobId: z.string().optional().describe('READ MODE — pass a broadcast’s job id (and nothing else) to get its status and per-recipient outcome instead of starting a new one'),
|
|
3438
|
-
|
|
3439
|
-
|
|
3438
|
+
// OPTIONAL IN THE SCHEMA, REQUIRED BY THE SERVER — and that split is the fix for a live defect, not laxity.
|
|
3439
|
+
// Declared required, the documented read mode ("pass a job id and nothing else") was UNREACHABLE: the SDK
|
|
3440
|
+
// rejected it with -32602 before the call left the client. The server still refuses a send that omits either,
|
|
3441
|
+
// by name and for free, so nothing is actually loosened.
|
|
3442
|
+
phoneNumberId: z.string().optional().describe('WHICH NUMBER IT SENDS FROM — from list_whatsapp_accounts (a WABA can have several). Required to SEND; omit only when reading a broadcast back with jobId'),
|
|
3443
|
+
template: z.string().optional().describe('an APPROVED template name. Required to SEND — a broadcast cannot be free-form text; omit only when reading a broadcast back with jobId'),
|
|
3440
3444
|
language: z.string().optional().describe('which language of that template, e.g. en_US. Omitted, Hermoso uses the first one it finds and reports which.'),
|
|
3441
3445
|
wabaId: z.string().optional().describe('the account the template is read from. Optional — it defaults to the account that owns phoneNumberId, and one naming a DIFFERENT account is refused'),
|
|
3442
3446
|
recipients: z.array(z.any()).optional().describe('the list: either phone numbers in full international form ["+14165550142", …], or objects { to, components } when each person gets different template values. Duplicates are removed and counted; invalid numbers are refused by name unless skipInvalid is true'),
|
|
@@ -3450,7 +3454,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3450
3454
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
3451
3455
|
}, wrap(async (a) => {
|
|
3452
3456
|
const d = await apiPost('/api/whatsapp/broadcast', { jobId: a.jobId, phoneNumberId: a.phoneNumberId, template: a.template, language: a.language, wabaId: a.wabaId, recipients: a.recipients, components: a.components, skipInvalid: a.skipInvalid, dryRun: a.dryRun, confirm: a.confirm, confirmCount: a.confirmCount });
|
|
3453
|
-
|
|
3457
|
+
// NEVER `undefined`: the MCP SDK validates the content block and answers `-32602 Invalid tools/call result` for a
|
|
3458
|
+
// missing `text`, which turns a SUCCESSFUL call into a protocol error the caller cannot read. The server always
|
|
3459
|
+
// sends a `note` now; this is the belt that stops a future field rename breaking the whole tool again.
|
|
3460
|
+
return ok(d.note || d.line || `WhatsApp broadcast: ${JSON.stringify(d).slice(0, 400)}`, d);
|
|
3454
3461
|
}));
|
|
3455
3462
|
|
|
3456
3463
|
server.registerTool('post_to_meta', {
|
|
@@ -15070,14 +15077,26 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
15070
15077
|
// failed must never be relayed as a problem, or a provider blip becomes a warning in every agent's context
|
|
15071
15078
|
// ([[failed-read-is-not-empty]]).
|
|
15072
15079
|
const gap = on.filter(c => c && c.scopeDrift && c.scopeDrift.status === 'missing');
|
|
15073
|
-
|
|
15080
|
+
// ── A POSTING-ONLY META CONNECTION IS A DIFFERENT CONNECTION, AND AN AGENT MUST BE TOLD ONCE (2026-08-26) ───
|
|
15081
|
+
// Meta can be connected with the full 25 permissions or with the five that publish and schedule. An agent that
|
|
15082
|
+
// reads "meta (active)" and then tries to build a campaign gets a 403 it will report as a broken tool. It is
|
|
15083
|
+
// told HERE, on the tool it already calls to ask what is connected, for the same reason the permission-gap note
|
|
15084
|
+
// above is. The sentence is the SERVER'S (`grantModeNote`) so this twin, the web card and the Studio agent
|
|
15085
|
+
// cannot word one fact three ways. `unknown` prints NOTHING — a grant we could not read must never be relayed
|
|
15086
|
+
// as a limitation ([[failed-read-is-not-empty]]).
|
|
15087
|
+
const posting = on.filter(c => c && c.grantMode === 'posting' && String(c.grantModeNote || '').trim());
|
|
15088
|
+
const lines = on.map(c => ` • ${c.provider}${c.agentLabel ? ` — ${c.agentLabel}` : ''} (${c.status || 'active'})${c.grantMode === 'posting' ? ' · POSTING ONLY' : ''}${c.scopeDrift?.status === 'missing' ? ` ⚠ missing ${c.scopeDrift.missing.length} permission(s) — needs a reconnect` : ''}`);
|
|
15089
|
+
const postingNote = posting.length
|
|
15090
|
+
? '\n\n' + posting.map(c => `ℹ ${c.grantModeNote}`).join('\n')
|
|
15091
|
+
+ '\nAds, catalog, WhatsApp and Business-portfolio tools will refuse on this connection, and that is the user\'s deliberate choice rather than a fault — do not retry them. Widening it is an OAuth consent screen, so the user does it in a browser: Workspace ▸ Connectors ▸ Meta ▸ Reconnect with everything.'
|
|
15092
|
+
: '';
|
|
15074
15093
|
const gapNote = gap.length
|
|
15075
15094
|
? `\n\n⚠ ${gap.length} connection(s) need their permissions updated:\n`
|
|
15076
15095
|
+ gap.map(c => ` • ${c.scopeDriftNote || `${c.provider}: missing ${(c.scopeDrift.missing || []).join(', ')}`}`).join('\n')
|
|
15077
15096
|
+ '\nThis is not something an agent can fix: re-authorizing is an OAuth consent screen, so the user has to do it in a browser — Workspace ▸ Connectors ▸ the connector ▸ Reconnect. Everything the connection already carries keeps working until then.'
|
|
15078
15097
|
: '';
|
|
15079
15098
|
const safe = { ...d, connectors: (d.connectors || []).map(({ accountLabel, ...c }) => c) };
|
|
15080
|
-
return ok(`${on.length} connected:\n${lines.join('\n') || ' (none)'}\nAvailable to connect: ${(d.providers || []).join(', ') || '(none configured)'}.${on.length ? '' : empty}${gapNote}`, safe);
|
|
15099
|
+
return ok(`${on.length} connected:\n${lines.join('\n') || ' (none)'}\nAvailable to connect: ${(d.providers || []).join(', ') || '(none configured)'}.${on.length ? '' : empty}${gapNote}${postingNote}`, safe);
|
|
15081
15100
|
}));
|
|
15082
15101
|
// ── CONNECTOR WRITES. Connecting needs a browser (OAuth consent) and is correctly NOT headless — but the other two
|
|
15083
15102
|
// halves of connector management are, and were web-only: DISCONNECTING, and choosing WHICH accounts a brand may
|
|
@@ -16645,9 +16664,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16645
16664
|
|
|
16646
16665
|
server.registerTool('backfill_posts', {
|
|
16647
16666
|
title: 'Import a channel’s past posts',
|
|
16648
|
-
description: "Import this brand's PAST posts from a channel into the performance record, so 'which hook works' can draw on history rather than only on what was published since Hermoso started recording. Supports facebook, instagram, threads, youtube, tiktok and
|
|
16667
|
+
description: "Import this brand's PAST posts from a channel into the performance record, so 'which hook works' can draw on history rather than only on what was published since Hermoso started recording. Supports facebook, instagram, threads, youtube, tiktok, pinterest and bluesky; the others say plainly why they cannot (LinkedIn and Reddit have no enumerate-my-posts endpoint on our grant, X bills per read so it is excluded from bulk import, Google Business has had no per-post insights since 2023, and Telegram's Bot API cannot read a chat's past messages at all — nothing published before Hermoso is recoverable through a bot token). BOUNDED, RESUMABLE AND QUOTED: it runs as a DRY RUN by default and tells you how many posts it found and what reading them will cost — pass confirm:true to import, and pass the returned cursor to continue. AN IMPORTED POST IS WEAKER EVIDENCE THAN A RECORDED ONE and is labelled 'backfilled': its hook is recovered ONLY where the post matches a Hermoso creation by asset or caption. A post made outside Hermoso stays UNATTRIBUTED — it counts toward channel and format totals but never votes on which hook works. Never guess a hook from a caption. Free.",
|
|
16649
16668
|
inputSchema: {
|
|
16650
|
-
channel: z.enum(['facebook', 'instagram', 'threads', 'youtube', 'tiktok', 'pinterest']).describe('which channel to import from'),
|
|
16669
|
+
channel: z.enum(['facebook', 'instagram', 'threads', 'youtube', 'tiktok', 'pinterest', 'bluesky']).describe('which channel to import from'),
|
|
16651
16670
|
confirm: z.boolean().optional().describe('actually import — omit for a dry run that only quotes the cost'),
|
|
16652
16671
|
limit: z.number().optional().describe('how many posts this page (default 50, max 200)'),
|
|
16653
16672
|
cursor: z.string().optional().describe('resume from a previous run'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.158",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI ad studio and marketing MCP server with 714 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",
|