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 +4 -1
- package/mcp/tools.mjs +25 -7
- package/package.json +1 -1
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
|
-
|
|
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')
|
|
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
|
-
|
|
2705
|
-
|
|
2706
|
-
if (why
|
|
2707
|
-
|
|
2708
|
-
|
|
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: '
|
|
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.
|
|
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",
|