hermoso 0.1.201 → 0.1.203
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/tools.mjs +115 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate finished image &
|
|
|
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
|
+
**789 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
|
**What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
|
|
@@ -171,7 +171,7 @@ block entirely if you signed in above; it is there for CI, where the process can
|
|
|
171
171
|
|
|
172
172
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
173
173
|
|
|
174
|
-
### What the
|
|
174
|
+
### What the 789 tools cover
|
|
175
175
|
|
|
176
176
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
177
177
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/tools.mjs
CHANGED
|
@@ -4141,6 +4141,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
4141
4141
|
locationId: z.string().optional().describe("GOOGLE BUSINESS PROFILE — which listing, e.g. 'locations/123' from list_business_locations. Needed when the account manages more than one storefront; it is never chosen for the user."),
|
|
4142
4142
|
visibility: z.enum(['public', 'unlisted', 'private', 'draft']).optional().describe("how it should be published — DEFAULT 'public' (live). Only pass something else if the user explicitly asked to stage/hide it. Not every channel supports every value; an impossible combination is refused when you schedule it, with the reason."),
|
|
4143
4143
|
visibilityByChannel: z.record(z.string()).optional().describe('override visibility for one channel, e.g. { "tiktok": "draft" } to go live everywhere but stage TikTok for review'),
|
|
4144
|
+
optimizeCopy: z.boolean().optional().describe('RECOMMENDED when one caption goes to several channels: fit the shared caption to each channel’s own rules at publish time wherever no per-channel caption was written — YouTube gets a keyword title, a structured multi-paragraph description and search tags; Instagram/TikTok hashtags; LinkedIn longer; X/Bluesky short; Pinterest keyword-rich. The angle and every claim stay the author’s; a channel with its own caption is left exactly as written. Off by default so nobody’s words are rewritten unasked.'),
|
|
4144
4145
|
},
|
|
4145
4146
|
outputSchema: { id: z.string().optional(), at: z.string().optional(), channels: z.array(z.string()).optional(), label: z.string().optional() },
|
|
4146
4147
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -5082,8 +5083,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5082
5083
|
account: z.string().optional().describe("WHICH connected account of this channel to post as — its @handle or id from list_connector_accounts. Needed only when the brand has more than one youtube account connected (several and none named is refused by name, never guessed); omit when there is one."),
|
|
5083
5084
|
...HOOK_ATTR,
|
|
5084
5085
|
videoUrl: z.string().describe('the video to post — a Hermoso render URL or an upload_file url'),
|
|
5085
|
-
title: z.string().optional().describe('
|
|
5086
|
-
description: z.string().optional().describe('video
|
|
5086
|
+
title: z.string().optional().describe('REQUIRED in practice — the headline every search result and the Shorts feed shows (≤100 chars); an upload with no title is refused'),
|
|
5087
|
+
description: z.string().optional().describe('REQUIRED in practice — what the video shows, a line about the brand, a link and a few #hashtags (≤5000 chars); YouTube search, suggested videos and the Shorts feed rank on it, so an upload with no description (and no caption) is refused'),
|
|
5087
5088
|
tags: z.array(z.string()).optional().describe('up to 30 tags'),
|
|
5088
5089
|
privacy: z.enum(['private', 'unlisted', 'public']).optional().describe('default unlisted (anyone-with-link, ad-ready); public = live + searchable on the channel (confirm first); private = eyes-only (cannot run as an ad)'),
|
|
5089
5090
|
categoryId: z.string().optional().describe('YouTube category id, NUMERIC — default "22" (People & Blogs), which is wrong for most ads. 1 Film & Animation · 2 Autos & Vehicles · 10 Music · 15 Pets & Animals · 17 Sports · 19 Travel & Events · 20 Gaming · 22 People & Blogs · 23 Comedy · 24 Entertainment · 25 News & Politics · 26 Howto & Style · 27 Education · 28 Science & Technology · 29 Nonprofits & Activism. The assignable set is region-specific, so the value is forwarded as given and YouTube has the last word.'),
|
|
@@ -6686,6 +6687,92 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
6686
6687
|
const d = await apiPost('/api/meta/audience/delete', a);
|
|
6687
6688
|
return ok(d.note, d);
|
|
6688
6689
|
}));
|
|
6690
|
+
|
|
6691
|
+
// ── MESSENGER MARKETING MESSAGES (2026-09-05). Meta's paid re-engagement channel: messages OUTSIDE the 24-hour
|
|
6692
|
+
// window to people who opted in, billed by Meta to the brand's ad account per delivered message. Flow 2 of Meta's
|
|
6693
|
+
// onboarding (user token + marketing_messages_messenger) rides the existing Meta connection. App-Review gated:
|
|
6694
|
+
// until Meta grants the scope it works for app-role holders only, and list_messenger_subscribers says which.
|
|
6695
|
+
// Every endpoint, limit and error code: lib/messenger-marketing.mjs (read live 2026-09-05).
|
|
6696
|
+
server.registerTool('list_messenger_subscribers', {
|
|
6697
|
+
title: 'List Messenger marketing-message subscribers',
|
|
6698
|
+
description: 'The people who OPTED IN to marketing messages from a Facebook Page — their subscription tokens, the only address a paid Messenger marketing message can go to (there is no "message everyone"). Also reports whether this Meta connection carries the marketing_messages_messenger permission; a connection made before it was added must be reconnected. Free.',
|
|
6699
|
+
inputSchema: {
|
|
6700
|
+
pageId: z.string().optional().describe('Facebook Page id (from list_meta_pages); defaults to the brand’s one shared Page'),
|
|
6701
|
+
limit: z.number().optional().describe('1–1000, default 100'),
|
|
6702
|
+
customAudienceIds: z.string().optional().describe('comma-separated Messenger custom audience ids to filter by'),
|
|
6703
|
+
},
|
|
6704
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
6705
|
+
}, wrap(async (a) => {
|
|
6706
|
+
const d = await apiGet('/api/meta/marketing-messages/subscribers', a);
|
|
6707
|
+
const lines = (d.subscribers || []).slice(0, 50).map(r => `• ${r.token}${r.topic ? ` — “${r.topic}”` : ''}${r.status ? ` (${r.status})` : ''}${r.nextEligible ? `, next eligible ${r.nextEligible}` : ''}`);
|
|
6708
|
+
return ok(`${d.note}${lines.length ? `\n${lines.join('\n')}` : ''}${(d.subscribers || []).length > 50 ? `\n…and ${d.subscribers.length - 50} more` : ''}`, d);
|
|
6709
|
+
}));
|
|
6710
|
+
server.registerTool('create_messenger_marketing_campaign', {
|
|
6711
|
+
title: 'Create a Messenger marketing-message campaign',
|
|
6712
|
+
description: 'The container a paid Messenger marketing message is sent from (Meta act_<AD>/message_campaign). Budgets in USD; Meta bills per DELIVERED message against the campaign budget, and creating it sends and charges nothing. dailyBudgetUsd OR lifetimeBudgetUsd (omit both and Meta sets an estimated daily cap; Meta may spend up to 175% of a daily budget on one day, never more than 7× per week). REGION LAW: Meta lets us serve businesses in 20 countries only — US, Mexico, Brazil, India, Australia, Singapore, UAE, Saudi Arabia, Hong Kong, Taiwan, Thailand, Malaysia, Indonesia, Philippines, Vietnam, New Zealand, Chile, Colombia, Peru, Israel — NOT Canada, the EU or the UK — and cannot deliver to people in the EU, UK, Japan, South Korea or Australia. Political Pages are excluded. dryRun:true shows the exact body.',
|
|
6713
|
+
inputSchema: {
|
|
6714
|
+
adAccountId: z.string().describe('ad account id (act_… or digits)'),
|
|
6715
|
+
pageId: z.string().optional().describe('the sending Page; defaults to the brand’s one shared Page'),
|
|
6716
|
+
name: z.string(),
|
|
6717
|
+
dailyBudgetUsd: z.number().optional(),
|
|
6718
|
+
lifetimeBudgetUsd: z.number().optional(),
|
|
6719
|
+
pixelId: z.string().optional().describe('Meta Pixel for offsite conversion attribution'),
|
|
6720
|
+
startTime: z.string().optional().describe('ISO time, now or within 30 days'),
|
|
6721
|
+
endTime: z.string().optional(),
|
|
6722
|
+
dryRun: z.boolean().optional(),
|
|
6723
|
+
},
|
|
6724
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
6725
|
+
}, wrap(async (a) => { const d = await apiPost('/api/meta/marketing-messages/campaign', a); return ok(d.note, d); }));
|
|
6726
|
+
server.registerTool('list_messenger_marketing_campaigns', {
|
|
6727
|
+
title: 'List Messenger marketing-message campaigns',
|
|
6728
|
+
description: 'The marketing-message campaigns on an ad account: name, id, budgets, schedule. Free.',
|
|
6729
|
+
inputSchema: { adAccountId: z.string() },
|
|
6730
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
6731
|
+
}, wrap(async (a) => {
|
|
6732
|
+
const d = await apiGet('/api/meta/marketing-messages/campaigns', a);
|
|
6733
|
+
return ok(`${d.note}${(d.campaigns || []).length ? `\n${d.campaigns.map(c => `• ${c.name || '(unnamed)'} — ${c.campaignId}${c.dailyBudgetUsd != null ? `, $${c.dailyBudgetUsd}/day` : ''}${c.lifetimeBudgetUsd != null ? `, $${c.lifetimeBudgetUsd} lifetime` : ''}`).join('\n')}` : ''}`, d);
|
|
6734
|
+
}));
|
|
6735
|
+
server.registerTool('estimate_messenger_marketing_delivery', {
|
|
6736
|
+
title: 'Estimate Messenger marketing-message delivery and cost',
|
|
6737
|
+
description: 'Meta’s own estimate of how many marketing messages a budget would deliver from a Page and what it would cost, before anything is created. Free. Meta marks the metric “in development”, so treat it as a range.',
|
|
6738
|
+
inputSchema: { adAccountId: z.string(), pageId: z.string().optional(), dailyBudgetUsd: z.number().optional(), lifetimeBudgetUsd: z.number().optional(), lifetimeDays: z.number().optional() },
|
|
6739
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
6740
|
+
}, wrap(async (a) => { const d = await apiGet('/api/meta/marketing-messages/estimate', a); return ok(d.note, d); }));
|
|
6741
|
+
server.registerTool('send_messenger_marketing_message', {
|
|
6742
|
+
title: 'Send a paid Messenger marketing message',
|
|
6743
|
+
description: 'Send a PAID marketing message on Messenger to opted-in subscribers (Meta act_<AD>/messages). One message per subscriber per 12 hours — Meta’s rule, enforced before dispatch and by Meta. message.type: text | button (text + up to 3 web_url buttons) | generic (a card: title, subtitle, image, tap-through url, up to 3 buttons) | media (imageUrl or videoId + buttons). Give subscriptionTokens (≤200 per call, from list_messenger_subscribers) OR customAudienceId of a MESSENGER_SUBSCRIBER_LIST audience of 100+ people for a bulk send. Meta bills the ad account per delivered message; delivery, read and click events arrive on the Page webhook. dryRun:true previews the wire body and sends nothing. Meta’s frequency caps are silent: a refusal saying the person is capped is Meta protecting them, not a broken send.',
|
|
6744
|
+
inputSchema: {
|
|
6745
|
+
adAccountId: z.string(),
|
|
6746
|
+
campaignId: z.string().describe('from create_messenger_marketing_campaign'),
|
|
6747
|
+
subscriptionTokens: z.array(z.string()).optional(),
|
|
6748
|
+
customAudienceId: z.string().optional(),
|
|
6749
|
+
message: z.object({
|
|
6750
|
+
type: z.enum(['text', 'button', 'generic', 'media']),
|
|
6751
|
+
text: z.string().optional(), title: z.string().optional(), subtitle: z.string().optional(),
|
|
6752
|
+
imageUrl: z.string().optional(), videoId: z.string().optional(), url: z.string().optional().describe('card tap-through URL'),
|
|
6753
|
+
buttons: z.array(z.object({ title: z.string(), url: z.string() })).optional(),
|
|
6754
|
+
}),
|
|
6755
|
+
minConversationGapSeconds: z.number().optional().describe('hold the send if the person chatted with the Page within this many seconds'),
|
|
6756
|
+
dryRun: z.boolean().optional(),
|
|
6757
|
+
},
|
|
6758
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
6759
|
+
}, wrap(async (a) => {
|
|
6760
|
+
const d = await apiPost('/api/meta/marketing-messages/send', a);
|
|
6761
|
+
const bad = (d.results || []).filter(r => r.error);
|
|
6762
|
+
return ok(`${d.note}${bad.length ? `\nRefused: ${bad.slice(0, 10).map(r => `${r.token ? r.token.slice(0, 12) + '…' : 'audience'}: ${r.error}`).join('; ')}` : ''}`, d);
|
|
6763
|
+
}));
|
|
6764
|
+
server.registerTool('request_messenger_optin', {
|
|
6765
|
+
title: 'Ask a Messenger contact to subscribe to marketing messages',
|
|
6766
|
+
description: 'Send Meta’s opt-in request (the notification_messages template) to someone ALREADY in a Messenger conversation with the Page, inside the 24-hour window. If they accept, Meta posts a messaging_optin webhook with their subscription token and they appear in list_messenger_subscribers. One identical-title request per person per week. Free to send. The PSID comes from the Page inbox.',
|
|
6767
|
+
inputSchema: { pageId: z.string().optional(), psid: z.string().describe('the person’s Page-scoped id'), title: z.string().describe('what they are subscribing to, ≤65 characters'), imageUrl: z.string().optional(), timezone: z.string().optional().describe('their timezone, e.g. America/New_York'), dryRun: z.boolean().optional() },
|
|
6768
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
6769
|
+
}, wrap(async (a) => { const d = await apiPost('/api/meta/marketing-messages/optin', a); return ok(d.note, d); }));
|
|
6770
|
+
server.registerTool('delete_messenger_marketing_campaign', {
|
|
6771
|
+
title: 'Delete a Messenger marketing-message campaign',
|
|
6772
|
+
description: 'Delete a marketing-message campaign. Irreversible: the first call names it and refuses; confirm:true deletes.',
|
|
6773
|
+
inputSchema: { adAccountId: z.string(), campaignId: z.string(), confirm: z.boolean().optional() },
|
|
6774
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
6775
|
+
}, wrap(async (a) => { const d = await apiPost('/api/meta/marketing-messages/campaign/delete', a); return ok(d.note, d); }));
|
|
6689
6776
|
// ---------- Google Ads: read + manage (flagship, Meta-parity). Every spend change is confirm-gated. ----------
|
|
6690
6777
|
server.group('ads');
|
|
6691
6778
|
// GOOGLE MERCHANT CENTER (2026-08-07) — rides the google_ads connection (same grant, extra `content` scope).
|
|
@@ -11267,7 +11354,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11267
11354
|
budget: z.number().describe('budget in the ad account’s currency (not micro-currency — the conversion is handled)'),
|
|
11268
11355
|
objective: z.enum(['APP_INSTALLS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('default CLICKS'),
|
|
11269
11356
|
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
11270
|
-
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional(),
|
|
11357
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional(),
|
|
11271
11358
|
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
|
|
11272
11359
|
bidAmount: z.number().optional(),
|
|
11273
11360
|
startTime: z.string().optional().describe('ISO 8601'),
|
|
@@ -11287,7 +11374,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11287
11374
|
adAccountId: z.string().optional(),
|
|
11288
11375
|
budget: z.number().describe('budget in the ad account’s currency'),
|
|
11289
11376
|
objective: z.enum(['APP_INSTALLS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('default CLICKS'),
|
|
11290
|
-
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional().describe('default CPC — must fit the campaign objective'),
|
|
11377
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional().describe('default CPC — must fit the campaign objective'),
|
|
11291
11378
|
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
|
|
11292
11379
|
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
11293
11380
|
startTime: z.string().optional(),
|
|
@@ -11380,7 +11467,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11380
11467
|
name: z.string(),
|
|
11381
11468
|
budget: z.number().describe('budget in the ad account’s currency, paired with goalType'),
|
|
11382
11469
|
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional().describe('default DAILY_SPEND'),
|
|
11383
|
-
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).describe('must fit the campaign objective — CLICKS campaigns take CPC'),
|
|
11470
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).describe('must fit the campaign objective — CLICKS campaigns take CPC'),
|
|
11384
11471
|
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).describe('MANUAL_BIDDING needs bidAmount'),
|
|
11385
11472
|
bidAmount: z.number().optional().describe('bid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion'),
|
|
11386
11473
|
startTime: z.string().describe('ISO 8601, e.g. 2026-08-15T00:00:00Z — Reddit rejects the create without one'),
|
|
@@ -11432,7 +11519,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11432
11519
|
budget: z.number().optional(),
|
|
11433
11520
|
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
11434
11521
|
bidAmount: z.number().optional(),
|
|
11435
|
-
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional(),
|
|
11522
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6', 'CPV15']).optional(),
|
|
11436
11523
|
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
|
|
11437
11524
|
startTime: z.string().optional(),
|
|
11438
11525
|
endTime: z.string().optional(),
|
|
@@ -17155,6 +17242,28 @@ function memoryNoteVerdict(text) {
|
|
|
17155
17242
|
return adsOut('ads', d.totalAds ?? raw.length, ads, '', 'linkedin');
|
|
17156
17243
|
}));
|
|
17157
17244
|
|
|
17245
|
+
// ── CREATOR SEARCH (2026-09-05): the creators already winning in a niche, folded out of the organic posts Ad Spy
|
|
17246
|
+
// already searches. Real people, scored on median views + engagement + consistency; no marketplace scope needed.
|
|
17247
|
+
server.registerTool('find_creators', {
|
|
17248
|
+
title: 'Find the creators already winning in a niche',
|
|
17249
|
+
description: 'Scan organic TikTok, Instagram Reels and YouTube for a niche across a few query variants, fold the posts into creators, and rank them on median views, engagement rate and how often they show up for that niche; the top rows get follower counts. Real people, not AI actors — for influencer sourcing, UGC casting and partnership prospecting ("who should we send product to?"). About one credit per search call (platforms × queries, default 3 × 3) plus one per enriched profile; repeats inside 20 minutes are free. Then shortlist (save_to_swipefile), check a profile (instagram_profile / fetch_social_data), draft outreach (generate_text), or approve them for Partnership Ads (manage_meta_partnership_creator).',
|
|
17250
|
+
inputSchema: {
|
|
17251
|
+
niche: z.string().describe('product category, topic or hashtag — "calorie tracker app", "matcha", "#cleanbeauty"'),
|
|
17252
|
+
platforms: z.array(z.enum(['tiktok', 'instagram', 'youtube'])).optional().describe('default all three'),
|
|
17253
|
+
limit: z.number().optional().describe('creators to return, 1–30 (default 12)'),
|
|
17254
|
+
queries: z.number().optional().describe('query variants per platform, 1–4 (default 3); each is a paid search call'),
|
|
17255
|
+
minAvgViews: z.number().optional(),
|
|
17256
|
+
minEngagement: z.number().optional().describe('interactions per view, 0–1 (0.05 = 5%)'),
|
|
17257
|
+
enrich: z.boolean().optional().describe('read follower counts for the top 6 (default true, ~1 credit each)'),
|
|
17258
|
+
},
|
|
17259
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
17260
|
+
}, wrap(async (a) => {
|
|
17261
|
+
const d = await apiPost('/api/creators/search', a);
|
|
17262
|
+
const fmt = (v) => v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(v >= 1e5 ? 0 : 1)}K` : String(v);
|
|
17263
|
+
if (!(d.creators || []).length) return ok(`${d.summary} Nobody passed the floors — widen the niche, lower minAvgViews / minEngagement, or add platforms.`, d);
|
|
17264
|
+
const lines = d.creators.map(c => `• @${c.handle} (${c.platform})${c.name && c.name !== c.handle ? ` — ${c.name}` : ''}: ${c.posts} post${c.posts === 1 ? '' : 's'} in this niche, median ${fmt(c.medianPlays)} views, ${c.engagementRate == null ? 'engagement unknown' : `${(100 * c.engagementRate).toFixed(1)}% engagement`}${c.followers != null ? `, ${fmt(c.followers)} followers` : ''}, score ${c.score}${c.top?.link ? ` — top: ${c.top.link}` : ''}${c.profileUrl ? ` — ${c.profileUrl}` : ''}`);
|
|
17265
|
+
return ok(`${d.note}\n${lines.join('\n')}`, d);
|
|
17266
|
+
}));
|
|
17158
17267
|
server.registerTool('search_tiktok', {
|
|
17159
17268
|
_meta: openaiMeta(AD_SPY_URI, 'Searching TikTok videos…', 'Found TikTok videos'),
|
|
17160
17269
|
title: 'Search TikTok',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.203",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI ad studio and marketing MCP server with
|
|
5
|
+
"description": "AI ad studio and marketing MCP server with 789 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|