hermoso 0.1.190 → 0.1.193

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 (2) hide show
  1. package/mcp/tools.mjs +49 -6
  2. package/package.json +1 -1
package/mcp/tools.mjs CHANGED
@@ -1812,6 +1812,8 @@ export function parseToolScope(raw) {
1812
1812
  // everything on 2026-08-16, so a caller who genuinely wants all 436 tools needs a way to say so that is not
1813
1813
  // "list every group by name and hope none was added since".
1814
1814
  if (asked.includes('all')) return { groups: [...TOOL_GROUP_NAMES] };
1815
+ if (asked.includes('directory-full')) return { groups: [...DEFAULT_TOOL_GROUPS], directory: 'full' }; // the submitted listing: everything but the billing tools — see DIRECTORY_BILLING_TOOLS
1816
+ if (asked.includes('directory')) return { groups: [...DIRECTORY_GROUPS], directory: 'scoped' }; // the fully scoped fallback — see DIRECTORY_GROUPS
1815
1817
  const unknown = asked.filter((v) => !TOOL_GROUP_NAMES.includes(v));
1816
1818
  if (unknown.length) {
1817
1819
  // No tool COUNT in this message: a committed count goes stale (four different wrong numbers shipped at once
@@ -1825,6 +1827,39 @@ export function parseToolScope(raw) {
1825
1827
  // a HOST rule, not a capability we removed: every other surface still offers both.
1826
1828
  export const WITHHELD_FROM_WIDGET_HOSTS = new Set(['buy_credits', 'upgrade_plan', 'set_auto_reload']);
1827
1829
 
1830
+ // ── THE DIRECTORY ROSTER (2026-09-02) — `?tools=directory` ─────────────────────────────────────────────────────
1831
+ // Anthropic's Software Directory Policy prohibits "software that uses AI models to generate images, video, or
1832
+ // audio content" (design aids excepted) and software that "executes financial transactions on behalf of users".
1833
+ // Hermoso as a whole does both, so the Claude Connectors Directory listing is a SCOPED server: the `create` group
1834
+ // is out, every tool that calls a generative image/video/audio model is out wherever it lives, and the three tools
1835
+ // that move money are out (the same three ChatGPT is denied). Dave, 2026-09-02: "We're way more than ad generation,
1836
+ // we can focus on all the other huge benefits like our organic posting, ads management, analytics, dms, etc."
1837
+ // It is a CAGE, deliberately, unlike every other scope: enable_tools may not widen it into `create` or `all`,
1838
+ // because the listing's compliance acknowledgments are only honest if no path on the connection reaches a
1839
+ // generative model. Pinned by tools/directory-roster-check.mjs.
1840
+ export const DIRECTORY_GROUPS = ['research', 'channels', 'channel_admin', 'ads', 'analytics', 'files', 'workspace'];
1841
+ export const DIRECTORY_BANNED_GROUPS = ['create'];
1842
+ // Generative-media tools that live OUTSIDE `create` (post-production and variation lanes) plus the money movers.
1843
+ // Kept as an explicit set rather than a regex so a new generative tool has to be named here — a regex that
1844
+ // happened to miss it would list it in the directory.
1845
+ export const WITHHELD_FROM_DIRECTORY = new Set([
1846
+ 'buy_credits', 'upgrade_plan', 'set_auto_reload',
1847
+ 'generate_image', 'generate_video', 'generate_avatar', 'generate_voice', 'render_ad', 'plan_ad', 'plan_variations',
1848
+ 'make_thumbnail', 'make_explainer', 'product_sizzle', 'dub_video', 'change_voice', 'edit_video', 'recast_motion',
1849
+ 'upscale_video', 'reframe_video', 'multiply_ad', 'remix_static', 'stitch_video', 'fix_beat',
1850
+ ]);
1851
+ // TWO DIRECTORY MODES (Dave, 2026-09-02, after the directory turned out to list Tofu Ads — an AI ad-image generator
1852
+ // — under the policy's design-asset carve-out): `directory` is the fully scoped cage above; `directory-full` keeps
1853
+ // generation (an ad-creation workflow with the brand's own product and copy, the carve-out's shape) and withholds
1854
+ // ONLY the three tools that move money, which is the listing that was actually submitted. Both are cages for what
1855
+ // they withhold: enable_tools never re-lists a billing tool on either.
1856
+ export const DIRECTORY_BILLING_TOOLS = new Set(['buy_credits', 'upgrade_plan', 'set_auto_reload']);
1857
+ export const toolHeldBackByDirectory = (name, group, ctx) => {
1858
+ if (!ctx?.directory) return false;
1859
+ if (ctx.directory === 'full') return DIRECTORY_BILLING_TOOLS.has(name);
1860
+ return WITHHELD_FROM_DIRECTORY.has(name) || DIRECTORY_BANNED_GROUPS.includes(group);
1861
+ };
1862
+
1828
1863
  // ── A REFERENCE LOOKUP MUST NOT BE AN APP SURFACE (2026-08-24) ─────────────────────────────────────────────────
1829
1864
  // MEASURED IN CHATGPT, not theorised: "make a 4 second vertical video ad for a coffee roaster" produced a
1830
1865
  // full-height scrolling MODEL CATALOG card ahead of the ad, and the catalog is ~70 rows, so the thing the user
@@ -1969,6 +2004,10 @@ const makeEnableToolsHandler = (ctx) => async ({ groups }) => {
1969
2004
  const want = (Array.isArray(groups) ? groups : []).map((g) => String(g || '').trim().toLowerCase()).filter(Boolean);
1970
2005
  if (!want.length) return { content: [{ type: 'text', text: "Name at least one group to turn on, e.g. groups:['ads']." }], isError: true };
1971
2006
  const expand = want.includes('all') ? [...TOOL_GROUP_NAMES] : want;
2007
+ // THE DIRECTORY CAGE (2026-09-02): a connection scoped for the Claude directory may not be widened into the
2008
+ // generative group, by name or through 'all' — see DIRECTORY_GROUPS. The way to those tools is the app or the
2009
+ // unscoped server URL, and the refusal says so instead of silently trimming the ask.
2010
+ if (ctx.directory === 'scoped' && (want.includes('all') || expand.some((g) => DIRECTORY_BANNED_GROUPS.includes(g)))) return { content: [{ type: 'text', text: `This connection is the Claude directory listing, which deliberately carries no image, video or audio generation tools and no billing tools. Those live in the Hermoso app (https://app.hermoso.ai) and on the unscoped server URL https://app.hermoso.ai/mcp — not on this connection. Groups you can turn on here: ${DIRECTORY_GROUPS.join(', ')}.` }], isError: true };
1972
2011
  const unknown = expand.filter((g) => !TOOL_GROUP_NAMES.includes(g));
1973
2012
  // REFUSED BY NAME, never dropped: silently ignoring a typo would report success and leave the agent looking
1974
2013
  // for a tool that is still disabled — the failure this whole mechanism exists to avoid.
@@ -1989,6 +2028,7 @@ const makeEnableToolsHandler = (ctx) => async ({ groups }) => {
1989
2028
  // says how many and why, because a group that turns on "8 tools" when the agent expected 240 with no
1990
2029
  // explanation is the [[prompt-rosters-go-stale]] failure — the agent concludes the capability is missing.
1991
2030
  if (toolHeldBackByConnectors(name, ctx.conn)) { heldBack++; continue; }
2031
+ if (toolHeldBackByDirectory(name, grp, ctx)) continue; // the cage holds across a group flip too
1992
2032
  try { h.enable(); n++; } catch {}
1993
2033
  }
1994
2034
  }
@@ -2048,6 +2088,7 @@ function newToolScope(opts) {
2048
2088
  enabledGroups: asked, groupOf: Object.create(null), handleOf: Object.create(null),
2049
2089
  conn: opts.connectors || null,
2050
2090
  widgetHost: !!opts.widgetHost,
2091
+ directory: opts.directory === 'full' ? 'full' : (opts.directory ? 'scoped' : false),
2051
2092
  };
2052
2093
  }
2053
2094
  // THE THREE REASONS A REGISTERED TOOL IS HELD BACK. Applied identically on the build path, the replay path and
@@ -2067,6 +2108,7 @@ function applyToolGates(h, name, group, ctx, opts) {
2067
2108
  if (!ctx.enabledGroups.has(group)) { try { h.disable(); } catch {} }
2068
2109
  if (WITHHELD_FROM_WIDGET_HOSTS.has(name) && opts.widgetHost) { try { h.disable(); } catch {} }
2069
2110
  if (toolHeldBackByConnectors(name, ctx.conn)) { try { h.disable(); } catch {} }
2111
+ if (toolHeldBackByDirectory(name, group, ctx)) { try { h.disable(); } catch {} } // (4) the directory cage — see DIRECTORY_GROUPS
2070
2112
  }
2071
2113
 
2072
2114
  // Replay the cached canon onto a fresh server. This is the whole hot path for every session after the first.
@@ -12022,18 +12064,19 @@ function buildTools(rawServer, opts = {}, sink = null) {
12022
12064
  }));
