hermoso 0.1.486 → 0.1.488
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/README.md +2 -2
- package/mcp/openai-extensions.mjs +7 -7
- package/mcp/roster-scope.mjs +2 -2
- package/mcp/tools.mjs +171 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate image & video ad
|
|
|
5
5
|
composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
|
|
6
6
|
campaigns behind them, all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**1096 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
|
|
9
9
|
catalog with exact per-render credit costs plus the full capability map.
|
|
10
10
|
|
|
11
11
|
**No markup on models.** Stop overpaying markups on AI models. Plans: [hermoso.ai/pricing](https://hermoso.ai/pricing).
|
|
@@ -242,7 +242,7 @@ block entirely if you signed in above; it is there for CI, where the process can
|
|
|
242
242
|
|
|
243
243
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
244
244
|
|
|
245
|
-
### What the
|
|
245
|
+
### What the 1096 tools cover
|
|
246
246
|
|
|
247
247
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
248
248
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
|
@@ -184,7 +184,7 @@ export const ENTRY_ICON = Object.freeze({ src: 'data:image/svg+xml;base64,' + Bu
|
|
|
184
184
|
// BUTTONS AND ACCENT ARE OURS, NOT THE HOST'S (2026-10-03, founder, from ChatGPT dark mode): the primary button used
|
|
185
185
|
// the host's info blue (--color-text-info) with the page background as its text, which ChatGPT dark mode painted as
|
|
186
186
|
// near-black text on a mid blue. Buttons now use the Typeset tokens: --btn-bg/--btn-fg (near-black on light, near-white
|
|
187
|
-
// on dark), a hairline --ring on secondary buttons, and one
|
|
187
|
+
// on dark), a hairline --ring on secondary buttons, and one accent (#BE4420 as text on light, #FC734D on dark; the approved palette, 2026-10-31) for focus,
|
|
188
188
|
// selection and progress only. tools/chatgpt-extensions-check.mjs computes every button's text contrast in both themes.
|
|
189
189
|
// String.raw so regex backslashes in the inline script survive; nothing below uses ${} or a backtick. The one value
|
|
190
190
|
// spliced in is the bloom (%%BLOOM%%), so the hash below covers it.
|
|
@@ -196,11 +196,11 @@ const APP_STYLE = String.raw`<style>
|
|
|
196
196
|
--line:var(--color-border-secondary,light-dark(rgba(26,28,31,.1),rgba(255,255,255,.12)));
|
|
197
197
|
--card:color-mix(in oklab,var(--fg) 5%,var(--bg));
|
|
198
198
|
--card2:color-mix(in oklab,var(--fg) 9%,var(--bg));
|
|
199
|
-
--accent:light-dark(#
|
|
200
|
-
--accent-ink:light-dark(#
|
|
201
|
-
--accent-bg:light-dark(#
|
|
202
|
-
--btn-bg:light-dark(#
|
|
203
|
-
--btn-fg:light-dark(#ffffff,#
|
|
199
|
+
--accent:light-dark(#BE4420,#FC734D);
|
|
200
|
+
--accent-ink:light-dark(#BE4420,#FF8A68);
|
|
201
|
+
--accent-bg:light-dark(#FFECE6,#2A1712);
|
|
202
|
+
--btn-bg:light-dark(#02070A,#F5F7F8);
|
|
203
|
+
--btn-fg:light-dark(#ffffff,#02070A);
|
|
204
204
|
--ring:light-dark(rgba(10,10,10,.22),rgba(250,250,250,.28));
|
|
205
205
|
--ok:var(--color-text-success,light-dark(#118a3e,#62d38d));
|
|
206
206
|
--warn:var(--color-text-warning,light-dark(#b8450a,#ff8549));
|
|
@@ -1256,7 +1256,7 @@ export const neutralNote = (t) => {
|
|
|
1256
1256
|
// sweeps and drift watches queued for later (measured on a fresh local account: one `agentidle` "nudge", queued for
|
|
1257
1257
|
// tomorrow, would have shown as "1 render in progress"). Mirrors server.js JOB_TYPES_HIDDEN_FROM_V1, and the check
|
|
1258
1258
|
// fails if that list gains a type this one lacks. A job queued for LATER is not in progress either.
|
|
1259
|
-
export const PANEL_HIDDEN_JOB_TYPES = Object.freeze(['fallate', 'whatsappcoexsync', 'purge', 'agentidle', 'welcomewait', 'agentsetupmail', 'uploadretention', 'miniappretention', 'connectordrift', 'falwatch', 'faceassetsweep', 'searchvis', 'mcpcanary', 'welcomeads', 'lifecycleedu', 'connhealth', 'chargednothing', 'emailfeedback']);
|
|
1259
|
+
export const PANEL_HIDDEN_JOB_TYPES = Object.freeze(['fallate', 'storysweep', 'ttpublicurl', 'whatsappcoexsync', 'purge', 'agentidle', 'welcomewait', 'agentsetupmail', 'uploadretention', 'miniappretention', 'connectordrift', 'falwatch', 'faceassetsweep', 'searchvis', 'mcpcanary', 'welcomeads', 'lifecycleedu', 'connhealth', 'chargednothing', 'emailfeedback']);
|
|
1260
1260
|
const ageText = (h) => (h == null || !Number.isFinite(+h) ? '' : +h < 1 ? 'just now' : +h < 48 ? `${Math.round(+h)}h ago` : `${Math.round(+h / 24)}d ago`);
|
|
1261
1261
|
const VIDEO_RE = /\.(mp4|webm|mov|m4v)([?#]|$)/i;
|
|
1262
1262
|
const clip = (s, n) => { const t = String(s ?? '').replace(/\s+/g, ' ').trim(); return t.length > n ? t.slice(0, n - 1) + '…' : t; };
|
package/mcp/roster-scope.mjs
CHANGED
|
@@ -119,7 +119,7 @@ export const TOOL_PROVIDER_RULES = [
|
|
|
119
119
|
// tools/tiktok-roster-gate-check.mjs derives each tool's credential from server.js and fails when this disagrees.
|
|
120
120
|
[/^(post_to_tiktok|tiktok_post_status|tiktok_creator_info|tiktok_account|list_tiktok_videos)$/, 'tiktok'],
|
|
121
121
|
[/tiktok/, 'tiktok_ads'],
|
|
122
|
-
[/^post_to_x$|^delete_x_post$|^edit_x_post$|^post_x_article$|^send_x_dm$|^list_x_dms$|^search_x$|^list_x_blocks$|^hide_x_reply$|_x_broadcast|_x_chat|^x_chat_settings$|^x_(mentions|post|account|trends|user|search_counts|follows)/, 'x'],
|
|
122
|
+
[/^post_to_x$|^delete_x_post$|^edit_x_post$|^post_x_article$|^send_x_dm$|^list_x_dms$|^search_x$|^list_x_blocks$|^hide_x_reply$|_x_broadcast|_x_stream_source|_x_chat|^x_chat_settings$|^x_(mentions|post|account|trends|user|search_counts|follows)/, 'x'],
|
|
123
123
|
[/pinterest/, 'pinterest'],
|
|
124
124
|
[/reddit/, 'reddit'],
|
|
125
125
|
[/snapchat/, 'snapchat'],
|
|
@@ -176,7 +176,7 @@ export function toolProvider(name) {
|
|
|
176
176
|
// could connect Instagram and then had no tool to post, reply, read its inbox or measure with. Only routes verified
|
|
177
177
|
// to take the Login token are listed; a Page-only tool (ads, hashtag search, comment_on_meta_post) stays gated on meta.
|
|
178
178
|
export const INSTAGRAM_LOGIN_TOOLS = new Set([
|
|
179
|
-
'post_to_meta', 'list_meta_posts', 'list_instagram_media', 'list_meta_stories', 'get_meta_story', 'instagram_insights', 'meta_post_insights',
|
|
179
|
+
'post_to_meta', 'list_meta_posts', 'list_instagram_media', 'list_meta_stories', 'get_meta_story', 'instagram_story_insights', 'instagram_insights', 'meta_post_insights',
|
|
180
180
|
'list_meta_comments', 'reply_to_meta_comment', 'moderate_meta_comment', 'like_instagram',
|
|
181
181
|
'list_meta_conversations', 'read_meta_conversation', 'reply_to_meta_message',
|
|
182
182
|
'list_instagram_collab_invites', 'list_instagram_collab_media', 'respond_instagram_collab_invite', 'search_instagram_audio',
|
package/mcp/tools.mjs
CHANGED
|
@@ -259,7 +259,7 @@ export const CAPABILITY_MAP = [
|
|
|
259
259
|
'B) CREATE: on-brand image & video ads (real product composited in, copy + CTA baked). draft_brand / get_brand / update_brand (patch single fields without re-onboarding) / use_brand · list_brands / create_brand / delete_brand (one account holds MANY profiles: brands, clients or creators, so an agency runs every client through here; each has its own brand details, memory, swipefile, Library and connectors, and create_brand → draft_brand onboards a new one end to end) · plan_ad (concept + copy) → render_ad (the Studio quality pipeline) or generate_image / generate_video / generate_avatar (UGC creators + lip-sync) · list_creators / save_creator / delete_creator (the workspace’s REUSABLE CAST — saved creators with their portrait urls, so the SAME person stars in every ad; list them before ever generating a new one, then cast one into the ad with render_ad’s `creator`, which also skips the character-portrait render and so costs LESS than casting a stranger) · make_template_ad (native HTML ad formats) · make_motion_film (motion graphics: kinetic typography, logo intros, launch films; no video model) · clone_static / recast_motion / reframe_video / insert_scene / upscale_video / dub_video / change_voice / finish_video / fix_beat / hook_variants / stitch_video · plan_variations + score_ad (fan out + rank).',
|
|
260
260
|
'C) RAW MODEL PLAYGROUND — direct access to the full catalog (30+ image / video / voice / writing models, each with the exact per-render credit cost shown above), no ad framing: generate_image / generate_video (useBrand:false) for plain prompt-only renders, generate_voice for raw text-to-speech against any voice engine, and generate_text for the writing models (Claude / Gemini / GPT / Llama / DeepSeek…) — all against ANY catalog id.',
|
|
261
261
|
'D) ACCOUNT — hermoso_credits (balance) · billing_status (plan + your billing role) · buy_credits (one-click top-up on the saved card, or a first-purchase checkout link) · upgrade_plan / set_auto_reload (admin) · list_jobs / get_job (track async renders) · get_settings / update_settings (the LANGUAGE every ad, script and plan is written in (chat replies follow the language the user writes in) — set it once and every render obeys it, over MCP as well as in the app — plus app appearance and the weekly competitor-watch email) · list_alert_settings / set_alert_events / send_test_alert (where ALERTS go: Slack, Discord or a signed webhook, for renders, posts, approvals, competitor findings, leads and low credits) · list_team / invite_member / remove_member / set_role (who else can work in this profile).',
|
|
262
|
-
'E) PUBLISH & MANAGE YOUR CHANNELS — post, run ads, and organize files on the user’s OWN connected accounts (Settings ▸ Connectors), all driven over this MCP. Bring ANY file in with upload_file (desktop/external media, not just Hermoso renders). MANAGING THE CONNECTIONS THEMSELVES: list_connectors (what is linked, and what could be) · list_connector_accounts then set_connector_accounts (WHICH Facebook Pages / Instagram / Meta ad accounts / Google Ads customers / LinkedIn company Pages and ad accounts / Pinterest ad accounts / Microsoft Advertising accounts / Reddit ad accounts / Google Business listings / Google Analytics properties this profile may post to, spend from and read — one person often administers or has access to several belonging to different clients, only the chosen ones are usable anywhere, and an empty choice shares nothing) · connect_connector (connect a PASTE-A-KEY account from here: ' + Object.values(KEY_CONNECTORS).map((s) => s.label).join(', ') + '; offer it beside the Connectors page in the app and let the user choose, because a key pasted into a chat stays in its history) · disconnect_connector (drop a connection from this brand, without revoking access at the platform; confirm-gated because reconnecting a sign-in account needs a browser) · leave_connector (on a connector several teammates can each contribute their OWN account to, remove just YOURS — teammates’ accounts keep working and nothing is revoked at the provider). LINKING an account that connects through a provider sign-in screen (OAuth) is the one step that is not headless: hand the user its connect link, https://app.hermoso.ai/?connect=<provider>, or send them to Workspace ▸ Connectors in the app. META: list_meta_pages · instagram_insights (ACCOUNT-level Instagram performance — views, reach, accounts engaged, interactions, saves, profile link taps — plus the audience DEMOGRAPHICS by age / city / country / gender) · list_meta_stories (the Stories live right now on Instagram and Facebook Pages) / get_meta_story (one story id: live details, or that it expired) / list_instagram_media (the brand’s own recent Instagram posts, and where the media id every other Instagram tool needs comes from) · list_instagram_dm_automations / save_instagram_dm_automation / test_instagram_dm_automation / delete_instagram_dm_automation (INSTAGRAM DM AUTOMATIONS — a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply; test shows what a sample would send without sending) · list_dm_automations / save_dm_automation / test_dm_automation / delete_dm_automation (THE SAME ON EVERY CHANNEL — Instagram, Facebook Page comments and ads + Messenger, and X: a keyword mention or reply gets a public reply, a DM keyword a DM; X is billed in credits per delivered event and per send) · search_instagram_audio (licensed music and original sounds an Instagram Reel may use, by keyword or trending) · list_instagram_collab_invites then respond_instagram_collab_invite (collab-post invitations waiting on the account; accept or decline one, read back from Instagram) · list_instagram_collab_media (posts this account co-authors) · like_instagram (like a post or comment as the connected account) · post_to_meta (Facebook / Instagram / Threads) · list_meta_posts (the Page’s / Instagram account’s OWN existing posts with their ids — THIS is where the postId every other Meta read needs comes from; without it an agent that did not itself just publish has no way to name a post) · list_meta_ads + meta_insights (read existing campaigns/ad sets/ads + spend/CTR/CPC, with breakdowns by age / gender / placement / country) · preview_meta_ad (Meta renders the REAL ad per placement — a link the user can look at, valid 24h) · list_meta_pixels + create_meta_pixel (the pixel a conversion-optimised campaign REQUIRES — Meta will not let a build optimise for conversions without one, and until these existed a caller had no way to discover the id they had to pass) · estimate_meta_reach (how many people a targeting spec reaches, BEFORE a budget is committed) · list_meta_audiences / create_meta_audience (website-pixel retargeting, Page + Instagram engagement audiences, and lookalikes — creating one spends nothing) · list_meta_conversations / read_meta_conversation / reply_to_meta_message (MESSENGER AND INSTAGRAM DMs — the brand’s direct-message threads and a reply to someone who wrote first. Meta only permits a reply within 24 HOURS of the person acting, and read_meta_conversation says whether that window is open BEFORE anything is drafted; Hermoso sends replies only, never a proactive message or a message tag) · subscribe_meta_webhooks / meta_webhook_status / unsubscribe_meta_webhooks / list_meta_webhook_events (REAL-TIME EVENTS — have Meta PUSH new comments, mentions, lead-form submissions and inbound DMs to Hermoso instead of polling for them. Every other inbox read asks an edge “anything new?”; this is the only way to be TOLD, and it is how a lead arrives the moment it is submitted rather than when somebody thinks to look. An empty feed is ambiguous — check meta_webhook_status first, because an unsubscribed Page is silent and looks exactly like a quiet one) · instagram_collaborators (who ACCEPTED a Collab invite on an Instagram post — publishing only SENDS the invite, so this is the only way to know whether the post is actually live on the other account too) · list_instagram_shopping_catalogs / search_instagram_shopping_products / manage_instagram_product_tags (INSTAGRAM SHOPPING — make a post SHOPPABLE. Check eligibility and the account’s taggable catalogs, find the product ids, then pass productTags to post_to_meta so tapping the picture opens the product’s price sheet inside Instagram. Tagging needs an APPROVED Instagram Shop, so check FIRST — otherwise it fails after the media is already uploaded — and note that a tag whose product is not “approved” is stored and shown to nobody. Meta publishes no way to REMOVE a tag) · create_meta_catalog / update_meta_catalog / meta_catalog_blast_radius / delete_meta_catalog (BUILD AND RETIRE A CATALOG — create one on a named business portfolio, rename or re-point it, and, before ever proposing a delete, read meta_catalog_blast_radius: a catalog delete is PERMANENT with no archive and no undo, its product sets go with it, and any ad set still bound to one keeps spending with nothing to show) · list_meta_partnership_creators / manage_meta_partnership_creator (PARTNERSHIP ADS — the creators whose content this profile may run as an advert, and who may tag this brand as a paid partner. Two separate lists, neither implying the other, and neither defaults on; adding is a REQUEST the creator must accept, and an ad naming a creator who is only PENDING fails for a reason nothing in the error says) · list_meta_catalogs / list_meta_product_sets / list_meta_catalog_products (PRODUCT CATALOGS — the merchant’s own Meta catalogs, the product SETS inside each and the products themselves with Meta’s review status. A catalog is the input to Advantage+ catalog ads, the highest-performing ecommerce format on Meta: pass productCatalogId to create_meta_campaign and productSetId to create_meta_adset / create_meta_ad, and Meta builds every impression from the product’s own image, name and price — no render needed. An empty list is a fact about which business portfolio this login administers, NEVER about whether the merchant has a catalog) · create_meta_campaign / create_meta_ad / upload_meta_asset (build) · upload_meta_playable / list_meta_playables (PLAYABLE ads: the advertiser’s own interactive HTML game, run through create_meta_ad’s playableAssetId in an App Installs ad set) · list_meta_lead_forms / create_meta_lead_form (INSTANT LEAD FORMS — the form a lead ad opens INSIDE Facebook/Instagram instead of sending the click to a website; pass the id as create_meta_ad(objective:\"OUTCOME_LEADS\", leadFormId:…) and read the submissions with read_meta_leads) · update_meta_object / delete_meta_object / set_meta_campaign_status (edit, delete, activate — every spend + delete is confirm-gated) · delete_meta_audience (remove a custom audience or lookalike — its blast radius is the PEOPLE in it and the lookalikes built from it, which Meta refuses to delete around) · manage_meta_post (edit or delete a published post). THREADS (a separate connection from Meta, on its own API): post_to_meta(target:"threads") publishes · list_threads_posts · threads_insights · list_threads_replies / reply_to_thread / hide_thread_reply · list_threads_mentions · search_threads_keyword · repost_thread (amplify a customer’s post or one of your own to the brand’s profile — the Threads retweet, and there is NO documented un-repost) · delete_thread (confirm-gated; Threads has no EDIT at all, so delete-and-repost is the only correction) · threads_publishing_limit (how much of the rolling-24h quota is left — 250 posts, 1,000 replies, 100 DELETIONS, 500 location searches; check it before a bulk clean-up, because a quota refusal otherwise reads as a broken connection). SCHEDULING (one content calendar across every channel): schedule_post (queue a post for a future time to one or MORE channels at once — Facebook / Instagram / Threads / TikTok / YouTube / LinkedIn / X / Pinterest / Bluesky / Telegram (ten; Google Business Profile is accepted but held back on Google API access) — with per-channel captions; Hermoso publishes it at that time, nothing has to stay open — it goes LIVE PUBLICLY by default, and only stages as draft/unlisted/private if the user asks, and an impossible channel+visibility pair, an over-length caption or media the channel cannot carry is REFUSED while you are still there rather than failing hours later) · list_scheduled (what is queued and what already fired, with PER-CHANNEL outcomes) · reschedule_post (move a queued post to a new time, or change its caption, media, channels or target Page/board — send only what changes) · cancel_scheduled (pull a queued post before it goes out). POST PERFORMANCE (the loop that closes research → publish → learn — Hermoso records the HOOK and SUBJECT of everything it publishes, because those exist only at the moment of publishing and can never be recovered from a post id afterwards): list_published_posts (everything this brand has published across every channel, with the hook it was written to and its measured engagement) · post_performance (which HOOKS and SUBJECTS are getting traction — engagement rates compared WITHIN a channel and NEVER summed across them, with a verdict suppressed below 5 measured posts and the reason stated) · collect_post_metrics (pull fresh numbers ~24h and ~7d after each publish; a metric a channel cannot report is recorded ABSENT with its reason and never as zero, and X is skipped unless asked because it bills per call) · backfill_posts (import a channel’s past posts so the analysis has history — dry-run and cost-quoted first, and an imported post never votes on a hook unless it matched a Hermoso creation). YOUTUBE (publish, measure AND manage): post_to_youtube (publish a finished video to the brand’s channel — PUBLIC by default; unlisted (link-only, the ad-ready setting) or private only when the user asks) · list_youtube_videos (the channel’s OWN uploads with their video ids — call this to resolve “my latest video” yourself instead of asking the user for a link; it is where the videoId every other YouTube tool needs comes from, and it sees unlisted/private uploads a public search cannot) · update_youtube_video (retitle/re-describe/re-tag, and FLIP AN UNLISTED UPLOAD PUBLIC; confirm before going public) · delete_youtube_video (take one down for good — irreversible, so the unconfirmed call reports the video’s real title, privacy, views and comments first; use update_youtube_video(privacy:"private") when they only want it out of sight) · set_youtube_thumbnail (put a Hermoso thumbnail on an uploaded video — the biggest single lever on click-through, and YouTube otherwise picks a frame at random; needs a phone-verified channel) · update_youtube_channel (brand the CHANNEL ITSELF — banner art, description, keywords, country, the trailer non-subscribers see; everything else here brands the videos, this brands the page they sit on. It MERGES with the current settings, and it reports any field YouTube accepted but silently ignored, channel title above all) · set_youtube_watermark (the subscribe badge overlaid on EVERY video on the channel, including ones uploaded later — one square image brands the whole channel at once; the API publishes no way to read it back, so it reports accepted rather than confirmed) · list_youtube_video_stats (views, likes and comments for up to 50 videos IN ONE CALL, which is how to answer "how are my last twenty uploads doing" without one youtube_video_insights per video. It carries NO titles, because VideoStatsSnippet publishes only publishTime, so join on videoId with list_youtube_videos for names. YouTube calls this endpoint "intentionally not atomic", so a short answer is normal: the missing ids are named, and a missing id is never zero views) · youtube_video_insights (per-VIDEO views, watch time, average view PERCENTAGE/retention, likes, comments, shares, subscribers gained — the numbers that say whether a hook held; youtube_channel only gives channel-wide totals) · youtube_channel_report (the same numbers BROKEN DOWN — traffic source (search vs browse vs suggested vs shorts feed), the actual search terms, country/city, device, age+gender, subscribed vs not, and the audience-RETENTION curve showing exactly where viewers left) · list_youtube_comments + list_youtube_comment_replies + reply_to_youtube_comment (read viewer questions and objections in their own words, and answer as the channel) · moderate_youtube_comment (hide, reject, spam-report or delete an abusive comment — reject is reversible, delete is not) · list_youtube_playlists + manage_youtube_playlist + manage_youtube_playlist_items (organise the channel: create playlists, add/remove/re-order videos in them) · manage_youtube_playlist_image (a custom cover on a playlist — make_thumbnail renders the artwork, this is the call that puts it on. YouTube answers every failure here as an HTTP 500 whose real reason is buried inside it, and the tool unpacks that; if it comes back refused, check channel verification first) · manage_youtube_channel_section (the SHELVES ON THE CHANNEL HOMEPAGE — put a chosen playlist or a featured channel above YouTube’s own default layout, and re-order them. Every write is PUBLIC IMMEDIATELY, a delete has no undo, and YouTube’s own section list LAGS a write by a few seconds in both directions, so never treat a list taken straight afterwards as proof either way) · list_youtube_captions + manage_youtube_caption (real subtitle TRACKS — what YouTube indexes the video by and what a viewer toggles on, which is NOT the same as captions burned into the picture; downloading one is also the quickest way to get an existing video’s script back) · list_youtube_categories (which categoryId post_to_youtube will accept in a given country) · youtube_bulk_report (THE ONLY PLACE YOUTUBE PUBLISHES THUMBNAIL IMPRESSIONS AND THUMBNAIL CTR — a different, SCHEDULED API: the first call starts a job and returns nothing, then YouTube writes one file per day, the first within 48 hours, plus a 30-day backfill. It also carries per-card and per-end-screen metrics and an uncapped list of the search terms people arrived on) · list_youtube_report_jobs (whether that thumbnail history is already accumulating, and since when — check before promising a number) · delete_youtube_report_job (stop one; the job IS the history, so deleting it throws the accumulated files away) · youtube_channel (read title + subscriber/view/video counts for reporting). TIKTOK: post_to_tiktok (post a finished video — or a PHOTO POST, TikTok’s photo/slideshow format of 1 to 35 images where a single image is just a one-slide post —LIVE to the profile, or into TikTok drafts to review in the app) · tiktok_creator_info (the creator’s REAL privacy options — read them and let the user choose before any direct post) · tiktok_post_status (follow a post_to_tiktok that came back pending: done, failed or still processing) · tiktok_account (bio, verified status, follower/following/likes/video counts) · list_tiktok_videos (their own posts with views/likes/comments/shares — either the most recent, or specific videoIds read directly however old they are). ⚠️ TIKTOK HAS NO DELETE AND NO EDIT: its API publishes no way to remove a posted video or change its caption, privacy, cover or comment/duet/stitch settings — every one of those is fixed at publish time and there is no delete scope in TikTok’s scope catalogue at all. If the user wants a TikTok taken down or changed, say plainly that it has to be done in the TikTok app rather than hunting for a tool. TIKTOK ACCOUNT AUTHORIZATION (a SECOND, separate consent on the SAME TikTok app the TikTok Ads connection uses — holding one does NOT give you the other, so a brand fully connected for ads can still be unauthorized here, and that is a real third state rather than a broken session): tiktok_account_status (which state this profile is in, the TikTok business id, the scopes the grant carries and any MISSING from it — TikTok binds scopes at authorize time and never retroactively, so only a re-authorization picks up a new one — plus the exact URL to send the user to, because authorizing is the one step that needs a browser) · list_tiktok_comments + list_tiktok_comment_replies (the comments on the brand’s OWN posts, hidden ones included — TikTok’s answer to list_meta_comments and list_youtube_comments) · comment_on_tiktok_video · reply_to_tiktok_comment · moderate_tiktok_comment (LIKE / UNLIKE / HIDE / UNHIDE / DELETE — you can only DELETE a comment this account wrote, so HIDE is the tool for a stranger’s, and TikTok warns UNHIDE may not take effect when its own moderation is what hid it) · upload_tiktok_comment_image (a new comment will not take a raw image URL; a reply will) · set_tiktok_post_ad_authorization (THIS IS WHERE A SPARK ADS AUTHORIZATION CODE COMES FROM for the brand’s OWN post — previously a human had to copy one out of the TikTok app; hand the code to authorize_tiktok_ads_spark_post) · get_tiktok_post_ad_authorization · extend_tiktok_post_ad_authorization (the days are ADDED to what is left, not set as an absolute) · delete_tiktok_post_ad_authorization. BRAND MONITORING AND AUDIENCE, on that same account authorization (these need permissions added on 2026-08-20, so a brand that authorized before then holds a grant that predates them and has to authorize once more; tiktok_account_status names exactly which are missing, and the remedy is always to authorize the TikTok ACCOUNT again rather than to touch the advertiser connection, which is a separate grant and is unaffected): list_tiktok_mentions (public posts whose caption @-mentions the brand, TikTok’s answer to x_mentions and list_threads_mentions) · list_tiktok_mention_comments (comments whose text mentions it) · get_tiktok_mention (one mention in full, for the mentions webhook, and TikTok only keeps that data 48 hours) · tiktok_mention_top_terms (the top 20 keywords and top 20 hashtags inside those mentions) · list_tiktok_brand_hashtags + manage_tiktok_brand_hashtags + list_tiktok_brand_hashtag_posts (the hashtags TikTok counts as this brand’s, up to 50, and the posts carrying them; a new one is not counted for 24 hours and cannot be removed for 7 days) · tiktok_account_insights (follower demographics by age, gender, country and city plus the daily performance series, needing a BUSINESS account with 100+ followers, and capped at 60 days rather than the 90 the mention tools cover) · tiktok_category_benchmark (the same numbers averaged across an industry, so ‘are we ahead of our category’ is answerable). ALL OF THIS IS ORGANIC LISTENING ON THE BRAND’S OWN ACCOUNT, not ad research: for competitors’ ads use the ad-library research tools instead. TIKTOK POST INSIGHTS, also on the TikTok account authorization: tiktok_video_insights (per-POST analytics for the brand’s own posts — views, reach, average watch time, the per-second RETENTION curve, traffic sources, followers gained) · tiktok_spark_recommendations (TikTok rates the account’s organic posts HIGH / MEDIUM / LOW as Spark Ads candidates, optionally for an objective) · tiktok_hashtag_suggestions (10 hashtag ideas for a caption keyword, with each tag’s post count). TIKTOK ADS (a SEPARATE connection from the TikTok posting connector above — Settings ▸ Connectors ▸ TikTok Ads; a brand that posts to TikTok every day may still have no ad account here, so never read one as the other): list_tiktok_ads_accounts (the ADVERTISER accounts this profile can act on — every other TikTok Ads tool needs an advertiserId and this is where it comes from) · list_tiktok_ads_pixels + create_tiktok_ads_pixel + list_tiktok_ads_custom_conversions + tiktok_ads_pixel_stats (CONVERSION TRACKING — a conversion-optimised ad group dies at creation with "Please select a pixel" without one, so discover the pixel and its events BEFORE building the tree; note TikTok publishes no way to DELETE a pixel, so one you create is permanent) · list_tiktok_ads_campaigns (the whole tree — campaigns, ad groups and ads with their statuses) · tiktok_ads_report (impressions, clicks, spend, CTR, CPC, conversions and video views at any level) · list_tiktok_ads_identities (the TikTok accounts an ad may post AS — MANDATORY, with NO default: call it and let the USER pick, because the ad runs publicly under whichever account is named) · search_tiktok_ads_targeting (resolve location / interest / hashtag / language ids — an ad group cannot be created without location ids, and a guessed id targets the wrong people) · list_tiktok_ads_identity_posts (the ORGANIC posts an identity has already published — where a Spark Ad’s post id comes from) · list_tiktok_ads_spark_posts (the posts authorised for Spark Ads, i.e. promoting an organic post instead of uploading a new video) · authorize_tiktok_ads_spark_post + unbind_tiktok_ads_spark_post (add a creator’s post to that authorised set with the code they generated in the TikTok app, or release it again) · upload_tiktok_ads_playable / list_tiktok_ads_playables / delete_tiktok_ads_playable (PLAYABLE ads: the advertiser’s own interactive HTML zip, run through create_tiktok_smart_ad’s playableUrls) · upload_tiktok_ads_creative (THE STEP THAT TURNS A RENDER INTO AN AD — put a finished Hermoso video on the ad account and it hands back the videoId AND the coverImageId create_tiktok_ads_ad needs; there is no other source for either) · create_tiktok_ads_campaign → create_tiktok_ads_ad_group → create_tiktok_ads_ad (the tree) · set_tiktok_ads_budget · set_tiktok_ads_status (the ONLY switch that arms real money, confirm-gated) · delete_tiktok_ads_object (removal on TikTok is a STATUS, not a verb — the same route as set_tiktok_ads_status) · update_tiktok_ads_ad (EDIT a live ad in place — ad text, call-to-action, landing URL, UTMs, name, video and cover, carousel sound or Instant Form — instead of deleting and rebuilding it, which throws away its review history and delivery learning) · preview_tiktok_ads_ad (see an ad as it renders on TikTok, or preview a video BEFORE any ad is built, with TikTok’s own fix-it tips per placement) · tiktok_ads_review_info (TikTok’s review verdict per ad — approved, approved for only part of its targeting, or rejected — with its REASONS and suggested fix) · list_tiktok_ads_music (the ad-cleared sound a CAROUSEL ad needs) · tiktok_ads_creative_fatigue (TikTok’s own verdict on whether a creative is WEARING OUT, per day, with spend and cost per conversion — the signal to swap in a fresh variation) · tiktok_ads_video_retention (per-SECOND ad video performance: where viewers swipe away, the hook read for the next variation) · tiktok_ads_trending_hashtags + tiktok_trends (TikTok’s OWN first-party trend data: top hashtags by country, industry and period; trending pre-cleared Commercial Music Library sounds, the top videos on a sound, top search terms) · create_tiktok_smart_campaign → create_tiktok_smart_ad_group → create_tiktok_smart_ad (Smart+, TikTok’s Performance Max — born PAUSED) · list_tiktok_smart_campaigns · set_tiktok_smart_status (the Smart+ money switch, confirm-gated) · tiktok_bid_protection (the ad-credit compensation TikTok pays when a Smart+ object misses its bid) · tiktok_lead_ads_terms (read and accept TikTok’s Lead Ads terms, which a lead-generation ad needs first) · list_tiktok_ads_lead_forms + list_tiktok_ads_lead_fields + download_tiktok_ads_leads + manage_tiktok_ads_test_lead (LEAD ADS — an Instant Form is built in TikTok Ads Manager and NO API creates one, so list them to find the id a LEAD_GENERATION ad group needs. The lead REGION is required with no default: it selects which of three separate lead stores you read, and leaving it out is a THIRD value rather than “all”, so an advertiser who omits it downloads an empty file and wrongly concludes there are no leads) · list_tiktok_ads_audiences + create_tiktok_ads_audience + create_tiktok_ads_lookalike_audience + apply_tiktok_ads_audience + update_tiktok_ads_audience + delete_tiktok_ads_audience + tiktok_ads_audience_overlap (CUSTOM AUDIENCES and lookalikes — TikTok targeting is otherwise interests-and-geo only. A freshly created audience reports itself invalid for up to 48 hours BY DESIGN, so that is not a failure to retry) · list_tiktok_ads_business_centers + list_tiktok_ads_catalogs + create_tiktok_ads_catalog + list_tiktok_ads_catalog_products + list_tiktok_ads_catalog_sets + manage_tiktok_ads_catalog_feed + tiktok_ads_catalog_diagnostics (DPA / PRODUCT CATALOGS, the Shopify lane — a catalog is keyed on a BUSINESS CENTER id, NOT an advertiser id, so list the Business Centers first or every call refuses) · create_tiktok_ads_account (CREATE A NEW TIKTOK AD ACCOUNT for a business that has none, in a Business Center the user’s own login administers and in their company name; two steps, call without confirm first, and it can never be deleted) · list_tiktok_ads_apps + list_tiktok_ads_app_events (the registered apps an APP_INSTALL campaign needs — nothing else can produce an app id) · tiktok_ads_rf_inventory_estimate + create_tiktok_ads_rf_ad_group (REACH & FREQUENCY — a RESERVATION, so it is confirm-gated like a status change rather than born paused, and it needs a per-ad-account allowlist plus a signed branding contract that no endpoint reports. Always price it with the estimate first: TikTok silently books its own maximum rather than refusing an out-of-range value) · send_tiktok_ads_events (SERVER-SIDE conversion events — there is a vendor-sanctioned test code for exercising it without entering the advertiser’s real reporting), and its offline/crm sources take the event-set ids the two tools below mint) · list_tiktok_ads_offline_event_sets + manage_tiktok_ads_offline_event_set + send_tiktok_ads_offline_events (REAL-WORLD CONVERSIONS — an in-store purchase, a phone booking, a signed contract, reported so TikTok can attribute them to the ads that caused them. The timestamp is an ISO-8601 STRING here and a Unix NUMBER on send_tiktok_ads_events; a wrong-shaped one is accepted by TikTok and attributed to nothing. There is NO test code on this pair, so everything sent is a real permanent conversion — rehearse through send_tiktok_ads_events with eventSource “offline” and a testEventCode instead. Reporting also needs the connected user to be an ADMIN or OPERATOR of the advertiser, which managing the event SETS does not) · list_tiktok_ads_crm_event_sets + create_tiktok_ads_crm_event_set (LEAD-LIFECYCLE events — sending “this lead qualified / closed” back is what makes a LEAD_GENERATION campaign optimise toward leads that convert rather than form fills. TikTok publishes create and list and nothing else, so one of these is PERMANENT) · list_tiktok_tto_accounts + list_tiktok_creator_labels + discover_tiktok_creators + tiktok_creator_leaderboard + check_tiktok_creator_status + list_tiktok_tto_brand_profiles + create_tiktok_tto_brand_profile + list_tiktok_tto_campaigns + create_tiktok_tto_campaign + update_tiktok_tto_campaign + link_tiktok_tto_video + list_tiktok_tto_link_requests + tiktok_tto_campaign_report + request_tiktok_tto_spark_authorization + get_tiktok_tto_spark_authorization + manage_tiktok_tto_anchor (TIKTOK ONE / CREATOR MARKETPLACE: INFLUENCER MARKETING, and the only place in Hermoso that does it: find creators by audience size, engagement, price and who their followers actually are, check whether they have joined TikTok One, invite them to a campaign with an invite link, ask them to tag a video to it, and read every metric SPLIT ORGANIC VERSUS PAID. Its account id is a THIRD id space; not an advertiser id and not a Business Center id; so start at list_tiktok_tto_accounts. It rides this same connection with nothing extra to apply for. IT ALSO CLOSES THE SPARK ADS LOOP: request_tiktok_tto_spark_authorization asks a creator directly and get_tiktok_tto_spark_authorization returns the code authorize_tiktok_ads_spark_post takes, which is otherwise obtainable only by the creator pasting one out of the TikTok app. Two things put a notification in a real person’s inbox; a campaign invitation and a video-linking request; and a repeated linking request is a REMINDER that TikTok caps at two, so read list_tiktok_tto_link_requests before re-sending anything) · list_tiktok_ads_stores + list_tiktok_ads_store_products (TIKTOK SHOPS: what a Shopping Ads or GMV Max campaign sells from; the store list is keyed on an ad account and the product list on a BUSINESS CENTER, which each store row names) · tiktok_ads_verification_status + list_tiktok_ads_verification_documents + submit_tiktok_ads_verification (BUSINESS VERIFICATION: an unverified account hits limits that get diagnosed as something else, so it is worth reading during onboarding. Hermoso never handles a verification DOCUMENT: submitting sends account details plus the ids of images the user uploaded in TikTok Ads Manager, and the legal name and document number can never be changed afterwards, so it is confirm-gated) · list_tiktok_ads_payment_portfolios + list_tiktok_ads_payment_portfolio_links (HOW THE AD ACCOUNTS ARE FUNDED: read-only, because "why did delivery stop" is often a funding answer, and because deciding where a customer’s money sits is not ours to do) · create_tiktok_ads_rule + list_tiktok_ads_rules + update_tiktok_ads_rule + bind_tiktok_ads_rule + set_tiktok_ads_rule_status + tiktok_ads_rule_results (AUTOMATED RULES — standing instructions TikTok runs on the account unattended. THE SECOND SPEND SWITCH ON THIS PLATFORM and gated in TWO CLASSES: a rule that can only pause, decrease or email needs confirm:true, while one that can TURN_ON an object or RAISE a budget or bid needs confirm:true AND confirmScope echoing the token list_tiktok_ads_rules prints, computed from the rule as TikTok STORES it. Every rule is created TURNED OFF and read back to prove it, because TikTok publishes no way to create one in the off position. TikTok emails rule notifications to the DEVELOPER address on the app rather than to the advertiser, so tiktok_ads_rule_results is the only place a customer sees what a rule did — and TikTok itself says this endpoint is for direct advertisers and may refuse a platform-managed account entirely). · list_tiktok_ads_comments + tiktok_ads_comment_thread + moderate_tiktok_ads_comment + reply_to_tiktok_ads_comment + delete_tiktok_ads_comment (COMMENT MODERATION on your own TikTok ads — the platform where the comment section IS the ad. HIDE is the moderation verb and works on anyone’s comment and is reversible; DELETE only ever removes a comment your OWN identity posted, which TikTok reports per comment as canDelete. Comments are scoped to an AD GROUP and to nothing else, and the time window may span at most 30 DAYS, so an empty answer means “none in these 30 days” rather than “none ever”) · list_tiktok_ads_blocked_words + manage_tiktok_ads_blocked_words (a standing 500-word filter that auto-hides any comment containing one of these across EVERY ad on the account — nothing else in Hermoso does this, and removing a word republishes every comment it had hidden) · tiktok_ads_diagnosis (TikTok’s own issues-and-suggestions verdict on your ad groups — creative, bid/budget with its full estimated-delivery tables, and a pixel that has gone quiet. It covers ACTIVE ad groups only and omits any it has nothing to say about, so an empty answer is not a clean bill of health) · get_tiktok_ads_brand_safety + set_tiktok_ads_brand_safety (what content the ads may appear next to. Two things to say out loud: TikTok applies this to Smart+ campaigns and explicitly NOT to the regular campaigns create_tiktok_ads_campaign builds, and coverAllObjectives is a ONE-WAY DOOR TikTok cannot set back). TWO THINGS HERE ARE UNLIKE EVERY OTHER AD PLATFORM: TikTok creates objects ENABLED by default, so Hermoso forces every campaign, ad group and ad PAUSED with no override and nothing serves until set_tiktok_ads_status(confirm:true); and TikTok’s QPS is 1, so every call is serialized and a tree build or a bulk read is SLOW BY DESIGN — a throttle is not a broken connection. SNAPCHAT ADS (the tenth ad platform — Settings ▸ Connectors ▸ Snapchat Ads; a SEPARATE connection from Snapchat posting): list_snapchat_ads_accounts (the organizations and AD ACCOUNTS this profile can act on — every other Snapchat tool needs an adAccountId and this is where it comes from) · list_snapchat_ads_campaigns (the whole tree — campaigns, ad squads and ads) · snapchat_ads_report (impressions, spend, swipes and video quartiles at any level) · search_snapchat_ads_targeting (resolve country / region / interest / language ids — an ad squad cannot be created without at least one country) · upload_snapchat_ads_creative (put a finished render on the ad account as MEDIA and then as the CREATIVE an ad points at — Snapchat has no upload-from-URL, so Hermoso streams the bytes) · create_snapchat_ads_campaign → create_snapchat_ads_ad_squad → create_snapchat_ads_ad (the tree, every tier born PAUSED) · set_snapchat_ads_budget · set_snapchat_ads_status (the ONLY switch that arms real money, confirm-gated) · delete_snapchat_ads_object (a REAL delete verb here, unlike TikTok — irreversible, so offer PAUSED first). THREE THINGS TO SAY OUT LOUD ON THIS PLATFORM: money is MICRO-CURRENCY (1,000,000 = one unit), so quote plain amounts and let Hermoso convert, and never pass both units — under-converting fails loudly while double-converting asks for a budget a million times too large; the creative HEADLINE is capped at 34 characters and brandName at 32, far shorter than Meta or Google, and over-long copy is refused rather than truncated; and a Snapchat ad points at a CREATIVE, never at a media id. SNAPCHAT POSTING (Stories / Spotlights on a Public Profile) IS BUILT BUT NOT YET REACHABLE — Snap’s Public Profile API is allowlist-only and Hermoso has not been allowlisted, so the connector is deliberately not offered; say that plainly rather than looking for a tool. LINKEDIN: post_to_linkedin (publish a finished post to the connected LinkedIn PROFILE) · list_linkedin_pages (the company Pages this connection administers — call this first and let the USER pick, never guess a Page) · post_to_linkedin_page (publish as a company PAGE rather than a person — this is the one most brands actually want) · manage_linkedin_post (edit the copy of a published post, or delete it) \u00b7 list_linkedin_comments / reply_to_linkedin_comment / delete_linkedin_comment (moderate the comments on your Page\u2019s posts \u2014 a SEPARATE LinkedIn authorization grants these, see Connectors) · linkedin_page_analytics (ORGANIC Page performance — followers, follower gains, Page views, and post impressions/clicks/engagement, for the Page total or per post; this is the free organic read, NOT linkedin_ads_report). LINKEDIN ADS (full three-tier management): list_linkedin_ads_campaigns (ad accounts, then a chosen account’s campaign groups, campaigns and — with campaignId — the CREATIVES under them) · linkedin_ads_report (impressions, clicks, cost, conversions, leads) · search_linkedin_ads_targeting (resolve locations / titles / industries / seniorities / company sizes to the URNs LinkedIn demands — never invent one) · linkedin_audience_count (HOW MANY members that targeting actually reaches, before a budget is committed — and a returned 0 means fewer than 300 people, LinkedIn’s privacy floor and also its campaign minimum, never an empty audience) · linkedin_bid_pricing (LinkedIn’s own suggested bid and daily-budget range for that audience — quote it instead of guessing what LinkedIn costs) · create_linkedin_ads_campaign_group → create_linkedin_ads_campaign → create_linkedin_ads_creative (the tree, every tier born DRAFT) · set_linkedin_ads_budget / set_linkedin_ads_status / delete_linkedin_ads_object (budgets, activate/pause at any tier, delete — every spend change confirm-gated). LINKEDIN LEAD SYNC: list_linkedin_lead_forms · list_linkedin_leads / get_linkedin_lead (the LEADS its forms collected, answers named by field — PERSONAL DATA: show, never republish) · subscribe_linkedin_leads / list_linkedin_lead_events / list_linkedin_lead_subscriptions / delete_linkedin_lead_subscription (real-time push to Hermoso, optional forwardTo relay to a CRM; LinkedIn validates ONLY Hermoso’s own webhook). LinkedIn is a THREE-tier platform and the third tier is the one people forget: a campaign with no creative shows nothing, and all three tiers must be ACTIVE before a single impression is served. REDDIT ADS: list_reddit_ads_campaigns / reddit_ads_report (read the account tree + performance) · list_reddit_ads_profiles + list_reddit_ads_posts / create_reddit_ads_post / update_reddit_ads_post (the CREATIVE — a Reddit ad promotes a post) · create_reddit_ads_campaign / update_reddit_ads_campaign · create_reddit_ads_ad_group / update_reddit_ads_ad_group · create_reddit_ads_ad / update_reddit_ads_ad · create_reddit_ads_max_campaign / get_reddit_ads_max_template / update_reddit_ads_max_template (a Reddit MAX campaign: automated campaign, ad group and a template ad Reddit generates ads from, built from creative-library assets, all PAUSED) · set_reddit_ads_status (the ONLY switch that arms real spend, confirm-gated) · delete_reddit_ads_object (remove a campaign, ad group or ad — Reddit has no delete verb, removal is a status, and it refuses to delete anything touched in the last 3 hours) · delete_reddit_ads_saved_audience · search_reddit_ads_targeting / reddit_ads_forecast / reddit_ads_bid_suggestion (free planning) · list_reddit_ads_pixels + send_reddit_ads_conversions (conversion tracking — Reddit now requires a pixel on every ad group) · list_reddit_ads_audiences / create_reddit_ads_audience / update_reddit_ads_audience_users / delete_reddit_ads_audience (retargeting lists) · list_reddit_ads_saved_audiences / create_reddit_ads_saved_audience / update_reddit_ads_saved_audience · list_reddit_ads_lead_forms / create_reddit_ads_lead_form · reddit_ads_history (who changed what, when). TELEGRAM: post_to_telegram (publish to a channel, group or chat as the brand’s own bot — text up to 4096 characters, but only 1024 once any photo or video is attached; one image, one video, or an album of 2–10 in which photos and videos may be mixed. chatId IS ALWAYS REQUIRED and is never guessed: the Bot API publishes NO method that lists the chats a bot belongs to, so pass the public channel’s @username or the numeric id) · list_telegram_chats (chats that MESSAGED the bot in the last 24 hours — a shortcut for finding an id, NOT a roster, and a chat missing from it can still be posted to) · list_telegram_dms (what those chats actually SAID, newest per chat — free, and a rolling 24-hour window rather than an inbox: the Bot API has no history endpoint at all) · delete_telegram_message (confirm-gated; Telegram refuses once a message is more than 48 hours old). BLUESKY: post_to_bluesky (publish as the connected account — text up to 300 characters AND, separately, 3000 UTF-8 bytes, so an emoji-heavy post can be under 300 characters and still be refused; either up to 10 images OR one MP4 video, never both, because a Bluesky post record carries exactly one embed; links are made clickable automatically) · delete_bluesky_post (PERMANENTLY remove one of the account’s own posts — no trash and no undelete. Call it WITHOUT confirm first: it deletes nothing and reports the post’s real text and live like/repost/reply/quote counts, and once the post has any engagement it also wants confirmText echoing its text. Takes the AT-URI or just the record key from the bsky.app link) · list_bluesky_convos / read_bluesky_dm / send_bluesky_dm / mark_bluesky_convo_read (the account’s DIRECT MESSAGES — free, 1000 characters each, text only, and they need a PRIVILEGED app password: an ordinary one posts fine and cannot chat). Replies, mentions AND direct messages all arrive in list_inbox and are answered with reply_to_inbox_item. X / TWITTER: post_to_x (publish a post — text, an image or a video render WITH alt text, a POLL, a reply, or a whole thread, and optionally restrict who may reply; X is the ONE channel that bills per API request, a post carrying a LINK costs roughly 13× one without, and each profile has a rolling 24-hour ceiling on X spend that refuses a request whole rather than publishing half of it) · delete_x_post (remove one) · x_post_metrics (the PUBLIC counts — impressions, likes, reposts, replies, quotes, bookmarks) · x_post_insights (the ADVERTISER numbers for your own posts — link clicks, profile visits, video views and completion quartiles, up to 25 posts at once; this is what says whether a creative worked, and x_post_metrics cannot tell you, but it only sees the LAST 28 HOURS) · x_post_insights_historical (the same advertiser numbers over ANY date range — the one to use for anything older than yesterday) · x_mentions (who is talking to the brand, in their own words — the read half of the reply loop, and a source of real customer language for ad copy) · list_x_dms (the brand’s X DIRECT MESSAGES, grouped into conversations, saying which are waiting on a reply — billed per message returned, and X keeps only 30 days) · send_x_dm (reply privately to one named person; never a broadcast) · x_chat_settings / update_x_chat_settings (the X account’s DM privacy: who may message the brand, the request filter, read receipts and calls). X IS THE ONE CONNECTOR THAT COSTS CREDITS PER CALL — X charges us per API request, so posting, deleting, reading metrics, reading insights and pulling mentions each bill the user, a post CONTAINING A LINK costs 13× one without, and insights and mentions are billed PER POST RETURNED. Say so before posting a thread or pulling a big page of mentions, and prefer one post over five when the content allows. X ADS (the PAID half — a SEPARATE connection from the organic tools above: its own product on its own host with OAuth 1.0a signing, and X grants API access PER AD ACCOUNT rather than per app, so the customer adds Hermoso’s X user at business.x.com → Account access before anything here resolves): list_x_ads_accounts (the ad accounts this profile can act on, WITH the permission level held on each — read it before attempting a write) · list_x_ads_funding_instruments (a campaign cannot be created without one) · list_x_ads_campaigns / list_x_ads_line_items / list_x_ads_promoted_tweets / list_x_ads_targeting (the whole tree as it stands) · x_ads_report (impressions, clicks, spend and engagements at any level) · x_ads_geo_search / x_ads_targeting_search (resolve places and targeting values to the ids X demands — never invent one) · create_x_ads_campaign → create_x_ads_line_item → create_x_ads_promoted_tweet (the tree, every tier born PAUSED with no override; A CAMPAIGN ALONE CANNOT SERVE ON X — it needs a line item and a promoted post underneath it, and the read-back says so rather than letting you call it a finished ad) · add_x_ads_targeting · update_x_ads_campaign / update_x_ads_line_item (throttle or raise spend on a running campaign without rebuilding it) · set_x_ads_status (the ONLY switch that arms real money, confirm-gated) · delete_x_ads_object. PINTEREST — POSTING AND ADS ARE TWO SEPARATE CONNECTIONS on the same Pinterest login (Pinterest keeps ads access behind different permissions), so a brand can hold either without the other and connecting one does not connect the other; if an ads call says Pinterest Ads is not connected, that is the card to send them to, NOT the Pinterest posting one. ADS: pinterest_ads_async_report (the DEEP paid report — 914 days back where the quick one stops at 90, and three times the metric columns; generated asynchronously, so pass the returned token back rather than re-submitting) · pinterest_targeting_analytics (WHICH audience segment delivered — by keyword, interest, age, gender, location, placement) · pinterest_audience_insights (WHO the audience is: interest affinities plus demographics, the input to a creative brief rather than a performance report) · pinterest_analytics (ORGANIC performance — impressions, saves, Pin clicks, outbound clicks, for the account, the TOP PINS, the top video Pins, or one Pin; Pinterest keeps 90 days and publishes no board-level analytics at all) · create_pinterest_board (make a board — a NEW Pinterest account has none and a Pin needs one) · list_pinterest_boards (the user must pick a board — never choose one for them) · post_to_pinterest (create an image or video Pin on a chosen board, with a title, description and destination link) · list_pinterest_pins (the Pins on a board with their ids — where the pinId every Pin tool needs comes from, and it flags any Pin an ad is promoting) · update_pinterest_pin (retitle, re-describe, fix a dead link, move it — Pinterest keeps this endpoint in a limited BETA, so it may be refused outright and save_pinterest_pin is the generally-available way onto another board; a Pin’s picture can never be swapped by anyone) · save_pinterest_pin (copy a Pin onto another board) · delete_pinterest_pin (confirm-gated, and it says whether an ad is promoting the Pin first) · update_pinterest_board (rename, re-describe, or hide it — SECRET hides every Pin on the board, reversibly) · delete_pinterest_board (the heaviest one here: the board AND every Pin on it, confirm-gated with the Pin count echoed back — offer hiding it instead). GOOGLE ADS (full management): list_google_ads_campaigns (list accounts, then a customer’s campaigns + spend/CTR/CPC/conversions) · google_ads_report (any GAQL breakdown — ad groups, keywords, search terms, geo) · create_google_ads_campaign (paused) · set_google_ads_budget / set_google_ads_status (change budget, enable/pause — every spend change confirm-gated) · delete_google_ads_object (remove a campaign, ad group, ad, KEYWORD, asset LINK or conversion action — Google has no delete verb, `remove` is the terminal state and it cannot be undone; call it unconfirmed first to see the spend and the tree that go with it) · upload_google_ads_asset (add an image render or a YouTube video to the ad account’s asset library) · create_google_ads_performance_max_campaign (Google’s cross-surface campaign type, RETAIL INCLUDED — pass merchantCenterId to make it a Shopping-feed Performance Max advertising the WHOLE Merchant Center feed under one root listing group, and feedLabel to narrow it to a single feed; only PARTITIONING that feed by brand/category/custom label is refused by name) · add_google_ads_assets (sitelinks, callouts, structured snippets, call, promotion, price, lead-form, business name and logo assets, CREATED AND ATTACHED — an asset that is not attached shows nothing) · manage_google_ads_assets (list, edit, pause or detach them) · set_google_ads_tracking (UTMs / tracking template on a campaign, ad group or ad; new campaigns get standard UTMs by default) · manage_google_ads_audience + set_google_ads_audiences (remarketing lists, custom segments, affinity / in-market interests, demographics, Performance Max signals and search themes) · upload_google_ads_video (a render straight into Google Ads, no YouTube channel needed) · create_google_ads_demand_gen_campaign (YouTube Shorts / in-feed / in-stream video ads, born PAUSED) · list_google_ads_conversion_actions + create_google_ads_conversion_action (what Google counts as a result — MAXIMIZE_CONVERSIONS, TARGET_CPA, TARGET_ROAS and every Performance Max campaign are undeliverable without one, and Hermoso refuses to build them on an account that has none) · google_ads_keyword_ideas (Keyword Planner — real monthly search volume, competition and top-of-page bids; use it before choosing keywords) · google_ads_recommendations + manage_google_ads_recommendations (Google’s own advice and optimization score; apply / dismiss / auto-apply, spend-type ones only after the user’s yes) · preview_google_ads_ad (a shareable no-login preview link for a Performance Max asset group or YouTube video ad, for client approval) · google_ads_policy_issues (WHY Google disapproved or limited an ad, asset or keyword: the policy topic and the evidence) · google_ads_change_history (WHAT CHANGED ON THE ACCOUNT AND WHEN — the answer to “performance fell off a cliff on Tuesday, what happened?”. Its default source is field-level and reaches 30 days; the other source reaches 90 and is the ONLY one that sees Google Ads Editor and criterion edits, so check both before telling anyone nothing changed). GOOGLE MERCHANT CENTER (the product feed behind every Shopping ad and every free listing, on the SAME connection as Google Ads): register_merchant_developer (the ONE-TIME link between Hermoso’s Google Cloud project and the merchant’s account. Google refuses every other Merchant call until it is done, so run this first when calls are being refused) · list_merchant_accounts (which Merchant Centers this login can reach, and where the merchantCenterId every other tool needs comes from) · list_merchant_products (the feed itself, with each product’s disapprovals) · list_merchant_issues (account-level problems, the answer to "why is nothing showing at all") · merchant_issue_help + trigger_merchant_issue_action (Google’s OWN remediation steps for a problem, and the button that fires one. Several of those actions are one-shot in Google’s own words, so firing one is confirm-gated) · list_merchant_data_sources + create_merchant_data_source + delete_merchant_data_source (feeds. A product write only lands in an API-input feed, and most accounts have none until one is made, so check before writing) · upsert_merchant_product + update_merchant_product + delete_merchant_product (write the feed) · list_merchant_inventory + set_merchant_inventory (the per-STORE and per-REGION price, stock level and availability override on one product, which is what stops a Shopping ad advertising something the nearest store has sold out of. The write MERGES, because Google’s insert replaces the whole entry, and Google takes up to 30 minutes to reflect it on the product) · list_merchant_promotions + create_merchant_promotion (sale and discount badges on a listing. Google validates them asynchronously, so created is never the same as approved) · manage_merchant_notifications (Google POSTs to a URL THE MERCHANT RUNS the moment a product is disapproved, instead of someone having to poll) · merchant_account_status (WHY THE ACCOUNT IS OR IS NOT SERVING — the first thing to run when Shopping ads or free listings show nothing, and the one read that does not believe the program state: an account can report both programs ENABLED and serve in ZERO countries, because a region counts as active only where every requirement is met. It names Google’s own unmet requirements, then the settings that explain them: homepage claimed or not, business address, phone and support contact, active shipping services, return policies, terms accepted) · manage_merchant_settings (FIXES what merchant_account_status finds: shipping services merged by name so unmentioned ones are kept, return policies, business address and support contact, homepage claim, programs on or off; overwrites and removals ask first) · manage_merchant_conversion_source (WHERE MERCHANT CENTER GETS ITS CONVERSION DATA FROM, which is what free-listing and Shopping performance reporting is built on — a merchant with no conversion source sees clicks and no outcomes. Either a Google tag destination, whose MC-… id comes back only on the create and is the id the Google tag has to send conversions to, or a link to a GA4 property, which is IMMUTABLE and needs the connected Google account to be an admin there. A delete is an ARCHIVE and undelete restores it until the expiry Google reports) · merchant_quota (whether the account is simply out of daily API quota or out of product slots, which looks identical to a broken integration and is not. Google resets it at MIDDAY UTC) · merchant_report (the reports Google computes for free, including competitive visibility, best sellers and price competitiveness). MICROSOFT MERCHANT CENTER (the same job on Microsoft’s side, on the Microsoft Advertising connection): list_microsoft_merchant_stores · list_microsoft_merchant_products · upsert_microsoft_merchant_product · delete_microsoft_merchant_product · list_microsoft_merchant_issues · list_microsoft_merchant_catalogs + manage_microsoft_merchant_catalog. GOOGLE ANALYTICS (GA4 — the brand’s OWN site data, and a SEPARATE connection from Google Ads: a brand that spends on Ads every day may have no Analytics access at all, so never read one as the other): list_analytics_properties (call this FIRST — every other Analytics tool needs a NUMERIC property id, and what users actually know is the “G-XXXXXXX” Measurement ID from their tracking snippet, which no endpoint accepts; resolve it from this list rather than sending them hunting. It lists the properties SHARED WITH THIS PROFILE, not everything the Google account can see — Analytics access is handed out freely and one login often has Viewer on many clients’ properties, so the user ticks which belong to this profile and any other one is refused by name; an empty list means nothing is ticked yet, which set_connector_accounts or Settings ▸ Connectors ▸ Google Analytics ▸ Manage accounts fixes) · analytics_report (what happened — sessions, users, revenue, conversions and engagement broken down by channel, source/medium, campaign, landing page, country, device or date, i.e. the read that says whether the traffic an ad bought actually did anything) · analytics_realtime (who is on the site right now, ~30 minutes — a DIFFERENT metric set that rejects `sessions` outright, never a shortcut for analytics_report) · list_analytics_definitions (what the property already measures: its key events and its own custom dimensions, and the check to run before creating either) · create_analytics_key_event (mark an event GA4 already collects as a KEY EVENT — the 2024 rename of a conversion, and what makes it importable into Google Ads; marking an event the site never fires creates one that can never fire) · create_analytics_custom_dimension (register an event parameter the site already sends so reports can break down by it — say out loud first that a GA4 custom dimension CANNOT be deleted, only archived, and a property is capped at 50 event-scoped ones, so a typo permanently burns a slot) · list_analytics_data_streams (the streams on a property and the measurement ID (G-...) each one carries, which is what a gtag or GTM install needs and what nobody can find in the GA4 UI when asked) · get_analytics_stream_setup (the finished gtag <script> block to paste into the site — the last mile list_analytics_data_streams stops short of — plus whether enhanced measurement is really collecting scrolls, outbound clicks, site search, video, downloads and form interactions, and whether redaction is stripping campaign parameters out of recorded URLs. Web streams only. Read the master switch before believing a toggle: with enhanced measurement off for the stream, every toggle is inert whatever it says) · list_analytics_metadata (every dimension and metric this property can be asked for, including its own custom ones, which is what stops analytics_report guessing a field name) · check_analytics_compatibility (whether a dimension and metric can appear in the same report before spending a call finding out they cannot) · create_analytics_custom_metric + archive_analytics_custom_metric · archive_analytics_custom_dimension · delete_analytics_key_event (all one-way in the same sense as their create twins: archiving is not deleting and there is no un-archive) · list_analytics_google_ads_links + link_google_ads_to_analytics + unlink_google_ads_from_analytics (the join that makes a GA4 audience usable in Google Ads and a GA4 key event importable as a conversion — without it a perfectly good audience simply never appears in the ads account, with no error anywhere) · list_analytics_audiences + create_analytics_audience + archive_analytics_audience (GA4 remarketing audiences, the input to Google Ads remarketing. Archiving is one-way) · manage_analytics_measurement_protocol_secret (mint the API secret that lets the customer’s OWN SERVER send events straight into GA4, the Google twin of the conversions APIs already here for Reddit, Snapchat and OpenAI Ads. Say out loud that there is NO rotation anywhere in the API, so replacing a secret means create the new one, move every sender across, then delete the old one) · manage_analytics_channel_group (HOW GA4 BUCKETS TRAFFIC — the answer to “why is my campaign showing as Unassigned”, and the one number an ad studio is judged on. Read the Default channel group’s rules before diagnosing anything, then author your own group whose channels catch the campaigns Hermoso publishes. The rule fields are the eachScope… names, NOT the sessionSource / medium dimensions reports use, and GA4 stops at the first rule that matches so order decides everything) · manage_analytics_calculated_metric (the derived number a marketer actually reports — cost per purchase, revenue per session — built from metrics GA4 already collects and then available to analytics_report under its own permanent API name. The id is permanent, and a formula naming a metric the property does not collect is created happily and flagged invalid, so read that flag back) · manage_analytics_annotations (mark a launch or a post on the customer’s own GA4 charts, so the traffic it moved shows where they look) · analytics_data_retention (how long GA4 keeps event data — the 2-month default caps every exploration; lowering it deletes data, so it is confirm-gated). MICROSOFT ADVERTISING / BING ADS (full management, mirroring Google): list_microsoft_ads_campaigns (list the shared ad accounts, then a chosen account’s campaigns + budgets) · microsoft_ads_geo_search (resolve country / region / city names to the Microsoft location ids a campaign needs — call it when an ask is ambiguous and let the USER pick) · microsoft_ads_report (impressions, clicks, CTR, average CPC, spend, conversions — generated asynchronously, so it may come back pending and must be called again) · create_microsoft_ads_campaign (campaign → ad group → responsive search ad → keywords, always Paused; with no locations[] it is created serving WORLDWIDE, Microsoft’s own default, and the read-back warns loudly — relay that before anyone activates it) · create_microsoft_ads_ad_group / create_microsoft_ads_ad / add_microsoft_ads_keywords (fill in an existing account) · set_microsoft_ads_budget / set_microsoft_ads_status (change budget, activate/pause — every spend change confirm-gated; Microsoft statuses are Active/Paused, never Deleted) · search_microsoft_ads_profiles / list_microsoft_ads_profile_targeting / set_microsoft_ads_profile_targeting (LinkedIn profile targeting: company, industry, job function, seniority and job title bid adjustments on a Search, Shopping or DSA campaign; confirm-gated on an ACTIVE campaign) · delete_microsoft_ads_object (a REAL delete — campaign, ad group, ad or keyword — permanent, with no undelete; call it unconfirmed first to see what goes with it) · set_microsoft_ads_bidding (change a campaign’s BID STRATEGY — EnhancedCpc, MaxClicks, MaxConversions with an optional target CPA, MaxConversionValue with an optional target ROAS, TargetImpressionShare — read back) · list_microsoft_ads_negative_keywords + add_microsoft_ads_negative_keywords + remove_microsoft_ads_negative_keywords + manage_microsoft_ads_negative_keyword_list (NEGATIVE keywords: the searches a campaign or ad group must NOT pay for, Exact or Phrase only, and SHARED negative lists reused across campaigns; removing one on a LIVE campaign lets those searches trigger paid ads again, so it is confirm-gated) · list_microsoft_ads_uet_tags + create_microsoft_ads_uet_tag (the UET site tag and its install snippet — conversion goals, remarketing and conversion bidding all need one) · create_microsoft_ads_conversion_goal (Url / Event / OfflineConversion / Duration / PagesViewedPerVisit goals, read back) · list_microsoft_ads_audience_targeting + set_microsoft_ads_audience_targeting (put an audience — a customer list, remarketing, in-market — to work on a campaign or ad group, or EXCLUDE it, with a bid adjustment) · create_microsoft_ads_image_ad (turn a render into an IMAGE ad on the Microsoft Audience Network or a multimedia ad on Search; the picture is cropped to the 1.91:1 and 1:1 sizes Microsoft requires) · upload_microsoft_ads_video (put a render on the account as a video asset, or check one still processing; Microsoft refuses clips under 6 seconds) · microsoft_ads_keyword_ideas (Microsoft’s Keyword Planner — real search volume, competition and suggested bids, with NO planning-tier gate, unlike Google’s) · microsoft_ads_traffic_estimates (what those keywords would deliver at a named bid — a range, never one number) · microsoft_ads_budget_opportunities (where Microsoft says a budget is capping delivery, and what raising it is forecast to buy) · microsoft_ads_auction_insights (who ELSE is bidding on the same auctions — rival domains with their impression share, overlap and outranking share; shares of YOUR auctions, never a measure of a competitor’s whole account) · microsoft_ads_bulk_download (export the account as ONE bulk file — the only way to read ~185 Microsoft record types Hermoso cannot otherwise touch: sitelinks, callouts, structured snippets, labels, shared negative keyword lists, bid strategies, audiences, experiments, seasonality adjustments, conversion goals, asset groups, feeds) · microsoft_ads_bulk_upload (apply an edited bulk file — hundreds of objects in one request. IT IS GATED HARDER THAN ANYTHING ELSE ON THIS CONNECTOR, because a bulk file carries a Status column and can turn campaigns ON without ever touching set_microsoft_ads_status: confirm:true alone is refused, and you must first call it unconfirmed to get the row-by-row list of what it would ACTIVATE and DELETE, show that to the user, then echo both counts back as confirmActivations/confirmDeletions — or pass pauseInstead:true to land the file with every activation written as Paused) · list_microsoft_ads_conversion_goals (what the account counts as a conversion, and which goals are OFFLINE ones) · send_microsoft_ads_offline_conversions (close the loop: phone sales, in-store purchases and late-closing leads fed back so smart bidding stops optimising against website conversions alone — pass PLAIN emails and E.164 phones, hashing happens server-side to Microsoft’s own published spec) · microsoft_ads_editorial_reasons (WHY an ad or keyword was disapproved: the code, the word Microsoft objected to, where, and the appeal status) · list_microsoft_ads_audiences (the account’s Customer Match lists with their current sizes; a fresh list reads 0 for up to 48 hours and Microsoft will not use one under 300 people, so never call that a failed upload) · create_microsoft_ads_customer_list then apply_microsoft_ads_customer_list (build a Customer Match audience from PLAIN email addresses, normalized and SHA-256 hashed server-side to Microsoft’s own published spec so no plaintext ever leaves us; the user must be shown Microsoft’s Customer Match terms and agree first) · microsoft_ads_recommendations (what Microsoft ITSELF suggests changing, each one priced by Microsoft: budget raises carrying the current and recommended daily amount, new and broadened keywords, negative keywords it wants removed, and ads it has written. Every one INCREASES what the account buys, which is what they are for, so none is a free win and an empty list means Microsoft has no advice rather than that the account is optimal) · apply_microsoft_ads_recommendations (act on them, gated exactly like the bulk upload: confirm:true alone is REFUSED, so call it unconfirmed first to get every recommendation named with what it changes and Microsoft’s own cost estimate, show that to the user, then echo confirmCount and confirmCostIncrease back. Both are recomputed from a fresh read, and there is no undo) · dismiss_microsoft_ads_recommendations (take advice off the list. It cannot spend, so it needs no confirmation at all, and it is the right answer to “make it stop suggesting that” rather than applying something to clear it) · microsoft_ads_auto_apply (THE READ THAT ANSWERS “is Microsoft changing this account while nobody is looking?”, per type. An inherited account can already be opted in with nobody at the brand having done it) · set_microsoft_ads_auto_apply (turn that standing permission on or off. Switching any type ON is the strongest consent anywhere in Hermoso: Microsoft then writes and publishes its own ads under the brand’s name, deletes negative keywords so the account buys more searches, and changes conversion goals, unattended and indefinitely, with NOTHING to preview beforehand. So confirm:true is not enough and every type must be named in confirmTypes. Switching it OFF is never gated). GOOGLE BUSINESS PROFILE (the local-SEO channel — the listing panel on Google Search and Maps, which for a local business is where the demand actually is, and there is no delete): list_business_locations (the listings the connected Google account manages — call this first and let the USER pick when there is more than one; a Post on the wrong storefront is a public mistake) · post_to_google_business (publish a Post to the listing — text, ONE PHOTO and a call-to-action button; Google’s Posts API takes no video, so pass a still. EVENT and OFFER posts both require a title and a start date, and on an OFFER Google ignores the button link) · list_google_business_posts (what is showing right now, with each Post’s state) · delete_google_business_post (take one down — immediate and public, so confirm first) · list_google_business_reviews (the reviews on the listing, and which ones have NO reply yet — for a local business the highest-leverage surface there is) · reply_to_google_business_review (answer one publicly as the business; it is an UPSERT, so it replaces any existing reply) · list_google_business_questions + answer_google_business_question (the public Q&A on the listing) · google_business_search_keywords (the actual search terms people typed to find the listing — free local keyword data; low-volume terms are SUPPRESSED and come back as "fewer than N", never as zero) · google_business_insights (Search + Maps impressions, calls, website clicks, direction requests, messages, bookings — listing-level; Google discontinued per-Post insights in 2023 with no replacement, so never promise per-Post numbers) · get_business_location (everything the listing actually says — name, address, phone, website, categories, description, hours, service area — as the merchant set it; the answer to “what does our Google listing say?”) · update_business_location (change any of that — hours, phone, website, description, categories, even the name or address. It edits the live panel on Search and Maps with no draft and no undo, so call it WITHOUT confirm first: nothing is written, Google validates the payload, and you get the current value of every field you are about to change to show the user) · google_business_account (whose Business Profile account the listing is on, and whether the connected Google account’s role can edit it at all). Google gates this API behind a per-project access request and the default quota is zero, so the connection can be live and calls still refused — the error says so. CHATGPT ADS (ads under ChatGPT answers, via OpenAI’s Advertiser API — full management): list_openai_ads_campaigns (the ad account, then its campaigns, ad groups and ads with each ad’s review state) · openai_ads_report (impressions, clicks, spend, CTR, CPC, CPM at account / campaign / ad group / ad scope — run this first, it validates the key with zero spend risk) · openai_ads_geo_search (location ids) · list_openai_ads_audiences + create_openai_ads_audience (custom audiences — geo and these are the only list-based targeting this platform has; target them with customAudienceIds / excludedCustomAudienceIds on a campaign) · create_openai_ads_campaign (campaign → ad group → ad in one call, always PAUSED) · create_openai_ads_ad_group / create_openai_ads_ad (fill in an existing campaign) · update_openai_ads_object (rename, re-budget, rewrite context hints or the ad copy) · set_openai_ads_budget / set_openai_ads_status (change budget, activate, pause) · delete_openai_ads_object (ARCHIVE — this API has no delete and OpenAI say archiving is not reversible, so offer pausing first). TWO RULES THIS CHANNEL DOES NOT SHARE WITH THE OTHERS: it is connected by PASTING an Advertiser API key (no OAuth, no manager account, one key = one ad account), and it has exactly ONE creative format — a text plus image card, title 50 characters, body 100. There is NO VIDEO on ChatGPT Ads, so never offer a video ad here. GOOGLE DRIVE — ONE connection covering Drive, Sheets and Docs (full CRUD over the files Hermoso created there, plus any file the user hands over with the Google file picker in the app): save_to_drive · list_drive_files / get_drive_file · update_drive_file (rename/move/trash) · delete_drive_file · create_drive_folder. GOOGLE SHEETS (part of the Google Drive connection — export data to a spreadsheet the app creates, or read one the user picked; drive.file, no verification): create_sheet · append_to_sheet · read_sheet. GOOGLE DOCS (part of the Google Drive connection — export copy/brief/report as a doc, or read one the user picked; drive.file, no verification): create_doc · append_to_doc. GOOGLE SLIDES (part of the Google Drive connection — turn a swipefile collection into a real presentation, one slide per saved ad with the creative, brand, copy, run dates and platform; drive.file, no verification, no new scope): export_swipefile_deck — it CREATES a deck each time and cannot append to one the user already has, and a creative whose ad-library link has expired is reported rather than silently dropped. ONEDRIVE (full CRUD over the user’s Microsoft OneDrive): convert_onedrive_file (Microsoft converts a file server-side to PDF or JPG — ~130 formats including PowerPoint and Word decks, PSD, Illustrator, Sketch, 3D, video, iPhone HEIC and raw camera files; JPG needs both width and height) · save_to_onedrive · list_onedrive_files / get_onedrive_file · update_onedrive_file (rename/move) · delete_onedrive_file · create_onedrive_folder · share_onedrive_file (a view/edit link for a client) · onedrive_file_permissions (who can open it; revoke). INTERCOM (the brand’s own support inbox, nothing billed): list_intercom_conversations (filter by state, assignee, team, tag, channel, customer, dates, priority, unread, or app = the product the customer wrote from) · read_intercom_conversation · reply_to_intercom_conversation / add_intercom_note / manage_intercom_conversation (each a preview until confirm:true) · tag_intercom_conversation · list_intercom_tags · search_intercom_contacts · list_intercom_teammates · intercom_customer_language (customers’ own words for ad copy). VERCEL (the brand’s own sites, nothing billed): list_vercel_projects · list_vercel_deployments · list_vercel_domains · vercel_web_analytics (page views, visitors, top pages, referrers, countries) · vercel_event_analytics (custom-event conversions by campaign) · deploy_vercel_page (a static landing page, confirm-gated; preview by default) · delete_vercel_deployment. Use these standalone — Hermoso is a full posting/ads/file-storage control surface, not only an ad generator.',
|
|
262
|
+
'E) PUBLISH & MANAGE YOUR CHANNELS — post, run ads, and organize files on the user’s OWN connected accounts (Settings ▸ Connectors), all driven over this MCP. Bring ANY file in with upload_file (desktop/external media, not just Hermoso renders). MANAGING THE CONNECTIONS THEMSELVES: list_connectors (what is linked, and what could be) · list_connector_accounts then set_connector_accounts (WHICH Facebook Pages / Instagram / Meta ad accounts / Google Ads customers / LinkedIn company Pages and ad accounts / Pinterest ad accounts / Microsoft Advertising accounts / Reddit ad accounts / Google Business listings / Google Analytics properties this profile may post to, spend from and read — one person often administers or has access to several belonging to different clients, only the chosen ones are usable anywhere, and an empty choice shares nothing) · connect_connector (connect a PASTE-A-KEY account from here: ' + Object.values(KEY_CONNECTORS).map((s) => s.label).join(', ') + '; offer it beside the Connectors page in the app and let the user choose, because a key pasted into a chat stays in its history) · disconnect_connector (drop a connection from this brand, without revoking access at the platform; confirm-gated because reconnecting a sign-in account needs a browser) · leave_connector (on a connector several teammates can each contribute their OWN account to, remove just YOURS — teammates’ accounts keep working and nothing is revoked at the provider). LINKING an account that connects through a provider sign-in screen (OAuth) is the one step that is not headless: hand the user its connect link, https://app.hermoso.ai/?connect=<provider>, or send them to Workspace ▸ Connectors in the app. META: list_meta_pages · instagram_insights (ACCOUNT-level Instagram performance — views, reach, accounts engaged, interactions, saves, profile link taps — plus the audience DEMOGRAPHICS by age / city / country / gender) · list_meta_stories (the Stories live right now on Instagram and Facebook Pages) / get_meta_story (one story id: live details, or that it expired) / instagram_story_insights (a Story’s reach, views, replies, link clicks and navigation, kept readable after its 24 hours end) / list_instagram_media (the brand’s own recent Instagram posts, and where the media id every other Instagram tool needs comes from) · list_instagram_dm_automations / save_instagram_dm_automation / test_instagram_dm_automation / delete_instagram_dm_automation (INSTAGRAM DM AUTOMATIONS — a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply; test shows what a sample would send without sending) · list_dm_automations / save_dm_automation / test_dm_automation / delete_dm_automation (THE SAME ON EVERY CHANNEL — Instagram, Facebook Page comments and ads + Messenger, and X: a keyword mention or reply gets a public reply, a DM keyword a DM; X is billed in credits per delivered event and per send) · search_instagram_audio (licensed music and original sounds an Instagram Reel may use, by keyword or trending) · list_instagram_collab_invites then respond_instagram_collab_invite (collab-post invitations waiting on the account; accept or decline one, read back from Instagram) · list_instagram_collab_media (posts this account co-authors) · like_instagram (like a post or comment as the connected account) · post_to_meta (Facebook / Instagram / Threads) · list_meta_posts (the Page’s / Instagram account’s OWN existing posts with their ids — THIS is where the postId every other Meta read needs comes from; without it an agent that did not itself just publish has no way to name a post) · list_meta_ads + meta_insights (read existing campaigns/ad sets/ads + spend/CTR/CPC, with breakdowns by age / gender / placement / country) · preview_meta_ad (Meta renders the REAL ad per placement — a link the user can look at, valid 24h) · list_meta_pixels + create_meta_pixel (the pixel a conversion-optimised campaign REQUIRES — Meta will not let a build optimise for conversions without one, and until these existed a caller had no way to discover the id they had to pass) · estimate_meta_reach (how many people a targeting spec reaches, BEFORE a budget is committed) · list_meta_audiences / create_meta_audience (website-pixel retargeting, Page + Instagram engagement audiences, and lookalikes — creating one spends nothing) · list_meta_conversations / read_meta_conversation / reply_to_meta_message (MESSENGER AND INSTAGRAM DMs — the brand’s direct-message threads and a reply to someone who wrote first. Meta only permits a reply within 24 HOURS of the person acting, and read_meta_conversation says whether that window is open BEFORE anything is drafted; Hermoso sends replies only, never a proactive message or a message tag) · subscribe_meta_webhooks / meta_webhook_status / unsubscribe_meta_webhooks / list_meta_webhook_events (REAL-TIME EVENTS — have Meta PUSH new comments, mentions, lead-form submissions and inbound DMs to Hermoso instead of polling for them. Every other inbox read asks an edge “anything new?”; this is the only way to be TOLD, and it is how a lead arrives the moment it is submitted rather than when somebody thinks to look. An empty feed is ambiguous — check meta_webhook_status first, because an unsubscribed Page is silent and looks exactly like a quiet one) · instagram_collaborators (who ACCEPTED a Collab invite on an Instagram post — publishing only SENDS the invite, so this is the only way to know whether the post is actually live on the other account too) · list_instagram_shopping_catalogs / search_instagram_shopping_products / manage_instagram_product_tags (INSTAGRAM SHOPPING — make a post SHOPPABLE. Check eligibility and the account’s taggable catalogs, find the product ids, then pass productTags to post_to_meta so tapping the picture opens the product’s price sheet inside Instagram. Tagging needs an APPROVED Instagram Shop, so check FIRST — otherwise it fails after the media is already uploaded — and note that a tag whose product is not “approved” is stored and shown to nobody. Meta publishes no way to REMOVE a tag) · create_meta_catalog / update_meta_catalog / meta_catalog_blast_radius / delete_meta_catalog (BUILD AND RETIRE A CATALOG — create one on a named business portfolio, rename or re-point it, and, before ever proposing a delete, read meta_catalog_blast_radius: a catalog delete is PERMANENT with no archive and no undo, its product sets go with it, and any ad set still bound to one keeps spending with nothing to show) · list_meta_partnership_creators / manage_meta_partnership_creator (PARTNERSHIP ADS — the creators whose content this profile may run as an advert, and who may tag this brand as a paid partner. Two separate lists, neither implying the other, and neither defaults on; adding is a REQUEST the creator must accept, and an ad naming a creator who is only PENDING fails for a reason nothing in the error says) · list_meta_catalogs / list_meta_product_sets / list_meta_catalog_products (PRODUCT CATALOGS — the merchant’s own Meta catalogs, the product SETS inside each and the products themselves with Meta’s review status. A catalog is the input to Advantage+ catalog ads, the highest-performing ecommerce format on Meta: pass productCatalogId to create_meta_campaign and productSetId to create_meta_adset / create_meta_ad, and Meta builds every impression from the product’s own image, name and price — no render needed. An empty list is a fact about which business portfolio this login administers, NEVER about whether the merchant has a catalog) · create_meta_campaign / create_meta_ad / upload_meta_asset (build) · upload_meta_playable / list_meta_playables (PLAYABLE ads: the advertiser’s own interactive HTML game, run through create_meta_ad’s playableAssetId in an App Installs ad set) · list_meta_lead_forms / create_meta_lead_form (INSTANT LEAD FORMS — the form a lead ad opens INSIDE Facebook/Instagram instead of sending the click to a website; pass the id as create_meta_ad(objective:\"OUTCOME_LEADS\", leadFormId:…) and read the submissions with read_meta_leads) · update_meta_object / delete_meta_object / set_meta_campaign_status (edit, delete, activate — every spend + delete is confirm-gated) · delete_meta_audience (remove a custom audience or lookalike — its blast radius is the PEOPLE in it and the lookalikes built from it, which Meta refuses to delete around) · manage_meta_post (edit or delete a published post). THREADS (a separate connection from Meta, on its own API): post_to_meta(target:"threads") publishes · list_threads_posts · threads_insights · list_threads_replies / reply_to_thread / hide_thread_reply · list_threads_mentions · search_threads_keyword · repost_thread (amplify a customer’s post or one of your own to the brand’s profile — the Threads retweet, and there is NO documented un-repost) · delete_thread (confirm-gated; Threads has no EDIT at all, so delete-and-repost is the only correction) · threads_publishing_limit (how much of the rolling-24h quota is left — 250 posts, 1,000 replies, 100 DELETIONS, 500 location searches; check it before a bulk clean-up, because a quota refusal otherwise reads as a broken connection). SCHEDULING (one content calendar across every channel): schedule_post (queue a post for a future time to one or MORE channels at once — Facebook / Instagram / Threads / TikTok / YouTube / LinkedIn / X / Pinterest / Bluesky / Telegram (ten; Google Business Profile is accepted but held back on Google API access) — with per-channel captions; Hermoso publishes it at that time, nothing has to stay open — it goes LIVE PUBLICLY by default, and only stages as draft/unlisted/private if the user asks, and an impossible channel+visibility pair, an over-length caption or media the channel cannot carry is REFUSED while you are still there rather than failing hours later) · list_scheduled (what is queued and what already fired, with PER-CHANNEL outcomes) · reschedule_post (move a queued post to a new time, or change its caption, media, channels or target Page/board — send only what changes) · cancel_scheduled (pull a queued post before it goes out). POST PERFORMANCE (the loop that closes research → publish → learn — Hermoso records the HOOK and SUBJECT of everything it publishes, because those exist only at the moment of publishing and can never be recovered from a post id afterwards): list_published_posts (everything this brand has published across every channel, with the hook it was written to and its measured engagement) · post_performance (which HOOKS and SUBJECTS are getting traction — engagement rates compared WITHIN a channel and NEVER summed across them, with a verdict suppressed below 5 measured posts and the reason stated) · collect_post_metrics (pull fresh numbers ~24h and ~7d after each publish; a metric a channel cannot report is recorded ABSENT with its reason and never as zero, and X is skipped unless asked because it bills per call) · backfill_posts (import a channel’s past posts so the analysis has history — dry-run and cost-quoted first, and an imported post never votes on a hook unless it matched a Hermoso creation). YOUTUBE (publish, measure AND manage): post_to_youtube (publish a finished video to the brand’s channel — PUBLIC by default; unlisted (link-only, the ad-ready setting) or private only when the user asks) · list_youtube_videos (the channel’s OWN uploads with their video ids — call this to resolve “my latest video” yourself instead of asking the user for a link; it is where the videoId every other YouTube tool needs comes from, and it sees unlisted/private uploads a public search cannot) · update_youtube_video (retitle/re-describe/re-tag, and FLIP AN UNLISTED UPLOAD PUBLIC; confirm before going public) · delete_youtube_video (take one down for good — irreversible, so the unconfirmed call reports the video’s real title, privacy, views and comments first; use update_youtube_video(privacy:"private") when they only want it out of sight) · set_youtube_thumbnail (put a Hermoso thumbnail on an uploaded video — the biggest single lever on click-through, and YouTube otherwise picks a frame at random; needs a phone-verified channel) · update_youtube_channel (brand the CHANNEL ITSELF — banner art, description, keywords, country, the trailer non-subscribers see; everything else here brands the videos, this brands the page they sit on. It MERGES with the current settings, and it reports any field YouTube accepted but silently ignored, channel title above all) · set_youtube_watermark (the subscribe badge overlaid on EVERY video on the channel, including ones uploaded later — one square image brands the whole channel at once; the API publishes no way to read it back, so it reports accepted rather than confirmed) · list_youtube_video_stats (views, likes and comments for up to 50 videos IN ONE CALL, which is how to answer "how are my last twenty uploads doing" without one youtube_video_insights per video. It carries NO titles, because VideoStatsSnippet publishes only publishTime, so join on videoId with list_youtube_videos for names. YouTube calls this endpoint "intentionally not atomic", so a short answer is normal: the missing ids are named, and a missing id is never zero views) · youtube_video_insights (per-VIDEO views, watch time, average view PERCENTAGE/retention, likes, comments, shares, subscribers gained — the numbers that say whether a hook held; youtube_channel only gives channel-wide totals) · youtube_channel_report (the same numbers BROKEN DOWN — traffic source (search vs browse vs suggested vs shorts feed), the actual search terms, country/city, device, age+gender, subscribed vs not, and the audience-RETENTION curve showing exactly where viewers left) · list_youtube_comments + list_youtube_comment_replies + reply_to_youtube_comment (read viewer questions and objections in their own words, and answer as the channel) · moderate_youtube_comment (hide, reject, spam-report or delete an abusive comment — reject is reversible, delete is not) · list_youtube_playlists + manage_youtube_playlist + manage_youtube_playlist_items (organise the channel: create playlists, add/remove/re-order videos in them) · manage_youtube_playlist_image (a custom cover on a playlist — make_thumbnail renders the artwork, this is the call that puts it on. YouTube answers every failure here as an HTTP 500 whose real reason is buried inside it, and the tool unpacks that; if it comes back refused, check channel verification first) · manage_youtube_channel_section (the SHELVES ON THE CHANNEL HOMEPAGE — put a chosen playlist or a featured channel above YouTube’s own default layout, and re-order them. Every write is PUBLIC IMMEDIATELY, a delete has no undo, and YouTube’s own section list LAGS a write by a few seconds in both directions, so never treat a list taken straight afterwards as proof either way) · list_youtube_captions + manage_youtube_caption (real subtitle TRACKS — what YouTube indexes the video by and what a viewer toggles on, which is NOT the same as captions burned into the picture; downloading one is also the quickest way to get an existing video’s script back) · list_youtube_categories (which categoryId post_to_youtube will accept in a given country) · youtube_bulk_report (THE ONLY PLACE YOUTUBE PUBLISHES THUMBNAIL IMPRESSIONS AND THUMBNAIL CTR — a different, SCHEDULED API: the first call starts a job and returns nothing, then YouTube writes one file per day, the first within 48 hours, plus a 30-day backfill. It also carries per-card and per-end-screen metrics and an uncapped list of the search terms people arrived on) · list_youtube_report_jobs (whether that thumbnail history is already accumulating, and since when — check before promising a number) · delete_youtube_report_job (stop one; the job IS the history, so deleting it throws the accumulated files away) · youtube_channel (read title + subscriber/view/video counts for reporting). TIKTOK: post_to_tiktok (post a finished video — or a PHOTO POST, TikTok’s photo/slideshow format of 1 to 35 images where a single image is just a one-slide post —LIVE to the profile, or into TikTok drafts to review in the app) · tiktok_creator_info (the creator’s REAL privacy options — read them and let the user choose before any direct post) · tiktok_post_status (follow a post_to_tiktok that came back pending: done, failed or still processing) · tiktok_account (bio, verified status, follower/following/likes/video counts) · list_tiktok_videos (their own posts with views/likes/comments/shares — either the most recent, or specific videoIds read directly however old they are). ⚠️ TIKTOK HAS NO DELETE AND NO EDIT: its API publishes no way to remove a posted video or change its caption, privacy, cover or comment/duet/stitch settings — every one of those is fixed at publish time and there is no delete scope in TikTok’s scope catalogue at all. If the user wants a TikTok taken down or changed, say plainly that it has to be done in the TikTok app rather than hunting for a tool. TIKTOK ACCOUNT AUTHORIZATION (a SECOND, separate consent on the SAME TikTok app the TikTok Ads connection uses — holding one does NOT give you the other, so a brand fully connected for ads can still be unauthorized here, and that is a real third state rather than a broken session): tiktok_account_status (which state this profile is in, the TikTok business id, the scopes the grant carries and any MISSING from it — TikTok binds scopes at authorize time and never retroactively, so only a re-authorization picks up a new one — plus the exact URL to send the user to, because authorizing is the one step that needs a browser) · list_tiktok_comments + list_tiktok_comment_replies (the comments on the brand’s OWN posts, hidden ones included — TikTok’s answer to list_meta_comments and list_youtube_comments) · comment_on_tiktok_video · reply_to_tiktok_comment · moderate_tiktok_comment (LIKE / UNLIKE / HIDE / UNHIDE / DELETE — you can only DELETE a comment this account wrote, so HIDE is the tool for a stranger’s, and TikTok warns UNHIDE may not take effect when its own moderation is what hid it) · upload_tiktok_comment_image (a new comment will not take a raw image URL; a reply will) · set_tiktok_post_ad_authorization (THIS IS WHERE A SPARK ADS AUTHORIZATION CODE COMES FROM for the brand’s OWN post — previously a human had to copy one out of the TikTok app; hand the code to authorize_tiktok_ads_spark_post) · get_tiktok_post_ad_authorization · extend_tiktok_post_ad_authorization (the days are ADDED to what is left, not set as an absolute) · delete_tiktok_post_ad_authorization. BRAND MONITORING AND AUDIENCE, on that same account authorization (these need permissions added on 2026-08-20, so a brand that authorized before then holds a grant that predates them and has to authorize once more; tiktok_account_status names exactly which are missing, and the remedy is always to authorize the TikTok ACCOUNT again rather than to touch the advertiser connection, which is a separate grant and is unaffected): list_tiktok_mentions (public posts whose caption @-mentions the brand, TikTok’s answer to x_mentions and list_threads_mentions) · list_tiktok_mention_comments (comments whose text mentions it) · get_tiktok_mention (one mention in full, for the mentions webhook, and TikTok only keeps that data 48 hours) · tiktok_mention_top_terms (the top 20 keywords and top 20 hashtags inside those mentions) · list_tiktok_brand_hashtags + manage_tiktok_brand_hashtags + list_tiktok_brand_hashtag_posts (the hashtags TikTok counts as this brand’s, up to 50, and the posts carrying them; a new one is not counted for 24 hours and cannot be removed for 7 days) · tiktok_account_insights (follower demographics by age, gender, country and city plus the daily performance series, needing a BUSINESS account with 100+ followers, and capped at 60 days rather than the 90 the mention tools cover) · tiktok_category_benchmark (the same numbers averaged across an industry, so ‘are we ahead of our category’ is answerable). ALL OF THIS IS ORGANIC LISTENING ON THE BRAND’S OWN ACCOUNT, not ad research: for competitors’ ads use the ad-library research tools instead. TIKTOK POST INSIGHTS, also on the TikTok account authorization: tiktok_video_insights (per-POST analytics for the brand’s own posts — views, reach, average watch time, the per-second RETENTION curve, traffic sources, followers gained) · tiktok_spark_recommendations (TikTok rates the account’s organic posts HIGH / MEDIUM / LOW as Spark Ads candidates, optionally for an objective) · tiktok_hashtag_suggestions (10 hashtag ideas for a caption keyword, with each tag’s post count). TIKTOK ADS (a SEPARATE connection from the TikTok posting connector above — Settings ▸ Connectors ▸ TikTok Ads; a brand that posts to TikTok every day may still have no ad account here, so never read one as the other): list_tiktok_ads_accounts (the ADVERTISER accounts this profile can act on — every other TikTok Ads tool needs an advertiserId and this is where it comes from) · list_tiktok_ads_pixels + create_tiktok_ads_pixel + list_tiktok_ads_custom_conversions + tiktok_ads_pixel_stats (CONVERSION TRACKING — a conversion-optimised ad group dies at creation with "Please select a pixel" without one, so discover the pixel and its events BEFORE building the tree; note TikTok publishes no way to DELETE a pixel, so one you create is permanent) · list_tiktok_ads_campaigns (the whole tree — campaigns, ad groups and ads with their statuses) · tiktok_ads_report (impressions, clicks, spend, CTR, CPC, conversions and video views at any level) · list_tiktok_ads_identities (the TikTok accounts an ad may post AS — MANDATORY, with NO default: call it and let the USER pick, because the ad runs publicly under whichever account is named) · search_tiktok_ads_targeting (resolve location / interest / hashtag / language ids — an ad group cannot be created without location ids, and a guessed id targets the wrong people) · list_tiktok_ads_identity_posts (the ORGANIC posts an identity has already published — where a Spark Ad’s post id comes from) · list_tiktok_ads_spark_posts (the posts authorised for Spark Ads, i.e. promoting an organic post instead of uploading a new video) · authorize_tiktok_ads_spark_post + unbind_tiktok_ads_spark_post (add a creator’s post to that authorised set with the code they generated in the TikTok app, or release it again) · upload_tiktok_ads_playable / list_tiktok_ads_playables / delete_tiktok_ads_playable (PLAYABLE ads: the advertiser’s own interactive HTML zip, run through create_tiktok_smart_ad’s playableUrls) · upload_tiktok_ads_creative (THE STEP THAT TURNS A RENDER INTO AN AD — put a finished Hermoso video on the ad account and it hands back the videoId AND the coverImageId create_tiktok_ads_ad needs; there is no other source for either) · create_tiktok_ads_campaign → create_tiktok_ads_ad_group → create_tiktok_ads_ad (the tree) · set_tiktok_ads_budget · set_tiktok_ads_status (the ONLY switch that arms real money, confirm-gated) · delete_tiktok_ads_object (removal on TikTok is a STATUS, not a verb — the same route as set_tiktok_ads_status) · update_tiktok_ads_ad (EDIT a live ad in place — ad text, call-to-action, landing URL, UTMs, name, video and cover, carousel sound or Instant Form — instead of deleting and rebuilding it, which throws away its review history and delivery learning) · preview_tiktok_ads_ad (see an ad as it renders on TikTok, or preview a video BEFORE any ad is built, with TikTok’s own fix-it tips per placement) · tiktok_ads_review_info (TikTok’s review verdict per ad — approved, approved for only part of its targeting, or rejected — with its REASONS and suggested fix) · list_tiktok_ads_music (the ad-cleared sound a CAROUSEL ad needs) · tiktok_ads_creative_fatigue (TikTok’s own verdict on whether a creative is WEARING OUT, per day, with spend and cost per conversion — the signal to swap in a fresh variation) · tiktok_ads_video_retention (per-SECOND ad video performance: where viewers swipe away, the hook read for the next variation) · tiktok_ads_trending_hashtags + tiktok_trends (TikTok’s OWN first-party trend data: top hashtags by country, industry and period; trending pre-cleared Commercial Music Library sounds, the top videos on a sound, top search terms) · create_tiktok_smart_campaign → create_tiktok_smart_ad_group → create_tiktok_smart_ad (Smart+, TikTok’s Performance Max — born PAUSED) · list_tiktok_smart_campaigns · set_tiktok_smart_status (the Smart+ money switch, confirm-gated) · tiktok_bid_protection (the ad-credit compensation TikTok pays when a Smart+ object misses its bid) · tiktok_lead_ads_terms (read and accept TikTok’s Lead Ads terms, which a lead-generation ad needs first) · list_tiktok_ads_lead_forms + list_tiktok_ads_lead_fields + download_tiktok_ads_leads + manage_tiktok_ads_test_lead (LEAD ADS — an Instant Form is built in TikTok Ads Manager and NO API creates one, so list them to find the id a LEAD_GENERATION ad group needs. The lead REGION is required with no default: it selects which of three separate lead stores you read, and leaving it out is a THIRD value rather than “all”, so an advertiser who omits it downloads an empty file and wrongly concludes there are no leads) · list_tiktok_ads_audiences + create_tiktok_ads_audience + create_tiktok_ads_lookalike_audience + apply_tiktok_ads_audience + update_tiktok_ads_audience + delete_tiktok_ads_audience + tiktok_ads_audience_overlap (CUSTOM AUDIENCES and lookalikes — TikTok targeting is otherwise interests-and-geo only. A freshly created audience reports itself invalid for up to 48 hours BY DESIGN, so that is not a failure to retry) · list_tiktok_ads_business_centers + list_tiktok_ads_catalogs + create_tiktok_ads_catalog + list_tiktok_ads_catalog_products + list_tiktok_ads_catalog_sets + manage_tiktok_ads_catalog_feed + tiktok_ads_catalog_diagnostics (DPA / PRODUCT CATALOGS, the Shopify lane — a catalog is keyed on a BUSINESS CENTER id, NOT an advertiser id, so list the Business Centers first or every call refuses) · create_tiktok_ads_account (CREATE A NEW TIKTOK AD ACCOUNT for a business that has none, in a Business Center the user’s own login administers and in their company name; two steps, call without confirm first, and it can never be deleted) · list_tiktok_ads_apps + list_tiktok_ads_app_events (the registered apps an APP_INSTALL campaign needs — nothing else can produce an app id) · tiktok_ads_rf_inventory_estimate + create_tiktok_ads_rf_ad_group (REACH & FREQUENCY — a RESERVATION, so it is confirm-gated like a status change rather than born paused, and it needs a per-ad-account allowlist plus a signed branding contract that no endpoint reports. Always price it with the estimate first: TikTok silently books its own maximum rather than refusing an out-of-range value) · send_tiktok_ads_events (SERVER-SIDE conversion events — there is a vendor-sanctioned test code for exercising it without entering the advertiser’s real reporting), and its offline/crm sources take the event-set ids the two tools below mint) · list_tiktok_ads_offline_event_sets + manage_tiktok_ads_offline_event_set + send_tiktok_ads_offline_events (REAL-WORLD CONVERSIONS — an in-store purchase, a phone booking, a signed contract, reported so TikTok can attribute them to the ads that caused them. The timestamp is an ISO-8601 STRING here and a Unix NUMBER on send_tiktok_ads_events; a wrong-shaped one is accepted by TikTok and attributed to nothing. There is NO test code on this pair, so everything sent is a real permanent conversion — rehearse through send_tiktok_ads_events with eventSource “offline” and a testEventCode instead. Reporting also needs the connected user to be an ADMIN or OPERATOR of the advertiser, which managing the event SETS does not) · list_tiktok_ads_crm_event_sets + create_tiktok_ads_crm_event_set (LEAD-LIFECYCLE events — sending “this lead qualified / closed” back is what makes a LEAD_GENERATION campaign optimise toward leads that convert rather than form fills. TikTok publishes create and list and nothing else, so one of these is PERMANENT) · list_tiktok_tto_accounts + list_tiktok_creator_labels + discover_tiktok_creators + tiktok_creator_leaderboard + check_tiktok_creator_status + list_tiktok_tto_brand_profiles + create_tiktok_tto_brand_profile + list_tiktok_tto_campaigns + create_tiktok_tto_campaign + update_tiktok_tto_campaign + link_tiktok_tto_video + list_tiktok_tto_link_requests + tiktok_tto_campaign_report + request_tiktok_tto_spark_authorization + get_tiktok_tto_spark_authorization + manage_tiktok_tto_anchor (TIKTOK ONE / CREATOR MARKETPLACE: INFLUENCER MARKETING, and the only place in Hermoso that does it: find creators by audience size, engagement, price and who their followers actually are, check whether they have joined TikTok One, invite them to a campaign with an invite link, ask them to tag a video to it, and read every metric SPLIT ORGANIC VERSUS PAID. Its account id is a THIRD id space; not an advertiser id and not a Business Center id; so start at list_tiktok_tto_accounts. It rides this same connection with nothing extra to apply for. IT ALSO CLOSES THE SPARK ADS LOOP: request_tiktok_tto_spark_authorization asks a creator directly and get_tiktok_tto_spark_authorization returns the code authorize_tiktok_ads_spark_post takes, which is otherwise obtainable only by the creator pasting one out of the TikTok app. Two things put a notification in a real person’s inbox; a campaign invitation and a video-linking request; and a repeated linking request is a REMINDER that TikTok caps at two, so read list_tiktok_tto_link_requests before re-sending anything) · list_tiktok_ads_stores + list_tiktok_ads_store_products (TIKTOK SHOPS: what a Shopping Ads or GMV Max campaign sells from; the store list is keyed on an ad account and the product list on a BUSINESS CENTER, which each store row names) · tiktok_ads_verification_status + list_tiktok_ads_verification_documents + submit_tiktok_ads_verification (BUSINESS VERIFICATION: an unverified account hits limits that get diagnosed as something else, so it is worth reading during onboarding. Hermoso never handles a verification DOCUMENT: submitting sends account details plus the ids of images the user uploaded in TikTok Ads Manager, and the legal name and document number can never be changed afterwards, so it is confirm-gated) · list_tiktok_ads_payment_portfolios + list_tiktok_ads_payment_portfolio_links (HOW THE AD ACCOUNTS ARE FUNDED: read-only, because "why did delivery stop" is often a funding answer, and because deciding where a customer’s money sits is not ours to do) · create_tiktok_ads_rule + list_tiktok_ads_rules + update_tiktok_ads_rule + bind_tiktok_ads_rule + set_tiktok_ads_rule_status + tiktok_ads_rule_results (AUTOMATED RULES — standing instructions TikTok runs on the account unattended. THE SECOND SPEND SWITCH ON THIS PLATFORM and gated in TWO CLASSES: a rule that can only pause, decrease or email needs confirm:true, while one that can TURN_ON an object or RAISE a budget or bid needs confirm:true AND confirmScope echoing the token list_tiktok_ads_rules prints, computed from the rule as TikTok STORES it. Every rule is created TURNED OFF and read back to prove it, because TikTok publishes no way to create one in the off position. TikTok emails rule notifications to the DEVELOPER address on the app rather than to the advertiser, so tiktok_ads_rule_results is the only place a customer sees what a rule did — and TikTok itself says this endpoint is for direct advertisers and may refuse a platform-managed account entirely). · list_tiktok_ads_comments + tiktok_ads_comment_thread + moderate_tiktok_ads_comment + reply_to_tiktok_ads_comment + delete_tiktok_ads_comment (COMMENT MODERATION on your own TikTok ads — the platform where the comment section IS the ad. HIDE is the moderation verb and works on anyone’s comment and is reversible; DELETE only ever removes a comment your OWN identity posted, which TikTok reports per comment as canDelete. Comments are scoped to an AD GROUP and to nothing else, and the time window may span at most 30 DAYS, so an empty answer means “none in these 30 days” rather than “none ever”) · list_tiktok_ads_blocked_words + manage_tiktok_ads_blocked_words (a standing 500-word filter that auto-hides any comment containing one of these across EVERY ad on the account — nothing else in Hermoso does this, and removing a word republishes every comment it had hidden) · tiktok_ads_diagnosis (TikTok’s own issues-and-suggestions verdict on your ad groups — creative, bid/budget with its full estimated-delivery tables, and a pixel that has gone quiet. It covers ACTIVE ad groups only and omits any it has nothing to say about, so an empty answer is not a clean bill of health) · get_tiktok_ads_brand_safety + set_tiktok_ads_brand_safety (what content the ads may appear next to. Two things to say out loud: TikTok applies this to Smart+ campaigns and explicitly NOT to the regular campaigns create_tiktok_ads_campaign builds, and coverAllObjectives is a ONE-WAY DOOR TikTok cannot set back). TWO THINGS HERE ARE UNLIKE EVERY OTHER AD PLATFORM: TikTok creates objects ENABLED by default, so Hermoso forces every campaign, ad group and ad PAUSED with no override and nothing serves until set_tiktok_ads_status(confirm:true); and TikTok’s QPS is 1, so every call is serialized and a tree build or a bulk read is SLOW BY DESIGN — a throttle is not a broken connection. SNAPCHAT ADS (the tenth ad platform — Settings ▸ Connectors ▸ Snapchat Ads; a SEPARATE connection from Snapchat posting): list_snapchat_ads_accounts (the organizations and AD ACCOUNTS this profile can act on — every other Snapchat tool needs an adAccountId and this is where it comes from) · list_snapchat_ads_campaigns (the whole tree — campaigns, ad squads and ads) · snapchat_ads_report (impressions, spend, swipes and video quartiles at any level) · search_snapchat_ads_targeting (resolve country / region / interest / language ids — an ad squad cannot be created without at least one country) · upload_snapchat_ads_creative (put a finished render on the ad account as MEDIA and then as the CREATIVE an ad points at — Snapchat has no upload-from-URL, so Hermoso streams the bytes) · create_snapchat_ads_campaign → create_snapchat_ads_ad_squad → create_snapchat_ads_ad (the tree, every tier born PAUSED) · set_snapchat_ads_budget · set_snapchat_ads_status (the ONLY switch that arms real money, confirm-gated) · delete_snapchat_ads_object (a REAL delete verb here, unlike TikTok — irreversible, so offer PAUSED first). THREE THINGS TO SAY OUT LOUD ON THIS PLATFORM: money is MICRO-CURRENCY (1,000,000 = one unit), so quote plain amounts and let Hermoso convert, and never pass both units — under-converting fails loudly while double-converting asks for a budget a million times too large; the creative HEADLINE is capped at 34 characters and brandName at 32, far shorter than Meta or Google, and over-long copy is refused rather than truncated; and a Snapchat ad points at a CREATIVE, never at a media id. SNAPCHAT POSTING (Stories / Spotlights on a Public Profile) IS BUILT BUT NOT YET REACHABLE — Snap’s Public Profile API is allowlist-only and Hermoso has not been allowlisted, so the connector is deliberately not offered; say that plainly rather than looking for a tool. LINKEDIN: post_to_linkedin (publish a finished post to the connected LinkedIn PROFILE) · list_linkedin_pages (the company Pages this connection administers — call this first and let the USER pick, never guess a Page) · post_to_linkedin_page (publish as a company PAGE rather than a person — this is the one most brands actually want) · manage_linkedin_post (edit the copy of a published post, or delete it) \u00b7 list_linkedin_comments / reply_to_linkedin_comment / delete_linkedin_comment (moderate the comments on your Page\u2019s posts \u2014 a SEPARATE LinkedIn authorization grants these, see Connectors) · linkedin_page_analytics (ORGANIC Page performance — followers, follower gains, Page views, and post impressions/clicks/engagement, for the Page total or per post; this is the free organic read, NOT linkedin_ads_report). LINKEDIN ADS (full three-tier management): list_linkedin_ads_campaigns (ad accounts, then a chosen account’s campaign groups, campaigns and — with campaignId — the CREATIVES under them) · linkedin_ads_report (impressions, clicks, cost, conversions, leads) · search_linkedin_ads_targeting (resolve locations / titles / industries / seniorities / company sizes to the URNs LinkedIn demands — never invent one) · linkedin_audience_count (HOW MANY members that targeting actually reaches, before a budget is committed — and a returned 0 means fewer than 300 people, LinkedIn’s privacy floor and also its campaign minimum, never an empty audience) · linkedin_bid_pricing (LinkedIn’s own suggested bid and daily-budget range for that audience — quote it instead of guessing what LinkedIn costs) · create_linkedin_ads_campaign_group → create_linkedin_ads_campaign → create_linkedin_ads_creative (the tree, every tier born DRAFT) · set_linkedin_ads_budget / set_linkedin_ads_status / delete_linkedin_ads_object (budgets, activate/pause at any tier, delete — every spend change confirm-gated). LINKEDIN LEAD SYNC: list_linkedin_lead_forms · list_linkedin_leads / get_linkedin_lead (the LEADS its forms collected, answers named by field — PERSONAL DATA: show, never republish) · subscribe_linkedin_leads / list_linkedin_lead_events / list_linkedin_lead_subscriptions / delete_linkedin_lead_subscription (real-time push to Hermoso, optional forwardTo relay to a CRM; LinkedIn validates ONLY Hermoso’s own webhook). LinkedIn is a THREE-tier platform and the third tier is the one people forget: a campaign with no creative shows nothing, and all three tiers must be ACTIVE before a single impression is served. REDDIT ADS: list_reddit_ads_campaigns / reddit_ads_report (read the account tree + performance) · list_reddit_ads_profiles + list_reddit_ads_posts / create_reddit_ads_post / update_reddit_ads_post (the CREATIVE — a Reddit ad promotes a post) · create_reddit_ads_campaign / update_reddit_ads_campaign · create_reddit_ads_ad_group / update_reddit_ads_ad_group · create_reddit_ads_ad / update_reddit_ads_ad · create_reddit_ads_max_campaign / get_reddit_ads_max_template / update_reddit_ads_max_template (a Reddit MAX campaign: automated campaign, ad group and a template ad Reddit generates ads from, built from creative-library assets, all PAUSED) · set_reddit_ads_status (the ONLY switch that arms real spend, confirm-gated) · delete_reddit_ads_object (remove a campaign, ad group or ad — Reddit has no delete verb, removal is a status, and it refuses to delete anything touched in the last 3 hours) · delete_reddit_ads_saved_audience · search_reddit_ads_targeting / reddit_ads_forecast / reddit_ads_bid_suggestion (free planning) · list_reddit_ads_pixels + send_reddit_ads_conversions (conversion tracking — Reddit now requires a pixel on every ad group) · list_reddit_ads_audiences / create_reddit_ads_audience / update_reddit_ads_audience_users / delete_reddit_ads_audience (retargeting lists) · list_reddit_ads_saved_audiences / create_reddit_ads_saved_audience / update_reddit_ads_saved_audience · list_reddit_ads_lead_forms / create_reddit_ads_lead_form · reddit_ads_history (who changed what, when). TELEGRAM: post_to_telegram (publish to a channel, group or chat as the brand’s own bot — text up to 4096 characters, but only 1024 once any photo or video is attached; one image, one video, or an album of 2–10 in which photos and videos may be mixed. chatId IS ALWAYS REQUIRED and is never guessed: the Bot API publishes NO method that lists the chats a bot belongs to, so pass the public channel’s @username or the numeric id) · list_telegram_chats (chats that MESSAGED the bot in the last 24 hours — a shortcut for finding an id, NOT a roster, and a chat missing from it can still be posted to) · list_telegram_dms (what those chats actually SAID, newest per chat — free, and a rolling 24-hour window rather than an inbox: the Bot API has no history endpoint at all) · delete_telegram_message (confirm-gated; Telegram refuses once a message is more than 48 hours old). BLUESKY: post_to_bluesky (publish as the connected account — text up to 300 characters AND, separately, 3000 UTF-8 bytes, so an emoji-heavy post can be under 300 characters and still be refused; either up to 10 images OR one MP4 video, never both, because a Bluesky post record carries exactly one embed; links are made clickable automatically) · delete_bluesky_post (PERMANENTLY remove one of the account’s own posts — no trash and no undelete. Call it WITHOUT confirm first: it deletes nothing and reports the post’s real text and live like/repost/reply/quote counts, and once the post has any engagement it also wants confirmText echoing its text. Takes the AT-URI or just the record key from the bsky.app link) · list_bluesky_convos / read_bluesky_dm / send_bluesky_dm / mark_bluesky_convo_read (the account’s DIRECT MESSAGES — free, 1000 characters each, text only, and they need a PRIVILEGED app password: an ordinary one posts fine and cannot chat). Replies, mentions AND direct messages all arrive in list_inbox and are answered with reply_to_inbox_item. X / TWITTER: post_to_x (publish a post — text, an image or a video render WITH alt text, a POLL, a reply, or a whole thread, and optionally restrict who may reply; X is the ONE channel that bills per API request, a post carrying a LINK costs roughly 13× one without, and each profile has a rolling 24-hour ceiling on X spend that refuses a request whole rather than publishing half of it) · delete_x_post (remove one) · x_post_metrics (the PUBLIC counts — impressions, likes, reposts, replies, quotes, bookmarks) · x_post_insights (the ADVERTISER numbers for your own posts — link clicks, profile visits, video views and completion quartiles, up to 25 posts at once; this is what says whether a creative worked, and x_post_metrics cannot tell you, but it only sees the LAST 28 HOURS) · x_post_insights_historical (the same advertiser numbers over ANY date range — the one to use for anything older than yesterday) · x_mentions (who is talking to the brand, in their own words — the read half of the reply loop, and a source of real customer language for ad copy) · list_x_dms (the brand’s X DIRECT MESSAGES, grouped into conversations, saying which are waiting on a reply — billed per message returned, and X keeps only 30 days) · send_x_dm (reply privately to one named person; never a broadcast) · x_chat_settings / update_x_chat_settings (the X account’s DM privacy: who may message the brand, the request filter, read receipts and calls). X IS THE ONE CONNECTOR THAT COSTS CREDITS PER CALL — X charges us per API request, so posting, deleting, reading metrics, reading insights and pulling mentions each bill the user, a post CONTAINING A LINK costs 13× one without, and insights and mentions are billed PER POST RETURNED. Say so before posting a thread or pulling a big page of mentions, and prefer one post over five when the content allows. X ADS (the PAID half — a SEPARATE connection from the organic tools above: its own product on its own host with OAuth 1.0a signing, and X grants API access PER AD ACCOUNT rather than per app, so the customer adds Hermoso’s X user at business.x.com → Account access before anything here resolves): list_x_ads_accounts (the ad accounts this profile can act on, WITH the permission level held on each — read it before attempting a write) · list_x_ads_funding_instruments (a campaign cannot be created without one) · list_x_ads_campaigns / list_x_ads_line_items / list_x_ads_promoted_tweets / list_x_ads_targeting (the whole tree as it stands) · x_ads_report (impressions, clicks, spend and engagements at any level) · x_ads_geo_search / x_ads_targeting_search (resolve places and targeting values to the ids X demands — never invent one) · create_x_ads_campaign → create_x_ads_line_item → create_x_ads_promoted_tweet (the tree, every tier born PAUSED with no override; A CAMPAIGN ALONE CANNOT SERVE ON X — it needs a line item and a promoted post underneath it, and the read-back says so rather than letting you call it a finished ad) · add_x_ads_targeting · update_x_ads_campaign / update_x_ads_line_item (throttle or raise spend on a running campaign without rebuilding it) · set_x_ads_status (the ONLY switch that arms real money, confirm-gated) · delete_x_ads_object. PINTEREST — POSTING AND ADS ARE TWO SEPARATE CONNECTIONS on the same Pinterest login (Pinterest keeps ads access behind different permissions), so a brand can hold either without the other and connecting one does not connect the other; if an ads call says Pinterest Ads is not connected, that is the card to send them to, NOT the Pinterest posting one. ADS: pinterest_ads_async_report (the DEEP paid report — 914 days back where the quick one stops at 90, and three times the metric columns; generated asynchronously, so pass the returned token back rather than re-submitting) · pinterest_targeting_analytics (WHICH audience segment delivered — by keyword, interest, age, gender, location, placement) · pinterest_audience_insights (WHO the audience is: interest affinities plus demographics, the input to a creative brief rather than a performance report) · pinterest_analytics (ORGANIC performance — impressions, saves, Pin clicks, outbound clicks, for the account, the TOP PINS, the top video Pins, or one Pin; Pinterest keeps 90 days and publishes no board-level analytics at all) · create_pinterest_board (make a board — a NEW Pinterest account has none and a Pin needs one) · list_pinterest_boards (the user must pick a board — never choose one for them) · post_to_pinterest (create an image or video Pin on a chosen board, with a title, description and destination link) · list_pinterest_pins (the Pins on a board with their ids — where the pinId every Pin tool needs comes from, and it flags any Pin an ad is promoting) · update_pinterest_pin (retitle, re-describe, fix a dead link, move it — Pinterest keeps this endpoint in a limited BETA, so it may be refused outright and save_pinterest_pin is the generally-available way onto another board; a Pin’s picture can never be swapped by anyone) · save_pinterest_pin (copy a Pin onto another board) · delete_pinterest_pin (confirm-gated, and it says whether an ad is promoting the Pin first) · update_pinterest_board (rename, re-describe, or hide it — SECRET hides every Pin on the board, reversibly) · delete_pinterest_board (the heaviest one here: the board AND every Pin on it, confirm-gated with the Pin count echoed back — offer hiding it instead). GOOGLE ADS (full management): list_google_ads_campaigns (list accounts, then a customer’s campaigns + spend/CTR/CPC/conversions) · google_ads_report (any GAQL breakdown — ad groups, keywords, search terms, geo) · create_google_ads_campaign (paused) · set_google_ads_budget / set_google_ads_status (change budget, enable/pause — every spend change confirm-gated) · delete_google_ads_object (remove a campaign, ad group, ad, KEYWORD, asset LINK or conversion action — Google has no delete verb, `remove` is the terminal state and it cannot be undone; call it unconfirmed first to see the spend and the tree that go with it) · upload_google_ads_asset (add an image render or a YouTube video to the ad account’s asset library) · create_google_ads_performance_max_campaign (Google’s cross-surface campaign type, RETAIL INCLUDED — pass merchantCenterId to make it a Shopping-feed Performance Max advertising the WHOLE Merchant Center feed under one root listing group, and feedLabel to narrow it to a single feed; only PARTITIONING that feed by brand/category/custom label is refused by name) · add_google_ads_assets (sitelinks, callouts, structured snippets, call, promotion, price, lead-form, business name and logo assets, CREATED AND ATTACHED — an asset that is not attached shows nothing) · manage_google_ads_assets (list, edit, pause or detach them) · set_google_ads_tracking (UTMs / tracking template on a campaign, ad group or ad; new campaigns get standard UTMs by default) · manage_google_ads_audience + set_google_ads_audiences (remarketing lists, custom segments, affinity / in-market interests, demographics, Performance Max signals and search themes) · upload_google_ads_video (a render straight into Google Ads, no YouTube channel needed) · create_google_ads_demand_gen_campaign (YouTube Shorts / in-feed / in-stream video ads, born PAUSED) · list_google_ads_conversion_actions + create_google_ads_conversion_action (what Google counts as a result — MAXIMIZE_CONVERSIONS, TARGET_CPA, TARGET_ROAS and every Performance Max campaign are undeliverable without one, and Hermoso refuses to build them on an account that has none) · google_ads_keyword_ideas (Keyword Planner — real monthly search volume, competition and top-of-page bids; use it before choosing keywords) · google_ads_recommendations + manage_google_ads_recommendations (Google’s own advice and optimization score; apply / dismiss / auto-apply, spend-type ones only after the user’s yes) · preview_google_ads_ad (a shareable no-login preview link for a Performance Max asset group or YouTube video ad, for client approval) · google_ads_policy_issues (WHY Google disapproved or limited an ad, asset or keyword: the policy topic and the evidence) · google_ads_change_history (WHAT CHANGED ON THE ACCOUNT AND WHEN — the answer to “performance fell off a cliff on Tuesday, what happened?”. Its default source is field-level and reaches 30 days; the other source reaches 90 and is the ONLY one that sees Google Ads Editor and criterion edits, so check both before telling anyone nothing changed). GOOGLE MERCHANT CENTER (the product feed behind every Shopping ad and every free listing, on the SAME connection as Google Ads): register_merchant_developer (the ONE-TIME link between Hermoso’s Google Cloud project and the merchant’s account. Google refuses every other Merchant call until it is done, so run this first when calls are being refused) · list_merchant_accounts (which Merchant Centers this login can reach, and where the merchantCenterId every other tool needs comes from) · list_merchant_products (the feed itself, with each product’s disapprovals) · list_merchant_issues (account-level problems, the answer to "why is nothing showing at all") · merchant_issue_help + trigger_merchant_issue_action (Google’s OWN remediation steps for a problem, and the button that fires one. Several of those actions are one-shot in Google’s own words, so firing one is confirm-gated) · list_merchant_data_sources + create_merchant_data_source + delete_merchant_data_source (feeds. A product write only lands in an API-input feed, and most accounts have none until one is made, so check before writing) · upsert_merchant_product + update_merchant_product + delete_merchant_product (write the feed) · list_merchant_inventory + set_merchant_inventory (the per-STORE and per-REGION price, stock level and availability override on one product, which is what stops a Shopping ad advertising something the nearest store has sold out of. The write MERGES, because Google’s insert replaces the whole entry, and Google takes up to 30 minutes to reflect it on the product) · list_merchant_promotions + create_merchant_promotion (sale and discount badges on a listing. Google validates them asynchronously, so created is never the same as approved) · manage_merchant_notifications (Google POSTs to a URL THE MERCHANT RUNS the moment a product is disapproved, instead of someone having to poll) · merchant_account_status (WHY THE ACCOUNT IS OR IS NOT SERVING — the first thing to run when Shopping ads or free listings show nothing, and the one read that does not believe the program state: an account can report both programs ENABLED and serve in ZERO countries, because a region counts as active only where every requirement is met. It names Google’s own unmet requirements, then the settings that explain them: homepage claimed or not, business address, phone and support contact, active shipping services, return policies, terms accepted) · manage_merchant_settings (FIXES what merchant_account_status finds: shipping services merged by name so unmentioned ones are kept, return policies, business address and support contact, homepage claim, programs on or off; overwrites and removals ask first) · manage_merchant_conversion_source (WHERE MERCHANT CENTER GETS ITS CONVERSION DATA FROM, which is what free-listing and Shopping performance reporting is built on — a merchant with no conversion source sees clicks and no outcomes. Either a Google tag destination, whose MC-… id comes back only on the create and is the id the Google tag has to send conversions to, or a link to a GA4 property, which is IMMUTABLE and needs the connected Google account to be an admin there. A delete is an ARCHIVE and undelete restores it until the expiry Google reports) · merchant_quota (whether the account is simply out of daily API quota or out of product slots, which looks identical to a broken integration and is not. Google resets it at MIDDAY UTC) · merchant_report (the reports Google computes for free, including competitive visibility, best sellers and price competitiveness). MICROSOFT MERCHANT CENTER (the same job on Microsoft’s side, on the Microsoft Advertising connection): list_microsoft_merchant_stores · list_microsoft_merchant_products · upsert_microsoft_merchant_product · delete_microsoft_merchant_product · list_microsoft_merchant_issues · list_microsoft_merchant_catalogs + manage_microsoft_merchant_catalog. GOOGLE ANALYTICS (GA4 — the brand’s OWN site data, and a SEPARATE connection from Google Ads: a brand that spends on Ads every day may have no Analytics access at all, so never read one as the other): list_analytics_properties (call this FIRST — every other Analytics tool needs a NUMERIC property id, and what users actually know is the “G-XXXXXXX” Measurement ID from their tracking snippet, which no endpoint accepts; resolve it from this list rather than sending them hunting. It lists the properties SHARED WITH THIS PROFILE, not everything the Google account can see — Analytics access is handed out freely and one login often has Viewer on many clients’ properties, so the user ticks which belong to this profile and any other one is refused by name; an empty list means nothing is ticked yet, which set_connector_accounts or Settings ▸ Connectors ▸ Google Analytics ▸ Manage accounts fixes) · analytics_report (what happened — sessions, users, revenue, conversions and engagement broken down by channel, source/medium, campaign, landing page, country, device or date, i.e. the read that says whether the traffic an ad bought actually did anything) · analytics_realtime (who is on the site right now, ~30 minutes — a DIFFERENT metric set that rejects `sessions` outright, never a shortcut for analytics_report) · list_analytics_definitions (what the property already measures: its key events and its own custom dimensions, and the check to run before creating either) · create_analytics_key_event (mark an event GA4 already collects as a KEY EVENT — the 2024 rename of a conversion, and what makes it importable into Google Ads; marking an event the site never fires creates one that can never fire) · create_analytics_custom_dimension (register an event parameter the site already sends so reports can break down by it — say out loud first that a GA4 custom dimension CANNOT be deleted, only archived, and a property is capped at 50 event-scoped ones, so a typo permanently burns a slot) · list_analytics_data_streams (the streams on a property and the measurement ID (G-...) each one carries, which is what a gtag or GTM install needs and what nobody can find in the GA4 UI when asked) · get_analytics_stream_setup (the finished gtag <script> block to paste into the site — the last mile list_analytics_data_streams stops short of — plus whether enhanced measurement is really collecting scrolls, outbound clicks, site search, video, downloads and form interactions, and whether redaction is stripping campaign parameters out of recorded URLs. Web streams only. Read the master switch before believing a toggle: with enhanced measurement off for the stream, every toggle is inert whatever it says) · list_analytics_metadata (every dimension and metric this property can be asked for, including its own custom ones, which is what stops analytics_report guessing a field name) · check_analytics_compatibility (whether a dimension and metric can appear in the same report before spending a call finding out they cannot) · create_analytics_custom_metric + archive_analytics_custom_metric · archive_analytics_custom_dimension · delete_analytics_key_event (all one-way in the same sense as their create twins: archiving is not deleting and there is no un-archive) · list_analytics_google_ads_links + link_google_ads_to_analytics + unlink_google_ads_from_analytics (the join that makes a GA4 audience usable in Google Ads and a GA4 key event importable as a conversion — without it a perfectly good audience simply never appears in the ads account, with no error anywhere) · list_analytics_audiences + create_analytics_audience + archive_analytics_audience (GA4 remarketing audiences, the input to Google Ads remarketing. Archiving is one-way) · manage_analytics_measurement_protocol_secret (mint the API secret that lets the customer’s OWN SERVER send events straight into GA4, the Google twin of the conversions APIs already here for Reddit, Snapchat and OpenAI Ads. Say out loud that there is NO rotation anywhere in the API, so replacing a secret means create the new one, move every sender across, then delete the old one) · manage_analytics_channel_group (HOW GA4 BUCKETS TRAFFIC — the answer to “why is my campaign showing as Unassigned”, and the one number an ad studio is judged on. Read the Default channel group’s rules before diagnosing anything, then author your own group whose channels catch the campaigns Hermoso publishes. The rule fields are the eachScope… names, NOT the sessionSource / medium dimensions reports use, and GA4 stops at the first rule that matches so order decides everything) · manage_analytics_calculated_metric (the derived number a marketer actually reports — cost per purchase, revenue per session — built from metrics GA4 already collects and then available to analytics_report under its own permanent API name. The id is permanent, and a formula naming a metric the property does not collect is created happily and flagged invalid, so read that flag back) · manage_analytics_annotations (mark a launch or a post on the customer’s own GA4 charts, so the traffic it moved shows where they look) · analytics_data_retention (how long GA4 keeps event data — the 2-month default caps every exploration; lowering it deletes data, so it is confirm-gated). MICROSOFT ADVERTISING / BING ADS (full management, mirroring Google): list_microsoft_ads_campaigns (list the shared ad accounts, then a chosen account’s campaigns + budgets) · microsoft_ads_geo_search (resolve country / region / city names to the Microsoft location ids a campaign needs — call it when an ask is ambiguous and let the USER pick) · microsoft_ads_report (impressions, clicks, CTR, average CPC, spend, conversions — generated asynchronously, so it may come back pending and must be called again) · create_microsoft_ads_campaign (campaign → ad group → responsive search ad → keywords, always Paused; with no locations[] it is created serving WORLDWIDE, Microsoft’s own default, and the read-back warns loudly — relay that before anyone activates it) · create_microsoft_ads_ad_group / create_microsoft_ads_ad / add_microsoft_ads_keywords (fill in an existing account) · set_microsoft_ads_budget / set_microsoft_ads_status (change budget, activate/pause — every spend change confirm-gated; Microsoft statuses are Active/Paused, never Deleted) · search_microsoft_ads_profiles / list_microsoft_ads_profile_targeting / set_microsoft_ads_profile_targeting (LinkedIn profile targeting: company, industry, job function, seniority and job title bid adjustments on a Search, Shopping or DSA campaign; confirm-gated on an ACTIVE campaign) · delete_microsoft_ads_object (a REAL delete — campaign, ad group, ad or keyword — permanent, with no undelete; call it unconfirmed first to see what goes with it) · set_microsoft_ads_bidding (change a campaign’s BID STRATEGY — EnhancedCpc, MaxClicks, MaxConversions with an optional target CPA, MaxConversionValue with an optional target ROAS, TargetImpressionShare — read back) · list_microsoft_ads_negative_keywords + add_microsoft_ads_negative_keywords + remove_microsoft_ads_negative_keywords + manage_microsoft_ads_negative_keyword_list (NEGATIVE keywords: the searches a campaign or ad group must NOT pay for, Exact or Phrase only, and SHARED negative lists reused across campaigns; removing one on a LIVE campaign lets those searches trigger paid ads again, so it is confirm-gated) · list_microsoft_ads_uet_tags + create_microsoft_ads_uet_tag (the UET site tag and its install snippet — conversion goals, remarketing and conversion bidding all need one) · create_microsoft_ads_conversion_goal (Url / Event / OfflineConversion / Duration / PagesViewedPerVisit goals, read back) · list_microsoft_ads_audience_targeting + set_microsoft_ads_audience_targeting (put an audience — a customer list, remarketing, in-market — to work on a campaign or ad group, or EXCLUDE it, with a bid adjustment) · create_microsoft_ads_image_ad (turn a render into an IMAGE ad on the Microsoft Audience Network or a multimedia ad on Search; the picture is cropped to the 1.91:1 and 1:1 sizes Microsoft requires) · upload_microsoft_ads_video (put a render on the account as a video asset, or check one still processing; Microsoft refuses clips under 6 seconds) · microsoft_ads_keyword_ideas (Microsoft’s Keyword Planner — real search volume, competition and suggested bids, with NO planning-tier gate, unlike Google’s) · microsoft_ads_traffic_estimates (what those keywords would deliver at a named bid — a range, never one number) · microsoft_ads_budget_opportunities (where Microsoft says a budget is capping delivery, and what raising it is forecast to buy) · microsoft_ads_auction_insights (who ELSE is bidding on the same auctions — rival domains with their impression share, overlap and outranking share; shares of YOUR auctions, never a measure of a competitor’s whole account) · microsoft_ads_bulk_download (export the account as ONE bulk file — the only way to read ~185 Microsoft record types Hermoso cannot otherwise touch: sitelinks, callouts, structured snippets, labels, shared negative keyword lists, bid strategies, audiences, experiments, seasonality adjustments, conversion goals, asset groups, feeds) · microsoft_ads_bulk_upload (apply an edited bulk file — hundreds of objects in one request. IT IS GATED HARDER THAN ANYTHING ELSE ON THIS CONNECTOR, because a bulk file carries a Status column and can turn campaigns ON without ever touching set_microsoft_ads_status: confirm:true alone is refused, and you must first call it unconfirmed to get the row-by-row list of what it would ACTIVATE and DELETE, show that to the user, then echo both counts back as confirmActivations/confirmDeletions — or pass pauseInstead:true to land the file with every activation written as Paused) · list_microsoft_ads_conversion_goals (what the account counts as a conversion, and which goals are OFFLINE ones) · send_microsoft_ads_offline_conversions (close the loop: phone sales, in-store purchases and late-closing leads fed back so smart bidding stops optimising against website conversions alone — pass PLAIN emails and E.164 phones, hashing happens server-side to Microsoft’s own published spec) · microsoft_ads_editorial_reasons (WHY an ad or keyword was disapproved: the code, the word Microsoft objected to, where, and the appeal status) · list_microsoft_ads_audiences (the account’s Customer Match lists with their current sizes; a fresh list reads 0 for up to 48 hours and Microsoft will not use one under 300 people, so never call that a failed upload) · create_microsoft_ads_customer_list then apply_microsoft_ads_customer_list (build a Customer Match audience from PLAIN email addresses, normalized and SHA-256 hashed server-side to Microsoft’s own published spec so no plaintext ever leaves us; the user must be shown Microsoft’s Customer Match terms and agree first) · microsoft_ads_recommendations (what Microsoft ITSELF suggests changing, each one priced by Microsoft: budget raises carrying the current and recommended daily amount, new and broadened keywords, negative keywords it wants removed, and ads it has written. Every one INCREASES what the account buys, which is what they are for, so none is a free win and an empty list means Microsoft has no advice rather than that the account is optimal) · apply_microsoft_ads_recommendations (act on them, gated exactly like the bulk upload: confirm:true alone is REFUSED, so call it unconfirmed first to get every recommendation named with what it changes and Microsoft’s own cost estimate, show that to the user, then echo confirmCount and confirmCostIncrease back. Both are recomputed from a fresh read, and there is no undo) · dismiss_microsoft_ads_recommendations (take advice off the list. It cannot spend, so it needs no confirmation at all, and it is the right answer to “make it stop suggesting that” rather than applying something to clear it) · microsoft_ads_auto_apply (THE READ THAT ANSWERS “is Microsoft changing this account while nobody is looking?”, per type. An inherited account can already be opted in with nobody at the brand having done it) · set_microsoft_ads_auto_apply (turn that standing permission on or off. Switching any type ON is the strongest consent anywhere in Hermoso: Microsoft then writes and publishes its own ads under the brand’s name, deletes negative keywords so the account buys more searches, and changes conversion goals, unattended and indefinitely, with NOTHING to preview beforehand. So confirm:true is not enough and every type must be named in confirmTypes. Switching it OFF is never gated). GOOGLE BUSINESS PROFILE (the local-SEO channel — the listing panel on Google Search and Maps, which for a local business is where the demand actually is, and there is no delete): list_business_locations (the listings the connected Google account manages — call this first and let the USER pick when there is more than one; a Post on the wrong storefront is a public mistake) · post_to_google_business (publish a Post to the listing — text, ONE PHOTO and a call-to-action button; Google’s Posts API takes no video, so pass a still. EVENT and OFFER posts both require a title and a start date, and on an OFFER Google ignores the button link) · list_google_business_posts (what is showing right now, with each Post’s state) · delete_google_business_post (take one down — immediate and public, so confirm first) · list_google_business_reviews (the reviews on the listing, and which ones have NO reply yet — for a local business the highest-leverage surface there is) · reply_to_google_business_review (answer one publicly as the business; it is an UPSERT, so it replaces any existing reply) · list_google_business_questions + answer_google_business_question (the public Q&A on the listing) · google_business_search_keywords (the actual search terms people typed to find the listing — free local keyword data; low-volume terms are SUPPRESSED and come back as "fewer than N", never as zero) · google_business_insights (Search + Maps impressions, calls, website clicks, direction requests, messages, bookings — listing-level; Google discontinued per-Post insights in 2023 with no replacement, so never promise per-Post numbers) · get_business_location (everything the listing actually says — name, address, phone, website, categories, description, hours, service area — as the merchant set it; the answer to “what does our Google listing say?”) · update_business_location (change any of that — hours, phone, website, description, categories, even the name or address. It edits the live panel on Search and Maps with no draft and no undo, so call it WITHOUT confirm first: nothing is written, Google validates the payload, and you get the current value of every field you are about to change to show the user) · google_business_account (whose Business Profile account the listing is on, and whether the connected Google account’s role can edit it at all). Google gates this API behind a per-project access request and the default quota is zero, so the connection can be live and calls still refused — the error says so. CHATGPT ADS (ads under ChatGPT answers, via OpenAI’s Advertiser API — full management): list_openai_ads_campaigns (the ad account, then its campaigns, ad groups and ads with each ad’s review state) · openai_ads_report (impressions, clicks, spend, CTR, CPC, CPM at account / campaign / ad group / ad scope — run this first, it validates the key with zero spend risk) · openai_ads_geo_search (location ids) · list_openai_ads_audiences + create_openai_ads_audience (custom audiences — geo and these are the only list-based targeting this platform has; target them with customAudienceIds / excludedCustomAudienceIds on a campaign) · create_openai_ads_campaign (campaign → ad group → ad in one call, always PAUSED) · create_openai_ads_ad_group / create_openai_ads_ad (fill in an existing campaign) · update_openai_ads_object (rename, re-budget, rewrite context hints or the ad copy) · set_openai_ads_budget / set_openai_ads_status (change budget, activate, pause) · delete_openai_ads_object (ARCHIVE — this API has no delete and OpenAI say archiving is not reversible, so offer pausing first). TWO RULES THIS CHANNEL DOES NOT SHARE WITH THE OTHERS: it is connected by PASTING an Advertiser API key (no OAuth, no manager account, one key = one ad account), and it has exactly ONE creative format — a text plus image card, title 50 characters, body 100. There is NO VIDEO on ChatGPT Ads, so never offer a video ad here. GOOGLE DRIVE — ONE connection covering Drive, Sheets and Docs (full CRUD over the files Hermoso created there, plus any file the user hands over with the Google file picker in the app): save_to_drive · list_drive_files / get_drive_file · update_drive_file (rename/move/trash) · delete_drive_file · create_drive_folder. GOOGLE SHEETS (part of the Google Drive connection — export data to a spreadsheet the app creates, or read one the user picked; drive.file, no verification): create_sheet · append_to_sheet · read_sheet. GOOGLE DOCS (part of the Google Drive connection — export copy/brief/report as a doc, or read one the user picked; drive.file, no verification): create_doc · append_to_doc. GOOGLE SLIDES (part of the Google Drive connection — turn a swipefile collection into a real presentation, one slide per saved ad with the creative, brand, copy, run dates and platform; drive.file, no verification, no new scope): export_swipefile_deck — it CREATES a deck each time and cannot append to one the user already has, and a creative whose ad-library link has expired is reported rather than silently dropped. ONEDRIVE (full CRUD over the user’s Microsoft OneDrive): convert_onedrive_file (Microsoft converts a file server-side to PDF or JPG — ~130 formats including PowerPoint and Word decks, PSD, Illustrator, Sketch, 3D, video, iPhone HEIC and raw camera files; JPG needs both width and height) · save_to_onedrive · list_onedrive_files / get_onedrive_file · update_onedrive_file (rename/move) · delete_onedrive_file · create_onedrive_folder · share_onedrive_file (a view/edit link for a client) · onedrive_file_permissions (who can open it; revoke). INTERCOM (the brand’s own support inbox, nothing billed): list_intercom_conversations (filter by state, assignee, team, tag, channel, customer, dates, priority, unread, or app = the product the customer wrote from) · read_intercom_conversation · reply_to_intercom_conversation / add_intercom_note / manage_intercom_conversation (each a preview until confirm:true) · tag_intercom_conversation · list_intercom_tags · search_intercom_contacts · list_intercom_teammates · intercom_customer_language (customers’ own words for ad copy). VERCEL (the brand’s own sites, nothing billed): list_vercel_projects · list_vercel_deployments · list_vercel_domains · vercel_web_analytics (page views, visitors, top pages, referrers, countries) · vercel_event_analytics (custom-event conversions by campaign) · deploy_vercel_page (a static landing page, confirm-gated; preview by default) · delete_vercel_deployment. Use these standalone — Hermoso is a full posting/ads/file-storage control surface, not only an ad generator.',
|
|
263
263
|
'F) YOUR TOOL LIST IS A STARTING POINT, NOT THE PRODUCT \u2014 and the whole catalogue is two calls away. The listed roster is deliberately small (the core tools, plus whatever this connection asked for or you have switched on); every other tool in this map is held out of the LIST on SIZE alone and is fully built, fully live and fully callable. THE ROUTE THAT WORKS ON EVERY HOST, including the ones that cannot reload their tool list at all (claude.ai, ChatGPT): find_tools({query}) searches every tool by task or name and returns its parameters, its credit cost and its recent health in one line, then call_tool({name, args}) RUNS it \u2014 same account, same permissions, same result as if it had been listed. A direct tools/call to a name you already know works too. enable_tools({groups:[\u2026]}) additionally LISTS a whole group for hosts that re-list: `ads` (paid-campaign management on Meta, Google Ads, LinkedIn, Reddit, Microsoft, Pinterest, X, TikTok, Snapchat, ChatGPT Ads, Apple Search Ads \u2014 by far the heaviest group), `analytics` (GA4, Search Console), `channel_admin` (reading a channel back: insights, comments, DMs, catalogs, templates, webhooks, editing or deleting a published post), plus research, create, channels, files, workspace, or \'all\'. NEVER tell a user Hermoso cannot build a campaign, read a comment, answer a DM or pull a channel\u2019s numbers because you cannot see the tool \u2014 look it up and call it.',
|
|
264
264
|
].join('\n');
|
|
265
265
|
|
|
@@ -2610,15 +2610,30 @@ export const WIDGET_HOST_SPLITS = Object.freeze(withSplitAccounts({
|
|
|
2610
2610
|
splitOp('delete_youtube_comment', { action: 'delete' }, ['commentId!', 'confirm'], splitW(true, true), 'Delete a YouTube comment', 'Use this when a comment must be removed for good; YouTube has no undelete. Needs confirm:true. To only take it out of public view use reject_youtube_comment. Free.', { confirm: 'must be true to delete' }),
|
|
2611
2611
|
],
|
|
2612
2612
|
manage_x_broadcast: [
|
|
2613
|
-
splitOp('schedule_x_broadcast', { action: 'schedule' }, ['sourceId!', 'startAt!', 'manualPublish!', 'endAt', 'title', 'description', 'locale', 'availableForReplay', 'isLocked', 'chatOption', 'thumbnailMediaId', '
|
|
2614
|
-
splitOp('update_x_broadcast', { action: 'update' }, ['id!', 'startAt', 'endAt', 'manualPublish', 'title', 'description', 'locale', 'availableForReplay', 'isLocked', 'chatOption', 'thumbnailMediaId', '
|
|
2613
|
+
splitOp('schedule_x_broadcast', { action: 'schedule' }, ['sourceId!', 'startAt!', 'manualPublish!', 'endAt', 'title', 'description', 'locale', 'availableForReplay', 'isLocked', 'chatOption', 'thumbnailMediaId', 'confirm'], splitW(false, false), 'Schedule an X live broadcast', 'Use this when the brand wants to schedule an X live video broadcast. sourceId comes from list_x_stream_sources. manualPublish is required: true waits for start_x_broadcast; false makes X put the brand on air automatically at startAt and needs confirm:true.' + X_BILLED),
|
|
2614
|
+
splitOp('update_x_broadcast', { action: 'update' }, ['id!', 'startAt', 'endAt', 'manualPublish', 'title', 'description', 'locale', 'availableForReplay', 'isLocked', 'chatOption', 'thumbnailMediaId', 'confirm'], splitW(true, true), 'Change a scheduled X broadcast', 'Use this when a scheduled X broadcast needs a new time, title or setting. Only the fields you pass change. Turning manualPublish off needs confirm:true.' + X_BILLED),
|
|
2615
2615
|
splitOp('start_x_broadcast', { action: 'go_live' }, ['id!', 'confirm'], splitW(true, false), 'Go live on a scheduled X broadcast', 'Use this when a broadcast scheduled with manualPublish:true should go on air NOW, publicly, as the brand. Needs confirm:true after the user approved the exact broadcast and stream.' + X_BILLED, { confirm: 'must be true: this goes on air publicly' }),
|
|
2616
|
-
splitOp('
|
|
2616
|
+
splitOp('create_x_broadcast', { action: 'create' }, ['sourceId!'], splitW(false, false), 'Create an X broadcast on a stream source', 'Use this when the brand wants a broadcast without a schedule. It posts nothing until publish_x_broadcast, and X deletes it after 7 days if never published.' + X_BILLED),
|
|
2617
|
+
splitOp('publish_x_broadcast', { action: 'publish' }, ['id!', 'title', 'locale', 'chatOption', 'skipPost', 'confirm'], splitW(true, false), 'Publish an X broadcast', 'Use this when a created X broadcast should go on air NOW, publicly, as the brand. X also posts it unless skipPost is true. Needs confirm:true.' + X_BILLED, { confirm: 'must be true: this goes on air publicly' }),
|
|
2618
|
+
splitOp('end_x_broadcast', { action: 'end' }, ['id!', 'confirm'], splitW(true, true), 'End an X broadcast', 'Use this when a running X broadcast should stop for everyone watching. Cannot be undone; needs confirm:true.' + X_BILLED, { confirm: 'must be true to end' }),
|
|
2619
|
+
splitOp('delete_x_broadcast', { action: 'delete' }, ['id!', 'confirm'], splitW(true, true), 'Delete an X broadcast', 'Use this when a broadcast and its replay should be removed. End a running one first. Needs confirm:true.' + X_BILLED, { confirm: 'must be true to delete' }),
|
|
2620
|
+
splitOp('cancel_x_broadcast', { action: 'cancel' }, ['id!', 'confirm'], splitW(true, true), 'Cancel a scheduled X broadcast', 'Use this when a scheduled X broadcast should be deleted. Cannot be undone; needs confirm:true.' + X_BILLED, { confirm: 'must be true to cancel' }),
|
|
2621
|
+
],
|
|
2622
|
+
manage_x_stream_source: [
|
|
2623
|
+
splitOp('list_x_stream_sources', { action: 'list' }, [], SPLIT_RO, 'List the X stream sources', 'Use this when you need the brand’s RTMP stream sources on X: id, stream key, RTMPS URL, region, whether video is arriving. Read-only.' + X_BILLED),
|
|
2624
|
+
splitOp('get_x_stream_source', { action: 'get' }, ['id!'], SPLIT_RO, 'Read one X stream source', 'Use this when you need one stream source’s stream key, RTMPS URL, region and whether video is arriving. Read-only.' + X_BILLED),
|
|
2625
|
+
splitOp('get_x_ingest_region', { action: 'region' }, [], SPLIT_RO, 'Read the nearest X ingest region', 'Use this when you need the ingest region nearest X’s server, to pass when creating a stream source. Read-only.' + X_BILLED),
|
|
2626
|
+
splitOp('create_x_stream_source', { action: 'create' }, ['name!', 'region'], splitW(false, false), 'Create an X stream source', 'Use this when the brand needs a new RTMP ingest point for live video on X. The stream key is a secret, so never post it.' + X_BILLED),
|
|
2627
|
+
splitOp('rename_x_stream_source', { action: 'rename' }, ['id!', 'name!'], splitW(false, true), 'Rename an X stream source', 'Use this when a stream source needs a new label. The stream key does not change.' + X_BILLED),
|
|
2628
|
+
splitOp('delete_x_stream_source', { action: 'delete' }, ['id!', 'confirm'], splitW(true, true), 'Delete an X stream source', 'Use this when a stream source is no longer used. It kills the stream key; needs confirm:true.' + X_BILLED, { confirm: 'must be true to delete' }),
|
|
2617
2629
|
],
|
|
2618
2630
|
moderate_x_broadcast: [
|
|
2619
2631
|
splitOp('remove_x_broadcast_chat_message', { action: 'remove_message' }, ['broadcastId!', 'messageId!', 'confirm'], splitW(true, true), 'Remove a message from an X broadcast chat', 'Use this when one message in the brand’s live broadcast chat must be removed. Cannot be undone; needs confirm:true. Get messageId from read_x_broadcast_chat.' + X_BILLED, { confirm: 'must be true to remove' }),
|
|
2620
|
-
splitOp('mute_x_broadcast_chat_user', { action: 'mute' }, ['broadcastId!', 'userId', 'username', 'until', 'messageId'], splitW(true, true), 'Mute someone in an X broadcast chat', 'Use this when a viewer should be muted in the brand’s live broadcast chat. Pass userId or username; until ends a timeout (omit to mute
|
|
2632
|
+
splitOp('mute_x_broadcast_chat_user', { action: 'mute' }, ['broadcastId!', 'userId', 'username', 'until', 'messageId'], splitW(true, true), 'Mute someone in an X broadcast chat', 'Use this when a viewer should be muted in the brand’s live broadcast chat. Pass userId or username; until ends a timeout (omit to mute indefinitely); messageId also removes that message.' + X_BILLED),
|
|
2621
2633
|
splitOp('unmute_x_broadcast_chat_user', { action: 'unmute' }, ['broadcastId!', 'userId', 'username'], splitW(true, true), 'Unmute someone in an X broadcast chat', 'Use this when a muted viewer may chat again in the brand’s live broadcast. Pass userId or username.' + X_BILLED),
|
|
2634
|
+
splitOp('list_x_broadcast_chat_mutes', { action: 'list_mutes' }, [], SPLIT_RO, 'List who is muted in the X broadcast chats', 'Use this when you need everyone muted across the brand’s broadcast chats, with when each timeout ends. Read-only.' + X_BILLED),
|
|
2635
|
+
splitOp('mute_x_chat_user_everywhere', { action: 'mute_everywhere' }, ['userId', 'username', 'until', 'broadcastId', 'messageId'], splitW(true, true), 'Mute someone in every X broadcast chat', 'Use this when a viewer should be muted in all the brand’s broadcast chats. Pass userId or username; until ends a timeout. broadcastId is for moderating another host.' + X_BILLED),
|
|
2636
|
+
splitOp('unmute_x_chat_user_everywhere', { action: 'unmute_everywhere' }, ['userId', 'username', 'broadcastId'], splitW(true, true), 'Unmute someone in every X broadcast chat', 'Use this when a viewer muted across the brand’s broadcast chats may chat again. Pass userId or username.' + X_BILLED),
|
|
2622
2637
|
splitOp('list_x_broadcast_chat_moderators', { action: 'list_moderators' }, [], SPLIT_RO, 'List the X broadcast chat moderators', 'Use this when you need the brand’s persistent X broadcast chat moderators, who moderate every current and future broadcast. Read-only.' + X_BILLED),
|
|
2623
2638
|
splitOp('add_x_broadcast_chat_moderator', { action: 'add_moderator' }, ['userId', 'username'], splitW(false, true), 'Add an X broadcast chat moderator', 'Use this when someone should moderate the chat on every current and future broadcast of the brand. Pass userId or username.' + X_BILLED),
|
|
2624
2639
|
splitOp('remove_x_broadcast_chat_moderator', { action: 'remove_moderator' }, ['userId', 'username'], splitW(true, true), 'Remove an X broadcast chat moderator', 'Use this when someone should stop moderating the brand’s broadcast chats. Pass userId or username.' + X_BILLED),
|
|
@@ -5254,22 +5269,47 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5254
5269
|
// row carries no CDN urls, and get_meta_story returns one Story in full.
|
|
5255
5270
|
server.registerTool('list_meta_stories', {
|
|
5256
5271
|
title: 'List the Stories live right now',
|
|
5257
|
-
description: 'The Stories live right now on the connected Instagram accounts and Facebook Pages: id, caption (Instagram), posted time, hours left and permalink. Use it to check that a post or reel really went to Stories, or to match a story id against what is live. Meta serves a Story for 24 hours and leaves live video and reshared stories out of this list. A source that could not be read comes back in `unreadable`, never as an empty list. Read-only, 0 credits.',
|
|
5272
|
+
description: 'The Stories live right now on the connected Instagram accounts and Facebook Pages: id, caption (Instagram), posted time, hours left and permalink, and on Instagram `insights`: reach, views, replies, link clicks and navigation (forward, back, next story, exit). Use it to check that a post or reel really went to Stories, or to match a story id against what is live. includeEnded also returns `ended`, the Stories that have ended with the numbers kept from before they ended. Meta marks nothing on a Story to say it is a reshared reel, so `origin` appears only when the share went through Hermoso, and says how it was found. Meta serves a Story for 24 hours and leaves live video and reshared stories out of this list. A source that could not be read comes back in `unreadable`, never as an empty list. Read-only, 0 credits.',
|
|
5258
5273
|
inputSchema: {
|
|
5259
5274
|
...MANAGE_BRAND,
|
|
5260
5275
|
channel: z.enum(['instagram', 'facebook', 'all']).optional().describe('default all'),
|
|
5261
5276
|
account: z.string().optional().describe('one Instagram account: an @handle or id'),
|
|
5262
5277
|
pageId: z.string().optional().describe('one Facebook Page, and its linked Instagram'),
|
|
5263
5278
|
limit: z.number().optional().describe('rows, newest first (1-100, default 25)'),
|
|
5279
|
+
includeEnded: z.boolean().optional().describe('also list Stories that ended, with their kept numbers'),
|
|
5280
|
+
days: z.number().optional().describe('how far back includeEnded goes (1-400, default 30)'),
|
|
5264
5281
|
},
|
|
5265
|
-
outputSchema: { count: z.number().optional(), stories: z.array(z.any()).optional(), read: z.array(z.any()).optional(), unreadable: z.array(z.any()).optional(), note: z.string().optional(), caveat: z.string().optional() },
|
|
5282
|
+
outputSchema: { count: z.number().optional(), stories: z.array(z.any()).optional(), ended: z.array(z.any()).optional(), read: z.array(z.any()).optional(), unreadable: z.array(z.any()).optional(), note: z.string().optional(), caveat: z.string().optional() },
|
|
5266
5283
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
5267
5284
|
}, wrap(async (a) => {
|
|
5268
|
-
const d = await apiGet('/api/meta/stories', queryBrand(a, { channel: a.channel, account: a.account, pageId: a.pageId, limit: a.limit }));
|
|
5269
|
-
const lines = (d.stories || []).map(s => `• [${s.channel} ${s.account}] ${s.mediaType || 'story'}${s.caption ? ` ${JSON.stringify(s.caption.slice(0, 90))}` : ''} · posted ${s.postedAt || 'unknown'}${s.hoursLeft != null ? ` · ${s.hoursLeft}h left` : ''}\n id ${s.id}${s.permalink ? ` · ${s.permalink}` : ''}`);
|
|
5285
|
+
const d = await apiGet('/api/meta/stories', queryBrand(a, { channel: a.channel, account: a.account, pageId: a.pageId, limit: a.limit, ...(a.includeEnded ? { includeEnded: 'true' } : {}), ...(a.days ? { days: a.days } : {}) }));
|
|
5286
|
+
const lines = (d.stories || []).map(s => `• [${s.channel} ${s.account}] ${s.mediaType || 'story'}${s.caption ? ` ${JSON.stringify(s.caption.slice(0, 90))}` : ''} · posted ${s.postedAt || 'unknown'}${s.hoursLeft != null ? ` · ${s.hoursLeft}h left` : ''}\n id ${s.id}${s.permalink ? ` · ${s.permalink}` : ''}${s.insights ? `\n ${s.insights.summary}` : ''}${s.origin?.sameFileAsReel ? `\n Posted through Hermoso with the same file as reel ${s.origin.sameFileAsReel.postId}${s.origin.sameFileAsReel.url ? ` (${s.origin.sameFileAsReel.url})` : ''}.` : ''}`);
|
|
5287
|
+
const endedLines = (d.ended || []).map(s => `• [ended ${s.account}] posted ${s.postedAt || 'unknown'}${s.caption ? ` ${JSON.stringify(s.caption.slice(0, 90))}` : ''}\n id ${s.storyId}\n ${s.summary}`);
|
|
5270
5288
|
const read = (d.read || []).map(r => `${r.account} ${r.count}`).join(', ');
|
|
5271
5289
|
const bad = (d.unreadable || []).map(u => `${u.account}: ${u.error}`).join('; ');
|
|
5272
|
-
return ok(`${d.count} Stor${d.count === 1 ? 'y' : 'ies'} live${read ? ` (${read})` : ''}.\n${lines.join('\n')}${bad ? `\nCould not read ${bad}. That is not the same as no Stories there.` : ''}${d.note ? `\n${d.note}` : ''}\n${d.caveat || ''}`.trim(), d);
|
|
5290
|
+
return ok(`${d.count} Stor${d.count === 1 ? 'y' : 'ies'} live${read ? ` (${read})` : ''}.\n${lines.join('\n')}${endedLines.length ? `\nEnded:\n${endedLines.join('\n')}` : ''}${bad ? `\nCould not read ${bad}. That is not the same as no Stories there.` : ''}${d.note ? `\n${d.note}` : ''}\n${d.caveat || ''}`.trim(), d);
|
|
5291
|
+
}));
|
|
5292
|
+
// A STORY'S NUMBERS OUTLIVE IT (2026-10-31). Meta keeps them for 24 hours, so the server reads every live Story on a schedule
|
|
5293
|
+
// and takes the story_insights webhook; this reads what is kept. Every state but `numbers` is a statement about what is
|
|
5294
|
+
// unknown, never a zero.
|
|
5295
|
+
server.registerTool('instagram_story_insights', {
|
|
5296
|
+
title: 'An Instagram Story\u2019s numbers, kept past its 24 hours',
|
|
5297
|
+
description: 'An Instagram Story\u2019s numbers: reach, views, replies, shares, link clicks, profile visits, follows, and navigation (forward, back, next story, exit). Meta keeps them for 24 hours, so Hermoso reads every live Story on a schedule, including ones posted from the phone, and they stay readable here after the Story ends. Pass storyId for one, or nothing for the recent ones. `state` says what is known: numbers, below_minimum (Meta withholds them under 5 viewers), not_captured (it ended before a read), or why a read failed; none of those means zero. Meta returns 0 replies for Stories created in Europe and Japan, and the answer says so. Link clicks need a Facebook-login connection. Read-only, 0 credits.',
|
|
5298
|
+
inputSchema: {
|
|
5299
|
+
...MANAGE_BRAND,
|
|
5300
|
+
storyId: z.string().optional().describe('one Story id, from list_meta_stories'),
|
|
5301
|
+
account: z.string().optional().describe('one Instagram account: an @handle or id'),
|
|
5302
|
+
pageId: z.string().optional().describe('one Facebook Page, for its linked Instagram'),
|
|
5303
|
+
days: z.number().optional().describe('how far back (1-400, default 30)'),
|
|
5304
|
+
limit: z.number().optional().describe('rows, newest first (1-100, default 25)'),
|
|
5305
|
+
},
|
|
5306
|
+
outputSchema: { count: z.number().optional(), days: z.number().optional(), stories: z.array(z.any()).optional(), unreadable: z.array(z.any()).optional(), state: z.string().optional(), note: z.string().optional(), caveat: z.string().optional() },
|
|
5307
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
5308
|
+
}, wrap(async (a) => {
|
|
5309
|
+
const d = await apiGet('/api/meta/story-insights', queryBrand(a, { storyId: a.storyId, account: a.account, pageId: a.pageId, days: a.days, limit: a.limit }));
|
|
5310
|
+
const lines = (d.stories || []).map(s => `• [${s.account}] posted ${s.postedAt || 'unknown'}${s.ended ? ' (ended)' : ''}${s.caption ? ` ${JSON.stringify(s.caption.slice(0, 90))}` : ''}\n id ${s.storyId}${s.permalink ? ` · ${s.permalink}` : ''}\n ${s.summary}`);
|
|
5311
|
+
const bad = (d.unreadable || []).map(u => `${u.account || 'Instagram'}: ${u.error}`).join('; ');
|
|
5312
|
+
return ok(`${d.count} Stor${d.count === 1 ? 'y' : 'ies'} with a record in the last ${d.days} days.${d.state ? ` ${d.note}` : ''}\n${lines.join('\n')}${bad ? `\nCould not read ${bad}. That is not the same as no numbers.` : ''}${d.note && !d.state ? `\n${d.note}` : ''}\n${d.caveat || ''}`.trim(), d);
|
|
5273
5313
|
}));
|
|
5274
5314
|
server.registerTool('get_meta_story', {
|
|
5275
5315
|
title: 'Look up one Story by id',
|
|
@@ -6073,7 +6113,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6073
6113
|
paidPartnership: z.boolean().optional().describe('INSTAGRAM — the PAID PARTNERSHIP label. A COMPLIANCE DECLARATION: set it whenever the post is sponsored, gifted or otherwise paid for. Opt-in and never inferred — it is the poster’s own statement about their commercial relationship.'),
|
|
6074
6114
|
brandedContentSponsorIds: z.array(z.string()).optional().describe('INSTAGRAM — the numeric Instagram USER IDS of the brands behind that label (at most 2, and ids rather than @handles). Naming sponsors IS asking for the label, so setting these with `paidPartnership:false` is refused instead of publishing brand credits with no disclosure.'),
|
|
6075
6115
|
trialReel: z.enum(['MANUAL', 'SS_PERFORMANCE']).optional().describe('INSTAGRAM TRIAL REEL \u2014 publish this Reel to NON-FOLLOWERS ONLY at first (Instagram allows trials only on accounts above its follower threshold — about 1,000 followers; an ineligible account is refused by name and nothing is posted), 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.'),
|
|
6076
|
-
story: z.boolean().optional().describe('INSTAGRAM OR FACEBOOK PAGE STORY \u2014 publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel \u2014 any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.'),
|
|
6116
|
+
story: z.boolean().optional().describe('INSTAGRAM OR FACEBOOK PAGE STORY \u2014 publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A link sticker cannot be added through Meta\u2019s publishing API on either channel: add it in the Instagram app. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel \u2014 any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.'),
|
|
6077
6117
|
aiGenerated: z.boolean().optional().describe('INSTAGRAM / FACEBOOK REEL / THREADS \u2014 Meta\u2019s is_ai_generated self-disclosure (Threads: is_made_with_ai, the AI info label; a text-only Threads post is declared only when you pass true). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared, media that came through upload_file or from an external URL (the user\u2019s own photographs or footage) is NOT \u2014 a real photo must never carry Instagram\u2019s \u201cAI info\u201d label. Pass true or false only to override: false strips the label from something Hermoso would otherwise declare, true declares a render the user uploaded themselves.'),
|
|
6078
6118
|
altText: z.union([z.string(), z.array(z.string())]).optional().describe('ACCESSIBILITY \u2014 the description screen readers announce, and what the platform otherwise auto-generates badly or not at all. Describe what is actually IN the picture, never the caption. ONE STRING describes the picture; on a CAROUSEL it describes EVERY slide. Pass an ARRAY of strings instead to describe each slide separately, aligned to the slide order \u2014 use that on a multi-slide post, because one sentence read out over six different pictures is wrong for five of them. More descriptions than pictures is refused rather than dropped. WHERE IT LANDS, per Meta\u2019s own docs: INSTAGRAM image posts and the IMAGE slides of an Instagram carousel (up to 1000 characters; dropped rather than sent on a Reel or a video slide, which Meta do not support); FACEBOOK Page photos including every photo of an album, via alt_text_custom (Meta publish no length for it, so nothing is truncated); THREADS on a single-image or single-video post ONLY \u2014 Meta document no way to attach alt text to a Threads CAROUSEL slide, so a Threads carousel publishes undescribed and the reply says so rather than risking the whole post on a guess. (Meta\u2019s AI disclosure defaults from PROVENANCE: a Hermoso render is declared is_ai_generated; media that came through upload_file or an external URL, i.e. the user\u2019s own photos or footage, is NOT. Pass `aiGenerated` to force it either way.)'),
|
|
6079
6119
|
pageId: z.string().optional().describe('target Page id (from list_meta_pages); omit = first Page'),
|
|
@@ -6236,7 +6276,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6236
6276
|
instagramLocationId: z.string().optional().describe('INSTAGRAM — tag a place (called locationId on post_to_meta; locationId here is the Google Business listing). It is the pageId from search_meta_places, the NUMERIC ID OF A FACEBOOK PAGE associated with that location, not a place name and not coordinates; a non-numeric value is refused rather than sent.'),
|
|
6237
6277
|
brandedContentSponsorIds: z.array(z.string()).optional().describe('INSTAGRAM — the numeric Instagram USER IDS of the brands behind that label (at most 2, and ids rather than @handles). Naming sponsors IS asking for the label, so setting these with `paidPartnership:false` is refused instead of publishing brand credits with no disclosure.'),
|
|
6238
6278
|
trialReel: z.enum(['MANUAL', 'SS_PERFORMANCE']).optional().describe('INSTAGRAM TRIAL REEL \u2014 publish this Reel to NON-FOLLOWERS ONLY at first (Instagram allows trials only on accounts above its follower threshold — about 1,000 followers; an ineligible account is refused by name and nothing is posted), so a hook can be tested on a cold audience without spending it on the people who already follow the brand; Instagram 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. REELS ONLY and INSTAGRAM ONLY: an image, a carousel, or a Facebook/Threads channel 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, so Instagram must be one of the `channels` and the item must carry a video. Omit it for a normal Reel.'),
|
|
6239
|
-
story: z.boolean().optional().describe('INSTAGRAM OR FACEBOOK PAGE STORY \u2014 publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel \u2014 any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.'),
|
|
6279
|
+
story: z.boolean().optional().describe('INSTAGRAM OR FACEBOOK PAGE STORY \u2014 publish this as a 24-hour Story instead of a feed post. One image OR one video: Instagram has no carousel story, so a carousel is REFUSED BY NAME rather than quietly posted to the feed. A link sticker cannot be added through Meta\u2019s publishing API on either channel: add it in the Instagram app. An Instagram Story keeps its caption: people see the start under the Story in the app and tap for the rest (instagram.com does not show it). It takes NO collaborators, product tags or trialReel \u2014 any of those is refused by name and nothing is posted. FACEBOOK PAGE STORIES WORK TOO (target facebook): one image, or one vertical 9:16 video of 3–90 seconds, no caption (a message is refused by name), not schedulable natively — use schedule_post to queue one; the reply reads the story back from the Page.'),
|
|
6240
6280
|
aiGenerated: z.boolean().optional().describe('AI-CONTENT DISCLOSURE (Instagram / Facebook Reel is_ai_generated, TikTok is_aigc, YouTube containsSyntheticMedia, Pinterest ai_disclosures, Threads is_made_with_ai). OMIT IT and Hermoso decides from provenance: a Hermoso render is declared AI-generated, media that came through upload_file or from an external URL (the user\u2019s own photos or footage) is NOT. Pass true or false only to override.'),
|
|
6241
6281
|
// ── WHICH ACCOUNT (server-side SCHED_ID_FIELDS). Every one of these is an answer the publish helper REFUSES
|
|
6242
6282
|
// to guess, so a schedule that cannot carry it can only fail at fire time with nobody watching.
|
|
@@ -6305,7 +6345,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6305
6345
|
...(r.xArticle && r.xArticle.title ? { xArticle: { title: r.xArticle.title } } : {}),
|
|
6306
6346
|
media: r.videoUrl ? 'video' : slides > 1 ? `carousel of ${slides}` : (r.imageUrl || slides) ? 'image' : 'none',
|
|
6307
6347
|
...((r.videoUrl || r.imageUrl) ? { mediaUrl: r.videoUrl || r.imageUrl } : {}),
|
|
6308
|
-
...(Array.isArray(r.results) ? { results: r.results.map(x => ({ channel: x?.channel, ok: x?.ok, ...(x?.id ? { id: String(x.id) } : {}), ...(x?.url ? { url: x.url } : {}), ...(x?.error ? { error: String(x.error).slice(0, 300) } : {}) })) } : {}),
|
|
6348
|
+
...(Array.isArray(r.results) ? { results: r.results.map(x => ({ channel: x?.channel, ok: x?.ok, ...(x?.id ? { id: String(x.id) } : {}), ...(x?.url ? { url: x.url } : {}), ...(x?.urlStatus && x.urlStatus !== 'ready' ? { urlStatus: x.urlStatus, ...(x.urlNote ? { urlNote: x.urlNote } : {}) } : {}), ...(x?.error ? { error: String(x.error).slice(0, 300) } : {}) })) } : {}),
|
|
6309
6349
|
...(r.error ? { error: String(r.error).slice(0, 300) } : {}),
|
|
6310
6350
|
};
|
|
6311
6351
|
};
|
|
@@ -7944,10 +7984,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7944
7984
|
const XB_SCOPE_NOTE = ' Needs X connected with the broadcast permissions added 2026-09-21: an X connection made before then must be reconnected once under Settings > Connectors > X (the tool says so without calling X). Costs credits (X bills per call).';
|
|
7945
7985
|
server.registerTool('list_x_broadcasts', {
|
|
7946
7986
|
title: 'List X live broadcasts',
|
|
7947
|
-
description: "The connected X account's LIVE VIDEO broadcasts: past and current ones (title, state, share URL, live and total viewers, start/end) and SCHEDULED ones (start, end, whether it goes live automatically or waits for go_live
|
|
7987
|
+
description: "The connected X account's LIVE VIDEO broadcasts: past and current ones (title, state, share URL, live and total viewers, start/end) and SCHEDULED ones (start, end, whether it goes live automatically or waits for go_live). kind 'all' (default) reads both, 'live' or 'scheduled' one; pass id for a single broadcast. Read-only." + XB_SCOPE_NOTE,
|
|
7948
7988
|
inputSchema: {
|
|
7949
7989
|
kind: z.enum(['all', 'live', 'scheduled']).optional().describe('all (default), live or scheduled'),
|
|
7950
|
-
id: z.string().optional().describe('one broadcast id (alphanumeric, up to
|
|
7990
|
+
id: z.string().optional().describe('one broadcast id (alphanumeric, up to 24 characters)'),
|
|
7951
7991
|
ids: z.string().optional().describe('comma-separated broadcast ids (live kind, up to 100)'),
|
|
7952
7992
|
maxResults: z.number().optional().describe('1-100, default 25'),
|
|
7953
7993
|
paginationToken: z.string().optional().describe('next page token from the previous reply'),
|
|
@@ -7958,16 +7998,16 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7958
7998
|
}, wrap(async (a = {}) => {
|
|
7959
7999
|
const d = await apiGet('/api/x/broadcasts', a);
|
|
7960
8000
|
const live = (d.broadcasts || []).slice(0, 15).map((b) => `- ${b.title || '(untitled)'} [${b.id}] ${b.state || ''}${b.watched != null ? ` - ${b.watched} watched` : ''}${b.shareUrl ? ` - ${b.shareUrl}` : ''}`).join('\n');
|
|
7961
|
-
const sch = (d.scheduled || []).slice(0, 15).map((s) => `- ${s.title || '(untitled)'} [${s.id}] ${s.start || ''}${s.manualPublish ? ' - manual go-live' : ' - auto-publishes'}
|
|
7962
|
-
return ok([d.broadcasts ? `Broadcasts (${d.broadcasts.length}):\n${live || '(none)'}` : '', d.scheduled ? `Scheduled (${d.scheduled.length}):\n${sch || '(none)'}` : ''].filter(Boolean).join('\n\n') + `\nCost ${d.costCredits ?? '?'} credits.`, d);
|
|
8001
|
+
const sch = (d.scheduled || []).slice(0, 15).map((s) => `- ${s.title || '(untitled)'} [${s.id}] ${s.start || ''}${s.manualPublish ? ' - manual go-live' : ' - auto-publishes'}`).join('\n');
|
|
8002
|
+
return ok([d.broadcastsRefused ? `Past and live broadcasts could not be read: ${d.broadcastsRefused}` : '', d.broadcasts ? `Broadcasts (${d.broadcasts.length}):\n${live || '(none)'}` : '', d.scheduled ? `Scheduled (${d.scheduled.length}):\n${sch || '(none)'}` : ''].filter(Boolean).join('\n\n') + `\nCost ${d.costCredits ?? '?'} credits.`, d);
|
|
7963
8003
|
}));
|
|
7964
8004
|
server.registerTool('manage_x_broadcast', {
|
|
7965
8005
|
title: 'Schedule, update, go live on or cancel an X broadcast',
|
|
7966
|
-
description: "Schedule, update, go live on, or cancel an X LIVE VIDEO broadcast on the connected account. action 'schedule' needs sourceId (
|
|
8006
|
+
description: "Schedule, update, go live on, or cancel an X LIVE VIDEO broadcast on the connected account. action 'schedule' needs sourceId (from manage_x_stream_source), startAt, and manualPublish, which is REQUIRED with no default: true = nothing airs until action 'go_live'; false = X puts the brand ON AIR automatically at startAt (needs confirm:true). 'update' changes only the fields you pass (Hermoso re-sends the rest, because X's update replaces the whole schedule). 'go_live' airs a manualPublish schedule NOW, publicly, as the brand (confirm:true). 'cancel' deletes a schedule and cannot be undone (confirm:true). X retired recurring broadcasts: schedule each occurrence on its own. WITHOUT A SCHEDULE: 'create' makes a broadcast on sourceId that posts nothing, 'publish' puts it on air (confirm:true; skipPost:true skips the announcing post), 'end' stops one for everyone (confirm:true), 'delete' removes one and its replay (confirm:true; end it first). X limits broadcasting by account (Premium or 1,000 followers) and says so. Confirm the exact broadcast, time and stream with the user before passing confirm:true; a refusal is free." + XB_SCOPE_NOTE,
|
|
7967
8007
|
inputSchema: {
|
|
7968
|
-
action: z.enum(['schedule', 'update', 'go_live', 'cancel']).describe('schedule, update, go_live or
|
|
7969
|
-
id: z.string().optional().describe('broadcast id, for
|
|
7970
|
-
sourceId: z.string().optional().describe('stream
|
|
8008
|
+
action: z.enum(['schedule', 'update', 'go_live', 'cancel', 'create', 'publish', 'end', 'delete']).describe('schedule, update, go_live, cancel, create, publish, end or delete'),
|
|
8009
|
+
id: z.string().optional().describe('broadcast id, for every action but schedule and create'),
|
|
8010
|
+
sourceId: z.string().optional().describe('stream source id from manage_x_stream_source (schedule, create)'),
|
|
7971
8011
|
startAt: z.string().optional().describe('ISO 8601 or epoch ms'),
|
|
7972
8012
|
endAt: z.string().optional().describe('ISO 8601 or epoch ms; X requires one on update'),
|
|
7973
8013
|
manualPublish: z.boolean().optional().describe('required on schedule: true waits for go_live, false auto-publishes at startAt'),
|
|
@@ -7975,17 +8015,40 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
7975
8015
|
availableForReplay: z.boolean().optional().describe('true = keep the broadcast available as a replay'), isLocked: z.boolean().optional().describe("true = lock the broadcast (X's isLocked flag)"),
|
|
7976
8016
|
chatOption: z.string().optional().describe('X chat permission option (numeric string)'),
|
|
7977
8017
|
thumbnailMediaId: z.string().optional().describe('pre-live slate media id'),
|
|
7978
|
-
|
|
7979
|
-
rollForward: z.boolean().optional().describe('cancel only: true shifts a recurring series forward instead of leaving a gap'),
|
|
8018
|
+
skipPost: z.boolean().optional().describe('publish: true skips the post X would make announcing it'),
|
|
7980
8019
|
confirm: z.boolean().optional().describe('true only after the user approved this exact public or irreversible action'),
|
|
7981
8020
|
},
|
|
7982
8021
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
7983
8022
|
}, wrap(async (a) => {
|
|
7984
8023
|
const d = await apiPost('/api/x/broadcasts/manage', a);
|
|
7985
8024
|
if (d.action === 'cancel') return ok(d.deleted ? `Cancelled the scheduled broadcast ${d.id}. Cost ${d.costCredits ?? '?'} credits.` : `X did not confirm cancelling ${d.id}.`, d);
|
|
8025
|
+
if (d.action === 'create') return ok(`Created broadcast [${d.broadcast?.id}] on the source, not on air. ${d.note}${d.shareUrl ? ` ${d.shareUrl}` : ''} Cost ${d.costCredits ?? '?'} credits.`, d);
|
|
8026
|
+
if (d.action === 'publish') return ok(`Broadcast ${d.id} published, state ${d.state || '?'}. Cost ${d.costCredits ?? '?'} credits.`, d);
|
|
8027
|
+
if (d.action === 'end') return ok(d.ended ? `Ended broadcast ${d.id}. Cost ${d.costCredits ?? '?'} credits.` : `X did not confirm ending ${d.id} (state ${d.state || '?'}).`, d);
|
|
8028
|
+
if (d.action === 'delete') return ok(d.deleted ? `Deleted broadcast ${d.id} and its replay. Cost ${d.costCredits ?? '?'} credits.` : `X did not confirm deleting ${d.id}.`, d);
|
|
7986
8029
|
const s = d.scheduled || {};
|
|
7987
8030
|
return ok(`${d.action === 'go_live' ? 'Went live' : d.action === 'update' ? 'Updated' : 'Scheduled'}: ${s.title || '(untitled)'} [${s.id}] start ${s.start || '?'}, state ${s.state || '?'}.${d.note ? ` ${d.note}` : ''} Cost ${d.costCredits ?? '?'} credits.`, d);
|
|
7988
8031
|
}));
|
|
8032
|
+
server.registerTool('manage_x_stream_source', {
|
|
8033
|
+
title: 'Manage X stream sources',
|
|
8034
|
+
description: "The connected X account's STREAM SOURCES, the RTMP ingest points a live broadcast plays from. action 'list' (default) and 'get' (id) show each source's id, stream key, RTMPS URL, region and whether video is arriving; 'create' (name, optional region) makes one; 'rename' (id, name); 'delete' (id, confirm:true: it kills the stream key); 'region' reads the nearest ingest region. Pass a source's id as sourceId to manage_x_broadcast. The stream key is a secret, so never post it." + XB_SCOPE_NOTE,
|
|
8035
|
+
inputSchema: {
|
|
8036
|
+
action: z.enum(['list', 'get', 'create', 'rename', 'delete', 'region']).optional().describe("default 'list'"),
|
|
8037
|
+
id: z.string().optional().describe('source id, for get, rename and delete'),
|
|
8038
|
+
name: z.string().optional().describe('label, for create and rename'),
|
|
8039
|
+
region: z.string().optional().describe("create only: 'atla' or 'pdxa', or an AWS region X maps onto one"),
|
|
8040
|
+
confirm: z.boolean().optional().describe('true only after the user approved deleting this source'),
|
|
8041
|
+
},
|
|
8042
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
8043
|
+
}, wrap(async (a = {}) => {
|
|
8044
|
+
const d = await apiPost('/api/x/stream-sources', a);
|
|
8045
|
+
const cost = `Cost ${d.costCredits ?? '?'} credits.`;
|
|
8046
|
+
if (d.action === 'region') return ok(`Nearest ingest region: ${d.region || '?'}. ${cost}`, d);
|
|
8047
|
+
if (d.action === 'delete') return ok(`Deleted stream source ${d.id}. ${cost}`, d);
|
|
8048
|
+
if (d.action === 'list') return ok(`${d.count} stream source(s):\n` + (d.sources || []).map((x) => `- ${x.name || '(no name)'} [${x.id}] ${x.region || ''}${x.receivingVideo ? ' - receiving video' : ''}\n ${x.rtmpsUrl || ''}`).join('\n') + `\n${cost}`, d);
|
|
8049
|
+
const x = d.source || {};
|
|
8050
|
+
return ok(`${x.name || '(no name)'} [${x.id}] ${x.region || ''}${x.receivingVideo ? ' - receiving video' : ''}\nRTMPS ${x.rtmpsUrl || '?'}, stream key ${x.streamKey || '?'}.${d.note ? ` ${d.note}` : ''} ${cost}`, d);
|
|
8051
|
+
}));
|
|
7989
8052
|
server.registerTool('read_x_broadcast_chat', {
|
|
7990
8053
|
title: 'Read an X broadcast chat',
|
|
7991
8054
|
description: "The chat on one of the connected X account's live broadcasts, newest first: message id, text, author handle, time and reply-to. Use it to answer viewers or find a message to moderate. Read-only." + XB_SCOPE_NOTE,
|
|
@@ -8006,9 +8069,9 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
8006
8069
|
}));
|
|
8007
8070
|
server.registerTool('moderate_x_broadcast', {
|
|
8008
8071
|
title: 'Moderate an X broadcast chat',
|
|
8009
|
-
description: "Moderate the connected X account's live broadcast chat. action 'remove_message' (broadcastId + messageId; cannot be undone, confirm:true), 'mute' (broadcastId + userId or username; until = when a timeout ends, omit to mute
|
|
8072
|
+
description: "Moderate the connected X account's live broadcast chat. action 'remove_message' (broadcastId + messageId; cannot be undone, confirm:true), 'mute' (broadcastId + userId or username; until = when a timeout ends, omit to mute indefinitely; messageId removes that message while muting), 'unmute', 'list_mutes' (everyone muted across the brand's chats), 'mute_everywhere' / 'unmute_everywhere' (userId or username, every chat of the brand; when the brand moderates another host, broadcastId scopes it to that host), and the account's PERSISTENT chat moderators, which apply to every current and future broadcast: 'list_moderators', 'add_moderator', 'remove_moderator' (userId or username). Only the host or a moderator can moderate, and X says so if not. A username costs one extra lookup." + XB_SCOPE_NOTE,
|
|
8010
8073
|
inputSchema: {
|
|
8011
|
-
action: z.enum(['remove_message', 'mute', 'unmute', 'list_moderators', 'add_moderator', 'remove_moderator']).describe('remove_message, mute, unmute, list_moderators, add_moderator or
|
|
8074
|
+
action: z.enum(['remove_message', 'mute', 'unmute', 'list_moderators', 'add_moderator', 'remove_moderator', 'list_mutes', 'mute_everywhere', 'unmute_everywhere']).describe('remove_message, mute, unmute, list_moderators, add_moderator, remove_moderator, list_mutes, mute_everywhere or unmute_everywhere'),
|
|
8012
8075
|
broadcastId: z.string().optional().describe('broadcast id; for remove_message, mute and unmute'), messageId: z.string().optional().describe('chat message id to remove; with mute, also removes that message'),
|
|
8013
8076
|
userId: z.string().optional().describe('numeric X account id'), username: z.string().optional().describe('X handle, resolved to the id for you'),
|
|
8014
8077
|
until: z.string().optional().describe('mute end, ISO 8601 or epoch ms'),
|
|
@@ -8020,6 +8083,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
8020
8083
|
if (d.action === 'list_moderators') return ok(`${d.count} chat moderator(s):\n` + (d.moderators || []).map((u) => `- ${u.username} (${u.id})`).join('\n'), d);
|
|
8021
8084
|
if (d.action === 'remove_message') return ok(d.removed ? `Removed message ${d.messageId}.` : `X did not confirm removing ${d.messageId}.`, d);
|
|
8022
8085
|
if (d.action === 'mute' || d.action === 'unmute') return ok(`${d.username || d.userId} is ${d.muted ? 'muted' : 'not muted'} in broadcast ${d.broadcastId}.`, d);
|
|
8086
|
+
if (d.action === 'list_mutes') return ok(`${d.count} muted in the brand's chats:\n` + (d.mutes || []).map((m) => `- ${m.username || m.userId}${m.until ? ` until ${m.until}` : ''}`).join('\n'), d);
|
|
8087
|
+
if (d.action === 'mute_everywhere' || d.action === 'unmute_everywhere') return ok(`${d.username || d.userId} is ${d.muted ? 'muted' : 'not muted'} across the brand's chats.`, d);
|
|
8023
8088
|
return ok(`Chat moderators now: ${(d.moderatorIds || []).join(', ') || '(none)'}.`, d);
|
|
8024
8089
|
}));
|
|
8025
8090
|
// X CHAT (2026-10-02) — the /2/chat/* endpoints X's OpenAPI 2.169 added, as four tools; gates and payloads live in
|
|
@@ -8055,7 +8120,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
8055
8120
|
}));
|
|
8056
8121
|
server.registerTool('list_x_chats', {
|
|
8057
8122
|
title: 'List X Chat conversations',
|
|
8058
|
-
description: "The connected X account's X CHAT conversations: id (group ids start with g), type, muted, disappearing-message timer, screenshot controls, admins, members, PENDING join requests, which actions are admin-only, and the active invite link. X Chat is END-TO-END ENCRYPTED, so message text and group names cannot be read here (list_x_dms reads the plain direct-message history). view 'admin_requests' (id = a group the brand admins) lists who asked to become an admin; view 'invite' (inviteLink, or id + inviteToken) previews an invite link: group name, member count, whether the brand is in it. view 'message_requests' lists the MESSAGE REQUESTS waiting outside the inbox (inbox 'priority', the default, or 'low_quality' for X's filtered tab): who sent each one (@handle, name, followers, verified), which accounts the brand follows also follow them, and whether they changed their name or picture recently (X's impersonation hint); the message itself is encrypted. Show that to the user before manage_x_chat accept_request. withMembers:true resolves member @handles, and each user X returns is billed. Read-only. Billed per conversation or request returned, plus one user lookup per request sender, so keep maxResults small." + XC_SCOPE_NOTE,
|
|
8123
|
+
description: "The connected X account's X CHAT conversations: id (group ids start with g), type, muted, disappearing-message timer, screenshot controls, admins, members, PENDING join requests, which actions are admin-only, and the active invite link. X Chat is END-TO-END ENCRYPTED, so message text and group names cannot be read here (list_x_dms reads the plain direct-message history). A filtered page can be short or empty while more remain: keep paging with the same filters. view 'admin_requests' (id = a group the brand admins) lists who asked to become an admin; view 'invite' (inviteLink, or id + inviteToken) previews an invite link: group name, member count, whether the brand is in it. view 'message_requests' lists the MESSAGE REQUESTS waiting outside the inbox (inbox 'priority', the default, or 'low_quality' for X's filtered tab): who sent each one (@handle, name, followers, verified), which accounts the brand follows also follow them, and whether they changed their name or picture recently (X's impersonation hint); the message itself is encrypted. Show that to the user before manage_x_chat accept_request. withMembers:true resolves member @handles, and each user X returns is billed. Read-only. Billed per conversation or request returned, plus one user lookup per request sender, so keep maxResults small." + XC_SCOPE_NOTE,
|
|
8059
8124
|
inputSchema: {
|
|
8060
8125
|
view: z.enum(['conversations', 'admin_requests', 'invite', 'message_requests']).optional().describe("default 'conversations'"),
|
|
8061
8126
|
inbox: z.enum(['priority', 'low_quality']).optional().describe("message_requests: X's Priority tab (default) or the filtered low-quality one"),
|
|
@@ -8064,6 +8129,14 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
8064
8129
|
maxResults: z.number().optional().describe('1-100 (message_requests 1-50), default 10; every conversation or request returned is billed'),
|
|
8065
8130
|
paginationToken: z.string().optional().describe('nextToken from a previous call'),
|
|
8066
8131
|
withMembers: z.boolean().optional().describe('also resolve member/admin @handles (each one billed)'),
|
|
8132
|
+
unreadOnly: z.boolean().optional().describe('only unread conversations'),
|
|
8133
|
+
type: z.enum(['direct', 'group']).optional().describe('only 1:1 or only group conversations'),
|
|
8134
|
+
isArchived: z.boolean().optional().describe('true only archived, false only not archived'),
|
|
8135
|
+
isMuted: z.boolean().optional().describe('true only muted, false only not muted'),
|
|
8136
|
+
participantIds: z.string().optional().describe('1-10 numeric X account ids, comma-separated, all in the chat'),
|
|
8137
|
+
participantQuery: z.string().optional().describe('name or @username prefix, 1-50 characters'),
|
|
8138
|
+
startTime: z.string().optional().describe('latest message at or after this ISO 8601 time'),
|
|
8139
|
+
endTime: z.string().optional().describe('latest message before this ISO 8601 time'),
|
|
8067
8140
|
inviteLink: z.string().optional().describe('an x.com/i/chat/group_join/... link, for view invite'),
|
|
8068
8141
|
inviteToken: z.string().optional().describe('the 10-character token, with id, for view invite'),
|
|
8069
8142
|
},
|
|
@@ -8469,29 +8542,30 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
8469
8542
|
yourBrand: z.boolean().optional().describe('discloses that this promotes the creator’s own brand'),
|
|
8470
8543
|
platformCover: z.boolean().optional().describe('VIDEO COVER. Omit it (the default) and Hermoso sets the video\u2019s best frame \u2014 the same frame as its Library thumbnail \u2014 as the cover (TikTok video_cover_timestamp_ms, on a direct post \u2014 a draft takes no cover, you pick it in the TikTok app). true = send no cover and let the platform pick (usually the first frame). A cover you pass yourself always wins.'),
|
|
8471
8544
|
},
|
|
8472
|
-
outputSchema: { ok: z.boolean().optional(), publishId: z.string().optional(), status: z.string().optional(), destination: z.string().optional(), media: z.string().optional(), images: z.number().optional(), coverIndex: z.number().optional(), postId: z.string().nullable().optional(), url: z.string().nullable().optional(), account: z.string().nullable().optional(), pending: z.boolean().optional(), mediaConformedNote: z.string().optional(), reencodedNote: z.string().optional() },
|
|
8545
|
+
outputSchema: { ok: z.boolean().optional(), publishId: z.string().optional(), status: z.string().optional(), destination: z.string().optional(), media: z.string().optional(), images: z.number().optional(), coverIndex: z.number().optional(), postId: z.string().nullable().optional(), url: z.string().nullable().optional(), urlStatus: z.string().optional(), urlNote: z.string().optional(), account: z.string().nullable().optional(), pending: z.boolean().optional(), mediaConformedNote: z.string().optional(), reencodedNote: z.string().optional() },
|
|
8473
8546
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
8474
8547
|
}, wrap(async (a) => {
|
|
8475
8548
|
const d = await apiPost('/api/tiktok/post', a);
|
|
8476
8549
|
const what = d.media === 'photo' ? (d.images > 1 ? `photo post (${d.images} slides)` : 'photo post') : 'video';
|
|
8477
8550
|
if (d.destination === 'draft') return ok(`Sent the ${what} to TikTok${d.account ? ` on @${d.account}` : ''} — it's waiting in the TikTok app (inbox notification, or + ▸ drafts) for the user to finish and post.${[d.mediaConformedNote, d.reencodedNote].filter(Boolean).map(n => ` ${n}`).join('')}${d.pending ? ' TikTok was still processing when polling stopped; it usually lands within a minute.' : ''}`, d);
|
|
8478
|
-
return ok(`Posted the ${what} to TikTok${d.account ? ` as @${d.account}` : ''}${d.url ? ` — ${d.url}` : ''}.${d.pending ? ' TikTok was still processing when polling stopped — it normally appears within a minute or two. Do not post it again.' : ''}${[d.mediaConformedNote, d.reencodedNote].filter(Boolean).map(n => ` ${n}`).join('')}`, d);
|
|
8551
|
+
return ok(`Posted the ${what} to TikTok${d.account ? ` as @${d.account}` : ''}${d.url ? ` — ${d.url}` : ''}.${d.pending ? ' TikTok was still processing when polling stopped — it normally appears within a minute or two. Do not post it again.' : ''}${!d.url && d.urlNote ? ` ${d.urlNote}` : ''}${[d.mediaConformedNote, d.reencodedNote].filter(Boolean).map(n => ` ${n}`).join('')}`, d);
|
|
8479
8552
|
}));
|
|
8480
8553
|
// A publish TikTok was still PROCESSING when post_to_tiktok stopped polling came back `pending:true` with a publishId
|
|
8481
8554
|
// and the words "do not post it again", and nothing to follow it with: an agent asked call_tool for
|
|
8482
8555
|
// `tiktok_post_status` on 2026-10-01 and dead-ended. Same read as the web app's /api/tiktok/publish-status.
|
|
8483
8556
|
server.registerTool('tiktok_post_status', {
|
|
8484
8557
|
title: 'Check whether a TikTok post finished publishing',
|
|
8485
|
-
description: 'Follow a TikTok post that post_to_tiktok sent but TikTok was still processing (it came back pending:true). Pass the publishId it returned. Answers one of three states: done (PUBLISH_COMPLETE, with the public
|
|
8558
|
+
description: 'Follow a TikTok post that post_to_tiktok sent but TikTok was still processing (it came back pending:true). Pass the publishId it returned. Answers one of three states: done (PUBLISH_COMPLETE, with the public `url` once TikTok\u2019s moderation has named it; `urlStatus` is ready, pending, private or draft), failed (with TikTok\u2019s reason), or still processing, which is normal for a few minutes and is NOT a failure. Never post the video again while it is processing. Free, read-only. Needs TikTok connected (Settings > Connectors > TikTok).',
|
|
8486
8559
|
inputSchema: {
|
|
8487
8560
|
publishId: z.string().describe('the publishId post_to_tiktok returned'),
|
|
8488
8561
|
account: z.string().optional().describe('which connected TikTok account the post went out on (@handle or id from list_connector_accounts) when the profile has more than one'),
|
|
8489
8562
|
},
|
|
8490
|
-
outputSchema: { publishId: z.string().optional(), status: z.string().optional(), done: z.boolean().optional(), failed: z.boolean().optional(), postId: z.string().nullable().optional(), failReason: z.string().nullable().optional(), failMessage: z.string().optional(), uploadedBytes: z.number().nullable().optional() },
|
|
8563
|
+
outputSchema: { publishId: z.string().optional(), status: z.string().optional(), done: z.boolean().optional(), failed: z.boolean().optional(), postId: z.string().nullable().optional(), url: z.string().nullable().optional(), urlStatus: z.string().optional(), urlNote: z.string().optional(), foundBy: z.string().optional(), failReason: z.string().nullable().optional(), failMessage: z.string().optional(), uploadedBytes: z.number().nullable().optional() },
|
|
8491
8564
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
8492
8565
|
}, wrap(async (a) => {
|
|
8493
8566
|
const d = await apiGet('/api/tiktok/publish-status', { publishId: a.publishId, ...(a.account ? { account: a.account } : {}) });
|
|
8494
|
-
if (d.done) return ok(`TikTok finished publishing it${d.postId ? ` (post id ${d.postId})` : ''}
|
|
8567
|
+
if (d.done) return ok(`TikTok finished publishing it${d.url ? `: ${d.url}` : d.postId ? ` (post id ${d.postId})` : ''}.${d.urlNote ? ` ${d.urlNote}` : ''}`, d);
|
|
8568
|
+
if (d.urlStatus === 'draft') return ok(`It is waiting as a draft in the TikTok app, for the user to finish and post. ${d.urlNote || ''}`.trim(), d);
|
|
8495
8569
|
if (d.failed) return ok(d.failMessage || `TikTok did not publish it: ${d.failReason || 'no reason given'}.`, d);
|
|
8496
8570
|
return ok(`TikTok is still processing it (${d.status || 'no status yet'}). That is normal for a few minutes; check again shortly and do not post it again.`, d);
|
|
8497
8571
|
}));
|
|
@@ -10436,6 +10510,43 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10436
10510
|
// for — print it rather than re-asserting `a.status`, which would be a claim about the request, not the account.
|
|
10437
10511
|
return ok(d.note || `${d.level || 'campaign'} → ${d.verifiedStatus || a.status}.`, d);
|
|
10438
10512
|
}));
|
|
10513
|
+
server.registerTool('manage_google_ads_budget_networks', {
|
|
10514
|
+
title: 'Google Ads waterfall priorities and network spend caps',
|
|
10515
|
+
description: "Google Ads WATERFALL AND NETWORK CAPS: which network a budget spends on first and how much the others may take. action 'read' (default), 'set' (settings) or 'clear', on the ACCOUNT DEFAULT or, with budgetId, on one campaign budget, which then wins. Every channel a campaign type supports must be in a tier and caps only go on tier2; Google says which if not. A budget's read says whether it can use these and why not. 'set' and 'clear' move real spend on the next auction: show the user, get a yes, then confirm:true; dryRun:true validates without changing anything.",
|
|
10516
|
+
inputSchema: {
|
|
10517
|
+
action: z.enum(['read', 'set', 'clear']).optional().describe("default 'read'"),
|
|
10518
|
+
customerId: z.string().optional().describe('10-digit account id (dashes ok); omit to use the brand’s selected default account'),
|
|
10519
|
+
budgetId: z.string().optional().describe('numeric campaign budget id; omit for the account default'),
|
|
10520
|
+
settings: z.array(z.object({
|
|
10521
|
+
channelType: z.string().describe('campaign type: SEARCH, DISPLAY, SHOPPING, VIDEO, PERFORMANCE_MAX, DEMAND_GEN and so on'),
|
|
10522
|
+
tier1: z.array(z.enum(['OWNED_AND_OPERATED', 'SEARCH_PARTNER', 'GOOGLE_DISPLAY'])).optional().describe('channels that spend first'),
|
|
10523
|
+
tier2: z.array(z.enum(['OWNED_AND_OPERATED', 'SEARCH_PARTNER', 'GOOGLE_DISPLAY'])).optional().describe('channels that get what is left'),
|
|
10524
|
+
caps: z.array(z.object({ channel: z.string().describe('a channel in tier2'), percent: z.number().describe('most of the budget it may spend, 0-100') })).optional().describe('spend caps on tier2 channels'),
|
|
10525
|
+
})).optional().describe('for set: one entry per campaign type'),
|
|
10526
|
+
confirm: z.boolean().optional().describe('REQUIRED true for set and clear, after the user approved'),
|
|
10527
|
+
dryRun: z.boolean().optional().describe('validate with Google, write nothing'),
|
|
10528
|
+
loginCustomerId: z.string().optional().describe('manager id if operating through an MCC'),
|
|
10529
|
+
},
|
|
10530
|
+
outputSchema: { ok: z.boolean().optional(), changed: z.boolean().optional(), eligible: z.boolean().nullable().optional(), settings: z.array(z.any()).optional(), note: z.string().optional() },
|
|
10531
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
10532
|
+
}, wrap(async (a = {}) => {
|
|
10533
|
+
const d = await apiPost('/api/google-ads/budget-networks', a);
|
|
10534
|
+
return ok(d.note, d);
|
|
10535
|
+
}));
|
|
10536
|
+
server.registerTool('google_ads_local_services_eligibility', {
|
|
10537
|
+
title: 'Check Google Local Services eligibility',
|
|
10538
|
+
description: 'Ask Google whether a Business Profile listing can run LOCAL SERVICES ads on this Google Ads account: eligible or not, why not, and which lead types and categories it qualifies for. listingId is the numeric id at the end of a location name from list_business_locations. Read-only.',
|
|
10539
|
+
inputSchema: {
|
|
10540
|
+
listingId: z.string().describe('numeric Business Profile location id'),
|
|
10541
|
+
customerId: z.string().optional().describe('10-digit account id (dashes ok); omit to use the brand’s selected default account'),
|
|
10542
|
+
loginCustomerId: z.string().optional().describe('manager id if operating through an MCC'),
|
|
10543
|
+
},
|
|
10544
|
+
outputSchema: { ok: z.boolean().optional(), eligible: z.boolean().optional(), reasons: z.array(z.any()).optional(), goals: z.array(z.any()).optional(), note: z.string().optional() },
|
|
10545
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
10546
|
+
}, wrap(async (a) => {
|
|
10547
|
+
const d = await apiPost('/api/google-ads/local-services-eligibility', a);
|
|
10548
|
+
return ok(d.note, d);
|
|
10549
|
+
}));
|
|
10439
10550
|
server.registerTool('set_google_ads_networks', {
|
|
10440
10551
|
title: 'Change where a Google Ads campaign serves (Search partners / Display)',
|
|
10441
10552
|
description: 'Change WHERE an existing Google Ads campaign serves: Google Search, Search partners (target_search_network) and the Display Network (target_content_network), each true/false. The classic use is turning Search partners OFF on a Search campaign, or Display off. Current settings are read first and a no-op says so. On a LIVE (ENABLED) campaign this moves real spend on the next auction — show the user the before -> after, get an explicit yes, then call again with confirm:true. dryRun:true validates with Google and writes nothing. The result is READ BACK from Google before you are told it took; Google’s own rules (a Display campaign cannot take Google Search on, a Search campaign keeps Google Search on) are relayed by name.',
|
|
@@ -14669,7 +14780,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14669
14780
|
name: z.string().optional().describe('new campaign name'),
|
|
14670
14781
|
dailyBudget: z.number().optional().describe("in the ad account's currency"),
|
|
14671
14782
|
totalBudget: z.number().optional().describe('new lifetime budget in ad account currency'),
|
|
14672
|
-
standardDelivery: z.boolean().optional().describe('false = accelerated
|
|
14783
|
+
standardDelivery: z.boolean().optional().describe('false = accelerated. Works on a campaign-budget campaign; with line item budgets set it on the line item'),
|
|
14673
14784
|
purchaseOrderNumber: z.string().optional().describe('purchase order number recorded on the campaign'),
|
|
14674
14785
|
},
|
|
14675
14786
|
outputSchema: { accountId: z.string().optional(), campaignId: z.string().optional(), updated: z.array(z.string()).optional(), changed: z.number().optional(), status: z.string().nullable().optional(), note: z.string().optional() },
|
|
@@ -14687,6 +14798,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14687
14798
|
startTime: z.string().optional().describe('ISO 8601'), endTime: z.string().optional().describe('ISO 8601 end time for this line item'),
|
|
14688
14799
|
dailyBudget: z.number().optional().describe('only if the campaign is not budget-optimised'),
|
|
14689
14800
|
totalBudget: z.number().optional().describe('only if the campaign is not budget-optimised'),
|
|
14801
|
+
standardDelivery: z.boolean().optional().describe('false = accelerated, only with line item budgets; X refuses it under a campaign budget'),
|
|
14690
14802
|
},
|
|
14691
14803
|
outputSchema: { accountId: z.string().optional(), lineItemId: z.string().optional(), updated: z.array(z.string()).optional(), changed: z.number().optional(), status: z.string().nullable().optional(), note: z.string().optional() },
|
|
14692
14804
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
@@ -14776,6 +14888,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14776
14888
|
productType: z.enum(['MEDIA', 'PROMOTED_ACCOUNT', 'PROMOTED_TWEETS']).optional().describe('default PROMOTED_TWEETS \u2014 PROMOTED_ACCOUNT when objective is FOLLOWERS, and that line item serves the ACCOUNT attached with create_x_ads_promoted_account'),
|
|
14777
14889
|
placements: z.array(z.enum(['ALL_ON_TWITTER', 'PUBLISHER_NETWORK', 'TAP_BANNER', 'TAP_FULL', 'TAP_FULL_LANDSCAPE', 'TAP_MRECT', 'TAP_NATIVE', 'TWITTER_MEDIA_VIEWER', 'TWITTER_PROFILE', 'TWITTER_REPLIES', 'TWITTER_SEARCH', 'TWITTER_TIMELINE'])).optional().describe('default ALL_ON_TWITTER'),
|
|
14778
14890
|
goal: z.enum(['APP_CLICKS', 'APP_INSTALLS', 'APP_PURCHASES', 'ENGAGEMENT', 'FOLLOWERS', 'LINK_CLICKS', 'MAX_REACH', 'PREROLL', 'PREROLL_STARTS', 'REACH_WITH_ENGAGEMENT', 'SITE_VISITS', 'SOCIAL_ENGAGEMENT', 'VIDEO_VIEW', 'VIEW_15S', 'VIEW_3S_100PCT', 'VIEW_6S', 'WEBSITE_CONVERSIONS', 'WEBSITE_CONVERSIONS_V2']).optional().describe('optimization goal, e.g. LINK_CLICKS or WEBSITE_CONVERSIONS; these are goals, not objectives'),
|
|
14891
|
+
standardDelivery: z.boolean().optional().describe('false = accelerated, only with line item budgets; X refuses it under a campaign budget'),
|
|
14779
14892
|
bidStrategy: z.enum(['AUTO', 'GUARANTEED', 'MAX', 'TARGET']).optional().describe('AUTO lets X set the bid; MAX and TARGET need bidAmount'),
|
|
14780
14893
|
bidAmount: z.number().optional().describe("required for MAX/TARGET; in the ad account's currency"),
|
|
14781
14894
|
totalBudget: z.number().optional().describe('lifetime budget for this line item in ad account currency'),
|
|
@@ -16028,6 +16141,28 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16028
16141
|
const d = await apiPost('/api/reddit-ads/forecast', a);
|
|
16029
16142
|
return ok(d.note, d);
|
|
16030
16143
|
}));
|
|
16144
|
+
server.registerTool('reddit_ads_plan_forecast', {
|
|
16145
|
+
title: 'Forecast a Reddit campaign plan',
|
|
16146
|
+
description: 'Reddit Campaign Planner’s forecast for a proposed campaign: the audience, projected reach, impressions, clicks, frequency and CPM/CPC/CPV for this budget, and a curve of reach at ten budgets to compare. Creates nothing and spends nothing. Use reddit_ads_forecast for a quick audience size. Reddit allows 30 of these a minute. These are Reddit’s estimates, not a guarantee, and they reserve nothing; say so, and keep each result beside the inputs that made it.',
|
|
16147
|
+
inputSchema: {
|
|
16148
|
+
adAccountId: z.string().optional().describe('Reddit ad account id (a2_...); omit when only one is shared'),
|
|
16149
|
+
objective: z.enum(['APP_INSTALLS', 'BRAND_AWARENESS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'SALES', 'VIDEO_VIEWABLE_IMPRESSIONS']).describe('campaign objective'),
|
|
16150
|
+
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).describe('whether budget is per day or for the whole plan'),
|
|
16151
|
+
budget: z.number().describe('budget in the ad account’s currency (not micro-currency)'),
|
|
16152
|
+
startTime: z.string().optional().describe('ISO 8601; default now'),
|
|
16153
|
+
endTime: z.string().optional().describe('ISO 8601; default 30 days after the start, at most 365'),
|
|
16154
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional().describe('CPC, CPM, CPV, CPV6 or CPV15'),
|
|
16155
|
+
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional().describe('BIDLESS, MANUAL_BIDDING, MAXIMIZE_VOLUME or TARGET_CPX'),
|
|
16156
|
+
bidAmount: z.number().optional().describe('bid in the ad account currency'),
|
|
16157
|
+
useCatalog: z.boolean().optional().describe('SALES objective: true forecasts catalog sales'),
|
|
16158
|
+
targeting: z.record(z.any()).optional().describe('same shape as create_reddit_ads_ad_group targeting'),
|
|
16159
|
+
},
|
|
16160
|
+
outputSchema: { audience: z.any().optional(), projection: z.any().optional(), curve: z.array(z.any()).optional(), note: z.string().optional() },
|
|
16161
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
16162
|
+
}, wrap(async (a) => {
|
|
16163
|
+
const d = await apiPost('/api/reddit-ads/plan-forecast', a);
|
|
16164
|
+
return ok(d.note, d);
|
|
16165
|
+
}));
|
|
16031
16166
|
server.registerTool('reddit_ads_bid_suggestion', {
|
|
16032
16167
|
title: 'Ask Reddit what to bid',
|
|
16033
16168
|
description: 'Reddit’s own suggested bid for a given objective, bid type and targeting — a median, a sensible range, and the hard floor below which Reddit will not accept a bid. Amounts come back in the ad account’s currency. Free, creates nothing. Use it to pick bidAmount for create_reddit_ads_ad_group instead of guessing a number that either never wins an auction or overpays.',
|
|
@@ -25553,10 +25688,12 @@ function memoryNoteVerdict(text) {
|
|
|
25553
25688
|
const folTxt = Array.isArray(d.followers) ? d.followers.slice(0, 12).map(f => `• ${f.channel}${f.label ? ` ${f.label}` : ''}: ${f.last ? `${fmtN(f.last.followers)} on ${f.last.day}${f.change != null && f.first && f.first.day !== f.last.day ? ` (${f.change >= 0 ? '+' : ''}${fmtN(f.change)} since ${f.first.day})` : ''}` : 'unknown'}${f.lastWhy ? ` — latest read unknown: ${f.lastWhy.why}` : ''}`) : [];
|
|
25554
25689
|
const archTxt = d.archive?.used ? `\n\nIncludes ${d.archive.rows} older post(s) from the brand's archive.` : (d.archive?.unreadable ? `\n\n⚠ ${d.archive.why}` : '');
|
|
25555
25690
|
const overTime = `${boardTxt.length ? `\n\nBEST AND WORST POSTS (last ${d.days || 30} days):\n${boardTxt.join('\n')}` : ''}${trendTxt.length ? `\n\nOVER TIME (last ${d.trend.weeks.length} weeks, 7-day readings where they exist):\n${trendTxt.join('\n')}` : ''}${healthTxt.length ? `\n\nMEASUREMENT GAPS:\n${healthTxt.join('\n')}` : ''}${folTxt.length ? `\n\nFOLLOWERS (daily snapshot):\n${folTxt.join('\n')}` : (d.followers?.unreadable ? `\n\nFOLLOWERS: ${d.followers.why}` : '')}${archTxt}`;
|
|
25556
|
-
|
|
25691
|
+
// INSTAGRAM STORIES (2026-10-31): kept numbers for Stories posted by any route. Unknown is printed as unknown, never 0.
|
|
25692
|
+
const storyTxt = d.stories ? `\n\nINSTAGRAM STORIES (${d.stories.count ?? 0}, ${d.stories.withNumbers ?? 0} with numbers):\n${(d.stories.rows || []).slice(0, 10).map(x => `• ${x.account} ${x.postedAt || 'unknown'}${x.caption ? ` ${JSON.stringify(x.caption.slice(0, 60))}` : ''}: ${x.summary}`).join('\n')}${(d.stories.unreadable || []).length ? `\nCould not read some of them: ${d.stories.unreadable[0].error}` : ''}` : '';
|
|
25693
|
+
if (!gs.length) return ok(`Nothing to compare on "${d.axis}" yet. ${d.finding?.why || ''}`.trim() + overTime + storyTxt, d);
|
|
25557
25694
|
const rows = gs.map(g => `• "${g.key}" · ${g.channel} — ${g.meanRate == null ? (g.meanEngagement == null ? 'no measurable engagement' : `${g.meanEngagement.toFixed(1)} engagements (no reach denominator on this channel, so no rate)`) : `${(g.meanRate * 100).toFixed(2)}% engagement`} · ${g.n} post(s), ${g.nRated} measured${g.verdict === 'ready' ? '' : ` — ${g.suppressed}`}`);
|
|
25558
25695
|
const head = d.finding?.finding ? `FINDING: ${d.finding.finding}` : `NO FINDING YET: ${d.finding?.why || 'not enough measured posts'}`;
|
|
25559
|
-
return ok(`${head}\n\nBy ${d.axis}:\n${rows.join('\n')}${d.excludedUnattributed ? `\n\n${d.excludedUnattributed} post(s) were excluded from this axis because no hook was recorded for them — they still count toward channel and format totals.` : ''}${overTime}`, d);
|
|
25696
|
+
return ok(`${head}\n\nBy ${d.axis}:\n${rows.join('\n')}${d.excludedUnattributed ? `\n\n${d.excludedUnattributed} post(s) were excluded from this axis because no hook was recorded for them — they still count toward channel and format totals.` : ''}${overTime}${storyTxt}`, d);
|
|
25560
25697
|
}));
|
|
25561
25698
|
|
|
25562
25699
|
server.registerTool('diagnose_posts', {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.488",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI marketing agent and AI ad generator, run from your own AI agent.
|
|
5
|
+
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 1096 tools. No markup on models. Publishing, scheduling, ad campaign management, comments, DMs and analytics are free on every plan. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. 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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|