hermoso 0.1.113 → 0.1.114
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/tools.mjs +39 -8
- 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
|
+
**522 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
|
**It is not all-or-nothing.** Research, creation, publishing/scheduling and ads management are four *independent*
|
|
@@ -53,7 +53,7 @@ Cursor / Codex — add to `mcp.json` (Codex uses the TOML equivalent):
|
|
|
53
53
|
|
|
54
54
|
Then ask your agent: *“Generate an image ad with Hermoso.”*
|
|
55
55
|
|
|
56
|
-
### What the
|
|
56
|
+
### What the 522 tools cover
|
|
57
57
|
|
|
58
58
|
**Ad spy / research** — `find_competitors`, `competitor_teardown`, `pull_competitor_ads`, `research_ads`; the
|
|
59
59
|
Meta / Google / LinkedIn ad libraries (`search_meta_ads`, `search_google_ads`, `search_linkedin_ads`); organic
|
package/mcp/tools.mjs
CHANGED
|
@@ -2241,13 +2241,21 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
2241
2241
|
}));
|
|
2242
2242
|
server.registerTool('x_post_metrics', {
|
|
2243
2243
|
title: 'Read performance of a post on X',
|
|
2244
|
-
description:
|
|
2245
|
-
inputSchema: {
|
|
2246
|
-
|
|
2244
|
+
description: "THE X ANALYTICS TOOL THAT WORKS — impressions, likes, reposts, replies, quotes and bookmarks for any post, PLUS the advertiser numbers (link clicks, profile clicks, engagements) for YOUR OWN posts published in the last 30 days. X serves those private metrics on this same lookup with the user-context connection you already have; that is X's own design, not a workaround. Prefer this over x_post_insights, whose endpoint family X has retired. If a post is deleted, protected or suspended, X answers with no data at all and this says so — that is MISSING DATA, never zero engagement, and must never be reported as a measured zero. Costs a small number of credits (X bills per API read). Needs X connected.",
|
|
2245
|
+
inputSchema: {
|
|
2246
|
+
id: z.string().describe('the numeric X post id — the last part of the post URL'),
|
|
2247
|
+
publishedAt: z.number().optional().describe('epoch ms the post went out, if known — lets the private owned-post metrics be requested only inside X\'s 30-day window instead of costing a refused call'),
|
|
2248
|
+
},
|
|
2249
|
+
outputSchema: { id: z.string().optional(), found: z.boolean().optional(), note: z.string().optional(), text: z.string().optional(), postedAt: z.string().nullable().optional(), url: z.string().optional(), impressions: z.number().nullable().optional(), likes: z.number().nullable().optional(), reposts: z.number().nullable().optional(), replies: z.number().nullable().optional(), quotes: z.number().nullable().optional(), bookmarks: z.number().nullable().optional(), urlClicks: z.number().nullable().optional(), profileClicks: z.number().nullable().optional(), engagements: z.number().nullable().optional(), organicImpressions: z.number().nullable().optional(), organicLikes: z.number().nullable().optional(), privateMetrics: z.boolean().optional(), costCredits: z.number().optional() },
|
|
2247
2250
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
2248
2251
|
}, wrap(async (a) => {
|
|
2249
|
-
const d = await apiGet('/api/x/metrics', { id: a.id });
|
|
2250
|
-
|
|
2252
|
+
const d = await apiGet('/api/x/metrics', { id: a.id, publishedAt: a.publishedAt });
|
|
2253
|
+
// The private half is reported only when X actually served it — an absent link-click count means "outside the
|
|
2254
|
+
// 30-day window or not your post", which is not zero clicks and must not be printed as a number.
|
|
2255
|
+
const priv = d.privateMetrics
|
|
2256
|
+
? ` · ${d.urlClicks ?? '?'} link clicks, ${d.profileClicks ?? '?'} profile clicks, ${d.engagements ?? '?'} engagements`
|
|
2257
|
+
: '';
|
|
2258
|
+
return ok(`${d.impressions ?? '?'} impressions, ${d.likes ?? '?'} likes, ${d.reposts ?? '?'} reposts, ${d.replies ?? '?'} replies${priv} — ${d.url}`, d);
|
|
2251
2259
|
}));
|
|
2252
2260
|
server.registerTool('x_post_insights', {
|
|
2253
2261
|
title: 'Advertiser analytics for your own posts on X',
|
|
@@ -3792,6 +3800,21 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
3792
3800
|
+ (d.productUrl ? `\nListing: ${d.productUrl}` : ''), d);
|
|
3793
3801
|
}));
|
|
3794
3802
|
|
|
3803
|
+
server.registerTool('merchant_report', {
|
|
3804
|
+
title: 'Merchant Center reports — competitive visibility, best sellers, price benchmarks',
|
|
3805
|
+
description: "COMPETITOR INTELLIGENCE GOOGLE COMPUTES FOR FREE, for any retail brand with a Merchant Center. Ten report views, read with a SQL-like MCQL query. The three worth reaching for first: `competitive_visibility_competitor_view` (WHICH other domains appear beside this merchant, their rank, page-overlap and higher-position rates — i.e. who is actually beating them), `best_sellers_product_cluster_view` (what is SELLING on Google in a category right now, with an inventory_status saying whether this merchant even stocks it) and `price_insights_product_view` (Google's own suggested price plus the predicted click and conversion change). Also: competitive_visibility_benchmark_view, competitive_visibility_top_merchant_view, best_sellers_brand_view, price_competitiveness_product_view, product_view, product_performance_view, non_product_performance_view.\n\nMCQL IS NOT SQL: no OR, no subqueries, no GROUP BY, no aggregates, no JOIN, and ORDER BY may only name fields already in SELECT. Date filters use `WHERE date BETWEEN '2026-01-01' AND '2026-01-31'` or `WHERE date DURING LAST_30_DAYS`. Several views REQUIRE specific fields in SELECT and in WHERE — competitive visibility needs report_category_id + report_country_code + traffic_source, and top_merchant uniquely REQUIRES a date condition while FORBIDDING date in SELECT. An unknown view is refused by name with the list.\n\nNOT AVAILABLE ON MULTI-CLIENT (MCA) ACCOUNTS — pass a subaccount id. An empty result is often a normal state (price insights are only produced where Google predicts a substantial gain), and the note says which kind of empty it is. Read-only, free, no new permission.",
|
|
3806
|
+
inputSchema: {
|
|
3807
|
+
merchantCenterId: z.string().describe('from list_merchant_accounts — a STANDALONE account or a SUBACCOUNT, never a multi-client (MCA) account'),
|
|
3808
|
+
query: z.string().describe("the MCQL query, e.g. \"SELECT id, title, price, suggested_price, effectiveness FROM price_insights_product_view\""),
|
|
3809
|
+
pageSize: z.number().optional().describe('rows per response, 1–100000 (default 1000). 100,000 is GOOGLE\'s ceiling, not ours.'),
|
|
3810
|
+
pageToken: z.string().optional().describe('nextPageToken from a previous call — resend the IDENTICAL query and pageSize with it, which Google requires'),
|
|
3811
|
+
},
|
|
3812
|
+
outputSchema: { merchantCenterId: z.string().optional(), view: z.string().optional(), count: z.number().optional(), rows: z.array(z.any()).optional(), pageSize: z.number().optional(), truncated: z.boolean().optional(), nextPageToken: z.string().optional(), query: z.string().optional(), note: z.string().optional() },
|
|
3813
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
3814
|
+
}, wrap(async (a) => {
|
|
3815
|
+
const d = await apiPost('/api/merchant/report', { merchantCenterId: a.merchantCenterId, query: a.query, pageSize: a.pageSize, pageToken: a.pageToken });
|
|
3816
|
+
return ok(`${d.note}\n${JSON.stringify(d.rows || []).slice(0, 4000)}`, d);
|
|
3817
|
+
}));
|
|
3795
3818
|
server.registerTool('list_merchant_issues', {
|
|
3796
3819
|
description: "Read the account-level issues Google reports on a Merchant Center — the answer to \"why is this product not showing?\", which Google Ads reporting CANNOT give you, because a disapproved product has no impressions to report on. Needs merchantCenterId from list_merchant_accounts. Read-only and free.",
|
|
3797
3820
|
inputSchema: { merchantCenterId: z.string().describe('from list_merchant_accounts') },
|
|
@@ -4398,19 +4421,25 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
4398
4421
|
dimensions: z.array(z.string()).optional().describe('GA4 dimension names to break the metrics down by — omit for a single total row'),
|
|
4399
4422
|
startDate: z.string().optional().describe('YYYY-MM-DD or a GA4 relative date like "28daysAgo" (default 28daysAgo)'),
|
|
4400
4423
|
endDate: z.string().optional().describe('YYYY-MM-DD or "today" (default today)'),
|
|
4401
|
-
limit: z.number().optional().describe('rows, 1–
|
|
4424
|
+
limit: z.number().optional().describe('rows to return, 1–250000 (default 50). 250,000 is GOOGLE\'s per-request maximum, not ours — asking for more is silently capped there.'),
|
|
4425
|
+
offset: z.number().optional().describe('skip this many rows — how you page past `limit`. Use the nextOffset the previous call returns.'),
|
|
4402
4426
|
orderByMetric: z.string().optional().describe('sort by this metric — must be one of the metrics requested'),
|
|
4403
4427
|
orderDesc: z.boolean().optional().describe('default true (largest first) when orderByMetric is set'),
|
|
4404
4428
|
dimensionFilter: z.record(z.any()).optional().describe('NARROW THE REPORT — a GA4 FilterExpression, exactly one of andGroup | orGroup | notExpression | filter. Without it a report is the WHOLE property. Example: {"filter":{"fieldName":"sessionSource","stringFilter":{"matchType":"EXACT","value":"google"}}}; combine with {"andGroup":{"expressions":[…]}}. METRICS CANNOT BE USED HERE — use metricFilter.'),
|
|
4405
4429
|
metricFilter: z.record(z.any()).optional().describe('Filter the AGGREGATED rows, GA4\'s having-clause — same FilterExpression shape. Example: {"filter":{"fieldName":"sessions","numericFilter":{"operation":"GREATER_THAN","value":{"int64Value":"30"}}}}. DIMENSIONS CANNOT BE USED HERE — use dimensionFilter.'),
|
|
4406
4430
|
},
|
|
4407
|
-
outputSchema: { property: z.string().optional(), rows: z.array(z.any()).optional(), rowCount: z.number().optional(), dimensions: z.array(z.string()).optional(), metrics: z.array(z.string()).optional(), filtered: z.boolean().optional(), sampled: z.boolean().optional() },
|
|
4431
|
+
outputSchema: { property: z.string().optional(), rows: z.array(z.any()).optional(), rowCount: z.number().optional(), returned: z.number().optional(), offset: z.number().optional(), truncated: z.boolean().optional(), nextOffset: z.number().optional(), dimensions: z.array(z.string()).optional(), metrics: z.array(z.string()).optional(), filtered: z.boolean().optional(), sampled: z.boolean().optional() },
|
|
4408
4432
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
4409
4433
|
}, wrap(async (a) => {
|
|
4410
4434
|
const d = await apiPost('/api/analytics/report', a);
|
|
4411
4435
|
// SAY WHETHER IT WAS FILTERED. A narrow answer and a small property look identical in a row list, so a reader
|
|
4412
4436
|
// who did not send the filter cannot otherwise tell which one they are looking at.
|
|
4413
|
-
|
|
4437
|
+
// rowCount is GA4's TOTAL, `returned` is what is actually in this response. Printing only the total put a big
|
|
4438
|
+
// number above a short list and let a partial answer read as a complete one.
|
|
4439
|
+
const head = d.truncated
|
|
4440
|
+
? `${d.returned} of ${d.rowCount} row(s) for property ${d.property} — THIS IS A PARTIAL ANSWER. Call again with offset:${d.nextOffset} for the next page, or raise limit (max 250000).`
|
|
4441
|
+
: `${d.rowCount || 0} row(s) for property ${d.property}`;
|
|
4442
|
+
return ok(`${head}${d.filtered ? ' (FILTERED — this is a SUBSET of the property, not its total)' : ''}${d.sampled ? ' (SAMPLED — GA4 dropped rows to answer this; say so when reporting)' : ''}.\n${rowLines(d.rows)}`, d);
|
|
4414
4443
|
}));
|
|
4415
4444
|
server.registerTool('analytics_realtime', {
|
|
4416
4445
|
title: 'Who is on the site right now (GA4 realtime)',
|
|
@@ -5688,6 +5717,7 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
5688
5717
|
bid: z.string().optional().describe('MAX_CONVERSIONS only — the target CPA. Refused on MANUAL_CPT.'),
|
|
5689
5718
|
currency: z.string().optional().describe('Defaults to the connected ad account’s currency; an account cannot mix currencies.'),
|
|
5690
5719
|
startTime: z.string().optional(), endTime: z.string().optional().describe('Omit to run indefinitely.'),
|
|
5720
|
+
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.'),
|
|
5691
5721
|
},
|
|
5692
5722
|
outputSchema: { ok: z.boolean().optional(), level: z.string().optional(), id: z.string().optional(), verified: z.boolean().optional(), bornPaused: z.boolean().optional(), read: z.any().optional(), summary: z.string().optional(), note: z.string().optional() },
|
|
5693
5723
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
@@ -5767,6 +5797,7 @@ export function registerTools(rawServer, opts = {}) {
|
|
|
5767
5797
|
rules: z.array(z.object({ field: z.string(), operator: z.string(), value: z.any() })).optional().describe('Location group only (DYNAMIC). REPLACES every rule and puts the group back to PENDING.'),
|
|
5768
5798
|
description: z.string().optional().describe('Location group only.'),
|
|
5769
5799
|
startTime: z.string().optional(), endTime: z.string().optional(), currency: z.string().optional(),
|
|
5800
|
+
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.'),
|
|
5770
5801
|
},
|
|
5771
5802
|
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() },
|
|
5772
5803
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.114",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
|
-
"description": "AI ad studio + marketing MCP (
|
|
5
|
+
"description": "AI ad studio + marketing MCP (522 tools): build and manage ad campaigns on Meta, Google Ads, Reddit, X, TikTok, Snapchat, LinkedIn, Pinterest, Microsoft Advertising and ChatGPT Ads — generate finished video, image and UGC avatar ads, publish them to Facebook, Instagram, Threads, TikTok, YouTube, X, LinkedIn and Pinterest, spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus organic TikTok/Instagram/YouTube/Reddit, and read what they achieved in Google Analytics 4. CLI and Claude skills for Hermoso, the AI ad studio: brand onboarding, 30+ image/video models, finished-ad pipeline (script, voiceover, music, brand end card), ad scoring and competitor teardowns.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"hermoso": "bin/hermoso.mjs"
|