12023
12065
  server.registerTool('tiktok_ads_pixel_stats', {
12024
12066
  title: 'Read how often a TikTok pixel\u2019s events fired',
12025
- description: 'Event-level statistics for one TikTok pixel over a date range — how many times each event actually fired, which is how you tell a pixel that is installed and working from one that is installed and silent. Takes the pixel CODE (not the id) as reported by list_tiktok_ads_pixels. Read-only, free.',
12067
+ description: 'Event-level statistics for one TikTok pixel over a date range — how many times each event actually fired, which is how you tell a pixel that is installed and working from one that is installed and silent. Pass the pixelId or the pixelCode from list_tiktok_ads_pixels; default range is the last 7 days (UTC), at most 30. Counts are split into browser vs server per event. Read-only, free.',
12026
12068
  inputSchema: {
12027
12069
  advertiserId: z.string().optional().describe('from list_tiktok_ads_accounts'),
12028
- pixelCode: z.string().describe('the pixelCode from list_tiktok_ads_pixels \u2014 NOT the pixelId'),
12029
- startDate: z.string().optional().describe('YYYY-MM-DD'),
12030
- endDate: z.string().optional().describe('YYYY-MM-DD'),
12070
+ pixelId: z.string().optional().describe('the pixelId from list_tiktok_ads_pixels'),
12071
+ pixelCode: z.string().optional().describe('or the pixelCode from list_tiktok_ads_pixels \u2014 resolved to its id'),
12072
+ startDate: z.string().optional().describe('YYYY-MM-DD (UTC); default 6 days before endDate; the range may span at most 30 days'),
12073
+ endDate: z.string().optional().describe('YYYY-MM-DD (UTC); default today'),
12031
12074
  },
12032
- outputSchema: { advertiserId: z.string().optional(), pixelCode: z.string().optional(), stats: z.any().optional() },
12075
+ outputSchema: { advertiserId: z.string().optional(), pixelId: z.string().optional(), pixelCode: z.string().nullable().optional(), startDate: z.string().optional(), endDate: z.string().optional(), stats: z.any().optional() },
12033
12076
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
12034
12077
  }, wrap(async (a) => {
12035
12078
  const d = await apiGet('/api/tiktok-ads/pixel-stats', a);
12036
- return ok(`Pixel ${d.pixelCode} event stats.`, d);
12079
+ return ok(`Pixel ${d.pixelCode || d.pixelId} event stats ${d.startDate} \u2192 ${d.endDate}: ${(d.stats || []).length ? d.stats.map((x) => `${x.pixel_event_type || x.custom_event_type}: ${x.total_count} (browser ${x.browser_event_total_count}, server ${x.server_event_total_count})`).join('; ') : 'no events received in that range'}.`, d);
12037
12080
  }));
12038
12081
  server.registerTool('list_tiktok_ads_custom_conversions', {
12039
12082
  title: 'List TikTok Custom Conversions',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hermoso",
3
- "version": "0.1.190",
3
+ "version": "0.1.193",
4
4
  "mcpName": "io.github.hermoso-ai/hermoso",
5
5
  "description": "AI ad studio and marketing MCP server with 746 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",