hermoso 0.1.210 → 0.1.211
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 +72 -0
- 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
|
+
**795 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 795 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
|
@@ -3474,6 +3474,78 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
3474
3474
|
return ok(`${d.summary}${d.note ? ` ${d.note}` : ''}`, d);
|
|
3475
3475
|
}));
|
|
3476
3476
|
|
|
3477
|
+
// INSTAGRAM AUDIO + COLLAB INVITES (2026-09-06) — Instagram's 2026 changelog, on permissions already held. The
|
|
3478
|
+
// audio one is the reason to care: everything it returns is music Instagram has cleared for third-party use, i.e.
|
|
3479
|
+
// safe under a Reel. The invite pair is the OTHER direction from instagram_collaborators — a creator tagged the
|
|
3480
|
+
// brand, and the post reaches the brand's profile only when the brand accepts, which no agent could do before.
|
|
3481
|
+
server.registerTool('search_instagram_audio', {
|
|
3482
|
+
title: 'Trending or searched Instagram audio',
|
|
3483
|
+
description: 'Audio the brand may legally put under a Reel — music or original sound — with title, artist, length, whether it is eligible for ads, a preview link and a download link. Omit the query for what is TRENDING right now; pass one to search. Everything returned is audio Instagram has authorized for third-party use. Download links expire after roughly 1.5 days. Read-only, free.',
|
|
3484
|
+
inputSchema: {
|
|
3485
|
+
audioType: z.enum(['music', 'original_sound']).optional().describe('default music'),
|
|
3486
|
+
query: z.string().optional().describe('omit for trending audio'),
|
|
3487
|
+
limit: z.number().optional().describe('1–50, default 15'),
|
|
3488
|
+
account: z.string().optional().describe('which Instagram account — an @handle or id from list_connector_accounts("instagram"); omit for the one Page-linked account'),
|
|
3489
|
+
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
3490
|
+
},
|
|
3491
|
+
outputSchema: { instagramId: z.string().optional(), kind: z.string().optional(), query: z.string().nullable().optional(), trending: z.boolean().optional(), count: z.number().optional(), audio: z.array(z.any()).optional(), note: z.string().optional() },
|
|
3492
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3493
|
+
}, wrap(async (a) => {
|
|
3494
|
+
const d = await apiGet('/api/instagram/audio', { audioType: a.audioType, query: a.query, limit: a.limit, account: a.account, pageId: a.pageId });
|
|
3495
|
+
if (!d.count) return ok(`Instagram returned no ${d.kind === 'music' ? 'tracks' : 'original sounds'}${d.trending ? ' for trending' : ` for "${d.query}"`}.`, d);
|
|
3496
|
+
return ok(`${d.count} ${d.kind === 'music' ? 'track' : 'original sound'}(s)${d.trending ? ' trending' : ` for “${d.query}”`}:\n${d.audio.map(x => `• ${x.title || '(untitled)'} — ${x.artist || '—'}${x.seconds != null ? ` · ${x.seconds}s` : ''}${x.adsEligible ? ' · ads-eligible' : ''} · id ${x.id}${x.preview ? ` · ${x.preview}` : ''}`).join('\n')}\n${d.note}`, d);
|
|
3497
|
+
}));
|
|
3498
|
+
|
|
3499
|
+
server.registerTool('list_instagram_collab_invites', {
|
|
3500
|
+
title: 'Collab invites waiting on the brand',
|
|
3501
|
+
description: 'Posts where a creator tagged this Instagram account as a COLLABORATOR and is waiting for an answer. The post appears on the brand’s profile only once the brand accepts, and Instagram sends no notification here, so this list is the only way to see them. Answer each with respond_instagram_collab_invite. Read-only, free. Instagram allows 300 reads per account per day.',
|
|
3502
|
+
inputSchema: {
|
|
3503
|
+
limit: z.number().optional().describe('1–100, default 25'),
|
|
3504
|
+
after: z.string().optional().describe('cursor from a previous page'),
|
|
3505
|
+
account: z.string().optional().describe('which Instagram account — @handle or id; omit for the one Page-linked account'),
|
|
3506
|
+
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
3507
|
+
},
|
|
3508
|
+
outputSchema: { instagramId: z.string().optional(), count: z.number().optional(), invites: z.array(z.any()).optional(), cursor: z.string().nullable().optional() },
|
|
3509
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3510
|
+
}, wrap(async (a) => {
|
|
3511
|
+
const d = await apiGet('/api/instagram/collab-invites', { limit: a.limit, after: a.after, account: a.account, pageId: a.pageId });
|
|
3512
|
+
if (!d.count) return ok('No collab invites are waiting on this Instagram account.', d);
|
|
3513
|
+
return ok(`${d.count} collab invite(s) waiting:\n${d.invites.map(i => `• from @${i.from || '?'} — ${String(i.caption || '(no caption)').replace(/\s+/g, ' ').slice(0, 90)} · media ${i.mediaId}`).join('\n')}\nAnswer each with respond_instagram_collab_invite(mediaId, accept:true|false).`, d);
|
|
3514
|
+
}));
|
|
3515
|
+
|
|
3516
|
+
server.registerTool('respond_instagram_collab_invite', {
|
|
3517
|
+
title: 'Accept or decline a collab invite',
|
|
3518
|
+
description: 'Answer a collab invite from list_instagram_collab_invites. accept:true makes this account a co-author and the creator’s post appears on its profile; accept:false declines. `accept` is REQUIRED — never guess which way the user wants it. The reply is read back from Instagram (the invite leaves the pending list), not taken from the vendor’s 200. Free. Instagram allows 50 answers per account per day.',
|
|
3519
|
+
inputSchema: {
|
|
3520
|
+
mediaId: z.string().describe('the media id from list_instagram_collab_invites'),
|
|
3521
|
+
accept: z.boolean().describe('true = accept and co-author the post; false = decline'),
|
|
3522
|
+
account: z.string().optional().describe('which Instagram account — @handle or id; omit for the one Page-linked account'),
|
|
3523
|
+
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
3524
|
+
},
|
|
3525
|
+
outputSchema: { mediaId: z.string().optional(), accepted: z.boolean().optional(), vendorSuccess: z.boolean().optional(), confirmed: z.boolean().optional(), summary: z.string().optional() },
|
|
3526
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
3527
|
+
}, wrap(async (a) => {
|
|
3528
|
+
const d = await apiPost('/api/instagram/collab-invites/respond', { mediaId: a.mediaId, accept: a.accept, account: a.account, pageId: a.pageId });
|
|
3529
|
+
return ok(d.summary, d);
|
|
3530
|
+
}));
|
|
3531
|
+
|
|
3532
|
+
server.registerTool('list_instagram_collab_media', {
|
|
3533
|
+
title: 'Posts this account co-authors',
|
|
3534
|
+
description: 'Every collaborative post this Instagram account is a co-author on — who posted it, and the COMBINED engagement across all co-authors (total likes and comments, plus saves, shares and reposts where Instagram provides them). Read-only, free.',
|
|
3535
|
+
inputSchema: {
|
|
3536
|
+
limit: z.number().optional().describe('1–100, default 25'),
|
|
3537
|
+
after: z.string().optional().describe('cursor from a previous page'),
|
|
3538
|
+
account: z.string().optional().describe('which Instagram account — @handle or id; omit for the one Page-linked account'),
|
|
3539
|
+
pageId: z.string().optional().describe('Facebook Page id — omit when only one Page is connected'),
|
|
3540
|
+
},
|
|
3541
|
+
outputSchema: { instagramId: z.string().optional(), count: z.number().optional(), media: z.array(z.any()).optional(), cursor: z.string().nullable().optional() },
|
|
3542
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
3543
|
+
}, wrap(async (a) => {
|
|
3544
|
+
const d = await apiGet('/api/instagram/collab-media', { limit: a.limit, after: a.after, account: a.account, pageId: a.pageId });
|
|
3545
|
+
if (!d.count) return ok('This Instagram account is not a co-author on any collaborative post.', d);
|
|
3546
|
+
return ok(`${d.count} collaborative post(s):\n${d.media.map(m => `• [${m.mediaKind}] with @${m.by || '?'} — ${String(m.caption || '(no caption)').replace(/\s+/g, ' ').slice(0, 70)} — ${m.totalLikes ?? m.likes ?? '—'} likes, ${m.totalComments ?? m.comments ?? '—'} comments${m.saves != null ? `, ${m.saves} saves` : ''}${m.shares != null ? `, ${m.shares} shares` : ''} · ${m.url || m.id}`).join('\n')}`, d);
|
|
3547
|
+
}));
|
|
3548
|
+
|
|
3477
3549
|
server.registerTool('list_meta_comments', {
|
|
3478
3550
|
title: 'Read comments on a Meta post',
|
|
3479
3551
|
description: 'Read the comments under a Facebook Page post or Instagram media object — customer questions, objections and the exact language real people use about the product. Good raw material for ad copy, and the first step before replying or moderating. REPLIES: a reply is a comment ON a comment, and its id exists only under its PARENT — it is never returned by the post. Each row says how many replies it has; to read them (and to get the id reply_to_meta_comment / moderate_meta_comment need), call this tool again with postId set to that COMMENT id.',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.211",
|
|
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 795 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"
|