hermoso 0.1.248 → 0.1.249
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/README.md +2 -2
- package/mcp/http.mjs +14 -1
- package/mcp/tools.mjs +24 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ scripts. Research the ads already winning in a market, generate finished image &
|
|
|
5
5
|
composited in, copy + CTA included), publish them to your own social channels, and build & manage the ad
|
|
6
6
|
campaigns behind them — all over [MCP](https://modelcontextprotocol.io) tools, a CLI, or installable Claude skills.
|
|
7
7
|
|
|
8
|
-
**
|
|
8
|
+
**840 tools.** `tools/list` is always the authoritative set; `hermoso_capabilities` (free) returns the live model
|
|
9
9
|
catalog with exact per-render credit costs plus the full capability map.
|
|
10
10
|
|
|
11
11
|
**What it connects to.** Ad platforms: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads,
|
|
@@ -171,7 +171,7 @@ block entirely if you signed in above; it is there for CI, where the process can
|
|
|
171
171
|
|
|
172
172
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
173
173
|
|
|
174
|
-
### What the
|
|
174
|
+
### What the 840 tools cover
|
|
175
175
|
|
|
176
176
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
177
177
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/http.mjs
CHANGED
|
@@ -106,6 +106,8 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl, onSessionStar
|
|
|
106
106
|
// past that a GET is answered 503 + Retry-After, which costs the client a reconnect and costs tool calls nothing
|
|
107
107
|
// (they are POSTs and never touch this counter). Pinned by tools/mcp-get-stream-cap-check.mjs.
|
|
108
108
|
const GET_STREAM_MS = Math.max(1000, Number(process.env.MCP_GET_STREAM_MS || 5 * 60e3));
|
|
109
|
+
const MCP_CALL_WALL_MS = Number(process.env.MCP_CALL_WALL_MS) || 10 * 60 * 1000; // a tools/call still open after this is ended, so a ghost cannot pin a deploy or a slot
|
|
110
|
+
const inflightNameOf = (body) => { const msgs = Array.isArray(body) ? body : [body]; return msgs.map(m => m?.method === 'tools/call' ? String(m.params?.name || '?').slice(0, 60) : '').filter(Boolean).join(',') || '?'; };
|
|
109
111
|
const GET_STREAM_MAX = Math.max(1, Number(process.env.MCP_GET_STREAM_MAX || 64));
|
|
110
112
|
let openGetStreams = 0;
|
|
111
113
|
const streamStats = () => ({ open: openGetStreams, max: GET_STREAM_MAX, lifeMs: GET_STREAM_MS });
|
|
@@ -386,7 +388,18 @@ export function mountRemoteMcp(app, { verifyBearer, publicBaseUrl, onSessionStar
|
|
|
386
388
|
// forgeable value is exactly the hole resolveWs exists to close. The workspace a hosted connector acts in is
|
|
387
389
|
// pinned SERVER-SIDE on the agent key (use_brand → /api/keys/brand, membership-checked) and re-authorized by
|
|
388
390
|
// resolveWs on every request, so it resolves identically here and over stdio without this transport naming it.
|
|
389
|
-
|
|
391
|
+
// A TOOLS/CALL HAS A WALL CLOCK (2026-09-15). A POST /mcp tools/call:search_meta_ads sat "open" for 20 minutes —
|
|
392
|
+
// long past every upstream timeout in the tool — and blocked deploy-safe-check for as long, because the inflight
|
|
393
|
+
// row is released only when THIS response finishes. Whatever left it hanging (a client that vanished mid-stream,
|
|
394
|
+
// a promise that never settled), the response must end. MCP_CALL_WALL_MS ends it; the SDK's own error path has
|
|
395
|
+
// already had every chance by then, so the client sees a closed stream, which is what it would have concluded
|
|
396
|
+
// anyway. Never armed for initialize/list, which answer in milliseconds; cleared the moment the call settles.
|
|
397
|
+
const wall = methodsOf(req.body).includes('tools/call')
|
|
398
|
+
? 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)
|
|
399
|
+
: null;
|
|
400
|
+
if (wall && typeof wall.unref === 'function') wall.unref();
|
|
401
|
+
try { await mcpCtx.run({ token, remote: true, client: entry.client || '' }, () => entry.transport.handleRequest(req, res, req.body)); }
|
|
402
|
+
finally { if (wall) clearTimeout(wall); }
|
|
390
403
|
});
|
|
391
404
|
|
|
392
405
|
console.error(`[mcp-remote] mounted at ${BASE || '(set HERMOSO_PUBLIC_URL)'}/mcp`);
|
package/mcp/tools.mjs
CHANGED
|
@@ -230,6 +230,8 @@ export const MCP_INSTRUCTIONS = [
|
|
|
230
230
|
'• PAID ADS, LEAD FORMS, CLICK-TO-WHATSAPP, ANALYTICS: not in your starting tool list (size) but one call away — find_tools (search every tool by task) then call_tool (run it by name). enable_tools([\'ads\']) loads the group where the host reloads its list. All created PAUSED and read back. A tool missing from your list never means the feature is missing.',
|
|
231
231
|
'• INSTAGRAM, two connectors, one channel: TWO WAYS AN INSTAGRAM ACCOUNT CONNECTS, SAME FEATURES: through Meta (the account is linked to a Facebook Page and comes with that Page — this is also the only path with ads) or directly through the Instagram connector (the account signs in on instagram.com by itself, no Facebook Page or Meta login — right for people who run several Instagram accounts under different logins). Either way it is one `instagram` channel with publishing, media, post and account insights, comments and Instagram Direct DMs; a Page-linked account is chosen with pageId, a direct account (or one of several) with account = an @handle or id from list_connector_accounts("instagram"). "Not connected to Meta" never means "no Instagram" — check the Instagram connector too.',
|
|
232
232
|
'• ADS, the tool names: create_meta_campaign / _adset / _ad, create_google_ads_campaign / _ad_group / _ad and the TikTok, LinkedIn, Pinterest, Reddit, Microsoft and OpenAI equivalents; meta_insights, google_ads_report and the per-platform reports. Everything is created PAUSED and read back before it is described.',
|
|
233
|
+
// Sits AFTER the ADS bullet on purpose: the 2 KB head every area must survive is full (tools/mcp-roster-connector-scope-check), and a first-call hint is worth less than a whole area.
|
|
234
|
+
'NOTHING SET UP YET? research_ads on any domain, or generate_image with useBrand:false, need no brand, account or upload.',
|
|
233
235
|
// ── YOUR ROSTER IS NOT THE PRODUCT (2026-08-26) ──────────────────────────────────────────────────────────────
|
|
234
236
|
// The roster is scoped to the accounts this workspace has connected, because a tool for an unconnected provider
|
|
235
237
|
// can only answer 401. That is a saving, and it has ONE failure mode, which this line exists to prevent: an
|
|
@@ -18239,6 +18241,28 @@ function memoryNoteVerdict(text) {
|
|
|
18239
18241
|
const lines = d.creators.map(c => `• @${c.handle} (${c.platform})${c.name && c.name !== c.handle ? ` — ${c.name}` : ''}: ${c.posts} post${c.posts === 1 ? '' : 's'} in this niche, median ${fmt(c.medianPlays)} views, ${c.engagementRate == null ? 'engagement unknown' : `${(100 * c.engagementRate).toFixed(1)}% engagement`}${c.followers != null ? `, ${fmt(c.followers)} followers` : ''}, score ${c.score}${c.top?.link ? ` — top: ${c.top.link}` : ''}${c.profileUrl ? ` — ${c.profileUrl}` : ''}`);
|
|
18240
18242
|
return ok(`${d.note}\n${lines.join('\n')}`, d);
|
|
18241
18243
|
}));
|
|
18244
|
+
// TOPIC SEARCH (2026-09-15, Dave: "search higgsfield, but not their ads themselves, just posts about them … similarly
|
|
18245
|
+
// just broad things like coffee"): the posts ABOUT a subject from anyone, all three organic platforms in one call.
|
|
18246
|
+
// find_creators is this same search one step later (posts folded into people); the per-platform search_* tools are
|
|
18247
|
+
// it one platform at a time.
|
|
18248
|
+
server.registerTool('search_posts', {
|
|
18249
|
+
title: 'Top posts about any topic, brand or product',
|
|
18250
|
+
description: 'The POSTS people make ABOUT a subject — a brand ("higgsfield"), a product, a hobby ("coffee"), a hashtag ("#homecafe") — from whoever posted them, across organic TikTok, Instagram Reels and YouTube in ONE call, ranked by views. Not the brand\'s own ads (search_meta_ads / research_ads) and not the people (find_creators folds these same posts into creators): use it to see what is actually being posted and watched about a subject, to find clips worth cloning (clone_video), and to read the hooks and angles an audience already responds to. About one credit per platform searched (one query each by default; `queries` adds "best X" / "X review" / #tag variants, each a paid call); repeats inside 20 minutes are free.',
|
|
18251
|
+
inputSchema: {
|
|
18252
|
+
topic: z.string().describe('subject, brand, product or hashtag — "higgsfield", "coffee", "#homecafe"'),
|
|
18253
|
+
platforms: z.array(z.enum(['tiktok', 'instagram', 'youtube'])).optional().describe('default all three'),
|
|
18254
|
+
limit: z.number().optional().describe('posts per platform, 1–60 (default 24)'),
|
|
18255
|
+
queries: z.number().optional().describe('query variants per platform, 1–4 (default 1); each is a paid search call'),
|
|
18256
|
+
},
|
|
18257
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
18258
|
+
}, wrap(async (a) => {
|
|
18259
|
+
const d = await apiPost('/api/posts/search', a);
|
|
18260
|
+
if (!d.shown) return ok(`${d.summary} Nothing matched — try broader words or add platforms.`, d);
|
|
18261
|
+
const fmt = (v) => v >= 1e6 ? `${(v / 1e6).toFixed(1)}M` : v >= 1e3 ? `${(v / 1e3).toFixed(v >= 1e5 ? 0 : 1)}K` : String(v);
|
|
18262
|
+
const lines = [];
|
|
18263
|
+
for (const p of d.platforms) for (const t of (d.byPlatform?.[p] || [])) lines.push(`• [${p}] ${t.handle ? '@' + t.handle : ''}${t.plays ? ` ${fmt(t.plays)} views` : t.likes ? ` ${fmt(t.likes)} likes` : ''}: ${String(t.desc || '').slice(0, 120)}${t.link ? ` — ${t.link}` : ''}`);
|
|
18264
|
+
return ok(`${d.note}\n${lines.join('\n')}`, d);
|
|
18265
|
+
}));
|
|
18242
18266
|
server.registerTool('search_tiktok', {
|
|
18243
18267
|
_meta: openaiMeta(AD_SPY_URI, 'Searching TikTok videos…', 'Found TikTok videos'),
|
|
18244
18268
|
title: 'Search TikTok',
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.249",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI ad studio and marketing MCP server with
|
|
5
|
+
"description": "AI ad studio and marketing MCP server with 840 tools. Research the ads already running in any market, generate finished image, video and UGC avatar ads, publish and schedule them to your own channels, build and manage the ad campaigns behind them, and read what they achieved. AD PLATFORMS: Meta, Google Ads, TikTok Ads, LinkedIn Ads, Reddit Ads, X Ads, Pinterest Ads, Snapchat Ads, Microsoft Advertising, Apple Search Ads and ChatGPT Ads, plus product feeds in Google Merchant Center. PUBLISHING AND SCHEDULING: Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn, Pinterest, Bluesky and Telegram. AD RESEARCH: the Meta, Google and LinkedIn ad libraries plus organic TikTok, Instagram, YouTube, Threads and Reddit. ANALYTICS: Google Analytics 4, Google Search Console and every connected platform's own post and campaign insights. Also brand onboarding, 50+ image and video generation models, ad scoring, competitor teardowns, Google Drive and OneDrive, a CLI and installable Claude skills.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|