hermoso 0.1.195 → 0.1.196

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/mcp/tools.mjs +136 -5
  3. 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
- **741 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
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,
@@ -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 741 tools cover
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
@@ -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 phone numbers: Hermoso normalises and SHA-256 hashes them locally and only the digests are sent. 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.',
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: startDate YYYY-MM-DD inclusive, endDate YYYY-MM-DD EXCLUSIVE, amount in the account currency, optional name and insertion-order id. A window is a CEILING; raising it lets campaigns spend up to their budgets, so show the user old and new amounts.',
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: 'action "create" (default): create or REPLACE the SFTP credentials for a product feed — authenticationMethod "password" (the password is returned ONCE) or "ssh_key" (+ sshPublicKey); replacing invalidates the previous credentials. action "activate" / "pause": switch delivery on or off.',
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.195",
3
+ "version": "0.1.196",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
- "description": "AI ad studio and marketing MCP server with 741 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.",
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"