hermoso 0.1.88 → 0.1.90
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 +5 -5
- package/mcp/tools.mjs +167 -2
- 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
|
+
**427 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 427 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
|
|
@@ -90,9 +90,9 @@ publish). `schedule_post` / `list_scheduled` / `cancel_scheduled` give you one c
|
|
|
90
90
|
*X posting bills credits per API call (X charges per request); a post containing a link costs 13× one without.*
|
|
91
91
|
|
|
92
92
|
**Run the ads** — full campaign trees, built paused and read back before anything is reported, with every spend
|
|
93
|
-
change confirm-gated, on **
|
|
94
|
-
**Pinterest Ads**, **Microsoft Advertising**, **ChatGPT Ads** (OpenAI's Advertiser API), **X Ads**, **TikTok Ads
|
|
95
|
-
and **
|
|
93
|
+
change confirm-gated, on **eleven** platforms: **Meta**, **Google Ads**, **LinkedIn Ads**, **Reddit Ads**,
|
|
94
|
+
**Pinterest Ads**, **Microsoft Advertising**, **ChatGPT Ads** (OpenAI's Advertiser API), **X Ads**, **TikTok Ads**,
|
|
95
|
+
**Snapchat Ads** and **Apple Ads** (Apple Search Ads on the App Store). Each has list + report + create + budget/status tools
|
|
96
96
|
(e.g. `list_google_ads_campaigns`, `google_ads_report`, `create_google_ads_campaign`, `set_google_ads_budget`,
|
|
97
97
|
`set_google_ads_status`). *Snapchat needs one extra step the others do not: an ad points at a CREATIVE, and every
|
|
98
98
|
Snapchat creative must carry a Public Profile id — build it with `upload_snapchat_ads_creative`.*
|
package/mcp/tools.mjs
CHANGED
|
@@ -87,9 +87,9 @@ const okVideo = async (text, r) => {
|
|
|
87
87
|
// bytes and answers exactly such a URL, so it is the universal bridge and the honest thing to tell an agent.
|
|
88
88
|
//
|
|
89
89
|
// ONE const, spliced into BOTH the capability map and MCP_INSTRUCTIONS, so this fact cannot be live on one surface
|
|
90
|
-
// and stale on the other. The "
|
|
90
|
+
// and stale on the other. The "eleven ad platforms" count is asserted against the roster by
|
|
91
91
|
// tools/agent-independence-check.mjs — a hand-written number in a prompt is how a roster goes stale silently.
|
|
92
|
-
export const INDEPENDENCE = 'INDEPENDENT AREAS, NOT A PIPELINE — research, creation, publishing/scheduling and ads management each work ON THEIR OWN, and NO tool requires that you used another one first: publish or schedule media the user already has and generate nothing here (upload_file turns any local or external file into a URL the publish, schedule and ad-build tools accept), build and read campaigns on their OWN ad accounts with their OWN creative across all
|
|
92
|
+
export const INDEPENDENCE = 'INDEPENDENT AREAS, NOT A PIPELINE — research, creation, publishing/scheduling and ads management each work ON THEIR OWN, and NO tool requires that you used another one first: publish or schedule media the user already has and generate nothing here (upload_file turns any local or external file into a URL the publish, schedule and ad-build tools accept), build and read campaigns on their OWN ad accounts with their OWN creative across all eleven ad platforms, research competitors with no brand drafted and no channel connected, or generate a file with nothing connected at all and simply hand back the URL. Use one area, several, or all of it together — never tell a user they have to start somewhere else first.';
|
|
93
93
|
|
|
94
94
|
// X ADS: THE ENTRY BELOW USED TO BE A DENIAL, AND IT WAS FALSE (fixed 2026-08-10). It read "X ADS ARE NOT
|
|
95
95
|
// AVAILABLE: … Hermoso cannot create or manage X ad campaigns, so say that plainly instead of offering it" — an
|
|
@@ -4514,6 +4514,171 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
4514
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() },
|
|
4515
4515
|
annotations: { openWorldHint: true },
|
|
4516
4516
|
}, wrap(async (a) => { const d = await apiPost('/api/x-ads/status', a); return ok(d.note, d); }));
|
|
4517
|
+
// ── APPLE ADS (Apple Search Ads) — READS on the Campaign Management API v5 (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. To BUILD on this account, use create_apple_ads_campaign / create_apple_ads_ad_group / add_apple_ads_keywords: everything is created PAUSED and only set_apple_ads_status(confirm:true) can arm spend.',
|
|
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); }));
|
|
4576
|
+
// ── APPLE ADS: BUILDING, NOT JUST READING (2026-08-14) ────────────────────────────────────────────────────────
|
|
4577
|
+
// These run on Apple's OTHER API — the Apple Ads Platform API (api.ads.apple.com/v1) — while the reads above stay
|
|
4578
|
+
// on the Campaign Management API (v5). THE CREDENTIAL IS THE SAME: verified live by spending one minted token on
|
|
4579
|
+
// both hosts in the same second, so a connection made before this shipped can write with no reconnect and no new
|
|
4580
|
+
// scope. Everything is created PAUSED with no caller override, every reply is built from a read-back of what
|
|
4581
|
+
// Apple stored (never from what we sent), and exactly two switches take a confirmation — enabling, which is the
|
|
4582
|
+
// only thing that arms real money, and deleting, which Apple cannot undo.
|
|
4583
|
+
server.registerTool('create_apple_ads_campaign', {
|
|
4584
|
+
title: 'Create an Apple Ads campaign',
|
|
4585
|
+
description: 'Create an Apple Ads (Apple Search Ads) campaign promoting an iOS app on the App Store. CREATED PAUSED ALWAYS — there is no override, so nothing can spend until you separately call set_apple_ads_status(status:"ENABLED", confirm:true). REQUIRED: name (unique across the whole Apple Ads ORGANIZATION, not just this ad account — Apple 400s DUPLICATE_CAMPAIGN_NAME, and the org can contain campaigns you cannot see), appAdamId (the App Store id, e.g. "6752439949" — read it off any existing campaign’s promotedObjectId), dailyBudget (decimal string, e.g. "25.00"), countries (ISO 3166-1 alpha-2). placements defaults to APPSTORE_SEARCH_RESULTS, the classic Search Ads placement, which needs NO creative — Apple renders your App Store product page. bidStrategyType defaults to MANUAL_CPT (a fixed price per tap); MAX_CONVERSIONS optimizes for installs and is the only strategy that takes a campaign-level bid, where it means the target CPA. A MANUAL_CPT per-tap bid does NOT belong here: Apple refuses it at campaign level with an error naming a field you never sent — pass defaultBid to create_apple_ads_ad_group instead. Free.',
|
|
4586
|
+
inputSchema: {
|
|
4587
|
+
name: z.string().describe('REQUIRED. Unique across the entire Apple Ads organization.'),
|
|
4588
|
+
appAdamId: z.string().describe('REQUIRED. App Store id of the promoted app, e.g. "6752439949".'),
|
|
4589
|
+
dailyBudget: z.string().describe('REQUIRED. Decimal string, e.g. "25.00".'),
|
|
4590
|
+
countries: z.array(z.string()).describe('REQUIRED. ISO 3166-1 alpha-2 codes, e.g. ["US","CA"].'),
|
|
4591
|
+
placements: z.array(z.string()).optional().describe('Default ["APPSTORE_SEARCH_RESULTS"]. Also APPSTORE_SEARCH_TAB, APPSTORE_TODAY_TAB, APPSTORE_PRODUCT_PAGES, or MAPS_SEARCH_RESULTS / MAPS_SEARCH_HOME. One campaign cannot mix App Store and Maps placements.'),
|
|
4592
|
+
bidStrategyType: z.enum(['MANUAL_CPT', 'MAX_CONVERSIONS']).optional().describe('Default MANUAL_CPT.'),
|
|
4593
|
+
bid: z.string().optional().describe('MAX_CONVERSIONS only — the target CPA. Refused on MANUAL_CPT.'),
|
|
4594
|
+
currency: z.string().optional().describe('Defaults to the connected ad account’s currency; an account cannot mix currencies.'),
|
|
4595
|
+
startTime: z.string().optional(), endTime: z.string().optional().describe('Omit to run indefinitely.'),
|
|
4596
|
+
},
|
|
4597
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), bornPaused: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
4598
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4599
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/campaign', a); return ok(`Apple Ads campaign created PAUSED:\n${d.summary}\n${d.note || ''}`, d); }));
|
|
4600
|
+
|
|
4601
|
+
server.registerTool('create_apple_ads_ad_group', {
|
|
4602
|
+
title: 'Create an Apple Ads ad group',
|
|
4603
|
+
description: 'Create an ad group inside an Apple Ads campaign — this is where the bid and the keywords live. CREATED PAUSED ALWAYS. REQUIRED: campaignId, name. defaultBid is the most you will pay per tap (decimal string, e.g. "1.50") and applies to every keyword with no bid of its own. startTime is optional here and defaults to now: Apple documents it as optional and then REJECTS the create without it, so Hermoso always supplies one. searchMatch:true opts into Apple’s automated keyword matching, which targets relevant search terms beyond your keyword list. pricingModel defaults to CPT and must match the campaign’s billing event. Keywords cannot be created inline — make the ad group, then call add_apple_ads_keywords. Like the campaign it lives in, it stays PAUSED until set_apple_ads_status(status:"ENABLED", confirm:true) arms it. Free.',
|
|
4604
|
+
inputSchema: {
|
|
4605
|
+
campaignId: z.string().describe('REQUIRED.'), name: z.string().describe('REQUIRED.'),
|
|
4606
|
+
defaultBid: z.string().optional().describe('Max cost per tap, e.g. "1.50".'),
|
|
4607
|
+
currency: z.string().optional(), startTime: z.string().optional().describe('Defaults to now.'),
|
|
4608
|
+
endTime: z.string().optional().describe('Omit to inherit the campaign end date.'),
|
|
4609
|
+
searchMatch: z.boolean().optional().describe('Opt into Apple’s automated keyword matching.'),
|
|
4610
|
+
pricingModel: z.enum(['CPT', 'CPM', 'CPA']).optional().describe('Default CPT; must match the campaign billing event.'),
|
|
4611
|
+
},
|
|
4612
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), bornPaused: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
4613
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4614
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/adgroup', a); return ok(`Apple Ads ad group created PAUSED:\n${d.summary}\n${d.note || ''}`, d); }));
|
|
4615
|
+
|
|
4616
|
+
server.registerTool('add_apple_ads_keywords', {
|
|
4617
|
+
title: 'Add Apple Ads keywords',
|
|
4618
|
+
description: 'Add targeting keywords to an Apple Ads ad group. keywords is [{text, matchType, bid}] — matchType EXACT (default), BROAD, PHRASE or CATEGORY, and bid optionally overrides the ad group default for that one term. Up to 1000 per call and 5000 per ad group. They are sent ONE AT A TIME on purpose: a single term Apple refuses cannot take the rest of the batch down, and the reply names every refusal with Apple’s own reason, then reads the ad group’s keywords back to prove what actually landed. Keyword text and match type are IMMUTABLE — to change either, delete the keyword and add it again. A keyword is created ENABLED because it cannot serve unless its ad group and campaign are enabled too; if the ad group is ALREADY live, adding keywords serves on the next auction and therefore takes confirm:true. Free.',
|
|
4619
|
+
inputSchema: {
|
|
4620
|
+
adGroupId: z.string().describe('REQUIRED.'),
|
|
4621
|
+
keywords: z.array(z.object({ text: z.string(), matchType: z.string().optional(), bid: z.string().optional() })).describe('REQUIRED. Up to 1000.'),
|
|
4622
|
+
matchType: z.enum(['EXACT', 'BROAD', 'PHRASE', 'CATEGORY']).optional().describe('Default for entries that do not set one.'),
|
|
4623
|
+
currency: z.string().optional(),
|
|
4624
|
+
confirm: z.boolean().optional().describe('Required ONLY when the ad group is already ENABLED.'),
|
|
4625
|
+
},
|
|
4626
|
+
outputSchema: { ok: z.boolean().optional(), adGroupId: z.string().optional(), added: z.number().optional(), failed: z.array(z.any()).optional(), verified: z.boolean().optional(), keywords: z.array(z.string()).optional(), note: z.string().optional() },
|
|
4627
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4628
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/keywords', a); return ok(`Added ${d.added} keyword(s) to Apple Ads ad group ${d.adGroupId} (verified: ${d.verified}).\n${(d.keywords || []).join('\n')}\n${d.note || ''}`, d); }));
|
|
4629
|
+
|
|
4630
|
+
server.registerTool('add_apple_ads_negative_keywords', {
|
|
4631
|
+
title: 'Add Apple Ads negative keywords',
|
|
4632
|
+
description: 'Add negative keywords to an Apple Ads campaign or ad group — search terms your ads must NOT show for. Pass exactly one of campaignId (campaign-wide) or adGroupId (that ad group only); Apple rejects a request carrying both. keywords is [{text, matchType}] with matchType BROAD (default) or EXACT. Never confirm-gated and never a spend risk, because a negative keyword only ever RESTRICTS where you show. Text and match type are immutable — delete and re-add to change either. Free.',
|
|
4633
|
+
inputSchema: {
|
|
4634
|
+
campaignId: z.string().optional().describe('Campaign-wide. Pass this OR adGroupId, never both.'),
|
|
4635
|
+
adGroupId: z.string().optional().describe('One ad group. Pass this OR campaignId, never both.'),
|
|
4636
|
+
keywords: z.array(z.object({ text: z.string(), matchType: z.string().optional() })).describe('REQUIRED.'),
|
|
4637
|
+
matchType: z.enum(['EXACT', 'BROAD', 'PHRASE', 'CATEGORY']).optional().describe('Default BROAD.'),
|
|
4638
|
+
},
|
|
4639
|
+
outputSchema: { ok: z.boolean().optional(), scope: z.string().optional(), added: z.number().optional(), failed: z.array(z.any()).optional(), verified: z.boolean().optional(), keywords: z.array(z.string()).optional(), note: z.string().optional() },
|
|
4640
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4641
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/negative-keywords', a); return ok(`Added ${d.added} negative keyword(s) (${d.scope}, verified: ${d.verified}).\n${(d.keywords || []).join('\n')}\n${d.note || ''}`, d); }));
|
|
4642
|
+
|
|
4643
|
+
server.registerTool('update_apple_ads_object', {
|
|
4644
|
+
title: 'Edit an Apple Ads campaign, ad group or keyword',
|
|
4645
|
+
description: 'Edit an existing Apple Ads campaign, ad group or keyword. Pass level and id plus ONLY the fields you want to change — Apple leaves every field you omit exactly as it is. Campaign: name, dailyBudget, startTime, endTime, countries, bidStrategyType. Ad group: name, defaultBid, startTime, endTime, searchMatch. Keyword: bid only — its text and match type are immutable, so delete and re-add to change either. WARNING on countries: an ARRAY REPLACES rather than merges, so the list you send becomes the campaign’s entire geographic targeting — send every country you want to keep, not just the new one. This tool deliberately CANNOT change a status: enabling arms real spend, so it lives behind set_apple_ads_status and its confirmation. Free.',
|
|
4646
|
+
inputSchema: {
|
|
4647
|
+
level: z.enum(['campaign', 'adgroup', 'keyword']).describe('REQUIRED.'), id: z.string().describe('REQUIRED.'),
|
|
4648
|
+
name: z.string().optional(), dailyBudget: z.string().optional().describe('Campaign only.'),
|
|
4649
|
+
defaultBid: z.string().optional().describe('Ad group only.'), bid: z.string().optional().describe('Keyword only.'),
|
|
4650
|
+
countries: z.array(z.string()).optional().describe('Campaign only. REPLACES the whole list.'),
|
|
4651
|
+
bidStrategyType: z.enum(['MANUAL_CPT', 'MAX_CONVERSIONS']).optional().describe('Campaign only.'),
|
|
4652
|
+
searchMatch: z.boolean().optional().describe('Ad group only.'),
|
|
4653
|
+
startTime: z.string().optional(), endTime: z.string().optional(), currency: z.string().optional(),
|
|
4654
|
+
},
|
|
4655
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
4656
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4657
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/update', a); return ok(`Apple Ads ${d.level} ${d.id} updated (verified: ${d.verified}):\n${d.summary}\n${d.note || ''}`, d); }));
|
|
4658
|
+
|
|
4659
|
+
server.registerTool('set_apple_ads_status', {
|
|
4660
|
+
title: 'Pause or enable an Apple Ads object',
|
|
4661
|
+
description: 'Pause or enable an Apple Ads campaign, ad group, keyword or ad. PAUSING is immediate and needs no confirmation. ENABLING IS THE ONE SWITCH THAT ARMS REAL MONEY: it requires confirm:true, and without it nothing changes and the refusal names the object Apple actually holds under that id — read back from Apple, never echoed from your input, because aiming at the wrong campaign is invisible until money moves. An object serves only when it AND every parent above it are ENABLED, so enabling a keyword inside a paused campaign spends nothing. Prefer pausing to deleting: pausing is reversible and Apple’s delete is not. The reply reports the status Apple STORED, which is a different claim from the one it accepted. Free.',
|
|
4662
|
+
inputSchema: {
|
|
4663
|
+
level: z.enum(['campaign', 'adgroup', 'keyword', 'negative_keyword', 'ad']).describe('REQUIRED.'),
|
|
4664
|
+
id: z.string().describe('REQUIRED.'), status: z.enum(['ENABLED', 'PAUSED']).describe('REQUIRED.'),
|
|
4665
|
+
confirm: z.boolean().optional().describe('REQUIRED to ENABLE — it arms real spend. Pausing needs none.'),
|
|
4666
|
+
},
|
|
4667
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), armsSpend: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
4668
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4669
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/status', a); return ok(`Apple Ads ${d.level} ${d.id} → ${d.requested?.status} (verified: ${d.verified}${d.armsSpend ? ', SPEND ARMED' : ''}):\n${d.summary}\n${d.note || ''}`, d); }));
|
|
4670
|
+
|
|
4671
|
+
server.registerTool('delete_apple_ads_object', {
|
|
4672
|
+
title: 'Delete an Apple Ads object',
|
|
4673
|
+
description: 'Delete an Apple Ads campaign, ad group, keyword, negative keyword or ad. Requires confirm:true, and optionally confirmName echoed back to prove you aimed at the right object. THIS CASCADES AND CANNOT BE UNDONE: Apple soft-deletes with no undelete, and deleting a campaign takes every ad group, keyword and ad underneath it. In almost every case set_apple_ads_status(status:"PAUSED") is what you actually want — it stops all spend and is reversible. The reply is confirmed by RE-READING the object: for a delete, an absent or deleted-flagged row is the proof, never the HTTP 200. Free.',
|
|
4674
|
+
inputSchema: {
|
|
4675
|
+
level: z.enum(['campaign', 'adgroup', 'keyword', 'negative_keyword', 'ad']).describe('REQUIRED.'),
|
|
4676
|
+
id: z.string().describe('REQUIRED.'), confirm: z.boolean().optional().describe('REQUIRED. Nothing is deleted without it.'),
|
|
4677
|
+
confirmName: z.string().optional().describe('Optional — echo the object’s exact name to prove you aimed at the right one.'),
|
|
4678
|
+
},
|
|
4679
|
+
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), deleted: z.string().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
4680
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4681
|
+
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/delete', a); return ok(`${d.summary}\n${d.note || ''}`, d); }));
|
|
4517
4682
|
// ── X: managing what you built — update, remove, and the reads both depend on (2026-08-05) ────────────────────
|
|
4518
4683
|
// Hermoso could build an X campaign and activate it and then change NOTHING about it, and could remove nothing at
|
|
4519
4684
|
// 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.90",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI ad studio + marketing MCP (
|
|
5
|
+
"description": "AI ad studio + marketing MCP (427 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"
|