hermoso 0.1.429 → 0.1.431
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/http.mjs +11 -3
- package/mcp/openai-extensions.mjs +1 -1
- package/mcp/tools.mjs +80 -43
- package/package.json +1 -1
package/mcp/http.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { mcpCtx, connectedProviders } from './client.mjs';
|
|
|
26
26
|
// beside cli/, whose package.json carries the release; inside the npm package there is no ../cli/ and ../package.json
|
|
27
27
|
// IS the CLI package. One resolver, so the byte-identical twins agree.
|
|
28
28
|
const PKG_VERSION = (() => { const r = createRequire(import.meta.url); for (const p of ['../cli/package.json', '../package.json']) { try { const v = r(p).version; if (v) return v; } catch {} } return '0.0.0'; })();
|
|
29
|
-
export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl, onSessionStart = null, onSessionEnd = null, onAnonDiscovery = null, onFirstCall = null, onConnectEvent = null } = {}) {
|
|
29
|
+
export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl, onSessionStart = null, onSessionEnd = null, onAnonDiscovery = null, onFirstCall = null, onConnectEvent = null, firstRunFor = null } = {}) {
|
|
30
30
|
// Outcomes of a real connect, for the host's connect watch (lib/mcp-connect-watch.mjs): a bearer we reject, and the
|
|
31
31
|
// first tools/list of a signed-in session (keyed by the token's hash, so a FRESH token's first listing marks a
|
|
32
32
|
// completed connect). Never throws into the request.
|
|
@@ -532,7 +532,15 @@ const inflightNameOf = (body) => { const msgs = Array.isArray(body) ? body : [bo
|
|
|
532
532
|
// WHO IS CALLING, captured at the ONE moment it is on the wire. `clientInfo` rides the `initialize`
|
|
533
533
|
// request and nothing afterwards, so it has to be remembered on the session or it is gone by the first
|
|
534
534
|
// tools/call. It is advisory only: it may not change auth, scope or spend — it decides PRESENTATION.
|
|
535
|
-
|
|
535
|
+
// A CHATGPT ACCOUNT THAT HAS MADE NOTHING YET (2026-10-19). Read once per session, ChatGPT hosts only, bounded so a slow
|
|
536
|
+
// read can never hold the connect: null (no note) is the answer to every failure. The first successful tool reply of
|
|
537
|
+
// the session then offers one concrete next ask (firstRunNote in tools.mjs). 9 of 13 ChatGPT signups since the
|
|
538
|
+
// 10-15 publish spent nothing, and a live "make me a video ad" chat on a connected account never called us.
|
|
539
|
+
let firstRun = null;
|
|
540
|
+
if (widgetHost && typeof firstRunFor === 'function' && user.accountId) {
|
|
541
|
+
try { firstRun = await Promise.race([Promise.resolve(firstRunFor({ accountId: user.accountId })).catch(() => null), new Promise((r) => { const t = setTimeout(() => r(null), 1500); if (t && typeof t.unref === 'function') t.unref(); })]); } catch { firstRun = null; }
|
|
542
|
+
}
|
|
543
|
+
entry = { transport, server, user, lastSeen: Date.now(), client: rememberedClient(req), ua: String(req.headers['user-agent'] || '').slice(0, 120), src: srcOf(req), startedAt: Date.now(), listed: false, calls: 0, connectSeen: true, firstRun: firstRun && typeof firstRun === 'object' ? firstRun : null };
|
|
536
544
|
if (typeof onSessionStart === 'function') { try { onSessionStart({ accountId: user.accountId || null, userId: user.userId || null, client: entry.client, ua: entry.ua, src: entry.src }); } catch {} }
|
|
537
545
|
// LOG THE NAME. `hostRendersWidgets()` matches it with a regex, and a regex over a string no one has
|
|
538
546
|
// ever read is a guess. One line per session (not per call) so a new host identifies itself once and
|
|
@@ -595,7 +603,7 @@ const inflightNameOf = (body) => { const msgs = Array.isArray(body) ? body : [bo
|
|
|
595
603
|
? setTimeout(() => { if (res.writableEnded) return; console.error(`[mcp] tools/call open past ${MCP_CALL_WALL_MS}ms — ending the response (${inflightNameOf(req.body)})`); try { res.end(); } catch {} }, MCP_CALL_WALL_MS)
|
|
596
604
|
: null;
|
|
597
605
|
if (wall && typeof wall.unref === 'function') wall.unref();
|
|
598
|
-
try { await mcpCtx.run({ token, remote: true, client: entry.client || '' }, () => entry.transport.handleRequest(req, res, req.body)); }
|
|
606
|
+
try { await mcpCtx.run({ token, remote: true, client: entry.client || '', firstRun: entry.firstRun || null }, () => entry.transport.handleRequest(req, res, req.body)); }
|
|
599
607
|
finally { if (wall) clearTimeout(wall); }
|
|
600
608
|
});
|
|
601
609
|
|
|
@@ -1810,7 +1810,7 @@ export function libraryToolDefs() {
|
|
|
1810
1810
|
campaignId: z.string().max(80).describe('the campaign id'),
|
|
1811
1811
|
status: z.enum(['active', 'paused']).describe("'paused' stops it; 'active' turns it on and starts spending"),
|
|
1812
1812
|
},
|
|
1813
|
-
annotations: { title: 'Pause or turn on a campaign', readOnlyHint: false, destructiveHint:
|
|
1813
|
+
annotations: { title: 'Pause or turn on a campaign', readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
1814
1814
|
_meta: { ...APP_ONLY },
|
|
1815
1815
|
},
|
|
1816
1816
|
};
|
package/mcp/tools.mjs
CHANGED
|
@@ -526,6 +526,21 @@ const wrap = (fn) => {
|
|
|
526
526
|
const _txt = Array.isArray(r?.content) ? r.content.find((b) => b && b.type === 'text' && typeof b.text === 'string') : null;
|
|
527
527
|
if (_c > 0 && !_st.chargeSaid && _txt && !CHARGED_RE.test(_txt.text)) _txt.text += r?.structuredContent?.stillRendering ? `\nCharged ${_c} credit${_c === 1 ? '' : 's'} so far; the render's own charge is reported by get_job when it finishes.` : chargeSentence(_c);
|
|
528
528
|
} catch {}
|
|
529
|
+
// The FIRST successful reply of a ChatGPT session, on an account that has made nothing yet, offers one concrete
|
|
530
|
+
// next ask (firstRunNote). Spent once per session whatever the tool was, so a session that is already creating
|
|
531
|
+
// is never told to.
|
|
532
|
+
try {
|
|
533
|
+
const _fr = mcpCtx.getStore()?.firstRun;
|
|
534
|
+
if (_fr && !_fr.said && r && !r.isError) {
|
|
535
|
+
_fr.said = true;
|
|
536
|
+
const _n = ((_st.apiCharged || 0) + (_st.jobCharged || 0)) > 0 ? null : firstRunNote(_tool, _fr, r);
|
|
537
|
+
if (_n) {
|
|
538
|
+
const _last = [...r.content].reverse().find((b) => b && b.type === 'text' && typeof b.text === 'string');
|
|
539
|
+
if (_last) _last.text += `\n\n${_n.text}`; else r.content.push({ type: 'text', text: _n.text });
|
|
540
|
+
return withHints(r, [_n.hint]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch {}
|
|
529
544
|
return r;
|
|
530
545
|
}
|
|
531
546
|
catch (e) {
|
|
@@ -2544,6 +2559,28 @@ export const withStaleNote = (out, ctx) => {
|
|
|
2544
2559
|
if (last) last.text = `${last.text}\n\n${STALE_ROSTER_NOTE}`; else out.content.push({ type: 'text', text: STALE_ROSTER_NOTE });
|
|
2545
2560
|
return out;
|
|
2546
2561
|
};
|
|
2562
|
+
// ── A CONNECTED ACCOUNT THAT HAS MADE NOTHING IS OFFERED ONE CONCRETE FIRST ASK (2026-10-19) ─────────────────────────
|
|
2563
|
+
// Since the ChatGPT app was published (10-15), 9 of 13 accounts whose agent was ChatGPT spent nothing, and a live "make me
|
|
2564
|
+
// a video ad for my coffee shop" on a connected account was answered by ChatGPT's own slideshow with no Hermoso call. So
|
|
2565
|
+
// the first successful reply of a ChatGPT session on an account with no image or video render (the server decides:
|
|
2566
|
+
// mcpFirstRunFor, read once per session in http.mjs) ends with one next step the user can say yes to: their brand's
|
|
2567
|
+
// video ad when a brand is saved, else drafting the brand from their site. A tool that is already making something
|
|
2568
|
+
// never carries it. Agent-facing, like STALE_ROSTER_NOTE; the same advice rides _meta as a hint. Pure, so a check RUNS it.
|
|
2569
|
+
export const FIRST_RUN_QUIET_TOOLS = new Set(['plan_ad', 'render_ad', 'generate_image', 'generate_video', 'make_template_ad', 'clone_video', 'clone_static', 'remix_static', 'draft_brand', 'edit_image', 'multiply_ad', 'get_job', 'list_jobs']);
|
|
2570
|
+
export const FIRST_RUN_APP_URL = 'https://app.hermoso.ai';
|
|
2571
|
+
export function firstRunNote(tool, fr, r) {
|
|
2572
|
+
if (!fr || fr.fresh !== true || !r || r.isError || !Array.isArray(r.content) || FIRST_RUN_QUIET_TOOLS.has(String(tool || ''))) return null;
|
|
2573
|
+
const name = String(fr.brand?.name || '').replace(/["“”\n\r]/g, '').replace(/\s+/g, ' ').trim().slice(0, 60);
|
|
2574
|
+
const ask = name ? `Make a 10-second video ad for ${name}` : 'Set up my brand from my website';
|
|
2575
|
+
const how = name ? 'plan_ad with durationSeconds 10, then render_ad' : 'draft_brand with their website, or a one-line description when they have none';
|
|
2576
|
+
const claim = fr.unclaimed === true ? ` It has no credits yet: the free starter credits are claimed once in the Hermoso app at ${FIRST_RUN_APP_URL} (a quick check that they are a person), so offer that link first.` : '';
|
|
2577
|
+
return {
|
|
2578
|
+
text: `Note for the assistant, not for the user: this account has not made anything with Hermoso yet.${claim} After you answer, offer the user one concrete next step they can say yes to, for example "${ask}" (${how}).`,
|
|
2579
|
+
hint: fr.unclaimed === true ? { do: `send the user to ${FIRST_RUN_APP_URL} to claim the free starter credits`, why: 'the balance is zero until they do, so every paid first step would be refused' }
|
|
2580
|
+
: name ? { do: 'plan_ad({ durationSeconds: 10 }), then render_ad', why: `nothing has been made on this account yet; a short video ad for ${name} is the first thing to offer` }
|
|
2581
|
+
: { do: "draft_brand({ domain: '<their website>' })", why: 'nothing has been made and no brand is saved; with the brand drafted, every ad is made for it' },
|
|
2582
|
+
};
|
|
2583
|
+
}
|
|
2547
2584
|
// ── AN UNDECLARED ARGUMENT IS REFUSED, NEVER STRIPPED (2026-09-30) ──────────────────────────────────────────────
|
|
2548
2585
|
// Measured on the hosted connector: `call_tool {name:'update_brand', args:{brand:'pmuojpxnfrx0', voice:…}}` — update_brand
|
|
2549
2586
|
// did not declare `brand`, zod's default object STRIPS unknown keys, so the call validated, `brand` vanished, and the
|
|
@@ -5154,7 +5191,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
5154
5191
|
hide: z.boolean().optional().describe('false to UNHIDE (default true)'),
|
|
5155
5192
|
},
|
|
5156
5193
|
outputSchema: { ok: z.boolean().optional(), hidden: z.boolean().optional() },
|
|
5157
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
5194
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
5158
5195
|
}, wrap(async (a) => {
|
|
5159
5196
|
const d = await apiPost('/api/threads/reply/hide', { replyId: a.replyId, hide: a.hide });
|
|
5160
5197
|
return ok(`Reply ${a.replyId} ${d.hidden ? 'hidden' : 'unhidden'}.`, d);
|
|
@@ -9199,7 +9236,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
9199
9236
|
instoreProductLocation: z.string().nullable().optional().describe('local only, max 20 bytes'),
|
|
9200
9237
|
},
|
|
9201
9238
|
outputSchema: { merchantCenterId: z.string().optional(), kind: z.string().optional(), key: z.string().optional(), productKey: z.string().optional(), replaced: z.boolean().optional(), preserved: z.array(z.string()).optional(), cleared: z.array(z.string()).optional(), attributes: z.any().optional(), warning: z.string().optional(), note: z.string().optional() },
|
|
9202
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
9239
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
9203
9240
|
}, wrap(async (a) => {
|
|
9204
9241
|
const d = await apiPost('/api/merchant/inventory', a);
|
|
9205
9242
|
return ok(d.note, d);
|
|
@@ -9686,7 +9723,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
9686
9723
|
loginCustomerId: z.string().optional().describe('manager id if operating through an MCC'),
|
|
9687
9724
|
},
|
|
9688
9725
|
outputSchema: { ok: z.boolean().optional(), campaignId: z.string().optional(), changed: z.boolean().optional(), networks: z.object({}).passthrough().optional(), note: z.string().optional() },
|
|
9689
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
9726
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
9690
9727
|
}, wrap(async (a) => {
|
|
9691
9728
|
const d = await apiPost('/api/google-ads/networks', a);
|
|
9692
9729
|
return ok(d.note || `campaign ${a.campaignId} networks updated.`, d);
|
|
@@ -11461,7 +11498,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11461
11498
|
description: "Subscribe a Stripe customer (cus_…) to a price (price_…), optional quantity and trialDays. THIS CHARGES THE CUSTOMER'S SAVED PAYMENT METHOD when there is no trial, so it ALWAYS needs confirm:true (test keys included) after you show the user exactly what will be created. Read back; an INCOMPLETE status means no chargeable card is on file. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).",
|
|
11462
11499
|
inputSchema: { customerId: z.string(), priceId: z.string(), quantity: z.number().optional(), trialDays: z.number().optional(), metadata: z.record(z.any()).optional(), confirm: z.boolean().optional() },
|
|
11463
11500
|
outputSchema: { ok: z.boolean().optional(), note: z.string().optional(), count: z.number().optional(), hasMore: z.boolean().optional() },
|
|
11464
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
11501
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
11465
11502
|
}, wrap(async (a) => { const d = await apiPost('/api/stripe/subscription', a); return ok(d.note, d); }));
|
|
11466
11503
|
server.registerTool('cancel_stripe_subscription', {
|
|
11467
11504
|
title: 'cancel stripe subscription',
|
|
@@ -11475,7 +11512,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11475
11512
|
description: "Refund a Stripe charge (ch_… or a pi_… payment intent): the full refundable amount, or a partial `amount` in major units; optional reason duplicate / fraudulent / requested_by_customer. ALWAYS needs confirm:true, test keys included. Read back. Needs Stripe connected (Settings > Connectors > Stripe, or connect_connector).",
|
|
11476
11513
|
inputSchema: { chargeId: z.string(), amount: z.number().optional(), reason: z.string().optional(), confirm: z.boolean().optional() },
|
|
11477
11514
|
outputSchema: { ok: z.boolean().optional(), note: z.string().optional(), count: z.number().optional(), hasMore: z.boolean().optional() },
|
|
11478
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
11515
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
11479
11516
|
}, wrap(async (a) => { const d = await apiPost('/api/stripe/refund', a); return ok(d.note, d); }));
|
|
11480
11517
|
// ---------- HubSpot (2026-09-09): the brand's OWN CRM — contacts, an upsert by email, lead forms → contacts, deals ----------
|
|
11481
11518
|
server.registerTool('list_hubspot_contacts', {
|
|
@@ -11886,7 +11923,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
11886
11923
|
update: z.array(z.object({ criterionId: z.string(), bidAdjustment: z.number().describe('whole percent, -90 to 900') })).optional(),
|
|
11887
11924
|
confirm: z.boolean().optional().describe('REQUIRED true when the campaign is ACTIVE'),
|
|
11888
11925
|
},
|
|
11889
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
11926
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
11890
11927
|
}, wrap(async (a) => { const d = await apiPost('/api/microsoft-ads/profile-targeting', a); return ok(d.note, d); }));
|
|
11891
11928
|
server.registerTool('set_microsoft_ads_budget', {
|
|
11892
11929
|
title: 'Set a Microsoft Advertising daily budget',
|
|
@@ -12159,7 +12196,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
12159
12196
|
clickUrl: z.string().optional(), impressionUrl: z.string().optional(), confirm: z.boolean().optional().describe('required for a spend change on a LIVE campaign'),
|
|
12160
12197
|
},
|
|
12161
12198
|
outputSchema: { ...alOut, changed: z.array(z.string()).optional(), campaign: z.any().optional() },
|
|
12162
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
12199
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
12163
12200
|
}, wrap(async (a) => { const d = await apiPost('/api/applovin-ads/campaign/update', a); return ok(d.note, d); }));
|
|
12164
12201
|
server.registerTool('set_applovin_ads_status', {
|
|
12165
12202
|
title: 'Turn an AppLovin campaign or creative set live or paused',
|
|
@@ -12183,7 +12220,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
12183
12220
|
description: 'Create an AppLovin creative set from uploaded asset ids, born PAUSED, optionally inside a campaign. It must hold EITHER an HTML playable OR a portrait image (IMG_INTER_P) plus a video; Hermoso checks that against AppLovin’s own asset types before creating it.' + AL_CM_NOTE,
|
|
12184
12221
|
inputSchema: { name: z.string(), assetIds: z.array(z.string()).describe('AppLovin asset ids'), campaignId: z.string().optional(), countries: alIds, languages: alIds.describe('e.g. ENGLISH'), url: z.string().optional().describe('landing page for this set only (WEB)') },
|
|
12185
12222
|
outputSchema: { ...alOut, creativeSet: z.any().optional(), pausedAfterCreate: z.boolean().optional() },
|
|
12186
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
12223
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
12187
12224
|
}, wrap(async (a) => { const d = await apiPost('/api/applovin-ads/creative-set', a); return ok(d.note, d); }));
|
|
12188
12225
|
server.registerTool('remove_applovin_ads_creative_set', {
|
|
12189
12226
|
title: 'Remove an AppLovin creative set from its campaigns',
|
|
@@ -12197,14 +12234,14 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
12197
12234
|
description: 'Edit an AppLovin creative set, read back: rename it, change its countries or languages (an empty list means all), its landing page (url; "" falls back to the campaign’s), replace its assets (assetIds, checked against AppLovin’s own asset types first), or add it to more campaigns (addToCampaignIds, up to 20). Adding a LIVE set to a LIVE campaign needs confirm:true. Status is set_applovin_ads_status.' + AL_CM_NOTE,
|
|
12198
12235
|
inputSchema: { creativeSetId: z.string(), name: z.string().optional(), countries: alIds.describe('ISO codes; [] = all countries'), languages: alIds.describe('e.g. ENGLISH; [] = all'), url: z.string().optional().describe('landing page for this set; "" = the campaign’s'), assetIds: alIds.describe('REPLACES the set’s assets'), addToCampaignIds: alIds.describe('also run it in these campaigns (up to 20)'), confirm: z.boolean().optional().describe('required to add a LIVE set to a LIVE campaign') },
|
|
12199
12236
|
outputSchema: { ...alOut, changed: z.array(z.string()).optional(), creativeSet: z.any().optional() },
|
|
12200
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
12237
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
12201
12238
|
}, wrap(async (a) => { const d = await apiPost('/api/applovin-ads/creative-set/update', a); return ok(d.note, d); }));
|
|
12202
12239
|
server.registerTool('clone_applovin_ads_creative_set', {
|
|
12203
12240
|
title: 'Copy an AppLovin creative set into another campaign (paused)',
|
|
12204
12241
|
description: 'Copy an AppLovin creative set into another campaign, born PAUSED and read back. AppLovin only clones a set that already belongs to a campaign.' + AL_CM_NOTE,
|
|
12205
12242
|
inputSchema: { creativeSetId: z.string(), campaignId: z.string().describe('the campaign to copy it into') },
|
|
12206
12243
|
outputSchema: { ...alOut, sourceId: z.string().optional(), campaignId: z.string().optional(), version: z.string().optional(), creativeSet: z.any().optional(), pausedAfterCreate: z.boolean().optional() },
|
|
12207
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
12244
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
12208
12245
|
}, wrap(async (a) => { const d = await apiPost('/api/applovin-ads/creative-set/clone', a); return ok(d.note, d); }));
|
|
12209
12246
|
server.registerTool('list_applovin_ads_catalogs', {
|
|
12210
12247
|
title: 'List AppLovin catalogs',
|
|
@@ -12733,7 +12770,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
12733
12770
|
budgetOrderIds: z.array(z.string()).optional().describe('budget order id(s) from list_apple_ads_budget_orders to assign this campaign to — a budget order is a spend CEILING SHARED across every campaign assigned to it, and it works ALONGSIDE dailyBudget rather than instead of it. Without this a budget order can never be drawn from.'),
|
|
12734
12771
|
},
|
|
12735
12772
|
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
12736
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
12773
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
12737
12774
|
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/update', a); return ok(`Apple Ads ${d.level} ${d.id} updated (verified: ${d.verified}):\n${d.summary}\n${d.note || ''}`, d); }));
|
|
12738
12775
|
|
|
12739
12776
|
server.registerTool('set_apple_ads_status', {
|
|
@@ -13023,7 +13060,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13023
13060
|
confirm: z.boolean().optional().describe('REQUIRED true. Without it nothing changes and the refusal states the campaign and the exact budget or CPA change.'),
|
|
13024
13061
|
},
|
|
13025
13062
|
outputSchema: { ok: z.boolean().optional(), type: z.string().optional(), id: z.string().optional(), state: z.string().optional(), campaignId: z.string().nullable().optional(), armsSpend: z.boolean().optional(), verified: z.boolean().optional(), applied: z.any().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
13026
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13063
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13027
13064
|
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/recommendation/apply', a); return ok(`Apple Ads ${d.type} recommendation ${d.id} — ${d.verified ? 'APPLIED' : 'NOT CONFIRMED'}:\n${d.summary}\n${d.note || ''}`, d); }));
|
|
13028
13065
|
|
|
13029
13066
|
server.registerTool('dismiss_apple_ads_recommendation', {
|
|
@@ -13110,7 +13147,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13110
13147
|
invoiceName: z.string().optional(), orderNumber: z.string().optional(), clientName: z.string().optional(), primaryBuyerName: z.string().optional(),
|
|
13111
13148
|
},
|
|
13112
13149
|
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
13113
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13150
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13114
13151
|
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/budget-order', a); return ok(`Apple Ads budget order created:\n${d.summary}\n${d.note || ''}`, d); }));
|
|
13115
13152
|
|
|
13116
13153
|
server.registerTool('update_apple_ads_budget_order', {
|
|
@@ -13129,7 +13166,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13129
13166
|
primaryBuyerName: z.string().optional(), primaryBuyerEmail: z.string().optional(), billingEmail: z.string().optional(),
|
|
13130
13167
|
},
|
|
13131
13168
|
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), requested: z.any().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
13132
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13169
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13133
13170
|
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/budget-order/update', a); return ok(`Apple Ads budget order ${d.id} updated (verified: ${d.verified}):\n${d.summary}\n${d.note || ''}`, d); }));
|
|
13134
13171
|
|
|
13135
13172
|
server.registerTool('bulk_apple_ads_keywords', {
|
|
@@ -13144,7 +13181,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13144
13181
|
confirm: z.boolean().optional().describe('Required only when the target ad group or campaign is already ENABLED, where the new keywords could serve immediately.'),
|
|
13145
13182
|
},
|
|
13146
13183
|
outputSchema: { ok: z.boolean().optional(), kind: z.string().optional(), op: z.string().optional(), requested: z.number().optional(), succeeded: z.number().optional(), failed: z.number().optional(), verified: z.boolean().optional(), results: z.array(z.any()).optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
13147
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13184
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13148
13185
|
}, wrap(async (a) => { const d = await apiPost('/api/apple-ads/keywords/bulk', a); return ok(`${d.summary}\n${d.note || ''}`, d); }));
|
|
13149
13186
|
|
|
13150
13187
|
server.registerTool('search_apple_ads_geo', {
|
|
@@ -13230,7 +13267,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13230
13267
|
purchaseOrderNumber: z.string().optional(),
|
|
13231
13268
|
},
|
|
13232
13269
|
outputSchema: { accountId: z.string().optional(), campaignId: z.string().optional(), updated: z.array(z.string()).optional(), changed: z.number().optional(), status: z.string().nullable().optional(), note: z.string().optional() },
|
|
13233
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13270
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13234
13271
|
}, wrap(async (a) => { const d = await apiPost('/api/x-ads/campaign-update', a); return ok(d.note, d); }));
|
|
13235
13272
|
server.registerTool('update_x_ads_line_item', {
|
|
13236
13273
|
title: 'Change an X line item (ad group)',
|
|
@@ -13246,7 +13283,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13246
13283
|
totalBudget: z.number().optional().describe('only if the campaign is not budget-optimised'),
|
|
13247
13284
|
},
|
|
13248
13285
|
outputSchema: { accountId: z.string().optional(), lineItemId: z.string().optional(), updated: z.array(z.string()).optional(), changed: z.number().optional(), status: z.string().nullable().optional(), note: z.string().optional() },
|
|
13249
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13286
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13250
13287
|
}, wrap(async (a) => { const d = await apiPost('/api/x-ads/line-item-update', a); return ok(d.note, d); }));
|
|
13251
13288
|
server.registerTool('list_x_ads_promoted_tweets', {
|
|
13252
13289
|
title: 'List X promoted posts',
|
|
@@ -13467,7 +13504,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13467
13504
|
expectedRevision: z.union([z.number(), z.string()]).optional().describe('REQUIRED for replace, optional for add/remove — membershipRevision from get_openai_ads_audience'),
|
|
13468
13505
|
},
|
|
13469
13506
|
outputSchema: { operationId: z.string().optional(), audienceId: z.string().optional(), operation: z.string().optional(), status: z.string().optional(), membersSent: z.number().optional(), rejected: z.number().optional(), note: z.string().optional() },
|
|
13470
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13507
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13471
13508
|
}, wrap(async (a) => { const d = await apiPost('/api/openai-ads/audience/members', a); return ok(d.note, d); }));
|
|
13472
13509
|
server.registerTool('list_openai_ads_audience_operations', {
|
|
13473
13510
|
title: 'List ChatGPT Ads audience membership operations',
|
|
@@ -13616,7 +13653,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13616
13653
|
products: z.array(z.record(z.any())).describe('[{id, variants:[{id, title?, price?:{amount:int minor units, currency}, availability?:{available:bool}|{status:"in_stock"|"out_of_stock"}}]}]'),
|
|
13617
13654
|
},
|
|
13618
13655
|
outputSchema: { feedId: z.string().optional(), accepted: z.boolean().optional(), products: z.number().optional(), variants: z.number().optional(), note: z.string().optional() },
|
|
13619
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13656
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
13620
13657
|
}, wrap(async (a) => { const d = await apiPost('/api/openai-ads/feed-products', a); return ok(d.note, d); }));
|
|
13621
13658
|
server.registerTool('update_openai_ads_object', {
|
|
13622
13659
|
title: 'Edit a ChatGPT Ads campaign / ad group / ad',
|
|
@@ -13640,7 +13677,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13640
13677
|
confirm: z.boolean().optional().describe('REQUIRED true to change budget / bid / creative on a LIVE object'),
|
|
13641
13678
|
},
|
|
13642
13679
|
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), object: z.any().optional(), note: z.string().optional() },
|
|
13643
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13680
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
13644
13681
|
}, wrap(async (a) => {
|
|
13645
13682
|
const d = await apiPost('/api/openai-ads/update', a);
|
|
13646
13683
|
return ok(d.note, d);
|
|
@@ -13816,7 +13853,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13816
13853
|
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form/update', a))));
|
|
13817
13854
|
server.registerTool('publish_openai_ads_lead_form', {
|
|
13818
13855
|
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.',
|
|
13819
|
-
inputSchema: { leadFormId: z.string(), expectedDraftRevisionId: z.string() }, annotations: { readOnlyHint: false, destructiveHint:
|
|
13856
|
+
inputSchema: { leadFormId: z.string(), expectedDraftRevisionId: z.string() }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13820
13857
|
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/lead-form/publish', a))));
|
|
13821
13858
|
server.registerTool('archive_openai_ads_lead_form', {
|
|
13822
13859
|
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.',
|
|
@@ -13853,7 +13890,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13853
13890
|
businessAgentId: z.string().optional().describe('replace THIS agent’s configuration instead of creating a new one'),
|
|
13854
13891
|
name: z.string(), instructions: z.string(), description: z.string().optional(), privacyPolicyUrl: z.string().optional(),
|
|
13855
13892
|
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(),
|
|
13856
|
-
}, annotations: { readOnlyHint: false, destructiveHint:
|
|
13893
|
+
}, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13857
13894
|
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/business-agent', a))));
|
|
13858
13895
|
server.registerTool('get_openai_ads_business_agent', {
|
|
13859
13896
|
title: 'Read a ChatGPT Ads Business Agent', description: 'One Business Agent with its full configuration and publish state. Read-only, 0 credits.',
|
|
@@ -13865,7 +13902,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13865
13902
|
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/business-agent/preview', a))));
|
|
13866
13903
|
server.registerTool('publish_openai_ads_business_agent', {
|
|
13867
13904
|
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.',
|
|
13868
|
-
inputSchema: { businessAgentId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint:
|
|
13905
|
+
inputSchema: { businessAgentId: z.string(), confirm: z.boolean().optional() }, annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13869
13906
|
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/business-agent/publish', a))));
|
|
13870
13907
|
server.registerTool('list_openai_ads_spend_windows', {
|
|
13871
13908
|
title: 'List ChatGPT Ads account spending limits', description: 'Account-level SPENDING LIMITS on ChatGPT Ads, both kinds: the date-range SPEND-LIMIT WINDOWS (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) and the DAILY LIMIT (a per-day ceiling that renews at midnight in the account timezone, with what is spent and left today). Also returns the configuration `revision` that set_openai_ads_daily_spend_limit and remove_openai_ads_daily_spend_limit need as expectedRevision, and the earliest date a new daily limit may start. A limit caps spend; it never makes anything spend. OpenAI serves account spending limits only to ad accounts on postpaid INVOICE billing, and every spending-limit request, reads included, needs billing-management permission: a card-billed account is refused (a 403 that says only admins can do it) from any key, including one its own admin created. Read-only, 0 credits.',
|
|
@@ -13874,7 +13911,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
13874
13911
|
server.registerTool('set_openai_ads_spend_window', {
|
|
13875
13912
|
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). The live API serves this path since at least 2026-09-12 (it answered "Invalid URL" on 2026-09-03). It needs postpaid INVOICE billing and billing-management permission; a card-billed account is refused whatever key is used.',
|
|
13876
13913
|
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() },
|
|
13877
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
13914
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
13878
13915
|
}, wrap(async (a) => oaiNote(await apiPost('/api/openai-ads/spend-window', a))));
|
|
13879
13916
|
server.registerTool('delete_openai_ads_spend_window', {
|
|
13880
13917
|
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.',
|
|
@@ -14091,7 +14128,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14091
14128
|
confirm: z.boolean().optional().describe('REQUIRED true to change bid / budget / campaign on a LIVE ad group'),
|
|
14092
14129
|
},
|
|
14093
14130
|
outputSchema: { ok: z.boolean().optional(), adAccountId: z.string().optional(), campaignId: z.string().optional(), adGroupId: z.string().optional(), changed: z.array(z.string()).optional(), adGroup: z.record(z.any()).optional(), note: z.string().optional() },
|
|
14094
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14131
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14095
14132
|
}, wrap(async (a) => { const d = await apiPost('/api/pinterest/ads-ad-group/update', a); return ok(d.note, d); }));
|
|
14096
14133
|
server.registerTool('update_pinterest_ads_ad', {
|
|
14097
14134
|
title: 'Edit a Pinterest ad',
|
|
@@ -14119,7 +14156,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14119
14156
|
pinId: z.string().optional().describe('DRAFT ads only — Pinterest refuses it on any other status'),
|
|
14120
14157
|
},
|
|
14121
14158
|
outputSchema: { ok: z.boolean().optional(), adAccountId: z.string().optional(), adGroupId: z.string().optional(), adId: z.string().optional(), changed: z.array(z.string()).optional(), reviewStatus: z.string().optional(), ad: z.record(z.any()).optional(), note: z.string().optional() },
|
|
14122
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14159
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14123
14160
|
}, wrap(async (a) => { const d = await apiPost('/api/pinterest/ads-ad/update', a); return ok(d.note, d); }));
|
|
14124
14161
|
server.registerTool('set_pinterest_ads_budget', {
|
|
14125
14162
|
title: 'Set a Pinterest campaign budget',
|
|
@@ -14342,7 +14379,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14342
14379
|
allowComments: z.boolean().describe('REQUIRED — Reddit demands allow_comments on every post update, and it is the only field it permits'),
|
|
14343
14380
|
},
|
|
14344
14381
|
outputSchema: { id: z.string().optional(), headline: z.string().optional(), allowComments: z.boolean().optional(), note: z.string().optional() },
|
|
14345
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14382
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14346
14383
|
}, wrap(async (a) => {
|
|
14347
14384
|
const d = await apiPost('/api/reddit-ads/posts/update', a);
|
|
14348
14385
|
return ok(d.note, d);
|
|
@@ -14438,7 +14475,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14438
14475
|
schedule: z.array(z.any()).optional(),
|
|
14439
14476
|
},
|
|
14440
14477
|
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), budget: z.number().nullable().optional(), productSetId: z.string().nullable().optional(), shoppingType: z.string().nullable().optional(), note: z.string().optional() },
|
|
14441
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14478
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14442
14479
|
}, wrap(async (a) => {
|
|
14443
14480
|
const d = await apiPost('/api/reddit-ads/ad-groups/update', a);
|
|
14444
14481
|
return ok(d.note, d);
|
|
@@ -14531,7 +14568,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14531
14568
|
supplementaryText: z.string().optional().describe('up to 100 characters'),
|
|
14532
14569
|
userGeneratedContent: z.enum(['OPT_IN', 'OPT_OUT']).optional(),
|
|
14533
14570
|
},
|
|
14534
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14571
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
14535
14572
|
}, wrap(async (a) => { const d = await apiPost('/api/reddit-ads/max-template/update', a); return ok(d.note, d); }));
|
|
14536
14573
|
server.registerTool('update_reddit_ads_ad', {
|
|
14537
14574
|
title: 'Edit a Reddit ad',
|
|
@@ -14544,7 +14581,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14544
14581
|
clickUrl: z.string().optional().describe('pass an empty string to clear it'),
|
|
14545
14582
|
},
|
|
14546
14583
|
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), note: z.string().optional() },
|
|
14547
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14584
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14548
14585
|
}, wrap(async (a) => {
|
|
14549
14586
|
const d = await apiPost('/api/reddit-ads/ads/update', a);
|
|
14550
14587
|
return ok(d.note, d);
|
|
@@ -14563,7 +14600,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14563
14600
|
endTime: z.string().optional(),
|
|
14564
14601
|
},
|
|
14565
14602
|
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), note: z.string().optional() },
|
|
14566
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14603
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14567
14604
|
}, wrap(async (a) => {
|
|
14568
14605
|
const d = await apiPost('/api/reddit-ads/campaign/update', a);
|
|
14569
14606
|
return ok(d.note, d);
|
|
@@ -14777,7 +14814,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14777
14814
|
})).describe('up to 2,500 rows; every row must carry the same fields'),
|
|
14778
14815
|
},
|
|
14779
14816
|
outputSchema: { ok: z.boolean().optional(), customAudienceId: z.string().optional(), action: z.string().optional(), rows: z.number().optional(), sizeUpper: z.number().nullable().optional(), note: z.string().optional() },
|
|
14780
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14817
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
14781
14818
|
}, wrap(async (a) => {
|
|
14782
14819
|
const d = await apiPost('/api/reddit-ads/audiences/users', a);
|
|
14783
14820
|
return ok(d.note, d);
|
|
@@ -14830,7 +14867,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
14830
14867
|
targeting: z.record(z.any()).optional().describe('REPLACES the existing targeting'),
|
|
14831
14868
|
},
|
|
14832
14869
|
outputSchema: { id: z.string().optional(), name: z.string().optional(), status: z.string().optional(), activeAdGroups: z.number().nullable().optional(), note: z.string().optional() },
|
|
14833
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
14870
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
14834
14871
|
}, wrap(async (a) => {
|
|
14835
14872
|
const d = await apiPost('/api/reddit-ads/saved-audiences/update', a);
|
|
14836
14873
|
return ok(d.note, d);
|
|
@@ -15811,7 +15848,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
15811
15848
|
leadSource: z.string().optional().describe('INSTANT_FORM (default) or DIRECT_MESSAGE'),
|
|
15812
15849
|
},
|
|
15813
15850
|
outputSchema: { action: z.string().optional(), advertiserId: z.string().optional(), leadSource: z.string().optional(), leadId: z.string().nullable().optional(), pageId: z.string().nullable().optional(), campaignId: z.string().nullable().optional(), adgroupId: z.string().nullable().optional(), adId: z.string().nullable().optional(), leadData: z.any().optional(), deleted: z.boolean().optional(), note: z.string().optional() },
|
|
15814
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
15851
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
15815
15852
|
}, wrap(async (a) => {
|
|
15816
15853
|
const d = await apiPost('/api/tiktok-ads/test-lead', a);
|
|
15817
15854
|
return ok(`${d.action === 'delete' ? `Test lead ${d.leadId} deleted.` : d.leadId ? `Test lead ${d.leadId} on form ${d.pageId}: ${JSON.stringify(d.leadData)}` : 'No test lead exists on that form.'} ${d.note}`, d);
|
|
@@ -16207,7 +16244,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16207
16244
|
operation: z.enum(['HIDDEN', 'PUBLIC']).describe('HIDDEN takes them out of public view; PUBLIC puts them back'),
|
|
16208
16245
|
},
|
|
16209
16246
|
outputSchema: { advertiserId: z.string().optional(), commentIds: z.array(z.string()).optional(), operation: z.string().optional(), applied: z.boolean().optional(), note: z.string().optional() },
|
|
16210
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
16247
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
16211
16248
|
}, wrap(async (a) => {
|
|
16212
16249
|
const d = await apiPost('/api/tiktok-ads/comment-moderate', a);
|
|
16213
16250
|
return ok(d.note, d);
|
|
@@ -16327,7 +16364,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16327
16364
|
confirm: z.boolean().optional().describe('REQUIRED true only to turn coverAllObjectives ON for the first time, because that is irreversible'),
|
|
16328
16365
|
},
|
|
16329
16366
|
outputSchema: { advertiserId: z.string().optional(), applied: z.boolean().optional(), before: z.any().optional(), after: z.any().optional(), changed: z.array(z.string()).optional(), note: z.string().optional() },
|
|
16330
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
16367
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
16331
16368
|
}, wrap(async (a) => {
|
|
16332
16369
|
const d = await apiPost('/api/tiktok-ads/brand-safety', a);
|
|
16333
16370
|
return ok(d.note, d);
|
|
@@ -16440,7 +16477,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16440
16477
|
confirmScope: z.string().optional().describe('required in the same case — the scope token the refusal prints, computed from the version being sent'),
|
|
16441
16478
|
},
|
|
16442
16479
|
outputSchema: { advertiserId: z.string().optional(), ruleId: z.string().optional(), klass: z.string().optional(), canSpend: z.boolean().optional(), armingActions: z.array(z.string()).optional(), rule: z.any().optional(), verified: z.boolean().optional(), note: z.string().optional() },
|
|
16443
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
16480
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
16444
16481
|
}, wrap(async (a) => {
|
|
16445
16482
|
const d = await apiPost('/api/tiktok-ads/rule-update', a);
|
|
16446
16483
|
return ok(d.note, d);
|
|
@@ -16851,7 +16888,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
16851
16888
|
sendNotification: z.boolean().optional().describe('default false'),
|
|
16852
16889
|
},
|
|
16853
16890
|
outputSchema: { ttoAccountId: z.string().optional(), campaignId: z.string().optional(), handleNames: z.array(z.string()).optional(), advertiserIds: z.array(z.string()).optional(), added: z.any().optional(), note: z.string().optional() },
|
|
16854
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
16891
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
16855
16892
|
}, wrap(async (a) => {
|
|
16856
16893
|
const d = await apiPost('/api/tiktok-ads/tto-campaign-update', a);
|
|
16857
16894
|
return ok(d.note, d);
|
|
@@ -17273,7 +17310,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
17273
17310
|
endTime: z.string().optional().describe('ISO 8601'),
|
|
17274
17311
|
},
|
|
17275
17312
|
outputSchema: { id: z.string().optional(), adAccountId: z.string().optional(), campaignId: z.string().optional(), name: z.string().optional(), status: z.string().optional(), optimizationGoal: z.string().optional(), dailyBudget: z.number().nullable().optional(), dailyBudgetMicro: z.number().nullable().optional(), currency: z.string().nullable().optional(), verified: z.boolean().optional(), note: z.string().optional() },
|
|
17276
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
17313
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
17277
17314
|
}, wrap(async (a) => {
|
|
17278
17315
|
const d = await apiPost('/api/snapchat-ads/adsquad', a);
|
|
17279
17316
|
const money = d.dailyBudget != null ? ` Daily budget ${d.dailyBudget} ${d.currency || ''} (${d.dailyBudgetMicro} micro).` : '';
|
|
@@ -18578,7 +18615,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18578
18615
|
server.group('create');
|
|
18579
18616
|
server.registerTool('generate_image', {
|
|
18580
18617
|
title: 'Generate ad image',
|
|
18581
|
-
description: 'Use this when the user
|
|
18618
|
+
description: 'Use this when the user asks for an image ad, ad creative, product photo or social post graphic for their business or product. It renders a finished image on a real image model, on their saved brand (product photos, logo), and returns a URL ready to post. For a video, use render_ad. refImages (paths or URLs) composite a real product into the scene. MULTI-BRAND: hydration uses the SAVED brand; for a different brand (a fresh draft_brand) pass its own productImages/logo as refImages with useBrand:false, or the wrong product lands in the shot. Product photos also pick the ENGINE (the compositing model), so a named `model` is honoured only when no references ride; raw:true or useBrand:false keeps it. model = a catalog id from hermoso_capabilities (omit for the default). A PRODUCT IN A PERSON’S HANDS, or a garment on them: name an id marked `needsRefs` with a `refsMax`; person photo first, then up to three product/garment photos. It EDITS THE PHOTOGRAPH: the person is RE-POSED (stance and hands change; face, clothing, setting and light are kept), so it is the wrong tool when the rest must stay untouched. RAW MODEL ACCESS: ' + RAW_TOOL_NOTE + ' Seconds. Spends credits.',
|
|
18582
18619
|
inputSchema: {
|
|
18583
18620
|
prompt: z.string().optional().describe('REQUIRED on every model EXCEPT the pose rows below. the full image prompt — subject, composition, lighting, and any on-image ad text. ON A POSE MODEL (product-in-hand / try-on) THIS IS EXTRA DIRECTION AND IT IS OPTIONAL — leave it out and the pose is built for you. If you do write one, DESCRIBE THE POSE ("she holds the bottle upright in her right hand at chest height, label to camera"); do NOT phrase it as a swap ("replace the mug with the bottle"), which is REFUSED for free, because the product then comes out the size of whatever it replaced — a 30ml bottle rendered mug-sized in testing.'),
|
|
18584
18621
|
refImages: z.array(z.string()).optional().describe('local file paths or URLs of product/logo references to composite in. ON THE POSE MODELS — any row hermoso_capabilities marks `needsRefs` with a `refsMax`, such as putting your product in someone’s hands or a virtual try-on — THE ORDER IS THE CONTRACT AND IT IS NOT A COMPOSITE: refImages[0] is the PERSON photo, and the rest (up to `refsMax` minus one) are the product or garment photos. Reversed, you get the product wearing the person. A 4th product is dropped and the reply says so. ' + LIKENESS_TERMS),
|
|
@@ -18852,7 +18889,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18852
18889
|
server.group('create');
|
|
18853
18890
|
server.registerTool('render_ad', {
|
|
18854
18891
|
title: 'Render ad video',
|
|
18855
|
-
description: 'Use this when the user
|
|
18892
|
+
description: 'Use this when the user asks for a video ad, Reel, TikTok ad, UGC video or commercial for their business or product. It renders a finished ad on a real video model, on their brand (real product photos, brand voice), as an mp4 ready to post: timed shots, clean speech, text set in post, no music, captions or end card unless asked. Pass plan_ad’s full output as `creative` (plan_ad turns a one-line ask into the concept). UGC and cinematic plans render from a painted STORYBOARD (a cinematic spot paints a location anchor first, and a hero when someone is on camera); a product-only spot renders from the real product photo, and board:true or foundations:\'choose\' adds an identity sheet, a moodboard and a board. A plan that fits one clip of the render model renders as one continuous pass; a longer one as STITCHED ACTS (the fewest balanced clips), never time-compressed. Most models cap a clip at 15s and the longest at 30s: dryRun:true shows the act split free, before spending. CAST A SAVED CREATOR with `creator` (list_creators) so the same person stars in every ad; otherwise each render casts a new face. The user’s own pictures (creative.inserts, or `inserts`) are laid in full screen at their seconds. 1–3 min; poll get_job while it renders. Spends credits.',
|
|
18856
18893
|
inputSchema: {
|
|
18857
18894
|
creative: z.object({}).passthrough().describe('the FULL structured output of plan_ad (must contain video_storyboard)'),
|
|
18858
18895
|
creator: z.string().optional().describe('CAST A SAVED CREATOR in this ad — their id from list_creators, or the name you know them by (“Sarah”), or a PRESET AI creator from list_creators presets by exact name or id (free, no generation). Their saved portrait becomes the on-camera identity for the whole spot, so the same face carries across every act and across every ad you render for this brand (casting skips the portrait we would otherwise generate). Omit to let the ad cast a fresh person — EXCEPT for a CREATOR account (onboarded from their own @handle): their own saved likeness is cast by default when the plan has a person on camera and the account is on a paid Hermoso plan (a free account gets a fresh AI person), and the read-back says `default:true`; pass "none" to render without them. Refused for free, with nothing rendered, if the name matches nobody or more than one creator, if an explicitly named creator is cast on a plan with nobody on camera, or if a REAL person is cast on a free plan. ' + LIKENESS_TERMS),
|
|
@@ -18938,7 +18975,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
18938
18975
|
|
|
18939
18976
|
server.registerTool('make_template_ad', {
|
|
18940
18977
|
title: 'Make template ad',
|
|
18941
|
-
description: "
|
|
18978
|
+
description: "Use this when the user asks for a quick, low-cost ad or post built from their words and pictures: a slideshow, carousel, iMessage or ChatGPT chat, Notes, kinetic type, myth vs fact, an app tour, or their own HTML design. Rendered from HTML, no AI model, ~30s, a couple of credits, ready to post. Presets are SHORTCUTS; 'custom' is YOUR OWN design as config.html (+ css), so no layout, type, colour or motion is unsupported. custom: { html, css?, size? ('9:16' default | '4:5' | '1:1' | '16:9' | any 'W:H' | {w,h} px), durationSeconds? (1-60 = VIDEO; CSS/SVG animation and <video> are frame-stepped, scripts stripped), slides?:[{html, css?}] (2-35 = carousel) }; {{logo}} {{brandName}} {{domain}} {{accent}} fill from the brand; images and fonts load by public https URL; notes[] lists what failed to load. YOU write preset copy: short, casual, believable, finished phrases within budget. Preset ids and their fields are listed on `config`. config.music on a VIDEO: omit for a free library bed matched to its mood when the library is stocked (hermoso_capabilities hasMusic; else only its own sound effects), 'off' for silence, or words (a mood or description) to compose one for a flat fee.",
|
|
18942
18979
|
inputSchema: {
|
|
18943
18980
|
config: z.object({}).passthrough().describe("MUST include config.template: 'custom' or a preset id, plus its fields. PRESETS: 'slideshow' (IMAGES, TikTok photo mode / Reels 1080x1920, or size:'4:5' feed carousels; no branding): { slides:[{text, sub?, image?, blur?, background?, position?}] (2-35; words never rewritten), style? ('tiktok-classic'|'clean-minimal'|'note-style' or a look in words), textStyle?, video?:true (+ an MP4, which takes config.music like any video) }; 2 credits, +1 per slide past 5, +2 for the MP4. 'imessage-chat' (VIDEO ~15s): { thread:{contactName, messages:[{from:'them'|'me', text?, product?:{image,title,domain}}]}, theme?, endCard }. 'chatgpt-chat' (VIDEO): { question, answer (may **bold** the brand), productImage?, endCard }. 'apple-notes' (VIDEO): { title, lines[], theme?, endCard }. 'value-prop' (VIDEO ~17s): { hook ≤40ch, claims[3-5 ≤34ch], productImages[2-3], palette[], endCard }. 'static-mockup' (IMAGE): { style:'imessage'|'notes'|'card', size?:{w,h}, ...fields }. 'airdrop-carousel' (VIDEO): { brandName, products:[{image, title?}] (3-16), endCard }. 'app-ui-tour' (VIDEO): { hook?, appName, iconImage?, beats:[{screenImage, caption}] (2-6), endCard }. 'imessage-cascade' (VIDEO): { notifications:[{sender, text}] (4-8), backgroundImage?, endCard }. 'photo-grid' (VIDEO): { title?, photos:[{image, label?}] (4-9), endCard }. 'vignette' (VIDEO): { hook, lines[2-4 ≤40ch], heroImage, endCard }. 'kinetic-type' (VIDEO, own SFX): { phrases[3-6 ≤34ch], productImages?[≤4], endCard }. 'myth-vs-fact' (VIDEO with a real VOICEOVER, small extra charge): { pairs:[{myth ≤50ch, fact ≤60ch}] (2-4; [brackets] accent), endCard }, real truths only. 'carousel' (IMAGES, 5-10 branded 1080x1080): { cover:{hook?, title}, slides:[{headline, support?, stat?:{value, label}}] (3-8), cta:{headline, cta?, domain?}, productImage?, logo? }. endCard = { headline, cta, domain?, logo?, color? }; palette and fontStack optional."),
|
|
18944
18981
|
},
|
|
@@ -19331,7 +19368,7 @@ function buildTools(rawServer, opts = {}, sink = null) {
|
|
|
19331
19368
|
|
|
19332
19369
|
server.registerTool('generate_video', {
|
|
19333
19370
|
title: 'Generate video',
|
|
19334
|
-
description: 'Use this when the user
|
|
19371
|
+
description: 'Use this when the user asks for a video clip, b-roll or a shot from a description, or names a video model. For a finished video ad, Reel, TikTok ad or UGC video for their business, use render_ad. It renders one continuous clip on a real video model and returns its mp4 URL. A clip runs up to the model’s longest listed duration (the longest takes a multi-beat 30s spot in ONE take with native audio), so never assume an 8–10s cap or stitch what fits one clip; durationSeconds must be one of the model’s durations in hermoso_capabilities. NAME A MODEL in `model` to get it: an unnamed render uses a narrower auto-pool. 1–3 min. refImage anchors the first frame; ttsScript adds a voiceover. AUDIO IS NOT FREE BY DEFAULT: a clip with no audio of its own gets a music bed composed and CHARGED on top (see musicMood and audio), which can cost as much as a cheap draft clip. refVideo (a clip URL) EDITS that clip instead, keeping its canvas and length. RAW MODEL ACCESS: by default small guards are added (label safety with no reference image, a negative prompt where the model takes one, reference lines) and hex colours become names; ' + RAW_TOOL_NOTE + ' Spends credits (blocked on the Starter plan).',
|
|
19335
19372
|
inputSchema: {
|
|
19336
19373
|
prompt: z.string().optional().describe('the video prompt / shot description (for a refVideo edit, this is the transformation instruction); optional with `shots`'),
|
|
19337
19374
|
raw: z.boolean().optional().describe('Send the prompt to the model exactly as written, without the wording Hermoso usually adds (packaging and label guidance, reference lines, colour names for hex codes, quality hints). Use it when you want the model\'s own interpretation rather than Hermoso\'s render craft. Two vendor-required fixes still apply: extra @ImageN tokens are dropped and an over-long prompt is trimmed at a sentence (on Veo it is refused with the length and the cap instead). Billing, durable delivery and per-model validation are unchanged.'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.431",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "AI marketing agent and AI ad generator, run from your own AI agent. 922 tools. Publishing, scheduling, ad campaign management, comments, DMs and analytics cost no credits on every plan; credits are only for generating creative and for Ad Spy research. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads, ChatGPT Ads and AppLovin Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky, Telegram and Google Business Profile. INSTAGRAM DM AUTOMATIONS: a keyword comment, a DM, a story reply or an ig.me link gets an automatic private reply from the brand. 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",
|