@hubfluencer/mcp 0.9.1 → 0.10.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 CHANGED
@@ -68,7 +68,7 @@ claude mcp add hubfluencer --env HUBFLUENCER_API_TOKEN=YOUR_TOKEN -- npx -y @hub
68
68
 
69
69
  ## Tools
70
70
 
71
- 92 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
71
+ 93 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
72
72
  below are the ground truth. **"$0" means it spends no video credits**; planning/AI-helper tools
73
73
  instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_assists`: 1 credit
74
74
  → +10). Reads need the `video:read` scope; writes need `video:generate`. Nothing here needs
@@ -80,9 +80,9 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
80
80
  |---|---|---|
81
81
  | **`make_video`** | **One shot: prompt → finished MP4** (create → **price** → start → poll → download). `kind:"auto"` picks a multi-scene editor ad for ad/promo/story briefs, a short for simple ones (reported as `kind_inferred`). Pass `dry_run:true` to preview the cost without spending, or `max_credits` to cap it. | short **15**, editor ~**28** (`dry_run`/`max_credits` gate it) |
82
82
  | `create_short` | Create a short draft (12s vertical: two 6s AI segments + title overlay + music; 14s when it ends on a poster or brand-lockup end card). Set `headline` (≈4 words, ≤40 chars), `subheadline`, `music_vibe`, and an explicit `visual_language` (`kinetic_creator` is a strong default) — and **always `cta_text`** (a short without a CTA doesn't convert; blank `cta_text` → the server renders a neutral localized ask, e.g. "Learn more" — a truly CTA-less short is not currently supported). End-card fields: `closing_claim` (≤80), `brand_name` (≤40), `end_card` (`"auto"`\|`"none"`). Other opt-in conversion graphics: `offer_text`/`badge_text`/`star_rating` — only what you can substantiate. | **0** |
83
- | `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, end-card fields (`closing_claim`/`brand_name`/`poster_includes_lockup`/`end_card`), or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Apply text/style/CTA/end-card edits to a generated short free with `rerender_short`; footage/music edits need `generate_short`. | **0** |
83
+ | `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, end-card fields (`closing_claim`/`brand_name`/`poster_includes_lockup`/`end_card`), logo overlay placement (`short_logo_position`/`short_logo_treatment`), or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Apply text/style/CTA/end-card/logo edits to a generated short free with `rerender_short`; footage/music edits need `generate_short`. | **0** |
84
84
  | `generate_short` | Render an existing short — re-rolls the footage + music (fresh AI generation). If headline/subheadline/beats are all blank the server auto-writes the copy first (`skip_auto_text:true` for a deliberately bare clip). Safe to re-call (in-flight = reported, no double charge; a failed short can re-generate). | **15** |
85
- | `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks; footage or music changes need `generate_short` instead. Needs one completed generation first (422 `short_not_ready` otherwise); changed footage/music inputs return 422 `short_paid_regeneration_required`. A failed free re-render leaves the delivered video intact — retry `rerender_short`, it stays free. | **0** |
85
+ | `rerender_short` | **FREE re-render** — applies the short's current text/style/CTA/end-card/logo state over the already-paid footage + music. Use after `update_short` for copy/style/CTA tweaks, or after a logo change (`set_short_logo`, or `short_logo_position`/`short_logo_treatment`); footage or music changes need `generate_short` instead. Needs one completed generation first (422 `short_not_ready` otherwise); changed footage/music inputs return 422 `short_paid_regeneration_required`. A failed free re-render leaves the delivered video intact — retry `rerender_short`, it stays free. | **0** |
86
86
  | `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
87
87
  | `create_editor_ad` | Create an editor project, attach optional local product/logo/closing assets, and return the live autopilot quote. Omit `max_credits` to stop free; provide an approved cap to launch. For a mascot/hero product that must remain identical with no humans, pass `cast_mode:"product_only"` plus `product_image_path`. | **0** until capped launch |
88
88
  | `start_autopilot` | Quote autopilot on an **existing** editor draft when called with only `slug`; launch only with an explicit `max_credits` that covers the estimate and an adequate balance. On a completed project, pass `restart:true` to quote and build a fresh creative run; omit it to resume pending edits. | **0** quote; credits on capped launch |
@@ -133,6 +133,7 @@ images at **8 MiB (8,388,608 bytes)**; closing/short/catalog images at **20 MiB
133
133
  | `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`). |
134
134
  | `set_logo` | Upload a brand logo overlay (JPEG/PNG, ≤1 MiB; editor only; optional treatment/position/duration). |
135
135
  | `set_short_product` | Attach a product image (JPEG/PNG, ≤20 MiB) to a **short**. |
136
+ | `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. |
136
137
  | `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. |
137
138
 
138
139
  ### Status & delivery
@@ -233,7 +234,7 @@ edit are **0 credits** — you only pay to generate/render.
233
234
  |---|---|
234
235
  | Any draft create / edit / reorder | 0 |
235
236
  | Short render | **15** |
236
- | Short re-render (`rerender_short` — text/style/CTA/end-card over existing footage) | **0** |
237
+ | Short re-render (`rerender_short` — text/style/CTA/end-card/logo over existing footage) | **0** |
237
238
  | Editor AI scene (`generate_segment`) | **5** (4 each in a batch of ≥3) |
238
239
  | Editor scene re-roll (`regenerate_segment`, new version of a completed scene) | **4** |
239
240
  | Editor voice | **3** |
@@ -259,8 +260,9 @@ If it returns `terminal:false`, the render is still going — call `wait_for_com
259
260
  `create_editor_ad` (free configure + quote) → obtain spend approval → `start_autopilot {slug, max_credits}` → `wait_for_completion {kind:"editor"}` → `download_result`.
