hermoso 0.1.195 → 0.1.197
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 +4 -4
- package/mcp/tools.mjs +139 -8
- 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
|
+
**768 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
|
|
9
9
|
catalog with exact per-render credit costs plus the full capability map.
|
|
10
10
|
|
|
11
11
|
**What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
|
|
@@ -116,7 +116,7 @@ the routes.
|
|
|
116
116
|
|
|
117
117
|
## Instant: the hosted Claude.ai connector
|
|
118
118
|
|
|
119
|
-
Paste **`https://app.hermoso.ai/mcp`** into Claude → Settings → Connectors → *Add custom connector*, pick
|
|
119
|
+
Paste **`https://app.hermoso.ai/mcp?src=readme`** into Claude → Settings → Connectors → *Add custom connector*, pick
|
|
120
120
|
**Always required** when Claude asks about authentication (its detector suggests "None" because our discovery
|
|
121
121
|
handshake is open; "None" would leave every tool call unauthenticated), approve with your Hermoso account, done — the full toolset with your saved brand context, billed to your plan.
|
|
122
122
|
|
|
@@ -143,7 +143,7 @@ claude mcp add hermoso -e HERMOSO_TOKEN=hmk_… -- npx -y hermoso mcp
|
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
The hosted URL works in Claude Code too, but it is the worse path there and it is worth knowing why:
|
|
146
|
-
`claude mcp add --transport http hermoso https://app.hermoso.ai/mcp` is accepted, and then `claude mcp list`
|
|
146
|
+
`claude mcp add --transport http hermoso "https://app.hermoso.ai/mcp?src=readme"` is accepted, and then `claude mcp list`
|
|
147
147
|
reports `! Needs authentication` because the client will not start the OAuth flow by itself — you have to open a
|
|
148
148
|
session, run `/mcp`, find the server and press Authenticate. Measured against Claude Code 2.1.241 on 2026-08-23.
|
|
149
149
|
|
|
@@ -171,7 +171,7 @@ block entirely if you signed in above; it is there for CI, where the process can
|
|
|
171
171
|
|
|
172
172
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
173
173
|
|
|
174
|
-
### What the
|
|
174
|
+
### What the 768 tools cover
|
|
175
175
|
|
|
176
176
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
177
177
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/tools.mjs
CHANGED
|
@@ -2135,8 +2135,8 @@ const makeEnableToolsHandler = (ctx) => async ({ groups }) => {
|
|
|
2135
2135
|
const held = heldBack
|
|
2136
2136
|
? ` ${heldBack} more tool${heldBack === 1 ? ' is' : 's are'} built and ready but not listed because their account is not connected in this workspace yet — Hermoso supports them all; connect the account under Workspace ▸ Connectors (https://app.hermoso.ai/?connect=<provider>, or list_connectors to see what is linked) and they appear.`
|
|
2137
2137
|
: '';
|
|
2138
|
-
const route = '
|
|
2139
|
-
+ ' tool with no roster at all.';
|
|
2138
|
+
const route = 'call find_tools to locate the tool and call_tool to run it by name — that needs no reload and works on every host; or reconnect'
|
|
2139
|
+
+ ' with `?tools=all` on the server URL, or run the `hermoso` CLI, which reaches every tool with no roster at all.';
|
|
2140
2140
|
// A CACHED CLIENT AND AN UNCONNECTED ACCOUNT ARE DIFFERENT DIAGNOSES, AND ONLY ONE OF THEM IS EVER TRUE HERE
|
|
2141
2141
|
// (2026-08-26). The two branches below tell an agent that if the tools do not appear, its client cached the
|
|
2142
2142
|
// roster — correct when we really did enable something. When nothing was enabled BECAUSE nothing is connected,
|
|
@@ -2148,7 +2148,7 @@ const makeEnableToolsHandler = (ctx) => async ({ groups }) => {
|
|
|
2148
2148
|
: (n === 0 && heldBack
|
|
2149
2149
|
? `Switched on ${added.join(', ')} server-side, but nothing new is listed:${held} Active groups: ${enabled.join(', ')}.`
|
|
2150
2150
|
: fixedRoster
|
|
2151
|
-
? `Switched on ${added.join(', ')} server-side — but THIS host fixed its tool list when the connection was made and will not pick up the ${n} new tool${n === 1 ? '' : 's'} until it reconnects,
|
|
2151
|
+
? `Switched on ${added.join(', ')} server-side — but THIS host fixed its tool list when the connection was made and will not pick up the ${n} new tool${n === 1 ? '' : 's'} until it reconnects. USE THEM NOW ANYWAY: find_tools({query}) finds the tool and call_tool({name, args}) runs it through this same connection — no reconnect needed. Do not expect to see them in this conversation's list. To use them, ${route}${held} Active groups: ${enabled.join(', ')}.`
|
|
2152
2152
|
: `Switched on ${added.join(', ')} — ${n} more tool${n === 1 ? '' : 's'} are callable now. If they do not appear your client has cached its tool list, in which case ${route}${held} Active groups: ${enabled.join(', ')}.`);
|
|
2153
2153
|
// The agent took the route our own instructions name, on a host where it provably cannot show anything. That is
|
|
2154
2154
|
// our guidance failing, not the agent, so it is recorded on our side of the board.
|
|
@@ -9420,7 +9420,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
9420
9420
|
}));
|
|
9421
9421
|
server.registerTool('openai_ads_report', {
|
|
9422
9422
|
title: 'ChatGPT Ads performance report',
|
|
9423
|
-
description: 'Performance for ChatGPT Ads — impressions, clicks, spend, CTR, CPC, CPM. The scope follows the id you pass: none = the whole ad account, or campaignId / adGroupId / adId. granularity is hourly, daily, monthly or none (default daily); the default window is the last 30 days; segment by country or device for a breakdown, and level rolls the rows up by campaign / ad group / ad. A report with NO rows genuinely means there was NO delivery in that window — say exactly that; never present zeros as measured performance. Read-only and free, so run it FIRST after connecting: it proves the key works with zero spend risk.',
|
|
9423
|
+
description: 'Performance for ChatGPT Ads — impressions, clicks, spend, CTR, CPC, CPM. The scope follows the id you pass: none = the whole ad account, or campaignId / adGroupId / adId. PRODUCT-FEED CAMPAIGNS serving in the multi-product CAROUSEL unit also report per-card numbers: ask for them in `fields` — carousel_product_card_impressions, carousel_product_card_clicks, product_impressions, product_clicks, product_spend, product_ctr, product_cpc, product_cpm plus product_title / product_price / product_feed_id and the other product_* fields (complete from 2026-08-20 on a rolling 30-day basis). A card impression counts when a product card becomes viewable and is NOT a billable impression, so never add it to spend math. granularity is hourly, daily, monthly or none (default daily); the default window is the last 30 days; segment by country or device for a breakdown, and level rolls the rows up by campaign / ad group / ad. A report with NO rows genuinely means there was NO delivery in that window — say exactly that; never present zeros as measured performance. Read-only and free, so run it FIRST after connecting: it proves the key works with zero spend risk.',
|
|
9424
9424
|
inputSchema: {
|
|
9425
9425
|
campaignId: z.string().optional(), adGroupId: z.string().optional(), adId: z.string().optional(),
|
|
9426
9426
|
since: z.string().optional().describe('YYYY-MM-DD'), until: z.string().optional().describe('YYYY-MM-DD'),
|
|
@@ -10516,7 +10516,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10516
10516
|
}));
|
|
10517
10517
|
server.registerTool('create_openai_ads_pixel', {
|
|
10518
10518
|
title: 'Create a ChatGPT Ads pixel',
|
|
10519
|
-
description: 'Create a ChatGPT Ads web pixel — the thing that observes actions on the site. IT RECORDS NOTHING until its snippet is installed on the site, so say that rather than implying tracking is live. A pixel is a measurement definition and cannot spend. The next step is create_openai_ads_conversion_event, which says WHICH observed action counts as a conversion. AUTOMATIC ADVANCED MATCHING IS ON BY DEFAULT — that is OpenAI’s own default for pixels created through the Ads API since 2026-08-17, and it means the Pixel COLLECTS AND HASHES CUSTOMER INFORMATION (email, phone) from the page. Tell the user that in plain words, and pass automaticAdvancedMatching:false if they want it off. The reply reports what OpenAI actually stored: there is no endpoint to read a pixel back afterwards, so that one reply is the only record you will ever get.',
|
|
10519
|
+
description: 'Create a ChatGPT Ads web pixel — the thing that observes actions on the site. IT RECORDS NOTHING until its snippet is installed on the site, so say that rather than implying tracking is live. MATCH RATE: the snippet’s oaiq("init", { user: {…} }) call accepts email_sha256, phone_number_sha256, external_id_sha256, first_name_sha256, last_name_sha256 (all SHA-256, lowercase hex) plus raw country, city, region and postal_code — tell the site owner to pass whatever they know about a logged-in visitor, because every field improves conversion matching. A pixel is a measurement definition and cannot spend. The next step is create_openai_ads_conversion_event, which says WHICH observed action counts as a conversion. AUTOMATIC ADVANCED MATCHING IS ON BY DEFAULT — that is OpenAI’s own default for pixels created through the Ads API since 2026-08-17, and it means the Pixel COLLECTS AND HASHES CUSTOMER INFORMATION (email, phone) from the page. Tell the user that in plain words, and pass automaticAdvancedMatching:false if they want it off. The reply reports what OpenAI actually stored: there is no endpoint to read a pixel back afterwards, so that one reply is the only record you will ever get.',
|
|
10520
10520
|
inputSchema: {
|
|
10521
10521
|
name: z.string().describe('a name for the pixel'),
|
|
10522
10522
|
automaticAdvancedMatching: z.boolean().optional().describe('default true (OpenAI’s own default). false stops the Pixel collecting and hashing customer information from the page.'),
|
|
@@ -10567,7 +10567,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10567
10567
|
}, wrap(async (a) => { const d = await apiGet('/api/openai-ads/audience', a); return ok(d.note, d); }));
|
|
10568
10568
|
server.registerTool('update_openai_ads_audience_members', {
|
|
10569
10569
|
title: 'Add, remove or replace ChatGPT Ads audience members',
|
|
10570
|
-
description: 'Change who is IN a ChatGPT Ads custom audience. Pass plain emails and/or
|
|
10570
|
+
description: 'Change who is IN a ChatGPT Ads custom audience. Pass plain emails, phone numbers and/or Google Advertising IDs (GAID, UUID-shaped) in ONE list — identifier types may be mixed in a single request: Hermoso normalises and SHA-256 hashes emails and phones locally and only the digests are sent; a GAID rides raw, as the API defines it. SIZE RULES (OpenAI, read 2026-09-03): an audience of ANY size, even empty, can be used for EXCLUSION once ready; for inclusion or bid adjustments plan on 25,000 matched users; there is no 5M ceiling any more, and the reported size is a privacy band (under_25k, 25k_100k, 100k_500k, 500k_1m, 1m_5m, 5m_plus). THREE OPERATIONS: "add" puts people in, "remove" takes the named people OUT (the only way to stop advertising to a segment already in a list), "replace" swaps the WHOLE membership. THIS IS ASYNCHRONOUS — ChatGPT Ads returns an operation id and the change is NOT applied when this returns; poll it with get_openai_ads_audience_operation until it reports succeeded or failed. A replace REQUIRES expectedRevision (read membershipRevision from get_openai_ads_audience) so a wholesale swap cannot land on top of someone else’s change; a mismatch is refused by ChatGPT Ads and applies nothing.',
|
|
10571
10571
|
inputSchema: {
|
|
10572
10572
|
audienceId: z.string(),
|
|
10573
10573
|
operation: z.enum(['add', 'remove', 'replace']).describe('add | remove | replace — replace swaps the entire membership'),
|
|
@@ -10609,6 +10609,10 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10609
10609
|
description: z.string().optional(),
|
|
10610
10610
|
dailyBudget: z.number().optional().describe('daily cap in the AD ACCOUNT’S currency — ChatGPT Ads’ own minimum for a DAILY budget is 25.00'),
|
|
10611
10611
|
lifetimeBudget: z.number().optional().describe('lifetime cap in the account currency — no 25.00 floor applies here, so use this to spend less than that in total. Pass this and/or dailyBudget; a budget is required.'),
|
|
10612
|
+
businessAgentId: z.string().optional().describe('run a BUSINESS AGENT campaign (mode business_agent): the id from create_openai_ads_business_agent, which must be PUBLISHED'),
|
|
10613
|
+
objective: z.enum(['reach', 'clicks', 'conversions']).optional().describe('the campaign objective. objective "conversions" with billingEventType "impression" is the impression-billed, conversion-optimised shape'),
|
|
10614
|
+
billingEventType: z.enum(['impression', 'click']).optional().describe('what the campaign is billed on'),
|
|
10615
|
+
landingPageQueryTemplate: z.string().optional().describe('query-string template appended to landing URLs (e.g. utm_source=chatgpt&utm_campaign={campaign_id}) — ChatGPT Ads’ landing_page_configuration.query_string_template'),
|
|
10612
10616
|
biddingType: z.enum(['impressions', 'clicks', 'conversions']).optional().describe('default clicks (CPC). OpenAI suggests starting at a 3–5 max bid per click. "conversions" is oCPC — you still pay per click, but ChatGPT Ads optimises toward a conversion event, and it REQUIRES conversionEventSettingIds naming exactly one active event setting.'),
|
|
10613
10617
|
countries: z.array(z.string()).optional().describe('2-letter country codes'),
|
|
10614
10618
|
locationIds: z.array(z.string()).optional().describe('ids from openai_ads_geo_search — up to 2,500'),
|
|
@@ -10723,7 +10727,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10723
10727
|
}));
|
|
10724
10728
|
server.registerTool('set_openai_ads_budget', {
|
|
10725
10729
|
title: 'Set a ChatGPT Ads campaign budget',
|
|
10726
|
-
description: 'Change a ChatGPT Ads campaign’s budget — a daily cap, a lifetime cap, or both, in the ad account’s currency. ChatGPT Ads’ own minimum for a DAILY budget is 25.00 (measured live 2026-08-05; a LIFETIME budget has no such floor, so use one to spend less than that in total). Raising it on a LIVE (active) campaign increases real spend immediately, so you MUST show the user the old and new amounts, get an explicit yes, then call with confirm:true. Lowering it or changing a paused campaign is safe. The campaign is READ BACK after the change and the note is built from that.',
|
|
10730
|
+
description: 'Change a ChatGPT Ads campaign’s budget — a daily cap, a lifetime cap, or both, in the ad account’s currency. A LIFETIME (total) budget is PACED by ChatGPT Ads to spread spend evenly across the campaign’s dates (actual daily spend still varies with available delivery), so a total budget is not a first-come-first-served pool. ChatGPT Ads’ own minimum for a DAILY budget is 25.00 (measured live 2026-08-05; a LIFETIME budget has no such floor, so use one to spend less than that in total). Raising it on a LIVE (active) campaign increases real spend immediately, so you MUST show the user the old and new amounts, get an explicit yes, then call with confirm:true. Lowering it or changing a paused campaign is safe. The campaign is READ BACK after the change and the note is built from that.',
|
|
10727
10731
|
inputSchema: { campaignId: z.string(), dailyBudget: z.number().optional(), lifetimeBudget: z.number().optional(), confirm: z.boolean().optional().describe('REQUIRED true when the campaign is live') },
|
|
10728
10732
|
outputSchema: { ok: z.boolean().optional(), campaignId: z.string().optional(), campaign: z.any().optional(), note: z.string().optional() },
|
|
10729
10733
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
@@ -10755,7 +10759,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10755
10759
|
}, wrap(async (a) => { const d = await apiPost('/api/openai-ads/preview', a); return ok(d.note, d); }));
|
|
10756
10760
|
server.registerTool('send_openai_ads_conversions', {
|
|
10757
10761
|
title: 'Send server-side conversion events to ChatGPT Ads',
|
|
10758
|
-
description: 'Send conversion events to ChatGPT Ads from a SERVER (the Conversions API). This is the only way a conversion that did not happen in the browser — an offline sale, a webhook, a mobile backend, a CRM — is ever counted, and it is the half of the measurement loop that create_openai_ads_pixel and create_openai_ads_conversion_event exist to set up. TWO IDS LOOK ALIKE AND ONLY ONE WORKS: pass `pixelSnippetId`, which is OpenAI’s `pixel_id` and is what create_openai_ads_pixel returns under that name — NOT the `pixelId` (their `clidsrc_…` value), which is the conversion SOURCE id that an event setting takes as sourceIds. OpenAI’s own words: "Use `id` as a `source_ids` value when you create an event setting. Use `pixel_id` … when you send Conversions API events." THE KEY IS ALSO NOT THE ONE YOU THINK: `apiKey` is the CONVERSIONS API key from create_openai_ads_conversion_api_key, not the Advertiser API key this workspace is connected with — OpenAI return it exactly once so Hermoso holds no copy and it must be passed in. Each event needs an `id` and a `type`, plus `source_url` for a web event; the data SHAPE is fixed by the event type and Hermoso fills it in, and MONEY IS AN INTEGER IN THE CURRENCY’S MINOR UNIT (4250 means $42.50 — sending 42.50 is refused, not rounded). Timestamps must be inside the last 7 days and no more than 10 minutes ahead. ONE BAD EVENT FAILS THE WHOLE BATCH of up to 1,000, so Hermoso validates locally first and names the offending event and field instead of letting OpenAI discard all of them. Use validateOnly:true for a free dry run that VALIDATES AND SAVES NOTHING — never tell a user a validate-only run was measured. If the browser pixel and the server both send the same conversion, give them the SAME id so OpenAI deduplicates it. Attribution is not instant: read openai_ads_conversions later rather than promising a number now. Free — costs no credits.',
|
|
10762
|
+
description: 'Send conversion events to ChatGPT Ads from a SERVER (the Conversions API). USER MATCHING (schema of 2026-09-03): pass plaintext email / phone (with country code) / externalId / firstName / lastName and Hermoso hashes them locally, plus raw country / city / region / zip_code and gaid (Android advertising id); each becomes the API’s plural list (emails_sha256, phone_numbers_sha256, …), the first three unique values per list count, and more fields = better matching. This is the only way a conversion that did not happen in the browser — an offline sale, a webhook, a mobile backend, a CRM — is ever counted, and it is the half of the measurement loop that create_openai_ads_pixel and create_openai_ads_conversion_event exist to set up. TWO IDS LOOK ALIKE AND ONLY ONE WORKS: pass `pixelSnippetId`, which is OpenAI’s `pixel_id` and is what create_openai_ads_pixel returns under that name — NOT the `pixelId` (their `clidsrc_…` value), which is the conversion SOURCE id that an event setting takes as sourceIds. OpenAI’s own words: "Use `id` as a `source_ids` value when you create an event setting. Use `pixel_id` … when you send Conversions API events." THE KEY IS ALSO NOT THE ONE YOU THINK: `apiKey` is the CONVERSIONS API key from create_openai_ads_conversion_api_key, not the Advertiser API key this workspace is connected with — OpenAI return it exactly once so Hermoso holds no copy and it must be passed in. Each event needs an `id` and a `type`, plus `source_url` for a web event; the data SHAPE is fixed by the event type and Hermoso fills it in, and MONEY IS AN INTEGER IN THE CURRENCY’S MINOR UNIT (4250 means $42.50 — sending 42.50 is refused, not rounded). Timestamps must be inside the last 7 days and no more than 10 minutes ahead. ONE BAD EVENT FAILS THE WHOLE BATCH of up to 1,000, so Hermoso validates locally first and names the offending event and field instead of letting OpenAI discard all of them. Use validateOnly:true for a free dry run that VALIDATES AND SAVES NOTHING — never tell a user a validate-only run was measured. If the browser pixel and the server both send the same conversion, give them the SAME id so OpenAI deduplicates it. Attribution is not instant: read openai_ads_conversions later rather than promising a number now. Free — costs no credits.',
|
|
10759
10763
|
inputSchema: {
|
|
10760
10764
|
pixelSnippetId: z.string().describe('OpenAI’s pixel_id — create_openai_ads_pixel returns it as pixelSnippetId. NOT the clidsrc_… pixelId, which is the conversion source id.'),
|
|
10761
10765
|
apiKey: z.string().describe('the Conversions API key (create_openai_ads_conversion_api_key) — NOT the Advertiser API key the connector stores'),
|
|
@@ -10844,6 +10848,133 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
10844
10848
|
targetingSpec: z.record(z.any()).optional().describe('Pinterest targeting object, e.g. {"GEO":["US"],"AGE_BUCKET":["25-34","35-44"]} — at least one GEO or LOCATION is REQUIRED by Pinterest. Age: use AGE_BUCKET, or MINIMUM_AGE and MAXIMUM_AGE TOGETHER (18–65, with "65+" allowed as the maximum) — a minimum on its own is refused.'),
|
|
10845
10849
|
status: z.enum(['ACTIVE', 'PAUSED', 'DRAFT']).optional().describe('default PAUSED'),
|
|
10846
10850
|
};
|
|
10851
|
+
// ── ChatGPT ADS BREADTH (2026-09-03): lead forms, lead sync, business agents, spend windows, negative keywords,
|
|
10852
|
+
// product feeds — the 27 spec paths the audit found uncovered. Handlers live in lib/openai-ads-breadth.mjs.
|
|
10853
|
+
const oaiNote = (d) => ok(d.note, d);
|
|
10854
|
+
server.registerTool('list_openai_ads_lead_forms', {
|
|
10855
|
+
title: 'List ChatGPT Ads lead forms',
|
|
10856
|
+
description: 'The ChatGPT Ads LEAD FORMS on this ad account — id, status (draft/published/archived), draft and published revision ids, fields. A lead form is what a Business Agent uses to collect a name/email/choice inside the chat; it only collects once PUBLISHED, and leads only arrive somewhere once a lead-sync subscription exists (list_openai_ads_lead_sync). Read-only, 0 credits. Needs ChatGPT Ads connected.',
|
|
10857
|
+
inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10858
|
+
}, wrap(async () => oaiNote(await apiGet('/api/openai-ads/lead-forms'))));
|
|
10859
|
+
server.registerTool('create_openai_ads_lead_form', {
|
|
10860
|
+
title: 'Create a ChatGPT Ads lead form (draft)',
|
|
10861
|
+
description: 'Draft a ChatGPT Ads LEAD FORM: a name, an optional privacy policy URL, and fields — each {fieldId, fieldType "text" | "choice", label, required, options (choice only)}. Created as a DRAFT with a draftRevisionId; publish_openai_ads_lead_form makes it live, then reference it from a Business Agent (leadFormId). Needs ChatGPT Ads connected.',
|
|
10862
|
+
inputSchema: {
|
|
10863
|
+
name: z.string(), privacyPolicyUrl: z.string().optional(),
|
|
10864
|
+
fields: z.array(z.object({ fieldId: z.string().describe('your stable key for the field, e.g. "email"'), fieldType: z.enum(['text', 'choice']), label: z.string(), required: z.boolean().optional(), options: z.array(z.string()).optional().describe('choice fields only') })),
|
|
10865
|
+
}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10866
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form', a))));
|
|
10867
|
+
server.registerTool('get_openai_ads_lead_form', {
|
|
10868
|
+
title: 'Read a ChatGPT Ads lead form', description: 'One ChatGPT Ads lead form with its fields and revision ids (pass revisionId to read a specific revision). Read-only, 0 credits.',
|
|
10869
|
+
inputSchema: { leadFormId: z.string(), revisionId: z.string().optional() }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10870
|
+
}, wrap(async (a) => oaiNote(await apiGet('/api/openai-ads/lead-form', a))));
|
|
10871
|
+
server.registerTool('update_openai_ads_lead_form', {
|
|
10872
|
+
title: 'Save a new draft of a ChatGPT Ads lead form',
|
|
10873
|
+
description: 'Save a NEW DRAFT REVISION of a ChatGPT Ads lead form (the whole definition: name + fields are replaced). expectedDraftRevisionId must be the draftRevisionId you last read — ChatGPT Ads refuses a stale one so two editors cannot clobber each other. The published version is untouched until publish_openai_ads_lead_form.',
|
|
10874
|
+
inputSchema: { leadFormId: z.string(), expectedDraftRevisionId: z.string(), name: z.string(), privacyPolicyUrl: z.string().optional(), fields: z.array(z.object({ fieldId: z.string(), fieldType: z.enum(['text', 'choice']), label: z.string(), required: z.boolean().optional(), options: z.array(z.string()).optional() })) },
|
|
10875
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10876
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form/update', a))));
|
|
10877
|
+
server.registerTool('publish_openai_ads_lead_form', {
|
|
10878
|
+
title: 'Publish a ChatGPT Ads lead form', description: 'Publish the current draft of a ChatGPT Ads lead form so Business Agents can use it. expectedDraftRevisionId is the draftRevisionId you last read. Show the user the fields before publishing: the form is what real people will see.',
|
|
10879
|
+
inputSchema: { leadFormId: z.string(), expectedDraftRevisionId: z.string() }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10880
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form/publish', a))));
|
|
10881
|
+
server.registerTool('archive_openai_ads_lead_form', {
|
|
10882
|
+
title: 'Archive a ChatGPT Ads lead form', description: 'Archive a ChatGPT Ads lead form. Not reversible through the API, and refused by ChatGPT Ads while a PUBLISHED Business Agent references it. confirm:true is required; without it nothing changes and the reply says what would.',
|
|
10883
|
+
inputSchema: { leadFormId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
10884
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form/archive', a))));
|
|
10885
|
+
server.registerTool('test_openai_ads_lead_form', {
|
|
10886
|
+
title: 'Send a synthetic test lead', description: 'Queue an explicitly SYNTHETIC, signed test lead for a PUBLISHED lead form to the account’s lead-sync webhook, so the receiving system can be checked end to end. expectedPublishedRevisionId is the form’s publishedRevisionId. The payload is marked synthetic; never count it as a lead.',
|
|
10887
|
+
inputSchema: { leadFormId: z.string(), expectedPublishedRevisionId: z.string() }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10888
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form/test', a))));
|
|
10889
|
+
server.registerTool('list_openai_ads_lead_sync', {
|
|
10890
|
+
title: 'List ChatGPT Ads lead-sync subscriptions', description: 'The lead-sync subscription(s) on this ChatGPT Ads account — the webhook that receives every lead a lead form collects. With none, leads have nowhere to go. Read-only, 0 credits.',
|
|
10891
|
+
inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10892
|
+
}, wrap(async () => oaiNote(await apiGet('/api/openai-ads/lead-sync'))));
|
|
10893
|
+
server.registerTool('set_openai_ads_lead_sync', {
|
|
10894
|
+
title: 'Provision ChatGPT Ads lead delivery (webhook)', description: 'Provision lead delivery for this ChatGPT Ads account to an https destination URL (a managed webhook). Optionally pass your own signingSecret (16+ chars); otherwise ChatGPT Ads mints one. THE SIGNING SECRET IS RETURNED ONCE — every lead webhook is signed with it. Needs ChatGPT Ads connected.',
|
|
10895
|
+
inputSchema: { destinationUrl: z.string(), signingSecret: z.string().optional() }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10896
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-sync', a))));
|
|
10897
|
+
server.registerTool('delete_openai_ads_lead_sync', {
|
|
10898
|
+
title: 'Delete a ChatGPT Ads lead-sync subscription', description: 'Delete the lead-sync subscription: every lead form on the account stops delivering leads. confirm:true required.',
|
|
10899
|
+
inputSchema: { subscriptionId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
10900
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-sync/delete', a))));
|
|
10901
|
+
server.registerTool('list_openai_ads_business_agents', {
|
|
10902
|
+
title: 'List ChatGPT Ads Business Agents', description: 'The BUSINESS AGENTS on this ChatGPT Ads account — a Business Agent is a branded assistant ChatGPT serves inside an ad (mode business_agent on the campaign): instructions, conversation starters, optional product feeds, tools and a lead form. Shows draft/published status and pending changes. Read-only, 0 credits.',
|
|
10903
|
+
inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10904
|
+
}, wrap(async () => oaiNote(await apiGet('/api/openai-ads/business-agents'))));
|
|
10905
|
+
server.registerTool('list_openai_ads_business_agent_tools', {
|
|
10906
|
+
title: 'List tools a Business Agent may use', description: 'The eligible TOOLS installed for this ChatGPT Ads account that a Business Agent can be given (pass their ids as toolIds to create_openai_ads_business_agent). Read-only, 0 credits.',
|
|
10907
|
+
inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10908
|
+
}, wrap(async () => oaiNote(await apiGet('/api/openai-ads/business-agent-tools'))));
|
|
10909
|
+
server.registerTool('create_openai_ads_business_agent', {
|
|
10910
|
+
title: 'Create or replace a ChatGPT Ads Business Agent',
|
|
10911
|
+
description: 'Draft a ChatGPT Ads BUSINESS AGENT: name, instructions (the system prompt real users will experience — write it as the brand), optional description, privacy policy URL, conversation starters, product feed ids, connector ids, tool ids (list_openai_ads_business_agent_tools) and a published lead form id. Pass businessAgentId to REPLACE an existing agent’s configuration (it becomes a pending change). Nothing is served until publish_openai_ads_business_agent. Needs ChatGPT Ads connected.',
|
|
10912
|
+
inputSchema: {
|
|
10913
|
+
businessAgentId: z.string().optional().describe('replace THIS agent’s configuration instead of creating a new one'),
|
|
10914
|
+
name: z.string(), instructions: z.string(), description: z.string().optional(), privacyPolicyUrl: z.string().optional(),
|
|
10915
|
+
conversationStarters: z.array(z.string()).optional(), productFeedIds: z.array(z.string()).optional(), connectorIds: z.array(z.string()).optional(), toolIds: z.array(z.string()).optional(), leadFormId: z.string().optional(),
|
|
10916
|
+
}, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10917
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/business-agent', a))));
|
|
10918
|
+
server.registerTool('get_openai_ads_business_agent', {
|
|
10919
|
+
title: 'Read a ChatGPT Ads Business Agent', description: 'One Business Agent with its full configuration and publish state. Read-only, 0 credits.',
|
|
10920
|
+
inputSchema: { businessAgentId: z.string() }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10921
|
+
}, wrap(async (a) => oaiNote(await apiGet('/api/openai-ads/business-agent', a))));
|
|
10922
|
+
server.registerTool('preview_openai_ads_business_agent', {
|
|
10923
|
+
title: 'Chat with a Business Agent (preview)', description: 'Send a message (or a whole messages[] transcript) to a ChatGPT Ads Business Agent and get its reply — a PREVIEW that publishes and serves nothing. Use it to test instructions before publish_openai_ads_business_agent.',
|
|
10924
|
+
inputSchema: { businessAgentId: z.string(), message: z.string().optional(), messages: z.array(z.object({ role: z.string(), content: z.string() })).optional() }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10925
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/business-agent/preview', a))));
|
|
10926
|
+
server.registerTool('publish_openai_ads_business_agent', {
|
|
10927
|
+
title: 'Publish a ChatGPT Ads Business Agent', description: 'Publish a Business Agent’s draft so every campaign referencing it serves this version to real users. confirm:true required; show the user the instructions first.',
|
|
10928
|
+
inputSchema: { businessAgentId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10929
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/business-agent/publish', a))));
|
|
10930
|
+
server.registerTool('list_openai_ads_spend_windows', {
|
|
10931
|
+
title: 'List ChatGPT Ads spend-limit windows', description: 'Account-level SPEND-LIMIT WINDOWS on ChatGPT Ads: a ceiling on what the whole account may spend between an inclusive start date and an exclusive end date, with amount and spent so far. A window caps spend; it never makes anything spend. Read-only, 0 credits.',
|
|
10932
|
+
inputSchema: {}, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10933
|
+
}, wrap(async () => oaiNote(await apiGet('/api/openai-ads/spend-windows'))));
|
|
10934
|
+
server.registerTool('set_openai_ads_spend_window', {
|
|
10935
|
+
title: 'Create or edit a ChatGPT Ads spend-limit window', description: 'Create (no windowId) or edit (windowId) an account-level spend-limit window on ChatGPT Ads: startDate YYYY-MM-DD inclusive, endDate YYYY-MM-DD EXCLUSIVE (the window ends the day before), amount in the account currency (sent as micros), optional name and insertion-order id (ioId). A window is a CEILING on what the whole account may spend in that range; it never makes anything spend, and campaign budgets still apply underneath it. On edit, pass only the fields that change; a window whose canEdit is false (already started, per OpenAI) cannot be edited. Raising the amount lets campaigns spend up to their budgets, so show the user the old and new amounts. Reply carries the stored window (amount, spent so far, status). MEASURED 2026-09-03: OpenAI\'s live API answered "Invalid URL" for this path although it is in their published spec; the tool reports that honestly until they ship it.',
|
|
10936
|
+
inputSchema: { windowId: z.string().optional(), startDate: z.string().optional(), endDate: z.string().optional(), amount: z.number().optional(), name: z.string().optional(), ioId: z.string().optional() },
|
|
10937
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10938
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/spend-window', a))));
|
|
10939
|
+
server.registerTool('delete_openai_ads_spend_window', {
|
|
10940
|
+
title: 'Delete a ChatGPT Ads spend-limit window', description: 'Delete an active or scheduled spend-limit window before it ends — this REMOVES a spending ceiling. confirm:true required.',
|
|
10941
|
+
inputSchema: { windowId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
10942
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/spend-window/delete', a))));
|
|
10943
|
+
server.registerTool('set_openai_ads_negative_keywords', {
|
|
10944
|
+
title: 'Set ChatGPT Ads account negative keywords', description: 'REPLACE the account-level negative keywords on ChatGPT Ads — conversations matching them are ineligible for every campaign. Pass the COMPLETE list (this is a replace, not an add; [] clears it). The reply names the previous count so the user can see what changed.',
|
|
10945
|
+
inputSchema: { keywords: z.array(z.string()) }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10946
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/negative-keywords', a))));
|
|
10947
|
+
server.registerTool('list_openai_ads_feeds', {
|
|
10948
|
+
title: 'List ChatGPT Ads product feeds', description: 'The PRODUCT FEEDS on this ChatGPT Ads account (id, name, countries, currencies, product and campaign counts). A product-feed campaign (mode product_feed) advertises products from one of these; update_openai_ads_feed_products fills one. Read-only, 0 credits.',
|
|
10949
|
+
inputSchema: { limit: z.number().optional(), after: z.string().optional() }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10950
|
+
}, wrap(async (a) => oaiNote(await apiGet('/api/openai-ads/feeds', a))));
|
|
10951
|
+
server.registerTool('create_openai_ads_feed', {
|
|
10952
|
+
title: 'Create a ChatGPT Ads product feed', description: 'Create an EMPTY product feed on ChatGPT Ads with a name and the countries it serves (ISO codes). Fill it with update_openai_ads_feed_products, or set up SFTP bulk delivery with set_openai_ads_feed_sftp.',
|
|
10953
|
+
inputSchema: { name: z.string(), countries: z.array(z.string()).optional() }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10954
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/feed', a))));
|
|
10955
|
+
server.registerTool('list_openai_ads_feed_uploads', {
|
|
10956
|
+
title: 'List ChatGPT Ads feed uploads', description: 'Recent product-feed UPLOADS across the account with per-upload status and rows accepted / rejected / ads-eligible — the way to learn why products are not serving. Read-only, 0 credits.',
|
|
10957
|
+
inputSchema: { limit: z.number().optional() }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10958
|
+
}, wrap(async (a) => oaiNote(await apiGet('/api/openai-ads/feed-uploads', a))));
|
|
10959
|
+
server.registerTool('archive_openai_ads_feed', {
|
|
10960
|
+
title: 'Archive a ChatGPT Ads product feed', description: 'Archive a product feed — its products leave every campaign using it. confirm:true required.',
|
|
10961
|
+
inputSchema: { feedId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
10962
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/feed/archive', a))));
|
|
10963
|
+
server.registerTool('query_openai_ads_feed_products', {
|
|
10964
|
+
title: 'Query products in a ChatGPT Ads feed', description: 'List the products in a feed that match filters ({field, operator in|not_in|gt|gte|lt|lte|contains|not_contains|starts_with, values[]}) — the SAME filter shape an ad group’s productSet takes, so this previews exactly which products that ad group would advertise. Paginate with after. Read-only, 0 credits.',
|
|
10965
|
+
inputSchema: { feedId: z.string(), filters: z.array(z.object({ field: z.string(), operator: z.string(), values: z.array(z.string()) })).optional(), limit: z.number().optional(), after: z.string().optional() },
|
|
10966
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10967
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/feed/products/query', a))));
|
|
10968
|
+
server.registerTool('get_openai_ads_feed_sftp', {
|
|
10969
|
+
title: 'Read a feed’s SFTP delivery settings', description: 'Whether SFTP bulk delivery is enabled for a ChatGPT Ads product feed, its connection URI and authentication method. Read-only, 0 credits.',
|
|
10970
|
+
inputSchema: { feedId: z.string() }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
10971
|
+
}, wrap(async (a) => oaiNote(await apiGet('/api/openai-ads/feed/sftp', a))));
|
|
10972
|
+
server.registerTool('set_openai_ads_feed_sftp', {
|
|
10973
|
+
title: 'Set up, activate or pause feed SFTP delivery', description: 'Manage SFTP bulk delivery for a ChatGPT Ads product feed (the alternative to pushing products through update_openai_ads_feed_products). action "create" (default): create or REPLACE the feed\'s SFTP credentials — authenticationMethod "password" (OpenAI returns the password ONCE in this reply; store it) or "ssh_key" (pass the public key as sshPublicKey). Replacing credentials invalidates the previous ones immediately, so re-run only when the user means to rotate. action "activate" / "pause": switch delivery on or off without touching credentials. The reply carries the connection URI and whether delivery is enabled; get_openai_ads_feed_sftp reads the same state without changing it. Files delivered over SFTP show up in list_openai_ads_feed_uploads with accepted / rejected / ads-eligible row counts.',
|
|
10974
|
+
inputSchema: { feedId: z.string(), action: z.enum(['create', 'activate', 'pause']).optional(), authenticationMethod: z.enum(['password', 'ssh_key']).optional(), sshPublicKey: z.string().optional() },
|
|
10975
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
10976
|
+
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/feed/sftp', a))));
|
|
10977
|
+
|
|
10847
10978
|
server.registerTool('list_pinterest_ads_campaigns', {
|
|
10848
10979
|
title: 'List Pinterest ad accounts / campaigns',
|
|
10849
10980
|
description: 'Read the brand’s connected Pinterest 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 list that account’s campaigns (id, name, status, objective, budget, and Pinterest’s own summary status). Add campaignId to get that ONE campaign’s whole tree — its AD GROUPS and ADS with their ids, statuses and review status. That is the only way to enumerate them, and it matters: Pinterest has no delete, so an ad group you cannot see is one you cannot even archive. All money is in the AD ACCOUNT’S currency, which is not necessarily dollars. Pinterest’s own list default hides DRAFT and ARCHIVED objects; this asks for all four statuses so nothing is silently missing. Read-only, free. Needs Pinterest connected (Settings ▸ Connectors ▸ Pinterest) and the ad account ticked under Manage accounts.',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.197",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI ad studio and marketing MCP server with
|
|
5
|
+
"description": "AI ad studio and marketing MCP server with 768 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|