hermoso 0.1.234 → 0.1.236

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/mcp/client.mjs CHANGED
@@ -308,7 +308,10 @@ export async function connectedProviders() {
308
308
  if (!list) return { connected: new Set(), readOk: false }; // a shape we do not recognise is a failed read
309
309
  const offered = Array.isArray(r?.offered) ? new Set(r.offered.filter((p) => typeof p === 'string' && p)) : null; // null = server predates the field → fail open
310
310
  const gated = r?.gated && typeof r.gated === 'object' ? r.gated : {};
311
- return { connected: new Set(list.filter((p) => typeof p === 'string' && p)), readOk: true, offered, gated };
311
+ // connectLink: the app deep link for THIS brand with a {provider} slot, so a tool held back for a missing
312
+ // connection can hand the user the one-click link instead of only naming the Connectors page.
313
+ const connectLink = typeof r?.connectLink === 'string' && r.connectLink.includes('{provider}') ? r.connectLink : '';
314
+ return { connected: new Set(list.filter((p) => typeof p === 'string' && p)), readOk: true, offered, gated, connectLink };
312
315
  } catch { return { connected: new Set(), readOk: false }; }
313
316
  }
314
317
  // Upload raw file BYTES to /api/upload (150MB, persists → returns {url,kind,bytes}). Overrides the JSON content-type so
package/mcp/tools.mjs CHANGED
@@ -2066,7 +2066,15 @@ export const holdReasonText = (name, why, ctx = null) => {
2066
2066
  return `${name} is not available: Hermoso does not offer the "${prov}" connection yet${because}. There is nothing the user can connect, so do not point them at Settings ▸ Connectors and do not offer this capability.${reddit}`;
2067
2067
  }
2068
2068
  if (why === 'host_policy') return `${name} is not offered on this host (the host's own commerce policy). Use the Hermoso app or another client for it.`;
2069
- if (why === 'not_connected') return `${name} needs the "${toolProvider(name)}" connection and this workspace has not made it. Connect it under Settings ▸ Connectors in the Hermoso app${Object.prototype.hasOwnProperty.call(KEY_CONNECTORS, toolProvider(name)) ? ', or right here with connect_connector if the user prefers' : ''}, then call again.`;
2069
+ if (why === 'not_connected') {
2070
+ // THE LINK BELONGS HERE TOO (2026-09-13, found live on a brand with no YouTube). A tool held back for a missing
2071
+ // connection is answered by this sentence BEFORE any request reaches the server, so the brand-scoped link the
2072
+ // server puts on its own 401 never arrived. The template comes from the same providers read that decided the hold.
2073
+ const prov = toolProvider(name);
2074
+ const isKey = Object.prototype.hasOwnProperty.call(KEY_CONNECTORS, prov);
2075
+ const tpl = typeof ctx?.conn?.connectLink === 'string' && ctx.conn.connectLink.includes('{provider}') ? ctx.conn.connectLink : 'https://app.hermoso.ai/?connect={provider}';
2076
+ return `${name} needs the "${prov}" connection and this workspace has not made it. Connect it under Settings ▸ Connectors in the Hermoso app${isKey ? ', or right here with connect_connector if the user prefers' : `, or hand the user this one-click link: ${tpl.replace('{provider}', prov)} (it opens Hermoso on this brand and goes straight to the sign-in)`}, then call again.`;
2077
+ }
2070
2078
  if (why === 'directory') return `${name} is outside what this Claude directory connection may run. Use the Hermoso app, or connect the unscoped server URL.`;
2071
2079
  return null;
2072
2080
  };
@@ -2701,11 +2709,13 @@ function buildTools(rawServer, opts = {}, sink = null) {
2701
2709
  }
2702
2710
  if (n === 'call_tool' || n === 'find_tools' || n === 'enable_tools') return { content: [{ type: 'text', text: `${n} is a roster tool; call it directly.` }], isError: true };
2703
2711
  const why = toolHoldReason(n, ctx);
2704
- if (why) reportDeadEnd(why, n, holdReasonText(n, why, ctx));
2705
- if (why === 'not_offered') return { content: [{ type: 'text', text: holdReasonText(n, why, ctx) }], isError: true };
2706
- if (why === 'host_policy') return { content: [{ type: 'text', text: `${n} is not offered on this host (the host's own commerce policy). Use the Hermoso app or another client for it.` }], isError: true };
2707
- if (why === 'not_connected') return { content: [{ type: 'text', text: `${n} needs the "${toolProvider(n)}" connection and this workspace has not made it. Connect it under Settings ▸ Connectors in the Hermoso app${Object.prototype.hasOwnProperty.call(KEY_CONNECTORS, toolProvider(n)) ? ', or right here with connect_connector if the user prefers' : ''}, then call again.` }], isError: true };
2708
- if (why === 'directory') return { content: [{ type: 'text', text: `${n} is outside what this Claude directory connection may run. Use the Hermoso app, or connect the unscoped server URL.` }], isError: true };
2712
+ // ONE sentence per hold, from holdReasonText. call_tool used to spell its own copies, so the connect link added there
2713
+ // never reached claude.ai or ChatGPT, the two hosts that run held tools through here.
2714
+ if (why) {
2715
+ const t = holdReasonText(n, why, ctx);
2716
+ reportDeadEnd(why, n, t);
2717
+ if (t) return { content: [{ type: 'text', text: t }], isError: true };
2718
+ }
2709
2719
  let input = args && typeof args === 'object' ? args : {};