260
261
 
261
262
  **Iterate a short for free (never re-roll):** the first `generate_short` (15 credits) buys the footage +
262
- music; after that, every text/style/CTA/end-card tweak re-renders over them for **0 credits** —
263
- `update_short` (new hook/CTA/style) → `rerender_short` → compare → repeat → keep the winner. Only
263
+ music; after that, every text/style/CTA/end-card/logo tweak re-renders over them for **0 credits** —
264
+ `update_short` (new hook/CTA/style) → `rerender_short` → compare → repeat → keep the winner. Logo
265
+ placement (`set_short_logo`, or `short_logo_position`/`short_logo_treatment`) is free too. Only
264
266
  footage/music-affecting edits (`product_prompt`, product image, `creative_format`, `visual_language`,
265
267
  `theme`, `music_vibe`, `music_instruments`, `language`) need another `generate_short`. Edits that
266
268
  add/remove the end card (poster, `end_card`) can shift the video between 12s and 14s; the pinned music
package/dist/index.js CHANGED
@@ -29498,6 +29498,16 @@ function normalizeStatus(kind, slug, data) {
29498
29498
  video_url: videoUrl,
29499
29499
  error: d.error_message ?? null
29500
29500
  };
29501
+ if (typeof d.failed_stage === "string")
29502
+ status.failed_stage = d.failed_stage;
29503
+ if (typeof d.failure_code === "string")
29504
+ status.failure_code = d.failure_code;
29505
+ if (d.failure_details && typeof d.failure_details === "object")
29506
+ status.failure_details = d.failure_details;
29507
+ if (typeof d.failure_source === "string")
29508
+ status.failure_source = d.failure_source;
29509
+ if (typeof d.failure_attempt === "number")
29510
+ status.failure_attempt = d.failure_attempt;
29501
29511
  if (typeof d.latest_render_free === "boolean") {
29502
29512
  status.latest_render_free = d.latest_render_free;
29503
29513
  }
