hermoso 0.1.87 → 0.1.89
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 +66 -1
- 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
|
+
**420 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
|
**It is not all-or-nothing.** Research, creation, publishing/scheduling and ads management are four *independent*
|
|
@@ -53,7 +53,7 @@ Cursor / Codex — add to `mcp.json` (Codex uses the TOML equivalent):
|
|
|
53
53
|
|
|
54
54
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
55
55
|
|
|
56
|
-
### What the
|
|
56
|
+
### What the 420 tools cover
|
|
57
57
|
|
|
58
58
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
59
59
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/tools.mjs
CHANGED
|
@@ -1652,12 +1652,18 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
1652
1652
|
// HTTP route and the in-app agent call.
|
|
1653
1653
|
server.registerTool('retry_scheduled', {
|
|
1654
1654
|
title: 'Retry a failed scheduled post',
|
|
1655
|
-
description: 'Send a post that FAILED again. A scheduled post fans out across its channels INDEPENDENTLY, so a failure is usually PARTIAL — LinkedIn 401s while Instagram published fine — and this re-fires ONLY the channels that did not succeed by default (list_scheduled reports them as `retryable`). It re-queues the same content as a NEW post that goes out RIGHT AWAY — the queue picks it up on its next pass, within seconds — and the original keeps its failure record so the history still shows what went wrong. Naming a channel that already published is REFUSED rather than quietly posting a second time. Two independent belts stop a double-post: a channel that genuinely published can only REPLAY (nothing is posted), and a channel whose outcome is UNRESOLVED — the platform timed out and may be holding the post — refuses with that reason instead of guessing. Retry after fixing the cause:
|
|
1655
|
+
description: 'Send a post that FAILED again. A scheduled post fans out across its channels INDEPENDENTLY, so a failure is usually PARTIAL — LinkedIn 401s while Instagram published fine — and this re-fires ONLY the channels that did not succeed by default (list_scheduled reports them as `retryable`). It re-queues the same content as a NEW post that goes out RIGHT AWAY — the queue picks it up on its next pass, within seconds — and the original keeps its failure record so the history still shows what went wrong. Naming a channel that already published is REFUSED rather than quietly posting a second time. Two independent belts stop a double-post: a channel that genuinely published can only REPLAY (nothing is posted), and a channel whose outcome is UNRESOLVED — the platform timed out and may be holding the post — refuses with that reason instead of guessing. Retry after fixing the cause — and you can fix it IN THIS CALL: pass `boardId`, `pageId`, `linkedinOrganizationId`, `locationId`, `message` or `captions` to correct the value that failed, and the corrected post is re-validated exactly like a fresh schedule. That matters because the commonest cause is a field, not an outage: a Pin aimed at the wrong board fails identically however many times it is re-sent. Anything you do not name is copied from the original. To send the same thing again ON PURPOSE, use duplicate_scheduled.',
|
|
1656
1656
|
inputSchema: {
|
|
1657
1657
|
id: z.string().describe('the scheduled post id from list_scheduled'),
|
|
1658
1658
|
channels: z.array(z.enum(['facebook', 'instagram', 'threads', 'tiktok', 'youtube', 'linkedin', 'x', 'pinterest', 'google_business'])).optional().describe('retry only these channels (default: every channel that did not publish)'),
|
|
1659
1659
|
at: z.string().optional().describe('hold the retry until a later time — ISO timestamp or epoch milliseconds. Leave it out to retry immediately, which is almost always what you want. A time you name here must be at least a minute from now, exactly like any other scheduled post.'),
|
|
1660
1660
|
allowDuplicate: z.boolean().optional().describe('ONLY for a channel you have checked by hand and confirmed the post is genuinely NOT there. It bypasses the double-post protection and can publish a second public copy, so never set it to work around a refusal you have not investigated.'),
|
|
1661
|
+
boardId: z.string().optional().describe('CORRECT THE BOARD on retry — the Pinterest board the Pin goes on, from list_pinterest_boards. A Pin aimed at a board Pinterest refuses fails the same way on every retry until this is changed.'),
|
|
1662
|
+
pageId: z.string().optional().describe('CORRECT THE PAGE on retry — which connected Facebook Page (and its linked Instagram/Threads) publishes, from list_meta_pages.'),
|
|
1663
|
+
linkedinOrganizationId: z.string().optional().describe('CORRECT THE LINKEDIN AUTHOR on retry — the company Page id from list_linkedin_pages. Set it to an empty string to fall back to the personal profile.'),
|
|
1664
|
+
locationId: z.string().optional().describe('CORRECT THE LISTING on retry — which Google Business Profile location, e.g. "locations/123" from list_business_locations.'),
|
|
1665
|
+
message: z.string().optional().describe('CORRECT THE CAPTION on retry — use this when the original was refused for length or content. Anything not named here is copied from the original post.'),
|
|
1666
|
+
captions: z.record(z.string()).optional().describe('CORRECT ONE CHANNEL’S CAPTION on retry, e.g. { "x": "..." } when only that channel refused the text.'),
|
|
1661
1667
|
},
|
|
1662
1668
|
outputSchema: { id: z.string().optional(), at: z.string().optional(), channels: z.array(z.string()).optional(), retryOf: z.string().optional(), retrying: z.array(z.string()).optional(), alreadyPublished: z.array(z.string()).optional(), note: z.string().optional() },
|
|
1663
1669
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
@@ -4508,6 +4514,65 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
4508
4514
|
outputSchema: { campaignId: z.string().nullable().optional(), lineItemId: z.string().nullable().optional(), status: z.string().optional(), parentStatus: z.string().nullable().optional(), note: z.string().optional() },
|
|
4509
4515
|
annotations: { openWorldHint: true },
|
|
4510
4516
|
}, wrap(async (a) => { const d = await apiPost('/api/x-ads/status', a); return ok(d.note, d); }));
|
|
4517
|
+
// ── APPLE ADS (Apple Search Ads) — READ-ONLY (2026-08-14) ──────────────────────────────────────────────────────
|
|
4518
|
+
// Every description says read-only in its own words, because an agent that believes it can build a campaign here
|
|
4519
|
+
// will try, fail, and the user will blame Hermoso ([[prompt-rosters-go-stale]]). When write tools land, these
|
|
4520
|
+
// sentences move with them.
|
|
4521
|
+
server.registerTool('list_apple_ads_campaigns', {
|
|
4522
|
+
title: 'List Apple Ads campaigns',
|
|
4523
|
+
description: 'Read the brand’s connected Apple Ads (Apple Search Ads) campaigns — App Store search ads. Each row carries status, servingStatus, daily and total budget, the countries it runs in and, when a campaign cannot run, servingStateReasons stating exactly why. Read-only, free, zero spend risk. Needs Apple Ads connected (Settings ▸ Connectors ▸ Apple Ads): there is no OAuth — Hermoso generates an EC signing key, the user pastes the public half into Apple Ads ▸ Account Settings ▸ API and pastes back clientId / teamId / keyId. Hermoso CANNOT create or edit Apple Ads campaigns — never offer that.',
|
|
4524
|
+
inputSchema: {
|
|
4525
|
+
limit: z.number().optional().describe('page size, default 100, Apple’s max is 1000'),
|
|
4526
|
+
offset: z.number().optional().describe('offset pagination'),
|
|
4527
|
+
},
|
|
4528
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), count: z.number().optional(), total: z.number().nullable().optional(), campaigns: z.array(z.any()).optional(), note: z.string().optional() },
|
|
4529
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
4530
|
+
}, wrap(async (a) => { const d = await apiGet('/api/apple-ads/campaigns', a); return ok(`${d.count} Apple Ads campaign(s):\n${d.note}`, d); }));
|
|
4531
|
+
server.registerTool('list_apple_ads_ad_groups', {
|
|
4532
|
+
title: 'List Apple Ads ad groups',
|
|
4533
|
+
description: 'Ad groups inside one Apple Ads campaign — status, servingStatus, default bid, and whether Search Match (Apple’s automated keyword matching) is on. campaignId is REQUIRED: Apple publishes no org-wide ad-group list, so an ad group can only be reached through its campaign. Read-only, free.',
|
|
4534
|
+
inputSchema: {
|
|
4535
|
+
campaignId: z.string().describe('REQUIRED — from list_apple_ads_campaigns'),
|
|
4536
|
+
limit: z.number().optional(), offset: z.number().optional(),
|
|
4537
|
+
},
|
|
4538
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), campaignId: z.string().optional(), count: z.number().optional(), total: z.number().nullable().optional(), adGroups: z.array(z.any()).optional(), note: z.string().optional() },
|
|
4539
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
4540
|
+
}, wrap(async (a) => { const d = await apiGet('/api/apple-ads/adgroups', a); return ok(`${d.count} ad group(s) in Apple Ads campaign ${d.campaignId}:\n${d.note}`, d); }));
|
|
4541
|
+
server.registerTool('list_apple_ads_keywords', {
|
|
4542
|
+
title: 'List Apple Ads targeting or negative keywords',
|
|
4543
|
+
description: 'Targeting or negative keywords with their match type, status and bid. Apple splits these four ways and the paths are not interchangeable: TARGETING keywords are listed PER AD GROUP, so pass campaignId AND adGroupId. NEGATIVE keywords (negative:true) exist at campaign level — pass campaignId alone — or per ad group with both ids. Apple allows up to 5000 keywords per ad group. Read-only, free.',
|
|
4544
|
+
inputSchema: {
|
|
4545
|
+
campaignId: z.string().describe('REQUIRED'),
|
|
4546
|
+
adGroupId: z.string().optional().describe('required for TARGETING keywords; optional for negatives'),
|
|
4547
|
+
negative: z.boolean().optional().describe('list negative keywords instead of targeting keywords'),
|
|
4548
|
+
limit: z.number().optional(), offset: z.number().optional(),
|
|
4549
|
+
},
|
|
4550
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), campaignId: z.string().optional(), adGroupId: z.string().nullable().optional(), count: z.number().optional(), total: z.number().nullable().optional(), keywords: z.array(z.any()).optional(), note: z.string().optional() },
|
|
4551
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
4552
|
+
}, wrap(async (a) => { const d = await apiGet('/api/apple-ads/keywords', a); return ok(`${d.count} ${d.level === 'negativeKeyword' ? 'negative ' : ''}keyword(s):\n${d.note}`, d); }));
|
|
4553
|
+
server.registerTool('apple_ads_report', {
|
|
4554
|
+
title: 'Apple Ads performance report',
|
|
4555
|
+
description: 'Apple Ads performance — impressions, taps, installs, spend, TTR, CPT, CPA. level is campaign (organization-wide) or adgroup / keyword / searchterm / ad, and every level except campaign REQUIRES campaignId. startTime and endTime are REQUIRED, as YYYY-MM-DD. Apple enforces three interlocking rules and the reply reports back which were applied: with no granularity row totals are forced on; with granularity grand totals are forced off; grouping by a demographic or geo dimension forces BOTH off — so a total of zero can mean "Apple did not return that total", not "no spend". A report with NO rows genuinely means there was NO delivery in that window: say exactly that, and never present zeros as measured performance. Read-only and free, so run it first after connecting — it proves the credentials work with zero spend risk.',
|
|
4556
|
+
inputSchema: {
|
|
4557
|
+
level: z.enum(['campaign', 'adgroup', 'keyword', 'searchterm', 'ad']).optional().describe('default campaign'),
|
|
4558
|
+
campaignId: z.string().optional().describe('REQUIRED for every level except campaign'),
|
|
4559
|
+
startTime: z.string().describe('YYYY-MM-DD (required)'),
|
|
4560
|
+
endTime: z.string().describe('YYYY-MM-DD (required)'),
|
|
4561
|
+
granularity: z.enum(['HOURLY', 'DAILY', 'WEEKLY', 'MONTHLY']).optional(),
|
|
4562
|
+
groupBy: z.array(z.string()).optional().describe('adminArea, ageRange, countryCode, countryOrRegion, deviceClass, gender, locality'),
|
|
4563
|
+
timeZone: z.enum(['ORTZ', 'UTC']).optional().describe('ORTZ = the organization time zone, Apple’s default'),
|
|
4564
|
+
limit: z.number().optional(),
|
|
4565
|
+
},
|
|
4566
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), startTime: z.string().optional(), endTime: z.string().optional(), count: z.number().optional(), rows: z.array(z.any()).optional(), grandTotals: z.any().optional(), note: z.string().optional() },
|
|
4567
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
4568
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/report', a); return ok(`${d.note}\n${JSON.stringify((d.rows || []).slice(0, 40))}`, d); }));
|
|
4569
|
+
server.registerTool('list_apple_ads_orgs', {
|
|
4570
|
+
title: 'List Apple Ads organizations',
|
|
4571
|
+
description: 'The Apple Ads organizations these credentials can act as, with each one’s currency, time zone, payment model and the API roles held. Apple treats an orgId like a CAMPAIGN GROUP, so one login can cover several — an agency managing multiple clients has one per client. Reading this does NOT switch organization: Apple Ads is pinned to the ONE organization chosen when the connection was made, so an agent can never act as another client’s campaign group. To use a different one, reconnect Apple Ads and name it there. A payment model of null is worth reporting: Apple states that without one, campaigns cannot run. Read-only, free.',
|
|
4572
|
+
inputSchema: {},
|
|
4573
|
+
outputSchema: { ok: z.boolean().optional(), active: z.string().nullable().optional(), count: z.number().optional(), orgs: z.array(z.any()).optional(), note: z.string().optional() },
|
|
4574
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
4575
|
+
}, wrap(async () => { const d = await apiGet('/api/apple-ads/orgs', {}); return ok(d.note, d); }));
|
|
4511
4576
|
// ── X: managing what you built — update, remove, and the reads both depend on (2026-08-05) ────────────────────
|
|
4512
4577
|
// Hermoso could build an X campaign and activate it and then change NOTHING about it, and could remove nothing at
|
|
4513
4578
|
// all: X was the eighth ad platform in the product and the only one with no removal path. Every field offered
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.89",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI ad studio + marketing MCP (
|
|
5
|
+
"description": "AI ad studio + marketing MCP (420 tools): build and manage ad campaigns on Meta, Google Ads, Reddit, X, TikTok, Snapchat, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads \u2014 generate finished video, image and UGC avatar ads, publish them to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest, spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus organic TikTok/Instagram/YouTube/Reddit, and read what they achieved in Google Analytics 4. CLI and Claude skills for Hermoso, the AI ad studio: brand onboarding, 30+ image/video models, finished-ad pipeline (script, voiceover, music, brand end card), ad scoring and competitor teardowns.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|