hermoso 0.1.23 → 0.1.24
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 +67 -22
- package/mcp/tools.mjs +315 -3
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
# Hermoso — MCP, CLI & Skills
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
3
|
+
Run your whole marketing operation from **any AI agent**: Claude Code, Claude.ai, Cursor, Codex, or your own
|
|
4
|
+
scripts. Research the ads already winning in a market, generate finished image & video ads (your real product
|
|
5
|
+
composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
|
|
6
|
+
campaigns behind them — all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
7
|
+
|
|
8
|
+
**205 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
|
|
9
|
+
catalog with exact per-render credit costs plus the full capability map.
|
|
7
10
|
|
|
8
11
|
## Instant: the hosted Claude.ai connector
|
|
9
12
|
|
|
10
13
|
Paste **`https://app.hermoso.ai/mcp`** into Claude → Settings → Connectors → *Add custom connector*, approve with
|
|
11
|
-
your Hermoso account, done — the full
|
|
14
|
+
your Hermoso account, done — the full toolset with your saved brand context, billed to your plan.
|
|
12
15
|
|
|
13
16
|
## Quickstart for Claude Code / Cursor / scripts (2 minutes)
|
|
14
17
|
|
|
@@ -27,8 +30,8 @@ learned memory you set up in the web app apply automatically (`get_brand` shows
|
|
|
27
30
|
|
|
28
31
|
## 1. MCP server (stdio) — Claude Code / Cursor / Codex
|
|
29
32
|
|
|
30
|
-
`hermoso mcp` runs a stdio MCP server exposing the full
|
|
31
|
-
|
|
33
|
+
`hermoso mcp` runs a stdio MCP server exposing the full toolset. The published `hermoso` package means no clone —
|
|
34
|
+
`npx -y hermoso mcp` fetches and runs it:
|
|
32
35
|
|
|
33
36
|
```bash
|
|
34
37
|
claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp
|
|
@@ -43,25 +46,67 @@ Cursor / Codex — add to `mcp.json` (Codex uses the TOML equivalent):
|
|
|
43
46
|
|
|
44
47
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
`
|
|
56
|
-
|
|
49
|
+
### What the 205 tools cover
|
|
50
|
+
|
|
51
|
+
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
52
|
+
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
|
53
|
+
social (`search_tiktok`, `search_instagram`, `search_youtube`, `search_reddit`, `search_threads`);
|
|
54
|
+
`scrapecreators_fetch`, `mine_angles`, `analyze_video`, `check_ad_policy`, `list_skills` / `get_skill`.
|
|
55
|
+
|
|
56
|
+
**Create** — `draft_brand` → `plan_ad` → `render_ad` (the Studio quality pipeline: composited text, clean speech,
|
|
57
|
+
music, brand end card), or `generate_image` / `generate_video` / `generate_avatar` (UGC creators + lip-sync).
|
|
58
|
+
Also `make_template_ad` (native HTML ad formats), `make_explainer`, `product_sizzle`, `make_thumbnail`,
|
|
59
|
+
`remix_static`, `recast_motion`, `reframe_video`, `upscale_video`, `dub_video`, `change_voice`, `finish_video`,
|
|
60
|
+
`fix_beat`, `stitch_video`, `clip_video`, `post_edit`, plus `plan_variations` + `score_ad` to fan out and rank.
|
|
61
|
+
**Length is yours to set:** pass `durationSeconds` to `plan_ad` and the storyboard is *authored* to it — ≤15s
|
|
62
|
+
renders as one continuous clip, longer is stitched from acts (40s = 15+15+10), never time-compressed.
|
|
63
|
+
|
|
64
|
+
**Raw model playground** — the full catalog (30+ image / video / voice / writing models, each with its exact
|
|
65
|
+
per-render credit cost) with no ad framing: `generate_image` / `generate_video` with `useBrand:false`,
|
|
66
|
+
`generate_voice`, `generate_text`.
|
|
67
|
+
|
|
68
|
+
**Publish to your own channels** — Facebook, Instagram and Threads (`post_to_meta`), TikTok (`post_to_tiktok`),
|
|
69
|
+
YouTube (`post_to_youtube` + `update_youtube_video`, `youtube_video_insights`, comments read/reply), X
|
|
70
|
+
(`post_to_x`, `x_post_metrics`, `x_post_insights`, `x_mentions`), LinkedIn profile **and** company Pages
|
|
71
|
+
(`post_to_linkedin`, `post_to_linkedin_page`), Pinterest (`post_to_pinterest` + boards), and Google Business
|
|
72
|
+
Profile (`post_to_google_business` — Google grants this API per project, so access must be approved before Posts
|
|
73
|
+
publish). `schedule_post` / `list_scheduled` / `cancel_scheduled` give you one content calendar across channels.
|
|
74
|
+
`upload_file` brings in any external or local media, not just Hermoso renders.
|
|
75
|
+
*X posting bills credits per API call (X charges per request); a post containing a link costs 13× one without.*
|
|
76
|
+
|
|
77
|
+
**Run the ads** — full campaign trees, built paused and read back before anything is reported, with every spend
|
|
78
|
+
change confirm-gated, on **Meta**, **Google Ads**, **LinkedIn Ads**, **Pinterest Ads**, **Microsoft Advertising**
|
|
79
|
+
and **ChatGPT Ads** (OpenAI's Advertiser API). Each has list + report + create + budget/status tools
|
|
80
|
+
(e.g. `list_google_ads_campaigns`, `google_ads_report`, `create_google_ads_campaign`, `set_google_ads_budget`,
|
|
81
|
+
`set_google_ads_status`). *X Ads are not supported — X posting only.*
|
|
82
|
+
|
|
83
|
+
**Files** — Google Drive CRUD (`save_to_drive`, `list_drive_files`, `update_drive_file`, `delete_drive_file`,
|
|
84
|
+
`create_drive_folder`), Google Sheets (`create_sheet`, `append_to_sheet`, `read_sheet`), Google Docs
|
|
85
|
+
(`create_doc`, `append_to_doc`), and OneDrive (`save_to_onedrive` + full CRUD).
|
|
86
|
+
|
|
87
|
+
**Workspace & account** — brand workspaces (`list_brands`, `create_brand`, `use_brand`, `update_brand`,
|
|
88
|
+
`delete_brand` — one account holds many brands, so an agency runs every client through here), memory
|
|
89
|
+
(`remember`, `forget`, `list_memory`), custom skills (`save_skill`, `delete_skill`), AI employees
|
|
90
|
+
(`save_employee`, `list_employees`, `set_active_employee`), team (`list_team`, `invite_member`, `remove_member`,
|
|
91
|
+
`set_role`), settings (`get_settings`, `update_settings` — including the **language** every ad, script and plan
|
|
92
|
+
is written in), connectors (`list_connectors`, `list_connector_accounts`, `set_connector_accounts`,
|
|
93
|
+
`disconnect_connector`), and billing (`hermoso_credits`, `billing_status`, `buy_credits`, `upgrade_plan`,
|
|
94
|
+
`set_auto_reload`), plus `list_jobs` / `get_job` for async renders.
|
|
95
|
+
|
|
96
|
+
**Connector accounts are picked, not guessed.** One person often administers several Facebook Pages, Google Ads
|
|
97
|
+
customers or LinkedIn company Pages. Only the accounts ticked for a brand are usable — enforced server-side, and
|
|
98
|
+
an empty selection shares nothing. Linking a *new* account is the one step that is not headless (it is an OAuth
|
|
99
|
+
consent screen, so the user does it in the app).
|
|
100
|
+
|
|
101
|
+
Render jobs queue server-side and poll to completion, returning a served URL.
|
|
57
102
|
|
|
58
103
|
## 2. CLI — the token-cheap path for terminal agents
|
|
59
104
|
|
|
60
|
-
`bin/hermoso.mjs` mirrors the tools as subprocess commands, so an agent can shell out instead of carrying a
|
|
61
|
-
tool manifest.
|
|
105
|
+
`bin/hermoso.mjs` mirrors the core tools as subprocess commands, so an agent can shell out instead of carrying a
|
|
106
|
+
fat tool manifest.
|
|
62
107
|
|
|
63
108
|
```bash
|
|
64
|
-
npm install -g
|
|
109
|
+
npm install -g hermoso # installs `hermoso`
|
|
65
110
|
hermoso capabilities # valid model ids + costs (run first)
|
|
66
111
|
hermoso create --brand "YourBrand" --product "your best-selling product" --format image
|
|
67
112
|
hermoso generate image --prompt "…" --ref ./product.png --wait
|
|
@@ -88,7 +133,7 @@ Then invoke `/hermoso-ad-from-brand an ad for yourbrand.com — our hero product
|
|
|
88
133
|
| Env | Meaning |
|
|
89
134
|
| --- | --- |
|
|
90
135
|
| `HERMOSO_API_BASE` | The Hermoso API origin (default `https://app.hermoso.ai` — set `http://localhost:3000` if you run the app yourself) |
|
|
91
|
-
| `HERMOSO_TOKEN` | Bearer
|
|
136
|
+
| `HERMOSO_TOKEN` | Bearer agent key (`hmk_…`) — required against the hosted app |
|
|
92
137
|
| `HERMOSO_PROFILE` | Brand-workspace id, for accounts with multiple brand profiles |
|
|
93
138
|
|
|
94
139
|
`mcp/http.mjs` is the hosted remote-connector transport (paste-a-URL into Claude.ai → Connectors). It ships in
|
package/mcp/tools.mjs
CHANGED
|
@@ -1858,14 +1858,14 @@ export function registerTools(server) {
|
|
|
1858
1858
|
}));
|
|
1859
1859
|
server.registerTool('microsoft_ads_report', {
|
|
1860
1860
|
title: 'Microsoft Advertising performance report',
|
|
1861
|
-
description: 'Performance for a Microsoft Advertising account — impressions, clicks, CTR, average CPC, spend, conversions, broken down by campaign. Window via timePeriod (Today | Yesterday | LastSevenDays | ThisWeek | LastWeek | LastFourWeeks | ThisMonth | LastMonth | LastThreeMonths | LastSixMonths | ThisYear | LastYear) or since+until (YYYY-MM-DD). Microsoft generates reports ASYNCHRONOUSLY: this can return pending:true with a reportRequestId, and you must call again rather than reporting any numbers. A report that succeeds with ZERO rows genuinely means there was no delivery in that window — say exactly that; never present zeros as measured performance. Read-only, free.',
|
|
1861
|
+
description: 'Performance for a Microsoft Advertising account — impressions, clicks, CTR, average CPC, spend, conversions, broken down by campaign. Window via timePeriod (Today | Yesterday | LastSevenDays | Last14Days | Last30Days | ThisWeek | LastWeek | LastFourWeeks | ThisMonth | LastMonth | LastThreeMonths | LastSixMonths | ThisYear | LastYear | ThisWeekStartingMonday | LastWeekStartingMonday | LastFourWeeksStartingMonday) or since+until (YYYY-MM-DD) — default Last30Days. An unrecognised timePeriod is REFUSED, never silently swapped for another window. Microsoft generates reports ASYNCHRONOUSLY: this can return pending:true with a reportRequestId, and you must call again rather than reporting any numbers. A report that succeeds with ZERO rows genuinely means there was no delivery in that window — say exactly that; never present zeros as measured performance. Read-only, free.',
|
|
1862
1862
|
inputSchema: {
|
|
1863
1863
|
accountId: z.string().optional().describe('Microsoft ad account id — omit to use the brand’s single shared account'),
|
|
1864
|
-
timePeriod: z.string().optional().describe('predefined window, default
|
|
1864
|
+
timePeriod: z.string().optional().describe('predefined Microsoft window, default Last30Days — must be one of the values in the description; anything else is rejected'),
|
|
1865
1865
|
since: z.string().optional().describe('YYYY-MM-DD custom range start (with until)'),
|
|
1866
1866
|
until: z.string().optional().describe('YYYY-MM-DD custom range end'),
|
|
1867
1867
|
columns: z.array(z.string()).optional().describe('report columns — defaults to campaign performance'),
|
|
1868
|
-
reportRequestId: z.string().optional().describe('pick up a report that came back pending'),
|
|
1868
|
+
reportRequestId: z.string().optional().describe('pick up a report that came back pending — pass it back and this RESUMES that exact report instead of submitting a new one'),
|
|
1869
1869
|
},
|
|
1870
1870
|
outputSchema: { accountId: z.string().optional(), count: z.number().optional(), rows: z.array(z.any()).optional(), pending: z.boolean().optional(), reportRequestId: z.string().optional(), note: z.string().optional() },
|
|
1871
1871
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
@@ -2275,6 +2275,318 @@ export function registerTools(server) {
|
|
|
2275
2275
|
return ok(d.note || `${d.level || 'campaign'} → ${d.verifiedStatus || a.status}.`, d);
|
|
2276
2276
|
}));
|
|
2277
2277
|
|
|
2278
|
+
|
|
2279
|
+
// ══ REDDIT ADS (2026-07-31) ════════════════════════════════════════════════════════════════════════════════
|
|
2280
|
+
// Reddit's object graph is campaign → ad group → ad, and the AD HAS NO CREATIVE OF ITS OWN: it points at a
|
|
2281
|
+
// POST, which is published as a PROFILE. So the build order is profile → post → campaign → ad group → ad, and
|
|
2282
|
+
// an agent that stops at "campaign created" has produced something that can never serve an impression.
|
|
2283
|
+
// Every id is authorised against the accounts ticked under Manage accounts; nothing here can reach an ad
|
|
2284
|
+
// account the brand was not given, and everything is created PAUSED with no override.
|
|
2285
|
+
server.registerTool('list_reddit_ads_campaigns', {
|
|
2286
|
+
title: 'List Reddit ad accounts / campaigns',
|
|
2287
|
+
description: 'Read the brand’s connected Reddit AD account(s). Call with NO adAccountId to list the ad accounts shared with this brand — do this first to pick a target. Call WITH adAccountId to read that account’s whole tree at once: campaigns, ad groups and ads, each with its configured status and Reddit’s own effective status (the effective one is what says whether it could actually serve — PENDING_APPROVAL, CAMPAIGN_PAUSED, REJECTED and so on). Read-only, free. Needs Reddit Ads connected (Settings ▸ Connectors ▸ Reddit Ads) and the ad account ticked under Manage accounts.',
|
|
2288
|
+
inputSchema: { adAccountId: z.string().optional().describe('Reddit ad account id (a2_…) — omit to list the ad accounts shared with this brand') },
|
|
2289
|
+
outputSchema: { accounts: z.array(z.any()).optional(), adAccountId: z.string().optional(), name: z.string().optional(), campaigns: z.array(z.any()).optional(), adGroups: z.array(z.any()).optional(), ads: z.array(z.any()).optional() },
|
|
2290
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2291
|
+
}, wrap(async (a) => {
|
|
2292
|
+
if (!a.adAccountId) {
|
|
2293
|
+
const d = await apiGet('/api/reddit-ads/shared-accounts', {});
|
|
2294
|
+
if (!(d.accounts || []).length) return ok('No Reddit ad account is shared with this brand yet. Ask the user to tick the ones it may use in Settings ▸ Connectors ▸ Reddit Ads ▸ Manage accounts (or call set_connector_accounts), then retry.', d);
|
|
2295
|
+
const lines = (d.accounts || []).map(c => `• ${c.name} (${c.id})${c.currency ? ` — ${c.currency}` : ''}`);
|
|
2296
|
+
return ok(`${(d.accounts || []).length} Reddit ad account(s) shared with this brand:\n${lines.join('\n')}\nPass an adAccountId to read its campaigns, ad groups and ads.`, d);
|
|
2297
|
+
}
|
|
2298
|
+
const d = await apiGet('/api/reddit-ads/campaigns', { adAccountId: a.adAccountId });
|
|
2299
|
+
const c = (d.campaigns || []).map(x => `• campaign ${x.name} (${x.id}) — ${x.configured_status}/${x.effective_status}, ${x.objective}`);
|
|
2300
|
+
const g = (d.adGroups || []).map(x => `• ad group ${x.name} (${x.id}) — ${x.configured_status}/${x.effective_status}`);
|
|
2301
|
+
const s = (d.ads || []).map(x => `• ad ${x.name} (${x.id}) — ${x.configured_status}/${x.effective_status}`);
|
|
2302
|
+
return ok(`Reddit ad account ${d.adAccountId}${d.name ? ` (${d.name})` : ''}: ${(d.campaigns || []).length} campaign(s), ${(d.adGroups || []).length} ad group(s), ${(d.ads || []).length} ad(s).\n${[...c, ...g, ...s].join('\n') || '(empty)'}`, d);
|
|
2303
|
+
}));
|
|
2304
|
+
server.registerTool('reddit_ads_report', {
|
|
2305
|
+
title: 'Reddit ads performance report',
|
|
2306
|
+
description: 'Performance for a Reddit ad account — impressions, clicks, spend, CTR, CPC, eCPM, reach and any of Reddit’s ~450 metric fields, optionally broken down by campaign, ad group, ad, date, hour, community, country, gender, interest, keyword, placement and more. Money comes back in WHOLE UNITS of the ad account’s currency (Reddit reports micro-currency; the conversion is done for you) — and that currency is not necessarily dollars, so check the account. Reddit only accepts HOURLY window boundaries; plain YYYY-MM-DD dates are accepted here and snapped for you. A report with ZERO rows genuinely means nothing delivered in that window — say exactly that and never present zeros as measured performance. Read-only, free.',
|
|
2307
|
+
inputSchema: {
|
|
2308
|
+
adAccountId: z.string().optional(),
|
|
2309
|
+
since: z.string().optional().describe('YYYY-MM-DD or full ISO timestamp, default 30 days ago'),
|
|
2310
|
+
until: z.string().optional().describe('YYYY-MM-DD or full ISO timestamp, default today'),
|
|
2311
|
+
fields: z.array(z.string()).optional().describe('Reddit metric names, UPPER_SNAKE (IMPRESSIONS, CLICKS, SPEND, CTR, CPC, ECPM, REACH, FREQUENCY, CONVERSION_ROAS, VIDEO_WATCHED_100_PERCENT, CONVERSION_PURCHASE_TOTAL_VALUE…). Omit for a sensible default set; if you name one Reddit does not know, its error lists every valid value.'),
|
|
2312
|
+
breakdowns: z.array(z.enum(['AD_ACCOUNT_ID', 'AD_GROUP_ID', 'AD_ID', 'CAMPAIGN_ID', 'COUNTRY', 'DATE', 'HOUR', 'DMA', 'METRO', 'CAROUSEL_CARD', 'GALLERY_ITEM_ID', 'GENDER', 'INTEREST', 'KEYWORD', 'PLACEMENT', 'OS_TYPE', 'ASSET_ID', 'REGION', 'COMMUNITY', 'LANGUAGE'])).optional().describe('up to 3 (4 if both COUNTRY and REGION). Omit for one aggregate row.'),
|
|
2313
|
+
filter: z.string().optional().describe('Reddit filter expression to restrict rows to particular ids/values'),
|
|
2314
|
+
timeZoneId: z.string().optional().describe('IANA zone, e.g. America/New_York'),
|
|
2315
|
+
},
|
|
2316
|
+
outputSchema: { ok: z.boolean().optional(), adAccountId: z.string().optional(), since: z.string().optional(), until: z.string().optional(), count: z.number().optional(), rows: z.array(z.any()).optional(), note: z.string().optional() },
|
|
2317
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2318
|
+
}, wrap(async (a) => {
|
|
2319
|
+
const d = await apiPost('/api/reddit-ads/report', a);
|
|
2320
|
+
return ok(`${d.note}\n${JSON.stringify((d.rows || []).slice(0, 40))}`, d);
|
|
2321
|
+
}));
|
|
2322
|
+
server.registerTool('list_reddit_ads_profiles', {
|
|
2323
|
+
title: 'List the Reddit profiles an ad account can publish as',
|
|
2324
|
+
description: 'List the Reddit PROFILES attached to an ad account. A Reddit ad promotes a POST, and every post is published AS one of these profiles — so this is the first call in any Reddit creative build, and its id is what create_reddit_ads_post needs. If it comes back empty, the ad account has no profile attached and nothing can be advertised from it yet. Read-only, free.',
|
|
2325
|
+
inputSchema: { adAccountId: z.string().optional() },
|
|
2326
|
+
outputSchema: { adAccountId: z.string().optional(), count: z.number().optional(), profiles: z.array(z.any()).optional(), note: z.string().optional() },
|
|
2327
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2328
|
+
}, wrap(async (a) => {
|
|
2329
|
+
const d = await apiGet('/api/reddit-ads/profiles', a);
|
|
2330
|
+
return ok(`${d.note}\n${(d.profiles || []).map(p => `• ${p.name} (${p.id})`).join('\n')}`, d);
|
|
2331
|
+
}));
|
|
2332
|
+
server.registerTool('search_reddit_ads_targeting', {
|
|
2333
|
+
title: 'Resolve Reddit communities / geolocations / interests for targeting',
|
|
2334
|
+
description: 'Look up the exact values Reddit ad-group targeting expects, so none of them has to be guessed. kind:"communities" searches subreddits by keyword and returns each one’s NAME plus its subscriber count — targeting wants the bare name ("running"), NOT the t5_ id and NOT "r/running". kind:"geolocations" lists targetable places (pass country like US, or a city search) — targeting accepts a country code or one of the returned ids. kind:"interests" lists Reddit’s interest taxonomy — targeting wants the id ("pets_v3"). Read-only, free. Use this before create_reddit_ads_ad_group rather than inventing a community name.',
|
|
2335
|
+
inputSchema: {
|
|
2336
|
+
adAccountId: z.string().optional(),
|
|
2337
|
+
kind: z.enum(['communities', 'geolocations', 'interests']).optional().describe('default communities'),
|
|
2338
|
+
query: z.string().optional().describe('keyword — required for communities, filters interests, searches cities for geolocations'),
|
|
2339
|
+
country: z.string().optional().describe('2-letter country code, geolocations only'),
|
|
2340
|
+
postalCode: z.string().optional(),
|
|
2341
|
+
limit: z.number().optional().describe('max results, default 15'),
|
|
2342
|
+
},
|
|
2343
|
+
outputSchema: { kind: z.string().optional(), count: z.number().optional(), results: z.array(z.any()).optional(), note: z.string().optional() },
|
|
2344
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2345
|
+
}, wrap(async (a) => {
|
|
2346
|
+
const d = await apiGet('/api/reddit-ads/targeting', a);
|
|
2347
|
+
return ok(`${d.note}\n${JSON.stringify((d.results || []).slice(0, 30))}`, d);
|
|
2348
|
+
}));
|
|
2349
|
+
server.registerTool('reddit_ads_forecast', {
|
|
2350
|
+
title: 'Forecast Reddit audience size and delivery',
|
|
2351
|
+
description: 'Ask Reddit how big a targeting set is and roughly what a budget would buy — total reachable audience, the targeted slice, and estimated impressions, clicks and reach. Free, creates nothing, spends nothing, so run it BEFORE building an ad group to sanity-check targeting that may be far too narrow or far too broad. Budget is an ordinary amount in the ad account’s currency. These are Reddit’s estimates, not a guarantee — say so when reporting them.',
|
|
2352
|
+
inputSchema: {
|
|
2353
|
+
adAccountId: z.string().optional(),
|
|
2354
|
+
budget: z.number().describe('budget in the ad account’s currency (not micro-currency — the conversion is handled)'),
|
|
2355
|
+
objective: z.enum(['APP_INSTALLS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('default CLICKS'),
|
|
2356
|
+
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
2357
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional(),
|
|
2358
|
+
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
|
|
2359
|
+
bidAmount: z.number().optional(),
|
|
2360
|
+
startTime: z.string().optional().describe('ISO 8601'),
|
|
2361
|
+
endTime: z.string().optional(),
|
|
2362
|
+
targeting: z.any().optional().describe('same shape as create_reddit_ads_ad_group targeting'),
|
|
2363
|
+
},
|
|
2364
|
+
outputSchema: { totalAudienceSize: z.number().optional(), targetAudienceRange: z.any().optional(), deliveryEstimates: z.any().optional(), note: z.string().optional() },
|
|
2365
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2366
|
+
}, wrap(async (a) => {
|
|
2367
|
+
const d = await apiPost('/api/reddit-ads/forecast', a);
|
|
2368
|
+
return ok(d.note, d);
|
|
2369
|
+
}));
|
|
2370
|
+
server.registerTool('reddit_ads_bid_suggestion', {
|
|
2371
|
+
title: 'Ask Reddit what to bid',
|
|
2372
|
+
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.',
|
|
2373
|
+
inputSchema: {
|
|
2374
|
+
adAccountId: z.string().optional(),
|
|
2375
|
+
budget: z.number().describe('budget in the ad account’s currency'),
|
|
2376
|
+
objective: z.enum(['APP_INSTALLS', 'CATALOG_SALES', 'CLICKS', 'CONVERSIONS', 'IMPRESSIONS', 'LEAD_GENERATION', 'VIDEO_VIEWABLE_IMPRESSIONS']).optional().describe('default CLICKS'),
|
|
2377
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional().describe('default CPC — must fit the campaign objective'),
|
|
2378
|
+
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
|
|
2379
|
+
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
2380
|
+
startTime: z.string().optional(),
|
|
2381
|
+
endTime: z.string().optional(),
|
|
2382
|
+
currency: z.string().optional(),
|
|
2383
|
+
targeting: z.any().optional(),
|
|
2384
|
+
},
|
|
2385
|
+
outputSchema: { minBid: z.number().optional(), suggestedBid: z.number().optional(), suggestedRange: z.any().optional(), note: z.string().optional() },
|
|
2386
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2387
|
+
}, wrap(async (a) => {
|
|
2388
|
+
const d = await apiPost('/api/reddit-ads/bid-suggestion', a);
|
|
2389
|
+
return ok(d.note, d);
|
|
2390
|
+
}));
|
|
2391
|
+
server.registerTool('list_reddit_ads_posts', {
|
|
2392
|
+
title: 'List a Reddit profile’s ad posts',
|
|
2393
|
+
description: 'List the POSTS on a Reddit profile — these are the creatives Reddit ads promote. Use it to find an existing post to advertise rather than creating a near-duplicate. Call list_reddit_ads_profiles first for redditProfileId. Read-only, free.',
|
|
2394
|
+
inputSchema: {
|
|
2395
|
+
adAccountId: z.string().optional(),
|
|
2396
|
+
redditProfileId: z.string().describe('the Reddit profile id (t2_…) from list_reddit_ads_profiles'),
|
|
2397
|
+
type: z.enum(['TEXT', 'IMAGE', 'VIDEO', 'CAROUSEL']).optional(),
|
|
2398
|
+
limit: z.number().optional(),
|
|
2399
|
+
},
|
|
2400
|
+
outputSchema: { count: z.number().optional(), posts: z.array(z.any()).optional(), note: z.string().optional() },
|
|
2401
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2402
|
+
}, wrap(async (a) => {
|
|
2403
|
+
const d = await apiGet('/api/reddit-ads/posts', a);
|
|
2404
|
+
return ok(`${d.note}\n${(d.posts || []).map(p => `• ${p.type} "${p.headline}" (${p.id})`).join('\n')}`, d);
|
|
2405
|
+
}));
|
|
2406
|
+
server.registerTool('create_reddit_ads_post', {
|
|
2407
|
+
title: 'Create the Reddit post an ad will promote',
|
|
2408
|
+
description: 'Create the CREATIVE for a Reddit ad. This is the step people skip: a Reddit ad has no creative of its own — it points at a post — so a campaign and ad group with no post behind them can never serve. Types are TEXT (headline + body), IMAGE, VIDEO and CAROUSEL (up to 6 images). For image/video/carousel pass media[] with a PUBLIC mediaUrl; Reddit fetches and validates it itself (minimum 140×140), and a video also needs a thumbnailUrl. The destination for a click rides on the media entry’s destinationUrl, NOT on the ad. Reddit’s call-to-action values are human-readable strings with spaces and capitals — "Learn More", "Shop Now", "Sign Up" — not SCREAMING_SNAKE; the error lists all of them. The post is published on the profile immediately, so show the user the exact headline and body first.',
|
|
2409
|
+
inputSchema: {
|
|
2410
|
+
adAccountId: z.string().optional(),
|
|
2411
|
+
redditProfileId: z.string().describe('the Reddit profile id (t2_…) to publish as — from list_reddit_ads_profiles'),
|
|
2412
|
+
type: z.enum(['TEXT', 'IMAGE', 'VIDEO', 'CAROUSEL']).optional().describe('default TEXT'),
|
|
2413
|
+
headline: z.string().describe('the post title — this is the ad’s headline'),
|
|
2414
|
+
body: z.string().optional().describe('body copy, TEXT posts'),
|
|
2415
|
+
media: z.array(z.object({
|
|
2416
|
+
mediaUrl: z.string().optional().describe('PUBLIC url of the image/video — Reddit fetches it, minimum 140×140'),
|
|
2417
|
+
destinationUrl: z.string().optional().describe('where a click goes — required for image and carousel posts'),
|
|
2418
|
+
displayUrl: z.string().optional().describe('shown instead of the destination; must be the same domain'),
|
|
2419
|
+
caption: z.string().optional().describe('carousel card caption'),
|
|
2420
|
+
callToAction: z.string().optional().describe('e.g. "Learn More", "Shop Now" — exact human-readable strings'),
|
|
2421
|
+
})).optional().describe('one entry for IMAGE/VIDEO, up to 6 for CAROUSEL'),
|
|
2422
|
+
callToAction: z.string().optional().describe('applies to every media entry that has none'),
|
|
2423
|
+
thumbnailUrl: z.string().optional().describe('required for VIDEO posts'),
|
|
2424
|
+
allowComments: z.boolean().optional().describe('Reddit ads can carry a public comment thread — decide deliberately'),
|
|
2425
|
+
},
|
|
2426
|
+
outputSchema: { id: z.string().optional(), type: z.string().optional(), headline: z.string().optional(), postUrl: z.string().nullable().optional(), note: z.string().optional() },
|
|
2427
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
2428
|
+
}, wrap(async (a) => {
|
|
2429
|
+
const d = await apiPost('/api/reddit-ads/posts', a);
|
|
2430
|
+
return ok(`${d.note} Pass postId:"${d.id}" to create_reddit_ads_ad.`, d);
|
|
2431
|
+
}));
|
|
2432
|
+
server.registerTool('update_reddit_ads_post', {
|
|
2433
|
+
title: 'Edit a Reddit ad post',
|
|
2434
|
+
description: 'Edit an existing Reddit ad post’s headline, body or comment setting. The post is already public, so an edit is publicly visible — show the user the exact new text first.',
|
|
2435
|
+
inputSchema: {
|
|
2436
|
+
adAccountId: z.string().optional(),
|
|
2437
|
+
postId: z.string().describe('the post id (t3_…)'),
|
|
2438
|
+
headline: z.string().optional(),
|
|
2439
|
+
body: z.string().optional(),
|
|
2440
|
+
allowComments: z.boolean().optional(),
|
|
2441
|
+
},
|
|
2442
|
+
outputSchema: { id: z.string().optional(), headline: z.string().optional(), note: z.string().optional() },
|
|
2443
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
2444
|
+
}, wrap(async (a) => {
|
|
2445
|
+
const d = await apiPost('/api/reddit-ads/posts/update', a);
|
|
2446
|
+
return ok(d.note, d);
|
|
2447
|
+
}));
|
|
2448
|
+
server.registerTool('create_reddit_ads_ad_group', {
|
|
2449
|
+
title: 'Create a Reddit ad group (targeting, budget, bidding, schedule)',
|
|
2450
|
+
description: 'Create an ad group under an existing Reddit campaign — this is the tier that holds the budget, the bid and ALL the targeting. ALWAYS created PAUSED; it spends nothing until set_reddit_ads_status(confirm:true). Reddit requires more here than most platforms and refuses the create without it: a bidType, a bidStrategy, a startTime, a budget with its goalType, a bidAmount whenever the bid type is a paid rate, and a conversion pixel (resolved automatically when the ad account has exactly one). THE BID TYPE MUST FIT THE CAMPAIGN’S OBJECTIVE — a CLICKS campaign takes CPC and refuses CPM; Reddit’s error says which. Money is ordinary amounts in the ad account’s currency (micro-currency is handled for you). Resolve community names and interest ids with search_reddit_ads_targeting first, and consider reddit_ads_forecast + reddit_ads_bid_suggestion before committing. Everything is READ BACK from Reddit before you are told it exists — print the returned note verbatim.',
|
|
2451
|
+
inputSchema: {
|
|
2452
|
+
adAccountId: z.string().optional(),
|
|
2453
|
+
campaignId: z.string().describe('the campaign this ad group belongs to'),
|
|
2454
|
+
name: z.string(),
|
|
2455
|
+
budget: z.number().describe('budget in the ad account’s currency, paired with goalType'),
|
|
2456
|
+
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional().describe('default DAILY_SPEND'),
|
|
2457
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).describe('must fit the campaign objective — CLICKS campaigns take CPC'),
|
|
2458
|
+
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).describe('MANUAL_BIDDING needs bidAmount'),
|
|
2459
|
+
bidAmount: z.number().optional().describe('bid in the ad account’s currency — required for paid bid types; ask reddit_ads_bid_suggestion'),
|
|
2460
|
+
startTime: z.string().describe('ISO 8601, e.g. 2026-08-15T00:00:00Z — Reddit rejects the create without one'),
|
|
2461
|
+
endTime: z.string().optional(),
|
|
2462
|
+
conversionPixelId: z.string().optional().describe('only needed when the ad account has more than one pixel'),
|
|
2463
|
+
optimizationGoal: z.string().optional().describe('cannot be changed later'),
|
|
2464
|
+
targeting: z.object({
|
|
2465
|
+
communities: z.array(z.string()).optional().describe('bare subreddit NAMES, e.g. ["running"] — not t5_ ids, not "r/running"'),
|
|
2466
|
+
excludedCommunities: z.array(z.string()).optional(),
|
|
2467
|
+
geolocations: z.array(z.string()).optional().describe('country codes like ["US"], or ids from search_reddit_ads_targeting'),
|
|
2468
|
+
excludedGeolocations: z.array(z.string()).optional(),
|
|
2469
|
+
interests: z.array(z.string()).optional().describe('interest ids like ["pets_v3"]'),
|
|
2470
|
+
excludedInterests: z.array(z.string()).optional(),
|
|
2471
|
+
keywords: z.array(z.string()).optional(),
|
|
2472
|
+
excludedKeywords: z.array(z.string()).optional(),
|
|
2473
|
+
customAudienceIds: z.array(z.string()).optional(),
|
|
2474
|
+
excludedCustomAudienceIds: z.array(z.string()).optional(),
|
|
2475
|
+
carriers: z.array(z.string()).optional(),
|
|
2476
|
+
languages: z.array(z.string()).optional().describe('e.g. ["EN"]'),
|
|
2477
|
+
platforms: z.array(z.string()).optional().describe('ALL / DESKTOP / MOBILE_NATIVE / MOBILE_WEB — at least one mobile type is expected'),
|
|
2478
|
+
locations: z.array(z.string()).optional().describe('FEED and/or COMMENTS_PAGE'),
|
|
2479
|
+
viewModes: z.array(z.string()).optional().describe('ALL / CARD / CLASSIC / COMPACT / IMMERSIVE'),
|
|
2480
|
+
devices: z.array(z.any()).optional(),
|
|
2481
|
+
gender: z.string().optional().describe('MALE or FEMALE — omit for all'),
|
|
2482
|
+
expandTargeting: z.boolean().optional().describe('let Reddit widen the audience automatically'),
|
|
2483
|
+
}).optional(),
|
|
2484
|
+
schedule: z.array(z.object({
|
|
2485
|
+
startDay: z.number().describe('0 = Sunday … 6 = Saturday'),
|
|
2486
|
+
endDay: z.number(),
|
|
2487
|
+
startHour: z.number().describe('0-23, in the ad group’s time zone'),
|
|
2488
|
+
endHour: z.number(),
|
|
2489
|
+
})).optional().describe('weekly dayparting windows — omit to run all week'),
|
|
2490
|
+
},
|
|
2491
|
+
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), effectiveStatus: z.string().optional(), budget: z.number().nullable().optional(), note: z.string().optional() },
|
|
2492
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
2493
|
+
}, wrap(async (a) => {
|
|
2494
|
+
const d = await apiPost('/api/reddit-ads/ad-groups', a);
|
|
2495
|
+
return ok(`${d.note} To make it spend, use set_reddit_ads_status(confirm:true) after the user approves.`, d);
|
|
2496
|
+
}));
|
|
2497
|
+
server.registerTool('update_reddit_ads_ad_group', {
|
|
2498
|
+
title: 'Edit a Reddit ad group',
|
|
2499
|
+
description: 'Change an existing Reddit ad group’s name, budget, goal type, bid, schedule dates or targeting. Budget and bid are ordinary amounts in the ad account’s currency. Targeting is REPLACED by what you pass, not merged — send the whole set you want. This does NOT activate or pause anything; use set_reddit_ads_status for that. The result is read back from Reddit.',
|
|
2500
|
+
inputSchema: {
|
|
2501
|
+
adAccountId: z.string().optional(),
|
|
2502
|
+
adGroupId: z.string(),
|
|
2503
|
+
name: z.string().optional(),
|
|
2504
|
+
budget: z.number().optional(),
|
|
2505
|
+
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
2506
|
+
bidAmount: z.number().optional(),
|
|
2507
|
+
bidType: z.enum(['CPC', 'CPM', 'CPV', 'CPV6']).optional(),
|
|
2508
|
+
bidStrategy: z.enum(['BIDLESS', 'MANUAL_BIDDING', 'MAXIMIZE_VOLUME', 'TARGET_CPX']).optional(),
|
|
2509
|
+
startTime: z.string().optional(),
|
|
2510
|
+
endTime: z.string().optional(),
|
|
2511
|
+
targeting: z.any().optional().describe('same shape as create_reddit_ads_ad_group — REPLACES the existing targeting'),
|
|
2512
|
+
schedule: z.array(z.any()).optional(),
|
|
2513
|
+
},
|
|
2514
|
+
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), budget: z.number().nullable().optional(), note: z.string().optional() },
|
|
2515
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
2516
|
+
}, wrap(async (a) => {
|
|
2517
|
+
const d = await apiPost('/api/reddit-ads/ad-groups/update', a);
|
|
2518
|
+
return ok(d.note, d);
|
|
2519
|
+
}));
|
|
2520
|
+
server.registerTool('create_reddit_ads_ad', {
|
|
2521
|
+
title: 'Create a Reddit ad',
|
|
2522
|
+
description: 'Create the ad itself — the object that binds a POST (the creative) to an AD GROUP (the targeting and budget). Create the post first with create_reddit_ads_post and pass its id as postId; without a post there is nothing to show. ALWAYS created PAUSED, and Reddit additionally has to APPROVE it, so a fresh ad reports effective status PENDING_APPROVAL — report that rather than calling it live. GOTCHA: a TEXT ("free form") post’s ad may not carry clickUrl at all — Reddit refuses it. The click destination for image and link ads lives on the POST’s media destinationUrl, not here.',
|
|
2523
|
+
inputSchema: {
|
|
2524
|
+
adAccountId: z.string().optional(),
|
|
2525
|
+
adGroupId: z.string().describe('the ad group whose targeting and budget this ad runs under'),
|
|
2526
|
+
name: z.string(),
|
|
2527
|
+
postId: z.string().describe('the post to promote (t3_…) from create_reddit_ads_post or list_reddit_ads_posts'),
|
|
2528
|
+
clickUrl: z.string().optional().describe('leave unset for TEXT-post ads — Reddit rejects "Free form ads cannot have a click url"'),
|
|
2529
|
+
redditProfileId: z.string().optional().describe('the post author profile — required for catalog sales campaigns'),
|
|
2530
|
+
eventTrackers: z.array(z.object({ type: z.enum(['CLICK', 'VIEW']), url: z.string() })).optional().describe('third-party measurement URLs; only Reddit-approved providers are accepted'),
|
|
2531
|
+
},
|
|
2532
|
+
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), effectiveStatus: z.string().optional(), postId: z.string().nullable().optional(), note: z.string().optional() },
|
|
2533
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
2534
|
+
}, wrap(async (a) => {
|
|
2535
|
+
const d = await apiPost('/api/reddit-ads/ads', a);
|
|
2536
|
+
return ok(`${d.note} To make it spend, use set_reddit_ads_status(confirm:true) after the user approves.`, d);
|
|
2537
|
+
}));
|
|
2538
|
+
server.registerTool('update_reddit_ads_ad', {
|
|
2539
|
+
title: 'Edit a Reddit ad',
|
|
2540
|
+
description: 'Rename a Reddit ad, point it at a different post, or change its click url. Does not activate or pause it — use set_reddit_ads_status. Swapping the post changes what people see, so confirm the new creative with the user first. The result is read back from Reddit.',
|
|
2541
|
+
inputSchema: {
|
|
2542
|
+
adAccountId: z.string().optional(),
|
|
2543
|
+
adId: z.string(),
|
|
2544
|
+
name: z.string().optional(),
|
|
2545
|
+
postId: z.string().optional().describe('promote a different post'),
|
|
2546
|
+
clickUrl: z.string().optional().describe('pass an empty string to clear it'),
|
|
2547
|
+
},
|
|
2548
|
+
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), note: z.string().optional() },
|
|
2549
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
2550
|
+
}, wrap(async (a) => {
|
|
2551
|
+
const d = await apiPost('/api/reddit-ads/ads/update', a);
|
|
2552
|
+
return ok(d.note, d);
|
|
2553
|
+
}));
|
|
2554
|
+
server.registerTool('update_reddit_ads_campaign', {
|
|
2555
|
+
title: 'Edit a Reddit campaign',
|
|
2556
|
+
description: 'Change an existing Reddit campaign’s name, spend cap, budget, goal type or flight dates. Amounts are ordinary numbers in the ad account’s currency. This does NOT activate, pause or archive anything — use set_reddit_ads_status for that. The result is read back from Reddit before you are told it took.',
|
|
2557
|
+
inputSchema: {
|
|
2558
|
+
adAccountId: z.string().optional(),
|
|
2559
|
+
campaignId: z.string(),
|
|
2560
|
+
name: z.string().optional(),
|
|
2561
|
+
spendCap: z.number().optional().describe('lifetime spend ceiling in the ad account’s currency'),
|
|
2562
|
+
budget: z.number().optional(),
|
|
2563
|
+
goalType: z.enum(['DAILY_SPEND', 'LIFETIME_SPEND']).optional(),
|
|
2564
|
+
startTime: z.string().optional(),
|
|
2565
|
+
endTime: z.string().optional(),
|
|
2566
|
+
},
|
|
2567
|
+
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), note: z.string().optional() },
|
|
2568
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: true },
|
|
2569
|
+
}, wrap(async (a) => {
|
|
2570
|
+
const d = await apiPost('/api/reddit-ads/campaign/update', a);
|
|
2571
|
+
return ok(d.note, d);
|
|
2572
|
+
}));
|
|
2573
|
+
server.registerTool('set_reddit_ads_status', {
|
|
2574
|
+
title: 'Activate, pause, archive or delete a Reddit campaign / ad group / ad',
|
|
2575
|
+
description: 'The one switch that arms real money on Reddit, and the only way to retire anything. Pass kind ("campaign", "ad_group" or "ad") plus the object id. ACTIVE starts real spend as soon as Reddit approves — show the user exactly what will run and get an explicit yes, then call again with confirm:true. PAUSED is always safe and never gated. REDDIT HAS NO DELETE OPERATION: removal is a status. ARCHIVED retires an object and works immediately; DELETED is permanent AND time-gated — Reddit refuses to delete anything modified in the last 3 hours, so prefer ARCHIVED and only reach for DELETED when the user truly wants it gone. Both ARCHIVED and DELETED are confirm-gated. Remember Reddit’s three tiers all have to be ACTIVE for a single impression to serve: activating the campaign alone does nothing if its ad group and ad are still paused. The resulting status is READ BACK from Reddit.',
|
|
2576
|
+
inputSchema: {
|
|
2577
|
+
adAccountId: z.string().optional(),
|
|
2578
|
+
kind: z.enum(['campaign', 'ad_group', 'ad']),
|
|
2579
|
+
id: z.string(),
|
|
2580
|
+
status: z.enum(['ACTIVE', 'PAUSED', 'ARCHIVED', 'DELETED']).describe('ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire; DELETED = permanent, and blocked for 3h after any change'),
|
|
2581
|
+
confirm: z.boolean().optional().describe('REQUIRED true for ACTIVE (real spend), ARCHIVED and DELETED'),
|
|
2582
|
+
},
|
|
2583
|
+
outputSchema: { id: z.string().optional(), kind: z.string().optional(), name: z.string().optional(), status: z.string().optional(), effectiveStatus: z.string().optional(), note: z.string().optional() },
|
|
2584
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
2585
|
+
}, wrap(async (a) => {
|
|
2586
|
+
const d = await apiPost('/api/reddit-ads/status', a);
|
|
2587
|
+
return ok(d.note, d);
|
|
2588
|
+
}));
|
|
2589
|
+
|
|
2278
2590
|
// ══ LINKEDIN COMPANY PAGES + ADS (2026-07-30) ══════════════════════════════════════════════════════════════
|
|
2279
2591
|
server.registerTool('list_linkedin_pages', {
|
|
2280
2592
|
title: 'List the LinkedIn company Pages this account administers',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "Generate finished VIDEO ADS, image ads and UGC avatar ads for any brand with AI
|
|
5
|
+
"description": "Generate finished VIDEO ADS, image ads and UGC avatar ads for any brand with AI — spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus TikTok/Instagram/YouTube organic — then publish to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest and build & manage the ad campaigns behind them on Meta, Google Ads, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads. MCP server (205 tools), 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"
|
|
@@ -24,12 +24,15 @@
|
|
|
24
24
|
"claude",
|
|
25
25
|
"cli",
|
|
26
26
|
"competitor-analysis",
|
|
27
|
+
"google-ads",
|
|
27
28
|
"image-generation",
|
|
28
29
|
"marketing",
|
|
29
30
|
"mcp",
|
|
30
31
|
"mcp-server",
|
|
32
|
+
"meta-ads",
|
|
31
33
|
"model-context-protocol",
|
|
32
34
|
"seedance",
|
|
35
|
+
"social-media-publishing",
|
|
33
36
|
"ugc",
|
|
34
37
|
"veo",
|
|
35
38
|
"video-ads",
|