@hubfluencer/mcp 0.13.0 → 0.15.0
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 +13 -5
- package/dist/index.js +88 -15
- package/package.json +1 -1
- package/src/core.ts +18 -0
- package/src/index.ts +122 -18
- package/src/output-schemas.ts +6 -0
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
|
|
|
113
113
|
| `generate_all_segments` | Render every not-yet-completed scene in order (continuity preserved); capped ~280s → re-run to continue. | **5 / segment** |
|
|
114
114
|
| `set_narration_script` / `generate_narration` | Write your own voiceover script (free) **or** AI-draft one coherent script after every scene has completed with a known positive duration. AI drafting requires at least one scene without retained original audio and consumes an assist only when a fresh job is queued (exact retries dedupe for free). | free / 1 assist |
|
|
115
115
|
| `generate_voice` | Synthesize the voiceover. `voice_id` required (see `list_voices`). | **3** |
|
|
116
|
-
| `generate_music` | Generate background music (each call is a fresh take). | **5** |
|
|
116
|
+
| `generate_music` | Generate background music (each call is a fresh take). Optional `instruments` is a string array (≤10 non-empty hints, ≤80 chars each), not a comma-separated string. | **5** |
|
|
117
117
|
| `render` | Final MP4. Auto-charges and generates any pending or failed scenes first (4/scene in a batch of ≥3). | **0** |
|
|
118
118
|
| `list_renders` | List every render version for an editor project (`id`, `version`, `status`, `video_url`, `thumbnail_url`, `duration_seconds`, `error_message`, `retryable`, `combination_hash`, `is_stale`, `inserted_at`) — find a failed render or recover a finished URL. | **0** |
|
|
119
119
|
| `retry_render` | Re-run a **failed** render from its exact saved snapshot (recovery without re-driving the pipeline). | **0** |
|
|
@@ -137,19 +137,27 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
|
|
|
137
137
|
| `set_closing_image` | Set the ~2s end-card image (JPEG/PNG, ≤20 MiB; upload a file, or reuse the product image with `from_product:true`). |
|
|
138
138
|
| `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤5 MiB; editor only; optional treatment/position/duration). |
|
|
139
139
|
| `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
|
|
140
|
-
| `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**; choose its corner and `throughout`/`end_card`/`none` (hide) treatment. The logo is never sent to the video model. |
|
|
141
|
-
| `set_short_poster` | Set a short's end-card poster (JPEG/PNG, ≤20 MiB; extends the render 12s → 14s). Pass `poster_includes_lockup:true` if the poster already carries brand name/CTA/badges — suppresses the overlaid claim/CTA so they don't collide. |
|
|
140
|
+
| `set_short_logo` | Attach an exact deterministic logo overlay (JPEG/PNG, ≤5 MiB) to a **short**; choose its preferred corner and `throughout`/`end_card`/`none` (hide) treatment. Forge moves it away from text/CTA/chips. The logo is never sent to the video model. |
|
|
141
|
+
| `set_short_poster` | Set a short's end-card poster (JPEG/PNG, ≤20 MiB; extends the render 12s → 14s). Phone-shaped portrait art is cover-cropped to 9:16; square, classic portrait, and landscape art is contained over a blurred backdrop. Pass `poster_includes_lockup:true` if the poster already carries brand name/CTA/badges — suppresses the overlaid claim/CTA and an end-card-only logo so they don't collide. |
|
|
142
142
|
|
|
143
143
|
### Status & delivery
|
|
144
144
|
|
|
145
145
|
| Tool | What it does |
|
|
146
146
|
|---|---|
|
|
147
|
-
| `get_status` | Normalized `{stage, terminal, ready, video_url, error}` for a short / editor / tracking / slider. |
|
|
147
|
+
| `get_status` | Normalized `{stage, terminal, ready, video_url, error}` for a short / editor / tracking / slider. While a paid short is rendering, also exposes `segments_completed`, `segments_total`, and `active_segment_positions`; `generation_attempt` is the cancellation fence. |
|
|
148
148
|
| `wait_for_completion` | Block-poll until terminal (bounded ≤280s; returns `terminal:false` to re-call). `save_path` downloads the MP4 when ready. |
|
|
149
149
|
| `download_result` | Get (or save, to `HUBFLUENCER_OUTPUT_DIR`) the finished MP4 URL. Not for sliders — use `get_slider`. |
|
|
150
|
-
| `get_credits` / `list_voices` / `list_projects` | Credit balance; narration voices; recent projects (`include_completed:true` to recover a finished short's slug). |
|
|
150
|
+
| `get_credits` / `list_voices` / `list_projects` | Credit balance; narration voices (including normalized verified `languages`); recent projects (`include_completed:true` to recover a finished short's slug). |
|
|
151
151
|
| `get_ai_assists` / `unlock_ai_assists` | The free daily assist quota / spend 1 credit for +10. |
|
|
152
152
|
|
|
153
|
+
### Creative export contract
|
|
154
|
+
|
|
155
|
+
- Production prompts (`product_prompt`, `scenario_prompt`, and scene `prompt`) are visual directions. Narration scripts are read aloud verbatim, so `set_narration_script.script` must contain only the exact spoken words—never camera notes, prompt syntax, labels, or scene directions. Prefer `generate_narration` or Autopilot when exact wording is unnecessary.
|
|
156
|
+
- Do not rely on the video model to spell essential brand names, UI, signs, prices, offers, or CTAs. Put exact copy in Hubfluencer overlays, narration, or supplied ending artwork.
|
|
157
|
+
- `short_logo_position` is a preferred corner. Forge moves the logo away from headline, captions, CTA, offer, and badge areas. Use a tight transparent wordmark; use square/full-bleed art as the poster. `end_card` logos are suppressed when `poster_includes_lockup:true` to avoid double branding.
|
|
158
|
+
- Short posters fill the ending frame: phone-shaped portrait art is cover-cropped, while square, classic portrait, and landscape art is contained over a blurred backdrop. Editor closing images are always cover-cropped. Keep important content in the central 9:16 safe area. An Editor closing image gets no generic CTA, so put the CTA in the artwork or state it in the preceding narration/scene.
|
|
159
|
+
- Each Editor AI scene is 8 seconds. Request 3 scenes (about 24 seconds) for a concise social ad unless the brief needs a longer story.
|
|
160
|
+
|
|
153
161
|
For editor projects, `video_url` may remain the last delivered MP4 after the timeline changes.
|
|
154
162
|
It is current only when `ready:true` and `stale:false`. `stale:true` means the delivered render
|
|
155
163
|
does not match the live timeline; regenerate the reported stale scenes/audio and render again.
|
package/dist/index.js
CHANGED
|
@@ -29510,6 +29510,13 @@ function normalizeStatus(kind, slug, data) {
|
|
|
29510
29510
|
status.failure_attempt = d.failure_attempt;
|
|
29511
29511
|
if (typeof d.generation_attempt === "number")
|
|
29512
29512
|
status.generation_attempt = d.generation_attempt;
|
|
29513
|
+
if (typeof d.segments_completed === "number")
|
|
29514
|
+
status.segments_completed = d.segments_completed;
|
|
29515
|
+
if (typeof d.segments_total === "number")
|
|
29516
|
+
status.segments_total = d.segments_total;
|
|
29517
|
+
if (Array.isArray(d.active_segment_positions) && d.active_segment_positions.every((position) => typeof position === "number")) {
|
|
29518
|
+
status.active_segment_positions = d.active_segment_positions;
|
|
29519
|
+
}
|
|
29513
29520
|
if (typeof d.latest_render_free === "boolean") {
|
|
29514
29521
|
status.latest_render_free = d.latest_render_free;
|
|
29515
29522
|
}
|
|
@@ -30259,7 +30266,7 @@ function readStoredCredentials() {
|
|
|
30259
30266
|
// package.json
|
|
30260
30267
|
var package_default = {
|
|
30261
30268
|
name: "@hubfluencer/mcp",
|
|
30262
|
-
version: "0.
|
|
30269
|
+
version: "0.15.0",
|
|
30263
30270
|
description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
30264
30271
|
license: "MIT",
|
|
30265
30272
|
author: "Monocursive <contact@monocursive.com>",
|
|
@@ -30578,6 +30585,9 @@ var getStatusOutput = exports_external.object({
|
|
|
30578
30585
|
failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
|
|
30579
30586
|
failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
|
|
30580
30587
|
generation_attempt: exports_external.number().optional().describe("Short only: current paid attempt; echo when cancelling."),
|
|
30588
|
+
segments_completed: exports_external.number().optional(),
|
|
30589
|
+
segments_total: exports_external.number().optional(),
|
|
30590
|
+
active_segment_positions: exports_external.array(exports_external.number()).optional(),
|
|
30581
30591
|
latest_render_free: exports_external.boolean().optional(),
|
|
30582
30592
|
last_free_rerender_failed: exports_external.boolean().optional()
|
|
30583
30593
|
}).passthrough();
|
|
@@ -30598,6 +30608,9 @@ var waitForCompletionOutput = exports_external.object({
|
|
|
30598
30608
|
failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
|
|
30599
30609
|
failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
|
|
30600
30610
|
generation_attempt: exports_external.number().optional().describe("Short only: current paid attempt; echo when cancelling."),
|
|
30611
|
+
segments_completed: exports_external.number().optional(),
|
|
30612
|
+
segments_total: exports_external.number().optional(),
|
|
30613
|
+
active_segment_positions: exports_external.array(exports_external.number()).optional(),
|
|
30601
30614
|
latest_render_free: exports_external.boolean().optional(),
|
|
30602
30615
|
last_free_rerender_failed: exports_external.boolean().optional(),
|
|
30603
30616
|
saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
|
|
@@ -32127,10 +32140,33 @@ registerTool("get_credits", {
|
|
|
32127
32140
|
}));
|
|
32128
32141
|
registerTool("list_voices", {
|
|
32129
32142
|
title: "List voices",
|
|
32130
|
-
description: "Lists available narration voices
|
|
32143
|
+
description: "Lists available narration voices with voice_id, preview metadata, and verified language codes. " + "Choose a voice whose languages includes the editor project's language, then pass its voice_id to " + "create_editor_ad / make_video / generate_voice. Shorts have no voiceover.",
|
|
32131
32144
|
inputSchema: {},
|
|
32132
32145
|
annotations: { title: "List voices", ...RO }
|
|
32133
|
-
}, tool(async (_args, client) =>
|
|
32146
|
+
}, tool(async (_args, client) => {
|
|
32147
|
+
const response = asRecord(await client.get("/voices"));
|
|
32148
|
+
const data = asRecord(response.data ?? response);
|
|
32149
|
+
const voices = Array.isArray(data.voices) ? data.voices.map(asRecord) : [];
|
|
32150
|
+
return ok({
|
|
32151
|
+
voices: voices.map((voice) => {
|
|
32152
|
+
const verified = Array.isArray(voice.verified_languages) ? voice.verified_languages.map(asRecord) : [];
|
|
32153
|
+
return {
|
|
32154
|
+
voice_id: voice.voice_id,
|
|
32155
|
+
name: voice.name,
|
|
32156
|
+
category: voice.category,
|
|
32157
|
+
description: voice.description,
|
|
32158
|
+
labels: voice.labels,
|
|
32159
|
+
preview_url: voice.preview_url,
|
|
32160
|
+
languages: [
|
|
32161
|
+
...new Set(verified.map((language) => language.language).filter((language) => typeof language === "string"))
|
|
32162
|
+
]
|
|
32163
|
+
};
|
|
32164
|
+
}),
|
|
32165
|
+
has_more: data.has_more,
|
|
32166
|
+
total_count: data.total_count,
|
|
32167
|
+
next_page_token: data.next_page_token
|
|
32168
|
+
});
|
|
32169
|
+
}));
|
|
32134
32170
|
registerTool("list_projects", {
|
|
32135
32171
|
title: "List recent projects",
|
|
32136
32172
|
description: "Lists the caller's recent shorts and editor/video-factory projects, each normalized to " + "{slug, stage, terminal, ready, video_url}. By default the shorts listing is limited to " + "in-progress/failed; pass include_completed:true to also surface finished shorts — useful to " + "recover a lost slug. Capped to the most recent items per kind (use get_status for authoritative state).",
|
|
@@ -32301,7 +32337,7 @@ registerTool("update_short", {
|
|
|
32301
32337
|
brand_name: exports_external.string().max(40).optional().describe("Brand/product display name (≤40 chars) shown big on the closing brand-lockup slate when no " + `poster is set. Server falls back to your brand profile's name when unset. Pass "" to remove it.`),
|
|
32302
32338
|
end_card: exports_external.enum(["auto", "none"]).optional().describe('"auto" (default): end on the poster, or on a brand-lockup slate (brand_name + claim + CTA pill) ' + 'when no poster is set — a 14s render. "none": no end card at all (12s, for deliberate ' + "loop-style shorts)."),
|
|
32303
32339
|
poster_includes_lockup: exports_external.boolean().optional().describe("Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " + "claim/CTA so they don't collide (default false)."),
|
|
32304
|
-
short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("
|
|
32340
|
+
short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Preferred corner for the deterministic brand-logo overlay (set the image with set_short_logo). " + "Forge moves it to a clear rail when text/CTA/chips occupy that corner. Moving it is a FREE " + "re-render — logo fields never cost credits."),
|
|
32305
32341
|
short_logo_treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe('When the brand logo shows: "throughout" the footage, only on the "end_card", or "none" to hide it. ' + "FREE re-render."),
|
|
32306
32342
|
brand_profile_id: exports_external.number().int().optional().describe("Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " + "list_brand_profiles). Best set at create_short time (that path also freezes a brand snapshot); " + "on update it sets the linked profile. Must be one of your own profiles.")
|
|
32307
32343
|
},
|
|
@@ -32369,7 +32405,13 @@ registerTool("update_short", {
|
|
|
32369
32405
|
const res = await client.patch(`/shorts/${args.slug}`, body);
|
|
32370
32406
|
const data = asRecord(res).data ?? res;
|
|
32371
32407
|
return ok({
|
|
32372
|
-
|
|
32408
|
+
slug: args.slug,
|
|
32409
|
+
kind: "short",
|
|
32410
|
+
stage: asRecord(data).stage,
|
|
32411
|
+
status: asRecord(data).status,
|
|
32412
|
+
updated: body,
|
|
32413
|
+
end_card_mode: asRecord(data).end_card_mode,
|
|
32414
|
+
resolved_cta_text: asRecord(data).resolved_cta_text,
|
|
32373
32415
|
next: "Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " + `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` + "(prompt, product image, format, visual_language, theme, music_*, language) need " + "generate_short (15 credits)."
|
|
32374
32416
|
});
|
|
32375
32417
|
}));
|
|
@@ -32511,19 +32553,21 @@ registerTool("create_tracking_video", {
|
|
|
32511
32553
|
}));
|
|
32512
32554
|
registerTool("generate_short", {
|
|
32513
32555
|
title: "Generate (render) a short",
|
|
32514
|
-
description: "Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " + "and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " + "use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " + "Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " + "auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " + "bare clip). Then poll with get_status or wait_for_completion (kind=short).",
|
|
32556
|
+
description: "Deducts 15 credits and starts the render pipeline for an existing short — this RE-ROLLS the footage " + "and music (a fresh AI generation). For text/style/CTA/end-card tweaks on an already-generated short, " + "use rerender_short instead: it re-applies the overlay over the existing footage for 0 credits. " + "Safe to call again: a " + "duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " + "short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " + "auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " + "bare clip). A transient 5xx is reconciled against the project state: accepted work is returned as " + "in-progress, while an unchanged draft is retried once. Then poll with get_status or " + "wait_for_completion (kind=short).",
|
|
32515
32557
|
inputSchema: {
|
|
32516
32558
|
slug: exports_external.string().describe("Short slug from create_short"),
|
|
32517
32559
|
skip_auto_text: exports_external.boolean().optional().describe("When the draft has no headline/subheadline/text_beats the server auto-writes the copy; pass " + "true to skip that and render a deliberately bare clip (default false).")
|
|
32518
32560
|
},
|
|
32519
32561
|
annotations: { title: "Generate short", ...WRITE }
|
|
32520
32562
|
}, tool(async (args, client) => {
|
|
32563
|
+
const submit = () => client.post(`/shorts/${args.slug}/generate`, args.skip_auto_text ? { skip_auto_text: true } : undefined);
|
|
32521
32564
|
try {
|
|
32522
|
-
const res = await
|
|
32565
|
+
const res = await submit();
|
|
32523
32566
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
32524
32567
|
return ok(status);
|
|
32525
32568
|
} catch (e) {
|
|
32526
|
-
|
|
32569
|
+
const statusCode = e.status;
|
|
32570
|
+
if (statusCode === 409) {
|
|
32527
32571
|
return ok({
|
|
32528
32572
|
kind: "short",
|
|
32529
32573
|
slug: args.slug,
|
|
@@ -32534,6 +32578,35 @@ registerTool("generate_short", {
|
|
|
32534
32578
|
error: null
|
|
32535
32579
|
});
|
|
32536
32580
|
}
|
|
32581
|
+
if (statusCode !== undefined && statusCode >= 500) {
|
|
32582
|
+
let reconciled;
|
|
32583
|
+
try {
|
|
32584
|
+
reconciled = await fetchStatus(client, "short", args.slug);
|
|
32585
|
+
} catch (_reconcileError) {
|
|
32586
|
+
throw e;
|
|
32587
|
+
}
|
|
32588
|
+
if (reconciled.stage === "rendering")
|
|
32589
|
+
return ok(reconciled);
|
|
32590
|
+
if (reconciled.stage !== "draft")
|
|
32591
|
+
throw e;
|
|
32592
|
+
try {
|
|
32593
|
+
const retried = await submit();
|
|
32594
|
+
return ok(normalizeStatus("short", args.slug, asRecord(retried).data));
|
|
32595
|
+
} catch (retryError) {
|
|
32596
|
+
if (retryError.status === 409) {
|
|
32597
|
+
return ok({
|
|
32598
|
+
kind: "short",
|
|
32599
|
+
slug: args.slug,
|
|
32600
|
+
stage: "processing",
|
|
32601
|
+
terminal: false,
|
|
32602
|
+
ready: false,
|
|
32603
|
+
video_url: null,
|
|
32604
|
+
error: null
|
|
32605
|
+
});
|
|
32606
|
+
}
|
|
32607
|
+
throw retryError;
|
|
32608
|
+
}
|
|
32609
|
+
}
|
|
32537
32610
|
throw e;
|
|
32538
32611
|
}
|
|
32539
32612
|
}));
|
|
@@ -32805,9 +32878,9 @@ registerTool("edit_slider_slide", {
|
|
|
32805
32878
|
}));
|
|
32806
32879
|
registerTool("create_editor_ad", {
|
|
32807
32880
|
title: "Create, configure, and price a multi-scene editor ad",
|
|
32808
|
-
description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "A product image is analyzed into semantic project context and sent as a product-identity reference to every " + "generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " + "was authored; generated fine label text can still vary. Scene prompts are the complete content contract: describe any people, " + "products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " + "in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " + "headline/subheadline instead.)",
|
|
32881
|
+
description: "Creates an editor project, attaches any supplied local product/logo/closing assets BEFORE generation, " + "and prices autopilot (server-orchestrated " + "scenario → segments → narration → voice → music → render). Each AI-generated scene is a fixed 8 seconds. " + "Without max_credits it STOPS after the free draft/configuration and returns the live estimate. It starts " + "the paid pipeline only when max_credits is supplied and covers the estimate. Then poll with get_status / " + "wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " + "A product image is analyzed into semantic project context and sent as a product-identity reference to every " + "generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " + "was authored; generated fine label text can still vary. Do not depend on the video model for exact readable " + "brand names, UI, signs, prices, or CTA copy; put essential copy in narration or deterministic Short/poster overlays. " + "Scene prompts are the complete content contract: describe any people, " + "products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " + "in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " + "headline/subheadline instead.)",
|
|
32809
32882
|
inputSchema: {
|
|
32810
|
-
product_prompt: exports_external.string().min(10).describe("
|
|
32883
|
+
product_prompt: exports_external.string().min(10).describe("Creative brief for the ad (min 10 chars), not literal voiceover copy. Autopilot uses it to draft " + "narration. For exact spoken words, use the granular editor path and pass only read-aloud text to " + "set_narration_script."),
|
|
32811
32884
|
product_subject: exports_external.string().max(300).optional().describe('The concrete product or main subject the video must be about (e.g. "Bordeaux red wine, dark green ' + 'bottle with a gold label"). Distinct from the freeform brief: hard-grounds the AI scenario + narration ' + "so it cannot invent an unrelated product. Strongly recommended for ads."),
|
|
32812
32885
|
project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("social_ad for promotional/ad content (product focus + CTA), creative_story for brand narratives " + "(default)."),
|
|
32813
32886
|
language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag (use "es", not "es-MX"). ' + "Drives the WHOLE video: scenario, narration script, narration voice, and captions. " + "Ask the user / infer from their locale."),
|
|
@@ -33586,14 +33659,14 @@ registerTool("generate_voice", {
|
|
|
33586
33659
|
}));
|
|
33587
33660
|
registerTool("generate_music", {
|
|
33588
33661
|
title: "Generate the background music (5 credits)",
|
|
33589
|
-
description: "Generates background music. Costs 5 credits. Each call produces a fresh take (music is non-deterministic) — call again for another. `description` is an optional" + "music-direction prompt (≤1200 chars; longer is silently truncated server-side). 402 fails cleanly.",
|
|
33662
|
+
description: "Generates background music. Costs 5 credits. Each call produces a fresh take (music is non-deterministic) — call again for another. `description` is an optional" + " music-direction prompt (≤1200 chars; longer is silently truncated server-side). `instruments` is an " + "optional array of up to 10 non-empty hints (≤80 chars each), never a comma-separated string. 402 fails cleanly.",
|
|
33590
33663
|
inputSchema: {
|
|
33591
33664
|
slug: exports_external.string().describe("Editor project slug"),
|
|
33592
33665
|
description: exports_external.string().max(1200).optional().describe("Music direction prompt (≤1200 chars, truncated if longer)"),
|
|
33593
33666
|
mood: exports_external.string().optional().describe("Optional mood hint"),
|
|
33594
33667
|
genre: exports_external.string().optional().describe("Optional genre hint"),
|
|
33595
33668
|
tempo: exports_external.string().optional().describe("Optional tempo hint"),
|
|
33596
|
-
instruments: exports_external.string().optional().describe(
|
|
33669
|
+
instruments: exports_external.array(exports_external.string().min(1).max(80)).max(10).optional().describe('Optional instrument hints, e.g. ["piano", "strings"]')
|
|
33597
33670
|
},
|
|
33598
33671
|
annotations: { title: "Generate music", ...WRITE, idempotentHint: false }
|
|
33599
33672
|
}, tool(async (args, client) => {
|
|
@@ -33942,11 +34015,11 @@ registerTool("set_short_product", {
|
|
|
33942
34015
|
}));
|
|
33943
34016
|
registerTool("set_short_logo", {
|
|
33944
34017
|
title: "Set a Short brand logo overlay (0 credits)",
|
|
33945
|
-
description: "Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " + "AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " + "it appears throughout the footage or only on the end card.",
|
|
34018
|
+
description: "Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " + "AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " + "it appears throughout the footage or only on the end card. The corner is a preference: Forge moves the " + "logo to a clear rail when headline/caption/CTA/chip overlays occupy it. Prefer a tight transparent PNG " + "wordmark; use square/full-bleed artwork as a poster instead of a watermark.",
|
|
33946
34019
|
inputSchema: {
|
|
33947
34020
|
slug: exports_external.string().describe("Short slug"),
|
|
33948
34021
|
file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
33949
|
-
position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("
|
|
34022
|
+
position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Preferred logo corner (default top-right; auto-relocated to avoid text)"),
|
|
33950
34023
|
treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears (default throughout)")
|
|
33951
34024
|
},
|
|
33952
34025
|
annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
|
|
@@ -33978,7 +34051,7 @@ registerTool("set_short_poster", {
|
|
|
33978
34051
|
}));
|
|
33979
34052
|
registerTool("get_status", {
|
|
33980
34053
|
title: "Get generation status",
|
|
33981
|
-
description: "Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " + "tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " + "slider (a carousel of stills, not a video): its video_url is always null, ready:true means every " + "slide is composited, and you read the per-slide image URLs with get_slider.",
|
|
34054
|
+
description: "Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " + "tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " + "slider (a carousel of stills, not a video): its video_url is always null, ready:true means every " + "slide is composited, and you read the per-slide image URLs with get_slider. During paid short " + "generation, segments_completed / segments_total and active_segment_positions report real clip progress.",
|
|
33982
34055
|
inputSchema: { slug: exports_external.string(), kind: kindSchema },
|
|
33983
34056
|
outputSchema: getStatusOutput,
|
|
33984
34057
|
annotations: { title: "Get status", ...RO }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hubfluencer/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Monocursive <contact@monocursive.com>",
|
package/src/core.ts
CHANGED
|
@@ -277,6 +277,12 @@ export interface NormalizedStatus {
|
|
|
277
277
|
failure_attempt?: number;
|
|
278
278
|
/** Short only: current paid attempt; echo when cancelling. */
|
|
279
279
|
generation_attempt?: number;
|
|
280
|
+
/** Short only: completed/current Veo clip count for useful long-poll progress. */
|
|
281
|
+
segments_completed?: number;
|
|
282
|
+
/** Short only: total current Veo clips in the paid attempt. */
|
|
283
|
+
segments_total?: number;
|
|
284
|
+
/** Short only: clip positions that are pending or processing. */
|
|
285
|
+
active_segment_positions?: number[];
|
|
280
286
|
/** Editor only: the newest delivered render no longer matches the live project. */
|
|
281
287
|
stale?: boolean;
|
|
282
288
|
/** Editor only: current scene ids that are still generating. */
|
|
@@ -365,6 +371,18 @@ export function normalizeStatus(
|
|
|
365
371
|
status.failure_attempt = d.failure_attempt;
|
|
366
372
|
if (typeof d.generation_attempt === "number")
|
|
367
373
|
status.generation_attempt = d.generation_attempt;
|
|
374
|
+
if (typeof d.segments_completed === "number")
|
|
375
|
+
status.segments_completed = d.segments_completed;
|
|
376
|
+
if (typeof d.segments_total === "number")
|
|
377
|
+
status.segments_total = d.segments_total;
|
|
378
|
+
if (
|
|
379
|
+
Array.isArray(d.active_segment_positions) &&
|
|
380
|
+
d.active_segment_positions.every(
|
|
381
|
+
(position) => typeof position === "number",
|
|
382
|
+
)
|
|
383
|
+
) {
|
|
384
|
+
status.active_segment_positions = d.active_segment_positions as number[];
|
|
385
|
+
}
|
|
368
386
|
// Additive free-re-render bookkeeping (2026-07 API): surfaced only when
|
|
369
387
|
// the server actually sends the booleans, so older payloads normalize
|
|
370
388
|
// byte-identically. A failed free re-render does NOT flip stage to
|
package/src/index.ts
CHANGED
|
@@ -1412,12 +1412,46 @@ registerTool(
|
|
|
1412
1412
|
{
|
|
1413
1413
|
title: "List voices",
|
|
1414
1414
|
description:
|
|
1415
|
-
"Lists available narration voices
|
|
1416
|
-
"
|
|
1415
|
+
"Lists available narration voices with voice_id, preview metadata, and verified language codes. " +
|
|
1416
|
+
"Choose a voice whose languages includes the editor project's language, then pass its voice_id to " +
|
|
1417
|
+
"create_editor_ad / make_video / generate_voice. Shorts have no voiceover.",
|
|
1417
1418
|
inputSchema: {},
|
|
1418
1419
|
annotations: { title: "List voices", ...RO },
|
|
1419
1420
|
},
|
|
1420
|
-
tool(async (_args, client) =>
|
|
1421
|
+
tool(async (_args, client) => {
|
|
1422
|
+
const response = asRecord(await client.get("/voices"));
|
|
1423
|
+
const data = asRecord(response.data ?? response);
|
|
1424
|
+
const voices = Array.isArray(data.voices) ? data.voices.map(asRecord) : [];
|
|
1425
|
+
|
|
1426
|
+
return ok({
|
|
1427
|
+
voices: voices.map((voice) => {
|
|
1428
|
+
const verified = Array.isArray(voice.verified_languages)
|
|
1429
|
+
? voice.verified_languages.map(asRecord)
|
|
1430
|
+
: [];
|
|
1431
|
+
return {
|
|
1432
|
+
voice_id: voice.voice_id,
|
|
1433
|
+
name: voice.name,
|
|
1434
|
+
category: voice.category,
|
|
1435
|
+
description: voice.description,
|
|
1436
|
+
labels: voice.labels,
|
|
1437
|
+
preview_url: voice.preview_url,
|
|
1438
|
+
languages: [
|
|
1439
|
+
...new Set(
|
|
1440
|
+
verified
|
|
1441
|
+
.map((language) => language.language)
|
|
1442
|
+
.filter(
|
|
1443
|
+
(language): language is string =>
|
|
1444
|
+
typeof language === "string",
|
|
1445
|
+
),
|
|
1446
|
+
),
|
|
1447
|
+
],
|
|
1448
|
+
};
|
|
1449
|
+
}),
|
|
1450
|
+
has_more: data.has_more,
|
|
1451
|
+
total_count: data.total_count,
|
|
1452
|
+
next_page_token: data.next_page_token,
|
|
1453
|
+
});
|
|
1454
|
+
}),
|
|
1421
1455
|
);
|
|
1422
1456
|
|
|
1423
1457
|
// ── Projects ──────────────────────────────────────────────────────────────
|
|
@@ -1990,8 +2024,9 @@ registerTool(
|
|
|
1990
2024
|
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
1991
2025
|
.optional()
|
|
1992
2026
|
.describe(
|
|
1993
|
-
"
|
|
1994
|
-
"a
|
|
2027
|
+
"Preferred corner for the deterministic brand-logo overlay (set the image with set_short_logo). " +
|
|
2028
|
+
"Forge moves it to a clear rail when text/CTA/chips occupy that corner. Moving it is a FREE " +
|
|
2029
|
+
"re-render — logo fields never cost credits.",
|
|
1995
2030
|
),
|
|
1996
2031
|
short_logo_treatment: z
|
|
1997
2032
|
.enum(["throughout", "end_card", "none"])
|
|
@@ -2113,7 +2148,13 @@ registerTool(
|
|
|
2113
2148
|
);
|
|
2114
2149
|
const data = asRecord(res).data ?? res;
|
|
2115
2150
|
return ok({
|
|
2116
|
-
|
|
2151
|
+
slug: args.slug,
|
|
2152
|
+
kind: "short",
|
|
2153
|
+
stage: asRecord(data).stage,
|
|
2154
|
+
status: asRecord(data).status,
|
|
2155
|
+
updated: body,
|
|
2156
|
+
end_card_mode: asRecord(data).end_card_mode,
|
|
2157
|
+
resolved_cta_text: asRecord(data).resolved_cta_text,
|
|
2117
2158
|
next:
|
|
2118
2159
|
"Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " +
|
|
2119
2160
|
`rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` +
|
|
@@ -2502,7 +2543,9 @@ registerTool(
|
|
|
2502
2543
|
"duplicate while a render is in flight is reported as in-progress (no double charge), and a failed " +
|
|
2503
2544
|
"short can be re-generated. If the draft's headline/subheadline/text_beats are all blank the server " +
|
|
2504
2545
|
"auto-writes the headline + caption beats before rendering (pass skip_auto_text:true for a deliberately " +
|
|
2505
|
-
"bare clip).
|
|
2546
|
+
"bare clip). A transient 5xx is reconciled against the project state: accepted work is returned as " +
|
|
2547
|
+
"in-progress, while an unchanged draft is retried once. Then poll with get_status or " +
|
|
2548
|
+
"wait_for_completion (kind=short).",
|
|
2506
2549
|
inputSchema: {
|
|
2507
2550
|
slug: z.string().describe("Short slug from create_short"),
|
|
2508
2551
|
skip_auto_text: z
|
|
@@ -2524,17 +2567,21 @@ registerTool(
|
|
|
2524
2567
|
// of a failed short. Double-charge is prevented server-side — the batch
|
|
2525
2568
|
// claim's conditional update rolls back the losing transaction's credit
|
|
2526
2569
|
// deduction — and a 409 means a render is already running.
|
|
2527
|
-
|
|
2528
|
-
|
|
2570
|
+
const submit = () =>
|
|
2571
|
+
client.post<{ data: unknown }>(
|
|
2529
2572
|
`/shorts/${args.slug}/generate`,
|
|
2530
2573
|
// Only send a body when the caller opts out of server auto-copy —
|
|
2531
2574
|
// the bare POST stays byte-identical to the pre-0.8.2 wire shape.
|
|
2532
2575
|
args.skip_auto_text ? { skip_auto_text: true } : undefined,
|
|
2533
2576
|
);
|
|
2577
|
+
|
|
2578
|
+
try {
|
|
2579
|
+
const res = await submit();
|
|
2534
2580
|
const status = normalizeStatus("short", args.slug, asRecord(res).data);
|
|
2535
2581
|
return ok(status);
|
|
2536
2582
|
} catch (e) {
|
|
2537
|
-
|
|
2583
|
+
const statusCode = (e as { status?: number }).status;
|
|
2584
|
+
if (statusCode === 409) {
|
|
2538
2585
|
return ok({
|
|
2539
2586
|
kind: "short",
|
|
2540
2587
|
slug: args.slug,
|
|
@@ -2545,6 +2592,47 @@ registerTool(
|
|
|
2545
2592
|
error: null,
|
|
2546
2593
|
});
|
|
2547
2594
|
}
|
|
2595
|
+
|
|
2596
|
+
if (statusCode !== undefined && statusCode >= 500) {
|
|
2597
|
+
let reconciled: NormalizedStatus;
|
|
2598
|
+
try {
|
|
2599
|
+
reconciled = await fetchStatus(client, "short", args.slug);
|
|
2600
|
+
} catch (_reconcileError) {
|
|
2601
|
+
// Preserve the original POST failure when the read-side check also
|
|
2602
|
+
// fails; the client must never blind-retry a possibly accepted debit.
|
|
2603
|
+
throw e;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// Only the active render stage proves this POST crossed the paid
|
|
2607
|
+
// launch transaction. A previous completed/failed result can still be
|
|
2608
|
+
// visible after an early 5xx; returning it here would masquerade stale
|
|
2609
|
+
// work as the newly accepted generation.
|
|
2610
|
+
if (reconciled.stage === "rendering") return ok(reconciled);
|
|
2611
|
+
if (reconciled.stage !== "draft") throw e;
|
|
2612
|
+
|
|
2613
|
+
// A Short launch changes the durable factory out of draft in the
|
|
2614
|
+
// same transaction as the debit. Still-draft therefore proves the
|
|
2615
|
+
// failed response accepted no paid work, so one retry is safe.
|
|
2616
|
+
try {
|
|
2617
|
+
const retried = await submit();
|
|
2618
|
+
return ok(
|
|
2619
|
+
normalizeStatus("short", args.slug, asRecord(retried).data),
|
|
2620
|
+
);
|
|
2621
|
+
} catch (retryError) {
|
|
2622
|
+
if ((retryError as { status?: number }).status === 409) {
|
|
2623
|
+
return ok({
|
|
2624
|
+
kind: "short",
|
|
2625
|
+
slug: args.slug,
|
|
2626
|
+
stage: "processing",
|
|
2627
|
+
terminal: false,
|
|
2628
|
+
ready: false,
|
|
2629
|
+
video_url: null,
|
|
2630
|
+
error: null,
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
throw retryError;
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2548
2636
|
throw e;
|
|
2549
2637
|
}
|
|
2550
2638
|
}),
|
|
@@ -3092,7 +3180,9 @@ registerTool(
|
|
|
3092
3180
|
"wait_for_completion (kind=editor). Prefer make_video for a prompt-only one-shot. " +
|
|
3093
3181
|
"A product image is analyzed into semantic project context and sent as a product-identity reference to every " +
|
|
3094
3182
|
"generated Editor scene. The exact photo is also linked as the closing image unless a separate closing image " +
|
|
3095
|
-
"was authored; generated fine label text can still vary.
|
|
3183
|
+
"was authored; generated fine label text can still vary. Do not depend on the video model for exact readable " +
|
|
3184
|
+
"brand names, UI, signs, prices, or CTA copy; put essential copy in narration or deterministic Short/poster overlays. " +
|
|
3185
|
+
"Scene prompts are the complete content contract: describe any people, " +
|
|
3096
3186
|
"products, visual continuity, absences, and composition directly in the brief. (Editor ads carry their copy " +
|
|
3097
3187
|
"in-scene/narration, not as a title overlay; for an on-screen title/subtitle use a short with " +
|
|
3098
3188
|
"headline/subheadline instead.)",
|
|
@@ -3100,7 +3190,11 @@ registerTool(
|
|
|
3100
3190
|
product_prompt: z
|
|
3101
3191
|
.string()
|
|
3102
3192
|
.min(10)
|
|
3103
|
-
.describe(
|
|
3193
|
+
.describe(
|
|
3194
|
+
"Creative brief for the ad (min 10 chars), not literal voiceover copy. Autopilot uses it to draft " +
|
|
3195
|
+
"narration. For exact spoken words, use the granular editor path and pass only read-aloud text to " +
|
|
3196
|
+
"set_narration_script.",
|
|
3197
|
+
),
|
|
3104
3198
|
product_subject: z
|
|
3105
3199
|
.string()
|
|
3106
3200
|
.max(300)
|
|
@@ -4767,7 +4861,8 @@ registerTool(
|
|
|
4767
4861
|
title: "Generate the background music (5 credits)",
|
|
4768
4862
|
description:
|
|
4769
4863
|
"Generates background music. Costs 5 credits. Each call produces a fresh take (music is non-deterministic) — call again for another. `description` is an optional" +
|
|
4770
|
-
"music-direction prompt (≤1200 chars; longer is silently truncated server-side).
|
|
4864
|
+
" music-direction prompt (≤1200 chars; longer is silently truncated server-side). `instruments` is an " +
|
|
4865
|
+
"optional array of up to 10 non-empty hints (≤80 chars each), never a comma-separated string. 402 fails cleanly.",
|
|
4771
4866
|
inputSchema: {
|
|
4772
4867
|
slug: z.string().describe("Editor project slug"),
|
|
4773
4868
|
description: z
|
|
@@ -4778,7 +4873,11 @@ registerTool(
|
|
|
4778
4873
|
mood: z.string().optional().describe("Optional mood hint"),
|
|
4779
4874
|
genre: z.string().optional().describe("Optional genre hint"),
|
|
4780
4875
|
tempo: z.string().optional().describe("Optional tempo hint"),
|
|
4781
|
-
instruments: z
|
|
4876
|
+
instruments: z
|
|
4877
|
+
.array(z.string().min(1).max(80))
|
|
4878
|
+
.max(10)
|
|
4879
|
+
.optional()
|
|
4880
|
+
.describe('Optional instrument hints, e.g. ["piano", "strings"]'),
|
|
4782
4881
|
},
|
|
4783
4882
|
annotations: { title: "Generate music", ...WRITE, idempotentHint: false },
|
|
4784
4883
|
},
|
|
@@ -4790,7 +4889,7 @@ registerTool(
|
|
|
4790
4889
|
mood?: string;
|
|
4791
4890
|
genre?: string;
|
|
4792
4891
|
tempo?: string;
|
|
4793
|
-
instruments?: string;
|
|
4892
|
+
instruments?: string[];
|
|
4794
4893
|
},
|
|
4795
4894
|
client,
|
|
4796
4895
|
) => {
|
|
@@ -5551,14 +5650,18 @@ registerTool(
|
|
|
5551
5650
|
description:
|
|
5552
5651
|
"Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " +
|
|
5553
5652
|
"AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " +
|
|
5554
|
-
"it appears throughout the footage or only on the end card."
|
|
5653
|
+
"it appears throughout the footage or only on the end card. The corner is a preference: Forge moves the " +
|
|
5654
|
+
"logo to a clear rail when headline/caption/CTA/chip overlays occupy it. Prefer a tight transparent PNG " +
|
|
5655
|
+
"wordmark; use square/full-bleed artwork as a poster instead of a watermark.",
|
|
5555
5656
|
inputSchema: {
|
|
5556
5657
|
slug: z.string().describe("Short slug"),
|
|
5557
5658
|
file_path: z.string().describe("Local logo image (.png/.jpg/.jpeg)"),
|
|
5558
5659
|
position: z
|
|
5559
5660
|
.enum(["top-left", "top-right", "bottom-left", "bottom-right"])
|
|
5560
5661
|
.optional()
|
|
5561
|
-
.describe(
|
|
5662
|
+
.describe(
|
|
5663
|
+
"Preferred logo corner (default top-right; auto-relocated to avoid text)",
|
|
5664
|
+
),
|
|
5562
5665
|
treatment: z
|
|
5563
5666
|
.enum(["throughout", "end_card", "none"])
|
|
5564
5667
|
.optional()
|
|
@@ -5657,7 +5760,8 @@ registerTool(
|
|
|
5657
5760
|
"Returns a normalized status {stage, terminal, ready, video_url, error} for a short, editor, " +
|
|
5658
5761
|
"tracking, or slider project. ready:true means the post-ready MP4 is at video_url — except for a " +
|
|
5659
5762
|
"slider (a carousel of stills, not a video): its video_url is always null, ready:true means every " +
|
|
5660
|
-
"slide is composited, and you read the per-slide image URLs with get_slider."
|
|
5763
|
+
"slide is composited, and you read the per-slide image URLs with get_slider. During paid short " +
|
|
5764
|
+
"generation, segments_completed / segments_total and active_segment_positions report real clip progress.",
|
|
5661
5765
|
inputSchema: { slug: z.string(), kind: kindSchema },
|
|
5662
5766
|
outputSchema: getStatusOutput,
|
|
5663
5767
|
annotations: { title: "Get status", ...RO },
|
package/src/output-schemas.ts
CHANGED
|
@@ -95,6 +95,9 @@ export const getStatusOutput = z
|
|
|
95
95
|
.number()
|
|
96
96
|
.optional()
|
|
97
97
|
.describe("Short only: current paid attempt; echo when cancelling."),
|
|
98
|
+
segments_completed: z.number().optional(),
|
|
99
|
+
segments_total: z.number().optional(),
|
|
100
|
+
active_segment_positions: z.array(z.number()).optional(),
|
|
98
101
|
latest_render_free: z.boolean().optional(),
|
|
99
102
|
last_free_rerender_failed: z.boolean().optional(),
|
|
100
103
|
})
|
|
@@ -142,6 +145,9 @@ export const waitForCompletionOutput = z
|
|
|
142
145
|
.number()
|
|
143
146
|
.optional()
|
|
144
147
|
.describe("Short only: current paid attempt; echo when cancelling."),
|
|
148
|
+
segments_completed: z.number().optional(),
|
|
149
|
+
segments_total: z.number().optional(),
|
|
150
|
+
active_segment_positions: z.array(z.number()).optional(),
|
|
145
151
|
latest_render_free: z.boolean().optional(),
|
|
146
152
|
last_free_rerender_failed: z.boolean().optional(),
|
|
147
153
|
saved_to: z.union([z.string(), z.null()]).optional(),
|