hermoso 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp/client.mjs +12 -0
- package/mcp/tools.mjs +40 -8
- package/package.json +1 -1
package/mcp/client.mjs
CHANGED
|
@@ -50,6 +50,18 @@ export async function apiPut(p, body = {}) {
|
|
|
50
50
|
return unwrap(res);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// A hosted-connector call (via mcp/http.mjs) has an mcpCtx store; local stdio/CLI does not. Used to REFUSE local-path
|
|
54
|
+
// file reads on the hosted connector (it runs on the SERVER host, not the user's machine — an LFI/exfil vector).
|
|
55
|
+
export const isRemote = () => !!mcpCtx.getStore();
|
|
56
|
+
// Upload raw file BYTES to /api/upload (150MB, persists → returns {url,kind,bytes}). Overrides the JSON content-type so
|
|
57
|
+
// the server reads the raw body. Lets an agent post ARBITRARY user files (not just Hermoso renders).
|
|
58
|
+
export async function apiUpload(p, buf, { contentType = 'application/octet-stream', fileName = '' } = {}) {
|
|
59
|
+
const h = headers({ 'Content-Type': contentType });
|
|
60
|
+
if (fileName) h['x-file-name'] = encodeURIComponent(fileName);
|
|
61
|
+
const res = await fetch(`${API_BASE}${p}`, { method: 'POST', headers: h, body: buf });
|
|
62
|
+
return unwrap(res);
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
// /api/explore/chat streams Server-Sent-Events; collect to the terminal `done` payload {reply, results, actions}.
|
|
54
66
|
export async function apiSSE(p, body = {}) {
|
|
55
67
|
const res = await fetch(`${API_BASE}${p}`, { method: 'POST', headers: headers({ Accept: 'text/event-stream' }), body: JSON.stringify(body) });
|
package/mcp/tools.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// Spend tools hit routes guarded by gateSpend → requireAuth; locally the dev account always resolves (no auth
|
|
5
5
|
// needed today), and the SAME guard becomes authoritative under real auth — so this honors no-anon-spend as-is.
|
|
6
6
|
import { z } from 'zod';
|
|
7
|
-
import { apiGet, apiPost, apiPut, apiSSE, submitJob, getJob, jobResult, pollJob, toRef, API_BASE, PROFILE, mcpCtx } from './client.mjs';
|
|
7
|
+
import { apiGet, apiPost, apiPut, apiSSE, submitJob, getJob, jobResult, pollJob, toRef, apiUpload, isRemote, API_BASE, PROFILE, mcpCtx } from './client.mjs';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
8
9
|
|
|
9
10
|
const JOB_TIMEOUT = +(process.env.HERMOSO_JOB_TIMEOUT_MS || 10 * 60 * 1000);
|
|
10
11
|
const abs = (u) => (u && u.startsWith('/') ? API_BASE + u : u); // /generated/x.mp4 → clickable absolute URL
|
|
@@ -499,13 +500,42 @@ export function registerTools(server) {
|
|
|
499
500
|
const pages = pg.pages || [], adAccounts = aa.adAccounts || [];
|
|
500
501
|
return ok(`Pages: ${pages.map(p => p.name + (p.instagram ? ` (IG @${p.instagram.username})` : '')).join(', ') || 'none'}\nAd accounts: ${adAccounts.map(a => `${a.name} (act_${a.accountId}, ${a.currency}${a.active ? '' : ', inactive'})`).join(', ') || 'none'}`, { pages, adAccounts });
|
|
501
502
|
}));
|
|
503
|
+
// Ingest an ARBITRARY user file (desktop media, etc. — nothing to do with a Hermoso render) into Hermoso and get back a
|
|
504
|
+
// durable public URL to feed post_to_meta / upload_meta_asset / create_meta_ad. This is what makes the publishing tools
|
|
505
|
+
// work on the user's OWN files, not just generated ones.
|
|
506
|
+
const EXT_MIME = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', mp4: 'video/mp4', mov: 'video/quicktime', webm: 'video/webm', m4v: 'video/mp4' };
|
|
507
|
+
server.registerTool('upload_file', {
|
|
508
|
+
title: 'Upload a local file → durable public URL',
|
|
509
|
+
description: 'Persist an ARBITRARY user file (image or video, up to 150MB) into Hermoso and get back a durable public URL you can pass to post_to_meta / upload_meta_asset / create_meta_ad — including files that have NOTHING to do with a Hermoso render (e.g. media on the user\'s desktop). Provide exactly ONE source: `path` (a local file — works ONLY when Hermoso runs locally over stdio/CLI; the hosted connector can\'t see the user\'s machine), or `dataUri` (a base64 data: URI — keep under ~15MB on the hosted connector). If the file is ALREADY at a public https URL you do NOT need this — pass that URL straight to post_to_meta/upload_meta_asset and the server re-hosts it safely. Returns {url, kind, bytes}.',
|
|
510
|
+
inputSchema: {
|
|
511
|
+
path: z.string().optional().describe('local filesystem path (stdio/CLI only — refused on the hosted connector)'),
|
|
512
|
+
dataUri: z.string().optional().describe('base64 data: URI of the file bytes (data:<mime>;base64,<…>)'),
|
|
513
|
+
name: z.string().optional().describe('original file name — helps pick the right extension'),
|
|
514
|
+
},
|
|
515
|
+
outputSchema: { url: z.string().optional(), kind: z.string().optional(), bytes: z.number().optional() },
|
|
516
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
517
|
+
}, wrap(async (a) => {
|
|
518
|
+
let buf, contentType = 'application/octet-stream', fileName = a.name || '';
|
|
519
|
+
if (a.dataUri) {
|
|
520
|
+
const m = /^data:([^;]+);base64,(.*)$/s.exec(String(a.dataUri).trim());
|
|
521
|
+
if (!m) throw new Error('dataUri must be a base64 data: URI: data:<mime>;base64,<…>');
|
|
522
|
+
buf = Buffer.from(m[2], 'base64'); contentType = m[1];
|
|
523
|
+
} else if (a.path) {
|
|
524
|
+
if (isRemote()) throw new Error('`path` only works when Hermoso runs on your own machine (stdio/CLI). On the hosted connector I can\'t read your files — pass `dataUri`, or give the publishing tool a public https URL.');
|
|
525
|
+
buf = await readFile(a.path);
|
|
526
|
+
fileName = fileName || String(a.path).split(/[\\/]/).pop();
|
|
527
|
+
contentType = EXT_MIME[(fileName.split('.').pop() || '').toLowerCase()] || 'application/octet-stream';
|
|
528
|
+
} else throw new Error('Provide exactly one source: `path` (local file) or `dataUri`.');
|
|
529
|
+
const d = await apiUpload('/api/upload', buf, { contentType, fileName });
|
|
530
|
+
return ok(`Uploaded ${d.kind || 'file'} (${d.bytes || buf.length} bytes) → ${d.url}. Pass this url to post_to_meta / upload_meta_asset / create_meta_ad.`, { url: d.url, kind: d.kind, bytes: d.bytes });
|
|
531
|
+
}));
|
|
502
532
|
server.registerTool('post_to_meta', {
|
|
503
533
|
title: 'Post to Facebook, Instagram or Threads',
|
|
504
|
-
description: 'Publish to a connected Facebook Page, its linked Instagram, OR the brand’s Threads account — text/link/image/VIDEO
|
|
534
|
+
description: 'Publish to a connected Facebook Page, its linked Instagram, OR the brand’s Threads account — text/link/image/VIDEO. target:"facebook" (default) posts to the Page; target:"instagram" publishes a photo or Reel to the linked IG business account (needs an image or video); target:"threads" posts to the connected Threads account (text, image, or video). Works with ANY media — a finished Hermoso ad OR an arbitrary user file: imageUrl/videoUrl accept a public https URL, a data: URI, or a Hermoso /generated path; for a LOCAL file (e.g. on the user’s desktop) call upload_file first and pass the url it returns. This PUBLISHES immediately — confirm the copy + media with the user first. Needs a connected Meta account (Settings ▸ Connectors ▸ Meta) with posting permission; Threads needs its own connection.',
|
|
505
535
|
inputSchema: {
|
|
506
536
|
message: z.string().optional().describe('post text / caption'),
|
|
507
|
-
imageUrl: z.string().optional().describe('public https
|
|
508
|
-
videoUrl: z.string().optional().describe('public https
|
|
537
|
+
imageUrl: z.string().optional().describe('public https URL, a data: URI, or a Hermoso /generated path (upload_file gives you one for a local file)'),
|
|
538
|
+
videoUrl: z.string().optional().describe('public https URL, data: URI, or /generated path — FB video post / IG Reel'),
|
|
509
539
|
link: z.string().optional().describe('a URL to attach (FB text post only)'),
|
|
510
540
|
target: z.enum(['facebook', 'instagram', 'threads']).optional().describe('default facebook; instagram → the Page’s linked IG; threads → the brand’s connected Threads account'),
|
|
511
541
|
pageId: z.string().optional().describe('target Page id (from list_meta_pages); omit = first Page'),
|
|
@@ -518,18 +548,20 @@ export function registerTools(server) {
|
|
|
518
548
|
}));
|
|
519
549
|
server.registerTool('upload_meta_asset', {
|
|
520
550
|
title: 'Upload an asset to a Meta ad account',
|
|
521
|
-
description: 'Upload a finished
|
|
551
|
+
description: 'Upload creative(s) — a finished Hermoso ad OR arbitrary user files (e.g. a folder of media from the user’s desktop) — into a connected ad account’s ASSET LIBRARY so the user or a later ad-build step can use them in their OWN campaigns. Pass `url` for one file, or `urls` (up to 20) to BULK-upload in a single call. Each accepts a public https URL, a data: URI, or a Hermoso /generated path; for LOCAL files call upload_file first and pass the url(s) it returns. Image → image hash; video → video id. Pass adAccountId from list_meta_pages.',
|
|
522
552
|
inputSchema: {
|
|
523
553
|
adAccountId: z.string().describe('ad account id (digits or act_… — from list_meta_pages)'),
|
|
524
|
-
url: z.string().describe('public https
|
|
554
|
+
url: z.string().optional().describe('a single public https URL / data: URI / /generated path'),
|
|
555
|
+
urls: z.array(z.string()).optional().describe('up to 20 media URLs/paths for a one-call BULK upload'),
|
|
525
556
|
kind: z.enum(['image', 'video']).optional().describe('inferred from the URL if omitted'),
|
|
526
557
|
name: z.string().optional().describe('a label for the asset'),
|
|
527
558
|
},
|
|
528
|
-
outputSchema: { ok: z.boolean().optional(), kind: z.string().optional(), hash: z.string().optional(), videoId: z.string().optional() },
|
|
559
|
+
outputSchema: { ok: z.boolean().optional(), kind: z.string().optional(), hash: z.string().optional(), videoId: z.string().optional(), assets: z.array(z.object({ kind: z.string().optional(), hash: z.string().optional(), videoId: z.string().optional() })).optional() },
|
|
529
560
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
530
561
|
}, wrap(async (a) => {
|
|
531
562
|
const d = await apiPost('/api/meta/upload-asset', a);
|
|
532
|
-
|
|
563
|
+
const summary = d.assets ? `Uploaded ${d.assets.length} asset${d.assets.length > 1 ? 's' : ''} to the ad account library.` : `Uploaded ${d.kind} to the ad account library${d.hash ? ` (image hash ${d.hash})` : d.videoId ? ` (video id ${d.videoId})` : ''}.`;
|
|
564
|
+
return ok(`${summary} ${d.note || ''}`.trim(), d);
|
|
533
565
|
}));
|
|
534
566
|
server.registerTool('create_meta_campaign', {
|
|
535
567
|
title: 'Create a Meta ad campaign (paused)',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hermoso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"mcpName": "io.github.hermoso-ai/hermoso",
|
|
5
5
|
"description": "Generate finished VIDEO ADS, image ads and UGC avatar ads for any brand with AI — and spy on competitor ads across the Meta, Google and LinkedIn ad libraries plus TikTok/Instagram/YouTube organic. MCP server, 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",
|