2710
2720
  if (h.inputSchema) {
2711
2721
  const parsed = h.inputSchema.safeParse(input);
@@ -7707,6 +7717,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
7707
7717
  campaignResourceName: z.string().optional().describe('full resource name, e.g. customers/{cid}/campaigns/{id}'),
7708
7718
  status: z.enum(['ENABLED', 'PAUSED', 'REMOVED']).describe('ENABLED = start spending; PAUSED = stop; REMOVED = permanent'),
7709
7719
  confirm: z.boolean().optional().describe('REQUIRED true to ENABLE (real spend) or to REMOVE (permanent)'),
7720
+ confirmName: z.string().optional().describe('when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live'),
7721
+ confirmChildren: z.number().optional().describe('when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any'),
7710
7722
  loginCustomerId: z.string().optional().describe('manager id if operating through an MCC'),
7711
7723
  },
7712
7724
  outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), resourceName: z.string().optional(), status: z.string().optional(), verifiedStatus: z.string().optional(), note: z.string().optional() },
@@ -11301,6 +11313,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
11301
11313
  campaignId: z.string().optional(), adGroupId: z.string().optional(), adId: z.string().optional(),
11302
11314
  status: z.enum(['active', 'paused', 'archived']).describe('active = start spending; paused = stop; archived = permanent (not available at level "account")'),
11303
11315
  confirm: z.boolean().optional().describe('REQUIRED true to activate (real spend), to archive (irreversible), or for EITHER direction at level "account"'),
11316
+ confirmName: z.string().optional().describe('when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live'),
11317
+ confirmChildren: z.number().optional().describe('when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any'),
11304
11318
  },
11305
11319
  outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), object: z.any().optional(), note: z.string().optional() },
11306
11320
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, // destructive: the enum carries 'archived', which is terminal
@@ -11449,7 +11463,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
11449
11463
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
11450
11464
  }, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/daily-spend-limit/delete', a))));
11451
11465
  server.registerTool('set_openai_ads_negative_keywords', {
11452
- 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.',
11466
+ title: 'Set ChatGPT Ads account negative keywords', description: 'CURRENTLY UNAVAILABLE: OpenAI no longer serves account-level negative keywords. The live API answers "Invalid URL" (measured 2026-09-13) and the endpoint left its published ChatGPT Ads spec the same week, so nothing can be set through Hermoso or any other API client. Tell the user that plainly instead of retrying. When it did work it REPLACED the whole account list, and the API never offered a way to read the current one.',
11453
11467
  inputSchema: { keywords: z.array(z.string()) }, annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
11454
11468
  }, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/negative-keywords', a))));
11455
11469
  server.registerTool('list_openai_ads_feeds', {
@@ -11667,6 +11681,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
11667
11681
  adId: z.string().optional(),
11668
11682
  status: z.enum(['ACTIVE', 'PAUSED', 'ARCHIVED', 'DRAFT']).describe('ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire (Pinterest’s delete)'),
11669
11683
  confirm: z.boolean().optional().describe('REQUIRED true for ACTIVE (real spend) or ARCHIVED (irreversible retirement)'),
11684
+ confirmName: z.string().optional().describe('when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live'),
11685
+ confirmChildren: z.number().optional().describe('when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any'),
11670
11686
  },
11671
11687
  outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), status: z.string().optional(), verifiedStatus: z.string().nullable().optional(), note: z.string().optional() },
11672
11688
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
@@ -12015,6 +12031,8 @@ function buildTools(rawServer, opts = {}, sink = null) {
12015
12031
  id: z.string(),
12016
12032
  status: z.enum(['ACTIVE', 'PAUSED', 'ARCHIVED', 'DELETED']).describe('ACTIVE = start spending; PAUSED = stop; ARCHIVED = retire; DELETED = permanent, and blocked for 3h after any change'),
12017
12033
  confirm: z.boolean().optional().describe('REQUIRED true for ACTIVE (real spend), ARCHIVED and DELETED'),
12034
+ confirmName: z.string().optional().describe('when archiving or removing: its EXACT name, as the unconfirmed call names it. Required when it has children or is live'),
12035
+ confirmChildren: z.number().optional().describe('when archiving or removing: the exact number of children the unconfirmed call reported. Required when it has any'),
12018
12036
  },
12019
12037
  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() },
12020
12038
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.234",
3
+ "version": "0.1.236",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI ad studio and marketing MCP server with 828 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",