@@ -30233,7 +30243,7 @@ function readStoredCredentials() {
30233
30243
  // package.json
30234
30244
  var package_default = {
30235
30245
  name: "@hubfluencer/mcp",
30236
- version: "0.9.1",
30246
+ version: "0.10.0",
30237
30247
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30238
30248
  license: "MIT",
30239
30249
  author: "Monocursive <contact@monocursive.com>",
@@ -30546,6 +30556,11 @@ var getStatusOutput = exports_external.object({
30546
30556
  stale: exports_external.boolean().optional(),
30547
30557
  active_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30548
30558
  stale_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30559
+ failed_stage: exports_external.string().optional().describe("Short only: exact API failure phase behind a terminal failure."),
30560
+ failure_code: exports_external.string().optional().describe("Short only: stable machine-readable failure classification."),
30561
+ failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30562
+ failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30563
+ failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
30549
30564
  latest_render_free: exports_external.boolean().optional(),
30550
30565
  last_free_rerender_failed: exports_external.boolean().optional()
30551
30566
  }).passthrough();
@@ -30560,6 +30575,11 @@ var waitForCompletionOutput = exports_external.object({
30560
30575
  stale: exports_external.boolean().optional(),
30561
30576
  active_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30562
30577
  stale_segment_ids: exports_external.array(exports_external.union([exports_external.number(), exports_external.string()])).optional(),
30578
+ failed_stage: exports_external.string().optional().describe("Short only: exact API failure phase behind a terminal failure."),
30579
+ failure_code: exports_external.string().optional().describe("Short only: stable machine-readable failure classification."),
30580
+ failure_details: exports_external.record(exports_external.string(), exports_external.unknown()).optional().describe("Short only: structured provider/QC details for recovery."),
30581
+ failure_source: exports_external.string().optional().describe("Short only: worker/recovery layer that finalized the failure."),
30582
+ failure_attempt: exports_external.number().optional().describe("Short only: paid generation attempt number that failed."),
30563
30583
  latest_render_free: exports_external.boolean().optional(),
30564
30584
  last_free_rerender_failed: exports_external.boolean().optional(),
30565
30585
  saved_to: exports_external.union([exports_external.string(), exports_external.null()]).optional(),
@@ -31103,6 +31123,7 @@ var MAX_VIDEO_BYTES = 500000000;
31103
31123
  var MAX_IMAGE_BYTES = 20 * 1024 * 1024;
31104
31124
  var MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
31105
31125
  var MAX_LOGO_BYTES = 1 * 1024 * 1024;
31126
+ var MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
31106
31127
  var MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
31107
31128
  var MULTIPART_THRESHOLD = 50 * 1024 * 1024;
31108
31129
  var MAX_CATALOG_VIDEO_DURATION_SECONDS = 60;
@@ -31795,7 +31816,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
31795
31816
  8. RECURRING: create_series + plan_series_episodes → materialize_episode drafts each episode; get_series_dashboard shows upcoming/drafted/published lanes; mark_episode_posted closes a recurring show's loop.
31796
31817
 
31797
31818
  CREDITS (spent server-side; each tool prices before charging — see its description)
31798
- - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
31819
+ - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card/logo edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
31799
31820
  - Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video supports dry_run/max_credits; create_editor_ad/start_autopilot require max_credits before launching.
31800
31821
  - AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
31801
31822
 
@@ -32088,7 +32109,7 @@ registerTool("list_projects", {
32088
32109
  }));
32089
32110
  registerTool("create_short", {
32090
32111
  title: "Create a short (draft)",
32091
- description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " + "overlay that's an editor-only feature; use create_editor_ad for logo branding.)",
32112
+ description: "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " + "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " + "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " + "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card/logo " + "tweak re-renders over them for 0 credits (update_short → rerender_short). " + "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " + "title), and pick a music_vibe plus visual_language that fit the brand. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " + "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " + "To brand it further, attach a product image " + "(set_short_product), a deterministic brand logo (set_short_logo), or an end-card poster " + "(set_short_poster) before generate_short.",
32092
32113
  inputSchema: {
32093
32114
  product_prompt: exports_external.string().min(10).describe("What the short should be about"),
32094
32115
  language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en" (default). Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
@@ -32185,7 +32206,7 @@ registerTool("create_short", {
32185
32206
  }));
32186
32207
  registerTool("update_short", {
32187
32208
  title: "Update a short draft (0 credits)",
32188
- description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
32209
+ description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card/logo fields " + "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " + "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card, short_logo_position, " + "short_logo_treatment) on an " + "already-generated short? " + "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " + "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " + "generate_short (15 credits) again. Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "Always set cta_text — a short without a CTA doesn't convert (plus offer_text only if there's a real " + "deal). Note: blank cta_text → the server renders a neutral localized ask (e.g. 'Learn more'); a truly " + "CTA-less short is not currently supported. " + "Keep headline ≈4 words (≤40 chars). Set visual_language explicitly (kinetic_creator is a strong " + "default). " + "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " + 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' + "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " + "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " + "beats don't survive a later assist call. " + "404 for an unknown/foreign slug.",
32189
32210
  inputSchema: {
32190
32211
  slug: exports_external.string().describe("Short slug (from create_short)"),
32191
32212
  product_prompt: exports_external.string().min(10).optional().describe("Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short."),
@@ -32221,6 +32242,8 @@ registerTool("update_short", {
32221
32242
  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.`),
32222
32243
  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)."),
32223
32244
  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)."),
32245
+ short_logo_position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Corner for the deterministic brand-logo overlay (set the image with set_short_logo). Moving it is " + "a FREE re-render — logo fields never cost credits."),
32246
+ 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."),
32224
32247
  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.")
32225
32248
  },
32226
32249
  annotations: {
@@ -32278,13 +32301,17 @@ registerTool("update_short", {
32278
32301
  body.end_card = args.end_card;
32279
32302
  if (args.poster_includes_lockup !== undefined)
32280
32303
  body.poster_includes_lockup = args.poster_includes_lockup;
32304
+ if (args.short_logo_position !== undefined)
32305
+ body.short_logo_position = args.short_logo_position;
32306
+ if (args.short_logo_treatment !== undefined)
32307
+ body.short_logo_treatment = args.short_logo_treatment;
32281
32308
  if (args.brand_profile_id !== undefined)
32282
32309
  body.brand_profile_id = args.brand_profile_id;
32283
32310
  const res = await client.patch(`/shorts/${args.slug}`, body);
32284
32311
  const data = asRecord(res).data ?? res;
32285
32312
  return ok({
32286
32313
  ...asRecord(data),
32287
- next: "Saved. Only text/style/CTA/end-card 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)."
32314
+ 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)."
32288
32315
  });
32289
32316
  }));
32290
32317
  async function getTracking(client, slug, deadline) {
@@ -32453,7 +32480,7 @@ registerTool("generate_short", {
32453
32480
  }));
32454
32481
  registerTool("rerender_short", {
32455
32482
  title: "Re-render a short (FREE — 0 credits)",
32456
- description: "FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
32483
+ description: "FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " + "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " + "(set_short_logo, or short_logo_position/short_logo_treatment via update_short). Footage or music " + "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " + "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " + "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " + "Requires a completed generation to re-render over " + "(422 short_not_ready otherwise — run generate_short once first); while any generation/render is in " + "flight it reports in-progress (409) rather than duplicating. Then poll with get_status or " + "wait_for_completion (kind=short). A failed free re-render leaves the delivered video intact — retry " + "rerender_short, it stays free (the status fields latest_render_free / last_free_rerender_failed " + "report this when the server sends them).",
32457
32484
  inputSchema: {
32458
32485
  slug: exports_external.string().describe("Short slug (from create_short)")
32459
32486
  },
@@ -33633,9 +33660,28 @@ registerTool("set_short_product", {
33633
33660
  const res = await client.post(`/shorts/${args.slug}/product/confirm`, { s3_key, product_description: args.description });
33634
33661
  return ok(asRecord(res).data ?? res);
33635
33662
  }));
33663
+ registerTool("set_short_logo", {
33664
+ title: "Set a Short brand logo overlay (0 credits)",
33665
+ 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.",
33666
+ inputSchema: {
33667
+ slug: exports_external.string().describe("Short slug"),
33668
+ file_path: exports_external.string().describe("Local logo image (.png/.jpg/.jpeg)"),
33669
+ position: exports_external.enum(["top-left", "top-right", "bottom-left", "bottom-right"]).optional().describe("Logo corner (default top-right)"),
33670
+ treatment: exports_external.enum(["throughout", "end_card", "none"]).optional().describe("When the logo appears (default throughout)")
33671
+ },
33672
+ annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false }
33673
+ }, tool(async (args, client) => {
33674
+ const { s3_key } = await uploadImageFile(client, `/shorts/${args.slug}/logo/presign`, args.file_path, MAX_SHORT_LOGO_BYTES);
33675
+ const res = await client.post(`/shorts/${args.slug}/logo/confirm`, {
33676
+ s3_key,
33677
+ position: args.position ?? "top-right",
33678
+ treatment: args.treatment ?? "throughout"
33679
+ });
33680
+ return ok(asRecord(res).data ?? res);
33681
+ }));
33636
33682
  registerTool("set_short_poster", {
33637
33683
  title: "Set a short's end-card poster image (0 credits)",
33638
- description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
33684
+ description: "Uploads a local image as the SHORT's end-card poster — a closing still shown at the end (extends the " + "render from 12s to 14s). Accepted: " + IMAGE_EXTS.map((e) => `.${e}`).join(", ") + " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " + "again replaces it. Use set_short_logo for an exact, non-generative brand overlay.",
33639
33685
  inputSchema: {
33640
33686
  slug: exports_external.string().describe("Short slug (from create_short)"),
33641
33687
  file_path: exports_external.string().describe("Local poster image (.jpg/.jpeg/.png)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.9.1",
3
+ "version": "0.10.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
@@ -265,6 +265,16 @@ export interface NormalizedStatus {
265
265
  ready: boolean;
266
266
  video_url: string | null;
267
267
  error: string | null;
268
+ /** Short only: exact API failure phase instead of a collapsed terminal state. */
269
+ failed_stage?: string;
270
+ /** Short only: stable machine-readable terminal failure classification. */
271
+ failure_code?: string;
272
+ /** Short only: structured provider/QC details for recovery decisions. */
273
+ failure_details?: Record<string, unknown>;
274
+ /** Short only: worker/recovery layer that finalized the failed attempt. */
275
+ failure_source?: string;
276
+ /** Short only: paid generation attempt number that failed. */
277
+ failure_attempt?: number;
268
278
  /** Editor only: the newest delivered render no longer matches the live project. */
269
279
  stale?: boolean;
270
280
  /** Editor only: current scene ids that are still generating. */
@@ -341,6 +351,16 @@ export function normalizeStatus(
341
351
  video_url: videoUrl,
342
352
  error: (d.error_message as string) ?? null,
343
353
  };
354
+ if (typeof d.failed_stage === "string")
355
+ status.failed_stage = d.failed_stage;
356
+ if (typeof d.failure_code === "string")
357
+ status.failure_code = d.failure_code;
358
+ if (d.failure_details && typeof d.failure_details === "object")
359
+ status.failure_details = d.failure_details as Record<string, unknown>;
360
+ if (typeof d.failure_source === "string")
361
+ status.failure_source = d.failure_source;
362
+ if (typeof d.failure_attempt === "number")
363
+ status.failure_attempt = d.failure_attempt;
344
364
  // Additive free-re-render bookkeeping (2026-07 API): surfaced only when
345
365
  // the server actually sends the booleans, so older payloads normalize
346
366
  // byte-identically. A failed free re-render does NOT flip stage to
package/src/index.ts CHANGED
@@ -121,6 +121,7 @@ import {
121
121
  MAX_CLOSING_IMAGE_BYTES,
122
122
  MAX_LOGO_BYTES,
123
123
  MAX_PRODUCT_IMAGE_BYTES,
124
+ MAX_SHORT_LOGO_BYTES,
124
125
  type PreparedUploadFile,
125
126
  preparedUploadIdentity,
126
127
  prepareImageUpload,
@@ -614,7 +615,7 @@ DELEGATE THE CONTENT OPERATION (the full loop — steps 1-3 are $0)
614
615
  8. RECURRING: create_series + plan_series_episodes → materialize_episode drafts each episode; get_series_dashboard shows upcoming/drafted/published lanes; mark_episode_posted closes a recurring show's loop.
615
616
 
616
617
  CREDITS (spent server-side; each tool prices before charging — see its description)
617
- - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
618
+ - Free (0 credits): every draft create/edit, reorder, all slider re-composites, short re-renders (rerender_short — applies text/style/CTA/end-card/logo edits over a short's already-paid footage), and the whole onboard/plan/pack/materialize path (steps 1-3). get_credits shows the balance.
618
619
  - Charged: short render 15; editor AI scene 5 (4 in a batch of ≥3); voice 3; music 5; editor render 0 (auto-charges ungenerated scenes); slider 1/slide; tracking render 1 (refunded on failure). make_video supports dry_run/max_credits; create_editor_ad/start_autopilot require max_credits before launching.
619
620
  - AI ASSISTS (all generate_*/enhance/suggest/plan/hook tools) draw a FREE daily quota (20/day), NOT credits. On 429, set auto_unlock:true to spend 1 credit for +10 assists (retried once), or write the copy yourself and use the set_* tools.
620
621
 
@@ -1436,7 +1437,7 @@ registerTool(
1436
1437
  "Creates a short draft from a product prompt (min 10 chars). A short is a 12s vertical (two 6s AI " +
1437
1438
  "segments + an on-screen title overlay + music; 14s when it ends on a poster or brand-lockup end card). " +
1438
1439
  "Returns the slug, costs 0 credits. Follow with generate_short to render. Iteration is FREE after that " +
1439
- "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card " +
1440
+ "first generate: the 15 credits bought the footage + music, and every later text/style/CTA/end-card/logo " +
1440
1441
  "tweak re-renders over them for 0 credits (update_short → rerender_short). " +
1441
1442
  "BE PROACTIVE: don't ship a bare clip — set a headline (the on-screen TITLE) and subheadline (secondary " +
1442
1443
  "title), and pick a music_vibe plus visual_language that fit the brand. " +
@@ -1447,8 +1448,8 @@ registerTool(
1447
1448
  "default). If headline/subheadline/text_beats are left blank, the server auto-writes the headline + " +
1448
1449
  "caption beats at generate time (pass skip_auto_text:true to generate_short for a deliberately bare clip). " +
1449
1450
  "To brand it further, attach a product image " +
1450
- "(set_short_product) or an end-card poster (set_short_poster) before generate_short. (Shorts have no logo " +
1451
- "overlay that's an editor-only feature; use create_editor_ad for logo branding.)",
1451
+ "(set_short_product), a deterministic brand logo (set_short_logo), or an end-card poster " +
1452
+ "(set_short_poster) before generate_short.",
1452
1453
  inputSchema: {
1453
1454
  product_prompt: z
1454
1455
  .string()
@@ -1736,9 +1737,10 @@ registerTool(
1736
1737
  title: "Update a short draft (0 credits)",
1737
1738
  description:
1738
1739
  "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " +
1739
- "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card fields " +
1740
+ "0 credits, no re-render by itself. APPLYING the edit: changed only text/style/CTA/end-card/logo fields " +
1740
1741
  "(headline, subheadline, text_beats, cta_text, offer_text, badge_text, star_rating, colors, font, " +
1741
- "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card) on an " +
1742
+ "position, animation, closing_claim, brand_name, poster_includes_lockup, end_card, short_logo_position, " +
1743
+ "short_logo_treatment) on an " +
1742
1744
  "already-generated short? " +
1743
1745
  "rerender_short applies them for 0 credits. Footage/music-affecting edits (product_prompt, product " +
1744
1746
  "image, creative_format, visual_language, theme, music_vibe, music_instruments, language) need " +
@@ -1922,6 +1924,20 @@ registerTool(
1922
1924
  "Set true if your poster already contains brand name/CTA/badges — suppresses the overlaid " +
1923
1925
  "claim/CTA so they don't collide (default false).",
1924
1926
  ),
1927
+ short_logo_position: z
1928
+ .enum(["top-left", "top-right", "bottom-left", "bottom-right"])
1929
+ .optional()
1930
+ .describe(
1931
+ "Corner for the deterministic brand-logo overlay (set the image with set_short_logo). Moving it is " +
1932
+ "a FREE re-render — logo fields never cost credits.",
1933
+ ),
1934
+ short_logo_treatment: z
1935
+ .enum(["throughout", "end_card", "none"])
1936
+ .optional()
1937
+ .describe(
1938
+ 'When the brand logo shows: "throughout" the footage, only on the "end_card", or "none" to hide it. ' +
1939
+ "FREE re-render.",
1940
+ ),
1925
1941
  brand_profile_id: z
1926
1942
  .number()
1927
1943
  .int()
@@ -1967,6 +1983,12 @@ registerTool(
1967
1983
  brand_name?: string;
1968
1984
  end_card?: string;
1969
1985
  poster_includes_lockup?: boolean;
1986
+ short_logo_position?:
1987
+ | "top-left"
1988
+ | "top-right"
1989
+ | "bottom-left"
1990
+ | "bottom-right";
1991
+ short_logo_treatment?: "throughout" | "end_card" | "none";
1970
1992
  brand_profile_id?: number;
1971
1993
  },
1972
1994
  client,
@@ -2013,6 +2035,13 @@ registerTool(
2013
2035
  if (args.end_card !== undefined) body.end_card = args.end_card;
2014
2036
  if (args.poster_includes_lockup !== undefined)
2015
2037
  body.poster_includes_lockup = args.poster_includes_lockup;
2038
+ // Logo overlay placement — the API maps these body keys straight onto the
2039
+ // factory (short_logo_position/short_logo_treatment). They're excluded from
2040
+ // the paid-inputs snapshot, so a corner/treatment change is a FREE re-render.
2041
+ if (args.short_logo_position !== undefined)
2042
+ body.short_logo_position = args.short_logo_position;
2043
+ if (args.short_logo_treatment !== undefined)
2044
+ body.short_logo_treatment = args.short_logo_treatment;
2016
2045
  if (args.brand_profile_id !== undefined)
2017
2046
  body.brand_profile_id = args.brand_profile_id;
2018
2047
 
@@ -2024,7 +2053,7 @@ registerTool(
2024
2053
  return ok({
2025
2054
  ...asRecord(data),
2026
2055
  next:
2027
- "Saved. Only text/style/CTA/end-card changed on an already-generated short? " +
2056
+ "Saved. Only text/style/CTA/end-card/logo changed on an already-generated short? " +
2028
2057
  `rerender_short({ slug: "${args.slug}" }) applies it for 0 credits. Footage/music edits ` +
2029
2058
  "(prompt, product image, format, visual_language, theme, music_*, language) need " +
2030
2059
  "generate_short (15 credits).",
@@ -2464,8 +2493,9 @@ registerTool(
2464
2493
  {
2465
2494
  title: "Re-render a short (FREE — 0 credits)",
2466
2495
  description:
2467
- "FREE re-render: applies the short's current text/style/CTA/end-card state over the already-paid " +
2468
- "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks. Footage or music " +
2496
+ "FREE re-render: applies the short's current text/style/CTA/end-card/logo state over the already-paid " +
2497
+ "footage and music — 0 credits. Use after update_short for copy/style/CTA tweaks, or after a logo change " +
2498
+ "(set_short_logo, or short_logo_position/short_logo_treatment via update_short). Footage or music " +
2469
2499
  "changes need generate_short (15 credits) instead. Edits that add/remove the end card (poster, " +
2470
2500
  "end_card) can shift the video between 12s and 14s; when the pinned music bed is shorter than the " +
2471
2501
  "export it crossfades into a repeated continuation, then fades at the export end, without regeneration. " +
@@ -5043,11 +5073,11 @@ registerTool(
5043
5073
  ),
5044
5074
  );
5045
5075
 
5046
- // ── Shorts: branding images (product + end-card poster) ──────────────────────
5076
+ // ── Shorts: branding images (product + logo + end-card poster) ───────────────
5047
5077
  //
5048
5078
  // The editor branding tools above hit /editor/...; these hit /shorts/... A short
5049
- // has no logo overlay (editor-only), but it CAN feature a product image woven
5050
- // into the footage and a closing end-card poster. Both are 0 credits.
5079
+ // Product images are woven into AI footage; logos are deterministic Forge
5080
+ // overlays; posters are closing cards. All are 0 credits.
5051
5081
 
5052
5082
  registerTool(
5053
5083
  "set_short_product",
@@ -5096,6 +5126,57 @@ registerTool(
5096
5126
  ),
5097
5127
  );
5098
5128
 
5129
+ registerTool(
5130
+ "set_short_logo",
5131
+ {
5132
+ title: "Set a Short brand logo overlay (0 credits)",
5133
+ description:
5134
+ "Uploads an exact brand logo for deterministic compositing over a Short. The image is never sent to the " +
5135
+ "AI video provider, so lettering is preserved. PNG/JPEG, ≤5 MiB, 0 credits. Choose a corner and whether " +
5136
+ "it appears throughout the footage or only on the end card.",
5137
+ inputSchema: {
5138
+ slug: z.string().describe("Short slug"),
5139
+ file_path: z.string().describe("Local logo image (.png/.jpg/.jpeg)"),
5140
+ position: z
5141
+ .enum(["top-left", "top-right", "bottom-left", "bottom-right"])
5142
+ .optional()
5143
+ .describe("Logo corner (default top-right)"),
5144
+ treatment: z
5145
+ .enum(["throughout", "end_card", "none"])
5146
+ .optional()
5147
+ .describe("When the logo appears (default throughout)"),
5148
+ },
5149
+ annotations: { title: "Set Short logo", ...WRITE, idempotentHint: false },
5150
+ },
5151
+ tool(
5152
+ async (
5153
+ args: {
5154
+ slug: string;
5155
+ file_path: string;
5156
+ position?: "top-left" | "top-right" | "bottom-left" | "bottom-right";
5157
+ treatment?: "throughout" | "end_card" | "none";
5158
+ },
5159
+ client,
5160
+ ) => {
5161
+ const { s3_key } = await uploadImageFile(
5162
+ client,
5163
+ `/shorts/${args.slug}/logo/presign`,
5164
+ args.file_path,
5165
+ MAX_SHORT_LOGO_BYTES,
5166
+ );
5167
+ const res = await client.post<{ data: unknown }>(
5168
+ `/shorts/${args.slug}/logo/confirm`,
5169
+ {
5170
+ s3_key,
5171
+ position: args.position ?? "top-right",
5172
+ treatment: args.treatment ?? "throughout",
5173
+ },
5174
+ );
5175
+ return ok(asRecord(res).data ?? res);
5176
+ },
5177
+ ),
5178
+ );
5179
+
5099
5180
  registerTool(
5100
5181
  "set_short_poster",
5101
5182
  {
@@ -5105,7 +5186,7 @@ registerTool(
5105
5186
  "render from 12s to 14s). Accepted: " +
5106
5187
  IMAGE_EXTS.map((e) => `.${e}`).join(", ") +
5107
5188
  " (≤20 MiB = 20,971,520 bytes), confined to HUBFLUENCER_INPUT_DIR/cwd. 0 credits. There is one poster per short; calling " +
5108
- "again replaces it. (Shorts have no logo overlay — for a brand logo use an editor project + set_logo.)",
5189
+ "again replaces it. Use set_short_logo for an exact, non-generative brand overlay.",
5109
5190
  inputSchema: {
5110
5191
  slug: z.string().describe("Short slug (from create_short)"),
5111
5192
  file_path: z.string().describe("Local poster image (.jpg/.jpeg/.png)"),
@@ -65,8 +65,32 @@ export const getStatusOutput = z
65
65
  stale: z.boolean().optional(),
66
66
  active_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
67
67
  stale_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
68
- // Short only, additive (normalizeStatus forwards them gated on typeof
69
- // boolean, so the precise type can never reject a payload).
68
+ // Short only, additive (normalizeStatus forwards each gated on its typeof,
69
+ // so the precise type can never reject a payload).
70
+ failed_stage: z
71
+ .string()
72
+ .optional()
73
+ .describe(
74
+ "Short only: exact API failure phase behind a terminal failure.",
75
+ ),
76
+ failure_code: z
77
+ .string()
78
+ .optional()
79
+ .describe("Short only: stable machine-readable failure classification."),
80
+ failure_details: z
81
+ .record(z.string(), z.unknown())
82
+ .optional()
83
+ .describe("Short only: structured provider/QC details for recovery."),
84
+ failure_source: z
85
+ .string()
86
+ .optional()
87
+ .describe(
88
+ "Short only: worker/recovery layer that finalized the failure.",
89
+ ),
90
+ failure_attempt: z
91
+ .number()
92
+ .optional()
93
+ .describe("Short only: paid generation attempt number that failed."),
70
94
  latest_render_free: z.boolean().optional(),
71
95
  last_free_rerender_failed: z.boolean().optional(),
72
96
  })
@@ -85,6 +109,31 @@ export const waitForCompletionOutput = z
85
109
  stale: z.boolean().optional(),
86
110
  active_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
87
111
  stale_segment_ids: z.array(z.union([z.number(), z.string()])).optional(),
112
+ // Short only, additive — mirrors getStatusOutput (see there).
113
+ failed_stage: z
114
+ .string()
115
+ .optional()
116
+ .describe(
117
+ "Short only: exact API failure phase behind a terminal failure.",
118
+ ),
119
+ failure_code: z
120
+ .string()
121
+ .optional()
122
+ .describe("Short only: stable machine-readable failure classification."),
123
+ failure_details: z
124
+ .record(z.string(), z.unknown())
125
+ .optional()
126
+ .describe("Short only: structured provider/QC details for recovery."),
127
+ failure_source: z
128
+ .string()
129
+ .optional()
130
+ .describe(
131
+ "Short only: worker/recovery layer that finalized the failure.",
132
+ ),
133
+ failure_attempt: z
134
+ .number()
135
+ .optional()
136
+ .describe("Short only: paid generation attempt number that failed."),
88
137
  latest_render_free: z.boolean().optional(),
89
138
  last_free_rerender_failed: z.boolean().optional(),
90
139
  saved_to: z.union([z.string(), z.null()]).optional(),
package/src/uploads.ts CHANGED
@@ -64,6 +64,7 @@ const MAX_VIDEO_BYTES = 500_000_000; // EditorUpload.@max_upload_bytes
64
64
  const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
65
65
  export const MAX_PRODUCT_IMAGE_BYTES = 8 * 1024 * 1024;
66
66
  export const MAX_LOGO_BYTES = 1 * 1024 * 1024; // Editor.Logo.@max_logo_bytes
67
+ export const MAX_SHORT_LOGO_BYTES = 5 * 1024 * 1024;
67
68
  export const MAX_CLOSING_IMAGE_BYTES = 20 * 1024 * 1024;
68
69
  const MULTIPART_THRESHOLD = 50 * 1024 * 1024; // @multipart_min_size — at/above this, use multipart
69
70
  const MAX_CATALOG_VIDEO_DURATION_SECONDS = 60; // CatalogAsset video validator