@hubfluencer/mcp 0.6.0 → 0.7.1

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
@@ -29,7 +29,7 @@ Hubfluencer app, and a scoped access token is saved locally to `~/.hubfluencer/c
29
29
 
30
30
  ```bash
31
31
  npx -y @hubfluencer/mcp login "Claude Code"
32
- # 1. Open: https://hubfluencer.com/connect?code=ABCD-EFGH
32
+ # 1. Open: https://app.hubfluencer.com/connect?code=ABCD-EFGH
33
33
  # 2. Confirm code: ABCDEFGH
34
34
  # 3. Click Approve. → ✓ Connected.
35
35
  ```
@@ -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
- 88 tools, grouped by workflow. Every tool prices its action in its own `description` — costs
71
+ 89 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
@@ -79,7 +79,8 @@ instead draw a **free daily quota of 20 AI assists** (top up with `unlock_ai_ass
79
79
  | Tool | What it does | Cost |
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
- | `create_short` | Create a short draft (12s vertical: two 6s AI segments + title overlay + music; 14s with an end-card poster). Set `headline`/`subheadline`/`music_vibe`/`visual_language` so it isn't bare. | **0** |
82
+ | `create_short` | Create a short draft (12s vertical: two 6s AI segments + title overlay + music; 14s with an end-card poster). Set `headline`/`subheadline`/`music_vibe`/`visual_language` so it isn't bare. Optional opt-in conversion graphics: `offer_text`/`cta_text`/`badge_text`/`star_rating` (off unless you set them). | **0** |
83
+ | `update_short` | Patch an existing short draft — brief (`product_prompt`), copy, styling, or the opt-in conversion graphics; only the fields you pass change. Clear one with `""` (`[]` for `text_beats`, `0` for `star_rating`). Re-run `generate_short` to apply. | **0** |
83
84
  | `generate_short` | Render an existing short. Safe to re-call (in-flight = reported, no double charge; a failed short can re-generate). | **15** |
84
85
  | `generate_short_text` | AI-draft editable headline / subheadline / caption beats for a short. | 1 assist |
85
86
  | `create_editor_ad` | Create an editor project **and** run autopilot end-to-end. Style it with `creative_format` + `visual_language` + `theme`. | credits (est. first) |
package/dist/index.js CHANGED
@@ -30120,7 +30120,7 @@ function readStoredCredentials() {
30120
30120
  // package.json
30121
30121
  var package_default = {
30122
30122
  name: "@hubfluencer/mcp",
30123
- version: "0.6.0",
30123
+ version: "0.7.1",
30124
30124
  description: "Model Context Protocol server for Hubfluencer — let AI agents generate post-ready shorts and editor ads.",
30125
30125
  license: "MIT",
30126
30126
  author: "Monocursive <contact@monocursive.com>",
@@ -31429,6 +31429,21 @@ var VISUAL_LANGUAGES = [
31429
31429
  "startup_explainer",
31430
31430
  "luxury_minimal"
31431
31431
  ];
31432
+ var LANGUAGES = [
31433
+ "en",
31434
+ "fr",
31435
+ "es",
31436
+ "de",
31437
+ "it",
31438
+ "nl",
31439
+ "pl",
31440
+ "pt",
31441
+ "ru",
31442
+ "zh",
31443
+ "ja",
31444
+ "ko",
31445
+ "ar"
31446
+ ];
31432
31447
  var RO = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
31433
31448
  var WRITE = {
31434
31449
  readOnlyHint: false,
@@ -31511,7 +31526,7 @@ registerTool("make_video", {
31511
31526
  kind: exports_external.string().optional().describe("'short' (fast, 1 clip), 'editor' (multi-scene), or 'auto' (default — inferred)"),
31512
31527
  product_subject: exports_external.string().max(300).optional().describe('EDITOR only: the concrete product / main subject the video must be about (e.g. "Bordeaux red ' + 'wine, dark green bottle"). Hard-grounds the AI so it cannot invent an unrelated product. ' + "Recommended for ads."),
31513
31528
  project_intent: exports_external.enum(["social_ad", "creative_story"]).optional().describe("EDITOR only: social_ad for promotional content (product focus + CTA) or creative_story (default)."),
31514
- language: exports_external.string().optional().describe('Language code, e.g. "en" (default), "fr". Drives scenario, narration, voice, and captions.'),
31529
+ language: exports_external.enum(LANGUAGES).optional().describe('Language (default "en"). Bare ISO-639-1 code only — no region/script subtag ' + '(use "pt", never "pt-BR"/"en-US"). Drives scenario, narration, voice, and captions.'),
31515
31530
  aspect: exports_external.string().optional().describe("Aspect ratio for editor: 9:16 (default), 16:9, or 1:1"),
31516
31531
  voice_id: exports_external.string().optional().describe("Preferred narration voice id for editor ads (see list_voices); shorts have no voiceover"),
31517
31532
  headline: exports_external.string().optional().describe("SHORTS only: the on-screen TITLE overlay (≤160). Set this so the short isn't bare."),
@@ -31523,9 +31538,10 @@ registerTool("make_video", {
31523
31538
  offer_text: exports_external.string().optional().describe("SHORTS only: optional offer chip, e.g. -40% (≤16 chars)"),
31524
31539
  cta_text: exports_external.string().optional().describe("SHORTS only: optional CTA pill, e.g. Shop now (≤24 chars)"),
31525
31540
  badge_text: exports_external.string().optional().describe("SHORTS only: optional badge stamp, e.g. BEST SELLER (≤24 chars)"),
31526
- star_rating: exports_external.number().optional().describe("SHORTS only: optional 0..5 star rating under the subheadline"),
31541
+ star_rating: exports_external.number().int().min(0).max(5).optional().describe("SHORTS only: optional 0..5 star rating under the subheadline"),
31527
31542
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("SHORTS only: title overlay position (default bottom)"),
31528
31543
  text_animation: exports_external.enum([
31544
+ "auto",
31529
31545
  "reveal",
31530
31546
  "typewriter",
31531
31547
  "fade_in",
@@ -31533,9 +31549,10 @@ registerTool("make_video", {
31533
31549
  "bounce",
31534
31550
  "word_stagger",
31535
31551
  "word_spotlight"
31536
- ]).optional().describe("SHORTS only: title overlay animation (default fade_in)"),
31537
- font_family: exports_external.string().optional().describe("SHORTS only: overlay font family, e.g. ShortFontSpaceGrotesk"),
31552
+ ]).optional().describe("SHORTS only: title overlay animation. Defaults to auto (follow the visual_language's own " + "animation). Every concrete value (including fade_in) is an explicit pick that always wins."),
31553
+ font_family: exports_external.string().optional().describe("SHORTS only: overlay font. Defaults to auto (follow the visual_language's typography), or pass " + "one of the 15 ShortFont* faces (ShortFontSpaceGrotesk, ShortFontAnton, …). Every concrete face " + "is an explicit pick that always wins."),
31538
31554
  music_vibe: exports_external.string().optional().describe("SHORTS only: music mood — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz"),
31555
+ music_instruments: exports_external.array(exports_external.string()).optional().describe('SHORTS only: instrument hints woven into the music (≤10, each ≤40 chars), e.g. ["piano", "strings"]'),
31539
31556
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("SHORTS + EDITOR: narrative arc / segment structure. Omit for Auto/generic."),
31540
31557
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("SHORTS + EDITOR: visual style direction and render look. Default app choice is kinetic_creator."),
31541
31558
  theme: exports_external.string().optional().describe("Visual theme: editor genre overlay, and legacy shorts fallback only when visual_language is unset. " + "For editor, when visual_language is set it drives the look and 'none' is ignored. " + "none (literal — no imposed style), realistic (default), cinematic, " + "anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, " + "minimalist, cyberpunk"),
@@ -31572,6 +31589,14 @@ registerTool("make_video", {
31572
31589
  return fail("each text_beats entry must be at most 120 characters.");
31573
31590
  }
31574
31591
  }
31592
+ if (args.music_instruments !== undefined) {
31593
+ if (args.music_instruments.length > 10) {
31594
+ return fail("music_instruments must have at most 10 entries.");
31595
+ }
31596
+ if (args.music_instruments.some((i) => i.length > 40)) {
31597
+ return fail("each music_instruments entry must be at most 40 characters.");
31598
+ }
31599
+ }
31575
31600
  if (args.save_path)
31576
31601
  resolveSavePath(args.save_path);
31577
31602
  let slug;
@@ -31593,10 +31618,11 @@ registerTool("make_video", {
31593
31618
  short_text_animation: args.text_animation,
31594
31619
  short_font_family: args.font_family,
31595
31620
  music_vibe: args.music_vibe,
31621
+ music_instruments: args.music_instruments,
31596
31622
  creative_format: args.creative_format,
31597
31623
  visual_language: args.visual_language,
31598
31624
  theme: args.theme
31599
- }, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", JSON.stringify(args.text_beats ?? []), args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.music_vibe ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? ""));
31625
+ }, idemKey("make-short", args.prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", JSON.stringify(args.text_beats ?? []), args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.creative_format ?? "", args.visual_language ?? "", args.theme ?? ""));
31600
31626
  slug = created.data.slug;
31601
31627
  } else {
31602
31628
  const created = await client.post("/editor", {
@@ -31703,7 +31729,10 @@ registerTool("list_projects", {
31703
31729
  }, tool(async (args, client) => {
31704
31730
  const cap = Math.min(100, Math.max(1, args.limit ?? 25));
31705
31731
  const [shortsRaw, factoriesRaw] = await Promise.all([
31706
- client.get("/shorts", args.include_completed ? { include_completed: "true" } : undefined).catch(() => ({ data: [] })),
31732
+ client.get("/shorts", {
31733
+ ...args.include_completed ? { include_completed: "true" } : {},
31734
+ per_page: String(cap)
31735
+ }).catch(() => ({ data: [] })),
31707
31736
  client.get("/video-factories").catch(() => ({ data: [] }))
31708
31737
  ]);
31709
31738
  const rows = (raw) => {
@@ -31728,16 +31757,17 @@ registerTool("create_short", {
31728
31757
  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 an end-card poster is set). Returns the slug, " + "costs 0 credits. Follow with generate_short to render. " + "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. 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.)",
31729
31758
  inputSchema: {
31730
31759
  product_prompt: exports_external.string().min(10).describe("What the short should be about"),
31731
- language: exports_external.string().optional().describe('Language code, e.g. "en" (default)'),
31760
+ 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".'),
31732
31761
  headline: exports_external.string().max(160).optional().describe("The on-screen TITLE composited over the short (poster text overlay). ≤160 chars. Set this."),
31733
31762
  subheadline: exports_external.string().max(200).optional().describe("The SECONDARY title / supporting line under the headline. ≤200 chars."),
31734
31763
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Optional structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal. Omit for Auto."),
31735
31764
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling. Good default: kinetic_creator."),
31736
31765
  theme: exports_external.string().optional().describe("Deprecated for shorts: legacy visual theme used only when visual_language is unset."),
31737
31766
  music_vibe: exports_external.string().optional().describe("Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz."),
31738
- music_instruments: exports_external.array(exports_external.string()).optional().describe('Optional instrument hints, e.g. ["piano", "strings"]'),
31767
+ music_instruments: exports_external.array(exports_external.string().max(40)).max(10).optional().describe('Optional instrument hints (≤10, each ≤40 chars), e.g. ["piano", "strings"]'),
31739
31768
  text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits (default bottom)"),
31740
31769
  text_animation: exports_external.enum([
31770
+ "auto",
31741
31771
  "reveal",
31742
31772
  "typewriter",
31743
31773
  "fade_in",
@@ -31745,16 +31775,17 @@ registerTool("create_short", {
31745
31775
  "bounce",
31746
31776
  "word_stagger",
31747
31777
  "word_spotlight"
31748
- ]).optional().describe("Title overlay animation (default fade_in)"),
31749
- font_family: exports_external.string().optional().describe("Overlay font (default ShortFontSpaceGrotesk). One of: ShortFontSpaceGrotesk, ShortFontMontserrat, " + "ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, " + "ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " + "ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker."),
31778
+ ]).optional().describe("Title overlay animation. Defaults to auto (follow the visual_language's own animation). " + "Every concrete value (including fade_in) is an explicit pick that always wins."),
31779
+ font_family: exports_external.string().optional().describe("Overlay font. Defaults to auto (follow the visual_language's typography). Every concrete face " + "is an explicit pick that always wins. One of: auto, ShortFontSpaceGrotesk, ShortFontMontserrat, " + "ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, " + "ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " + "ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker."),
31750
31780
  headline_color: exports_external.string().optional().describe("Headline hex color, e.g. #ffffff"),
31751
31781
  subheadline_color: exports_external.string().optional().describe("Subheadline hex color, e.g. #ffffff"),
31752
31782
  accent_color: exports_external.string().optional().describe("6-digit brand accent hex color, e.g. #09EFBE"),
31753
31783
  offer_text: exports_external.string().max(16).optional().describe('Optional offer chip, e.g. "-40%" or "2 FOR 1"'),
31754
31784
  cta_text: exports_external.string().max(24).optional().describe('Optional CTA pill, e.g. "Shop now"'),
31755
31785
  badge_text: exports_external.string().max(24).optional().describe('Optional badge stamp, e.g. "BEST SELLER"'),
31756
- star_rating: exports_external.number().min(0).max(5).optional().describe("Optional 0..5 star rating under the subheadline"),
31757
- text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Optional caption beats shown sequentially instead of the static subheadline.")
31786
+ star_rating: exports_external.number().int().min(0).max(5).optional().describe("Optional 0..5 star rating under the subheadline"),
31787
+ text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Optional caption beats shown sequentially instead of the static subheadline."),
31788
+ 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). Its creative format, visual language, font, accent, and default CTA fill " + "any blank short fields and a snapshot is frozen onto the short. Must be one of your own profiles.")
31758
31789
  },
31759
31790
  outputSchema: createDraftOutput,
31760
31791
  annotations: { title: "Create short", ...WRITE, idempotentHint: true }
@@ -31800,13 +31831,103 @@ registerTool("create_short", {
31800
31831
  body.star_rating = args.star_rating;
31801
31832
  if (args.text_beats !== undefined)
31802
31833
  body.text_beats = args.text_beats;
31803
- const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), JSON.stringify(args.text_beats ?? [])));
31834
+ if (args.brand_profile_id !== undefined)
31835
+ body.brand_profile_id = args.brand_profile_id;
31836
+ const res = await client.post("/shorts", body, idemKey("create-short", args.product_prompt, args.language ?? "", args.headline ?? "", args.subheadline ?? "", args.creative_format ?? "", args.visual_language ?? "", args.theme ?? "", args.music_vibe ?? "", JSON.stringify(args.music_instruments ?? []), args.text_position ?? "", args.text_animation ?? "", args.font_family ?? "", args.headline_color ?? "", args.subheadline_color ?? "", args.accent_color ?? "", args.offer_text ?? "", args.cta_text ?? "", args.badge_text ?? "", String(args.star_rating ?? ""), JSON.stringify(args.text_beats ?? []), String(args.brand_profile_id ?? "")));
31804
31837
  return ok({
31805
31838
  slug: res.data.slug,
31806
31839
  kind: "short",
31807
31840
  next: "set_short_product / set_short_poster (optional branding), then generate_short"
31808
31841
  });
31809
31842
  }));
31843
+ registerTool("update_short", {
31844
+ title: "Update a short draft (0 credits)",
31845
+ description: "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " + "0 credits, no re-render (call generate_short afterwards to apply). Use this to edit the brief, copy, " + "styling, or the optional conversion graphics after create_short. " + "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.",
31846
+ inputSchema: {
31847
+ slug: exports_external.string().describe("Short slug (from create_short)"),
31848
+ product_prompt: exports_external.string().min(10).optional().describe("Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short."),
31849
+ language: exports_external.enum(LANGUAGES).optional().describe('Language code, e.g. "en". Bare ISO-639-1 code only — no region/script subtag like "en-US".'),
31850
+ headline: exports_external.string().max(160).optional().describe("On-screen TITLE overlay. ≤160 chars."),
31851
+ subheadline: exports_external.string().max(200).optional().describe("Secondary title / supporting line. ≤200 chars."),
31852
+ creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal."),
31853
+ visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual language for Veo direction and render styling."),
31854
+ theme: exports_external.string().optional().describe("Deprecated for shorts: legacy visual theme used only when visual_language is unset."),
31855
+ music_vibe: exports_external.string().optional().describe("Background-music mood: Upbeat, Cinematic, Minimal, Luxury, Playful, Jazz."),
31856
+ music_instruments: exports_external.array(exports_external.string().max(40)).max(10).optional().describe('Instrument hints (≤10, each ≤40 chars), e.g. ["piano", "strings"]'),
31857
+ text_position: exports_external.enum(["top", "center", "bottom"]).optional().describe("Where the title overlay sits."),
31858
+ text_animation: exports_external.enum([
31859
+ "auto",
31860
+ "reveal",
31861
+ "typewriter",
31862
+ "fade_in",
31863
+ "pop",
31864
+ "bounce",
31865
+ "word_stagger",
31866
+ "word_spotlight"
31867
+ ]).optional().describe("Title overlay animation. Defaults to auto (follow the visual_language's own animation). " + "Every concrete value (including fade_in) is an explicit pick that always wins."),
31868
+ font_family: exports_external.string().optional().describe("Overlay font. Defaults to auto (follow the visual_language's typography). Every concrete face " + "is an explicit pick that always wins. One of: auto, ShortFontSpaceGrotesk, ShortFontMontserrat, ShortFontTheBold, ShortFontImpact, " + "ShortFontLato, ShortFontAnton, ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, " + "ShortFontPoppins, ShortFontInter, ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, " + "ShortFontPermanentMarker."),
31869
+ headline_color: exports_external.string().optional().describe("Headline hex color, e.g. #ffffff"),
31870
+ subheadline_color: exports_external.string().optional().describe("Subheadline hex color, e.g. #ffffff"),
31871
+ accent_color: exports_external.string().optional().describe("6-digit brand accent hex color, e.g. #09EFBE"),
31872
+ offer_text: exports_external.string().max(16).optional().describe('Offer chip (opt-in), e.g. "-40%". Pass "" to remove it.'),
31873
+ cta_text: exports_external.string().max(24).optional().describe('CTA pill (opt-in), e.g. "Shop now". Pass "" to remove it.'),
31874
+ badge_text: exports_external.string().max(24).optional().describe('Badge stamp (opt-in), e.g. "BEST SELLER". Pass "" to remove it.'),
31875
+ star_rating: exports_external.number().int().min(0).max(5).optional().describe("0..5 star rating under the subheadline (opt-in). Pass 0 to remove it."),
31876
+ text_beats: exports_external.array(exports_external.string().max(120)).max(8).optional().describe("Caption beats shown sequentially instead of the static subheadline. Pass [] to clear."),
31877
+ 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.")
31878
+ },
31879
+ annotations: {
31880
+ title: "Update short",
31881
+ ...WRITE,
31882
+ idempotentHint: true
31883
+ }
31884
+ }, tool(async (args, client) => {
31885
+ const body = {};
31886
+ if (args.product_prompt !== undefined)
31887
+ body.product_prompt = args.product_prompt;
31888
+ if (args.language !== undefined)
31889
+ body.language = args.language;
31890
+ if (args.headline !== undefined)
31891
+ body.headline = args.headline;
31892
+ if (args.subheadline !== undefined)
31893
+ body.subheadline = args.subheadline;
31894
+ if (args.creative_format !== undefined)
31895
+ body.creative_format = args.creative_format;
31896
+ if (args.visual_language !== undefined)
31897
+ body.visual_language = args.visual_language;
31898
+ if (args.theme !== undefined)
31899
+ body.theme = args.theme;
31900
+ if (args.music_vibe !== undefined)
31901
+ body.music_vibe = args.music_vibe;
31902
+ if (args.music_instruments !== undefined)
31903
+ body.music_instruments = args.music_instruments;
31904
+ if (args.text_position !== undefined)
31905
+ body.short_text_position = args.text_position;
31906
+ if (args.text_animation !== undefined)
31907
+ body.short_text_animation = args.text_animation;
31908
+ if (args.font_family !== undefined)
31909
+ body.short_font_family = args.font_family;
31910
+ if (args.headline_color !== undefined)
31911
+ body.headline_color = args.headline_color;
31912
+ if (args.subheadline_color !== undefined)
31913
+ body.subheadline_color = args.subheadline_color;
31914
+ if (args.accent_color !== undefined)
31915
+ body.accent_color = args.accent_color;
31916
+ if (args.offer_text !== undefined)
31917
+ body.offer_text = args.offer_text;
31918
+ if (args.cta_text !== undefined)
31919
+ body.cta_text = args.cta_text;
31920
+ if (args.badge_text !== undefined)
31921
+ body.badge_text = args.badge_text;
31922
+ if (args.star_rating !== undefined)
31923
+ body.star_rating = args.star_rating;
31924
+ if (args.text_beats !== undefined)
31925
+ body.text_beats = args.text_beats;
31926
+ if (args.brand_profile_id !== undefined)
31927
+ body.brand_profile_id = args.brand_profile_id;
31928
+ const res = await client.patch(`/shorts/${args.slug}`, body);
31929
+ return ok(asRecord(res).data ?? res);
31930
+ }));
31810
31931
  async function getTracking(client, slug, deadline) {
31811
31932
  const res = await client.get(`/tracking/${slug}`, undefined, deadline);
31812
31933
  return res.data;
@@ -32178,7 +32299,7 @@ registerTool("create_editor_ad", {
32178
32299
  product_prompt: exports_external.string().min(10).describe("Brief for the ad (min 10 chars)"),
32179
32300
  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."),
32180
32301
  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)."),
32181
- language: exports_external.string().optional().describe('Language code, e.g. "en" (default), "fr", "es". Drives the WHOLE video: scenario, narration script, ' + "narration voice, and captions. Ask the user / infer from their locale."),
32302
+ 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."),
32182
32303
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32183
32304
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored."),
32184
32305
  theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
@@ -32215,7 +32336,7 @@ registerTool("start_autopilot", {
32215
32336
  description: "Starts server-orchestrated autopilot (scenario → segments → narration → voice → music → render) on an " + "EXISTING editor project/draft. Spends credits — preview the estimate first with the autopilot/cost endpoint " + "or make_video({ dry_run:true }). Each call is a genuine start attempt (like tapping Launch in the app): " + "if a run is already in progress for this slug the server returns already_running — it never double-starts " + "or double-charges. Once finished, failed, or cancelled, calling again relaunches it (resume after a top-up, raising your spend cap, or a mid-pipeline failure). Then poll with " + 'wait_for_completion({ slug, kind: "editor" }).',
32216
32337
  inputSchema: {
32217
32338
  slug: exports_external.string().describe("Editor project slug"),
32218
- language: exports_external.string().optional().describe('Optional language override applied before launch (ISO 639-1, e.g. "fr"). Drives scenario, ' + "narration, voice, and captions."),
32339
+ language: exports_external.enum(LANGUAGES).optional().describe('Optional language override applied before launch. Bare ISO-639-1 code only, e.g. "fr" no ' + "region/script subtag. Drives scenario, narration, voice, and captions."),
32219
32340
  product_subject: exports_external.string().max(300).optional().describe("Optional locked product / main subject applied before launch so the AI cannot invent an " + "unrelated product."),
32220
32341
  product_prompt: exports_external.string().optional().describe("Optional freeform brief to set/replace before launch.")
32221
32342
  },
@@ -32266,7 +32387,7 @@ registerTool("create_editor_draft", {
32266
32387
  inputSchema: {
32267
32388
  product_prompt: exports_external.string().min(10).max(5000).optional().describe("Brief for the ad — 10–5000 chars, or omit entirely"),
32268
32389
  product_subject: exports_external.string().max(300).optional().describe("The concrete product / main subject the video must be about. Hard-grounds the AI so it cannot " + "invent an unrelated product. Recommended for ads."),
32269
- language: exports_external.string().min(2).max(10).optional().describe('Language code, e.g. "en" (default). Drives scenario, narration, voice, and captions.'),
32390
+ 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". Drives scenario, narration, voice, and captions.'),
32270
32391
  creative_format: exports_external.enum(CREATIVE_FORMATS).optional().describe("Narrative arc / segment structure. Omit for Auto/generic."),
32271
32392
  visual_language: exports_external.enum(VISUAL_LANGUAGES).optional().describe("Visual style direction and render look. Good default: kinetic_creator. When set it drives the look and a theme of 'none' is ignored."),
32272
32393
  theme: exports_external.string().optional().describe('Visual theme / genre overlay (default "realistic"). One of: none (no imposed style — segments follow your prompts literally), realistic, cinematic, anime, sci_fi, fantasy, noir, superhero, horror, mockumentary, sports, gaming, retro_80s, minimalist, cyberpunk.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubfluencer/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
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/index.ts CHANGED
@@ -463,6 +463,26 @@ const VISUAL_LANGUAGES = [
463
463
  "startup_explainer",
464
464
  "luxury_minimal",
465
465
  ] as const;
466
+ // The languages the API's VideoFactory.language accepts (video_factory.ex
467
+ // @supported_languages, validate_inclusion at create + autopilot). It's a CLOSED
468
+ // set of bare ISO-639-1 codes — the server rejects region/script subtags ("en",
469
+ // never "en-US"/"pt-BR") — so every tool that writes a factory language exposes
470
+ // this exact enum rather than a free-form string.
471
+ const LANGUAGES = [
472
+ "en",
473
+ "fr",
474
+ "es",
475
+ "de",
476
+ "it",
477
+ "nl",
478
+ "pl",
479
+ "pt",
480
+ "ru",
481
+ "zh",
482
+ "ja",
483
+ "ko",
484
+ "ar",
485
+ ] as const;
466
486
 
467
487
  const RO = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
468
488
  const WRITE = {
@@ -652,10 +672,11 @@ registerTool(
652
672
  "EDITOR only: social_ad for promotional content (product focus + CTA) or creative_story (default).",
653
673
  ),
654
674
  language: z
655
- .string()
675
+ .enum(LANGUAGES)
656
676
  .optional()
657
677
  .describe(
658
- 'Language code, e.g. "en" (default), "fr". Drives scenario, narration, voice, and captions.',
678
+ 'Language (default "en"). Bare ISO-639-1 code only no region/script subtag ' +
679
+ '(use "pt", never "pt-BR"/"en-US"). Drives scenario, narration, voice, and captions.',
659
680
  ),
660
681
  aspect: z
661
682
  .string()
@@ -711,6 +732,9 @@ registerTool(
711
732
  ),
712
733
  star_rating: z
713
734
  .number()
735
+ .int()
736
+ .min(0)
737
+ .max(5)
714
738
  .optional()
715
739
  .describe(
716
740
  "SHORTS only: optional 0..5 star rating under the subheadline",
@@ -721,6 +745,7 @@ registerTool(
721
745
  .describe("SHORTS only: title overlay position (default bottom)"),
722
746
  text_animation: z
723
747
  .enum([
748
+ "auto",
724
749
  "reveal",
725
750
  "typewriter",
726
751
  "fade_in",
@@ -730,12 +755,17 @@ registerTool(
730
755
  "word_spotlight",
731
756
  ])
732
757
  .optional()
733
- .describe("SHORTS only: title overlay animation (default fade_in)"),
758
+ .describe(
759
+ "SHORTS only: title overlay animation. Defaults to auto (follow the visual_language's own " +
760
+ "animation). Every concrete value (including fade_in) is an explicit pick that always wins.",
761
+ ),
734
762
  font_family: z
735
763
  .string()
736
764
  .optional()
737
765
  .describe(
738
- "SHORTS only: overlay font family, e.g. ShortFontSpaceGrotesk",
766
+ "SHORTS only: overlay font. Defaults to auto (follow the visual_language's typography), or pass " +
767
+ "one of the 15 ShortFont* faces (ShortFontSpaceGrotesk, ShortFontAnton, …). Every concrete face " +
768
+ "is an explicit pick that always wins.",
739
769
  ),
740
770
  music_vibe: z
741
771
  .string()
@@ -743,6 +773,12 @@ registerTool(
743
773
  .describe(
744
774
  "SHORTS only: music mood — Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz",
745
775
  ),
776
+ music_instruments: z
777
+ .array(z.string())
778
+ .optional()
779
+ .describe(
780
+ 'SHORTS only: instrument hints woven into the music (≤10, each ≤40 chars), e.g. ["piano", "strings"]',
781
+ ),
746
782
  creative_format: z
747
783
  .enum(CREATIVE_FORMATS)
748
784
  .optional()
@@ -816,6 +852,7 @@ registerTool(
816
852
  text_animation?: string;
817
853
  font_family?: string;
818
854
  music_vibe?: string;
855
+ music_instruments?: string[];
819
856
  creative_format?: string;
820
857
  visual_language?: string;
821
858
  theme?: string;
@@ -865,6 +902,16 @@ registerTool(
865
902
  return fail("each text_beats entry must be at most 120 characters.");
866
903
  }
867
904
  }
905
+ if (args.music_instruments !== undefined) {
906
+ if (args.music_instruments.length > 10) {
907
+ return fail("music_instruments must have at most 10 entries.");
908
+ }
909
+ if (args.music_instruments.some((i) => i.length > 40)) {
910
+ return fail(
911
+ "each music_instruments entry must be at most 40 characters.",
912
+ );
913
+ }
914
+ }
868
915
 
869
916
  // Validate the save_path up front so we fail before spending credits.
870
917
  if (args.save_path) resolveSavePath(args.save_path);
@@ -897,6 +944,7 @@ registerTool(
897
944
  short_text_animation: args.text_animation,
898
945
  short_font_family: args.font_family,
899
946
  music_vibe: args.music_vibe,
947
+ music_instruments: args.music_instruments,
900
948
  creative_format: args.creative_format,
901
949
  visual_language: args.visual_language,
902
950
  theme: args.theme,
@@ -922,6 +970,7 @@ registerTool(
922
970
  args.text_animation ?? "",
923
971
  args.font_family ?? "",
924
972
  args.music_vibe ?? "",
973
+ JSON.stringify(args.music_instruments ?? []),
925
974
  args.creative_format ?? "",
926
975
  args.visual_language ?? "",
927
976
  args.theme ?? "",
@@ -1184,10 +1233,10 @@ registerTool(
1184
1233
  const cap = Math.min(100, Math.max(1, args.limit ?? 25));
1185
1234
  const [shortsRaw, factoriesRaw] = await Promise.all([
1186
1235
  client
1187
- .get<{ data: unknown }>(
1188
- "/shorts",
1189
- args.include_completed ? { include_completed: "true" } : undefined,
1190
- )
1236
+ .get<{ data: unknown }>("/shorts", {
1237
+ ...(args.include_completed ? { include_completed: "true" } : {}),
1238
+ per_page: String(cap),
1239
+ })
1191
1240
  .catch(() => ({ data: [] })),
1192
1241
  client
1193
1242
  .get<{ data: unknown }>("/video-factories")
@@ -1244,9 +1293,11 @@ registerTool(
1244
1293
  .min(10)
1245
1294
  .describe("What the short should be about"),
1246
1295
  language: z
1247
- .string()
1296
+ .enum(LANGUAGES)
1248
1297
  .optional()
1249
- .describe('Language code, e.g. "en" (default)'),
1298
+ .describe(
1299
+ 'Language code, e.g. "en" (default). Bare ISO-639-1 code only — no region/script subtag like "en-US".',
1300
+ ),
1250
1301
  headline: z
1251
1302
  .string()
1252
1303
  .max(160)
@@ -1286,15 +1337,19 @@ registerTool(
1286
1337
  "Background-music mood. Recognized: Upbeat (default), Cinematic, Minimal, Luxury, Playful, Jazz.",
1287
1338
  ),
1288
1339
  music_instruments: z
1289
- .array(z.string())
1340
+ .array(z.string().max(40))
1341
+ .max(10)
1290
1342
  .optional()
1291
- .describe('Optional instrument hints, e.g. ["piano", "strings"]'),
1343
+ .describe(
1344
+ 'Optional instrument hints (≤10, each ≤40 chars), e.g. ["piano", "strings"]',
1345
+ ),
1292
1346
  text_position: z
1293
1347
  .enum(["top", "center", "bottom"])
1294
1348
  .optional()
1295
1349
  .describe("Where the title overlay sits (default bottom)"),
1296
1350
  text_animation: z
1297
1351
  .enum([
1352
+ "auto",
1298
1353
  "reveal",
1299
1354
  "typewriter",
1300
1355
  "fade_in",
@@ -1304,12 +1359,16 @@ registerTool(
1304
1359
  "word_spotlight",
1305
1360
  ])
1306
1361
  .optional()
1307
- .describe("Title overlay animation (default fade_in)"),
1362
+ .describe(
1363
+ "Title overlay animation. Defaults to auto (follow the visual_language's own animation). " +
1364
+ "Every concrete value (including fade_in) is an explicit pick that always wins.",
1365
+ ),
1308
1366
  font_family: z
1309
1367
  .string()
1310
1368
  .optional()
1311
1369
  .describe(
1312
- "Overlay font (default ShortFontSpaceGrotesk). One of: ShortFontSpaceGrotesk, ShortFontMontserrat, " +
1370
+ "Overlay font. Defaults to auto (follow the visual_language's typography). Every concrete face " +
1371
+ "is an explicit pick that always wins. One of: auto, ShortFontSpaceGrotesk, ShortFontMontserrat, " +
1313
1372
  "ShortFontTheBold, ShortFontImpact, ShortFontLato, ShortFontAnton, " +
1314
1373
  "ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, ShortFontPoppins, ShortFontInter, " +
1315
1374
  "ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, ShortFontPermanentMarker.",
@@ -1343,6 +1402,7 @@ registerTool(
1343
1402
  .describe('Optional badge stamp, e.g. "BEST SELLER"'),
1344
1403
  star_rating: z
1345
1404
  .number()
1405
+ .int()
1346
1406
  .min(0)
1347
1407
  .max(5)
1348
1408
  .optional()
@@ -1354,6 +1414,15 @@ registerTool(
1354
1414
  .describe(
1355
1415
  "Optional caption beats shown sequentially instead of the static subheadline.",
1356
1416
  ),
1417
+ brand_profile_id: z
1418
+ .number()
1419
+ .int()
1420
+ .optional()
1421
+ .describe(
1422
+ "Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " +
1423
+ "list_brand_profiles). Its creative format, visual language, font, accent, and default CTA fill " +
1424
+ "any blank short fields and a snapshot is frozen onto the short. Must be one of your own profiles.",
1425
+ ),
1357
1426
  },
1358
1427
  outputSchema: createDraftOutput,
1359
1428
  annotations: { title: "Create short", ...WRITE, idempotentHint: true },
@@ -1381,6 +1450,7 @@ registerTool(
1381
1450
  badge_text?: string;
1382
1451
  star_rating?: number;
1383
1452
  text_beats?: string[];
1453
+ brand_profile_id?: number;
1384
1454
  },
1385
1455
  client,
1386
1456
  ) => {
@@ -1416,6 +1486,8 @@ registerTool(
1416
1486
  if (args.badge_text !== undefined) body.badge_text = args.badge_text;
1417
1487
  if (args.star_rating !== undefined) body.star_rating = args.star_rating;
1418
1488
  if (args.text_beats !== undefined) body.text_beats = args.text_beats;
1489
+ if (args.brand_profile_id !== undefined)
1490
+ body.brand_profile_id = args.brand_profile_id;
1419
1491
  const res = await client.post<{ data: { slug: string } }>(
1420
1492
  "/shorts",
1421
1493
  body,
@@ -1441,6 +1513,7 @@ registerTool(
1441
1513
  args.badge_text ?? "",
1442
1514
  String(args.star_rating ?? ""),
1443
1515
  JSON.stringify(args.text_beats ?? []),
1516
+ String(args.brand_profile_id ?? ""),
1444
1517
  ),
1445
1518
  );
1446
1519
  return ok({
@@ -1452,6 +1525,242 @@ registerTool(
1452
1525
  ),
1453
1526
  );
1454
1527
 
1528
+ registerTool(
1529
+ "update_short",
1530
+ {
1531
+ title: "Update a short draft (0 credits)",
1532
+ description:
1533
+ "Patches an existing short draft — only the fields you pass change; omitted fields are left untouched. " +
1534
+ "0 credits, no re-render (call generate_short afterwards to apply). Use this to edit the brief, copy, " +
1535
+ "styling, or the optional conversion graphics after create_short. " +
1536
+ "CLEARING a field: the conversion graphics (offer_text, cta_text, badge_text, star_rating) and " +
1537
+ 'text_beats are OPT-IN and OFF by default — pass "" (or 0 for star_rating, [] for text_beats) to REMOVE ' +
1538
+ "one that was set. The four conversion graphics are user-supplied only and never written by the AI copy " +
1539
+ "assist; text_beats (like headline/subheadline) IS rewritten by generate_short_text, so hand-crafted " +
1540
+ "beats don't survive a later assist call. " +
1541
+ "404 for an unknown/foreign slug.",
1542
+ inputSchema: {
1543
+ slug: z.string().describe("Short slug (from create_short)"),
1544
+ product_prompt: z
1545
+ .string()
1546
+ .min(10)
1547
+ .optional()
1548
+ .describe(
1549
+ "Rewrite the brief (what the short is about, ≥10 chars); drives the next generate_short.",
1550
+ ),
1551
+ language: z
1552
+ .enum(LANGUAGES)
1553
+ .optional()
1554
+ .describe(
1555
+ 'Language code, e.g. "en". Bare ISO-639-1 code only — no region/script subtag like "en-US".',
1556
+ ),
1557
+ headline: z
1558
+ .string()
1559
+ .max(160)
1560
+ .optional()
1561
+ .describe("On-screen TITLE overlay. ≤160 chars."),
1562
+ subheadline: z
1563
+ .string()
1564
+ .max(200)
1565
+ .optional()
1566
+ .describe("Secondary title / supporting line. ≤200 chars."),
1567
+ creative_format: z
1568
+ .enum(CREATIVE_FORMATS)
1569
+ .optional()
1570
+ .describe(
1571
+ "Structure: problem_solution, mistake_fix, myth_vs_reality, before_after, proof_demo, product_reveal.",
1572
+ ),
1573
+ visual_language: z
1574
+ .enum(VISUAL_LANGUAGES)
1575
+ .optional()
1576
+ .describe("Visual language for Veo direction and render styling."),
1577
+ theme: z
1578
+ .string()
1579
+ .optional()
1580
+ .describe(
1581
+ "Deprecated for shorts: legacy visual theme used only when visual_language is unset.",
1582
+ ),
1583
+ music_vibe: z
1584
+ .string()
1585
+ .optional()
1586
+ .describe(
1587
+ "Background-music mood: Upbeat, Cinematic, Minimal, Luxury, Playful, Jazz.",
1588
+ ),
1589
+ music_instruments: z
1590
+ .array(z.string().max(40))
1591
+ .max(10)
1592
+ .optional()
1593
+ .describe(
1594
+ 'Instrument hints (≤10, each ≤40 chars), e.g. ["piano", "strings"]',
1595
+ ),
1596
+ text_position: z
1597
+ .enum(["top", "center", "bottom"])
1598
+ .optional()
1599
+ .describe("Where the title overlay sits."),
1600
+ text_animation: z
1601
+ .enum([
1602
+ "auto",
1603
+ "reveal",
1604
+ "typewriter",
1605
+ "fade_in",
1606
+ "pop",
1607
+ "bounce",
1608
+ "word_stagger",
1609
+ "word_spotlight",
1610
+ ])
1611
+ .optional()
1612
+ .describe(
1613
+ "Title overlay animation. Defaults to auto (follow the visual_language's own animation). " +
1614
+ "Every concrete value (including fade_in) is an explicit pick that always wins.",
1615
+ ),
1616
+ font_family: z
1617
+ .string()
1618
+ .optional()
1619
+ .describe(
1620
+ "Overlay font. Defaults to auto (follow the visual_language's typography). Every concrete face " +
1621
+ "is an explicit pick that always wins. One of: auto, ShortFontSpaceGrotesk, ShortFontMontserrat, ShortFontTheBold, ShortFontImpact, " +
1622
+ "ShortFontLato, ShortFontAnton, ShortFontBebasNeue, ShortFontOswald, ShortFontArchivoBlack, " +
1623
+ "ShortFontPoppins, ShortFontInter, ShortFontTikTokSans, ShortFontBangers, ShortFontDMSerif, " +
1624
+ "ShortFontPermanentMarker.",
1625
+ ),
1626
+ headline_color: z
1627
+ .string()
1628
+ .optional()
1629
+ .describe("Headline hex color, e.g. #ffffff"),
1630
+ subheadline_color: z
1631
+ .string()
1632
+ .optional()
1633
+ .describe("Subheadline hex color, e.g. #ffffff"),
1634
+ accent_color: z
1635
+ .string()
1636
+ .optional()
1637
+ .describe("6-digit brand accent hex color, e.g. #09EFBE"),
1638
+ offer_text: z
1639
+ .string()
1640
+ .max(16)
1641
+ .optional()
1642
+ .describe('Offer chip (opt-in), e.g. "-40%". Pass "" to remove it.'),
1643
+ cta_text: z
1644
+ .string()
1645
+ .max(24)
1646
+ .optional()
1647
+ .describe('CTA pill (opt-in), e.g. "Shop now". Pass "" to remove it.'),
1648
+ badge_text: z
1649
+ .string()
1650
+ .max(24)
1651
+ .optional()
1652
+ .describe(
1653
+ 'Badge stamp (opt-in), e.g. "BEST SELLER". Pass "" to remove it.',
1654
+ ),
1655
+ star_rating: z
1656
+ .number()
1657
+ .int()
1658
+ .min(0)
1659
+ .max(5)
1660
+ .optional()
1661
+ .describe(
1662
+ "0..5 star rating under the subheadline (opt-in). Pass 0 to remove it.",
1663
+ ),
1664
+ text_beats: z
1665
+ .array(z.string().max(120))
1666
+ .max(8)
1667
+ .optional()
1668
+ .describe(
1669
+ "Caption beats shown sequentially instead of the static subheadline. Pass [] to clear.",
1670
+ ),
1671
+ brand_profile_id: z
1672
+ .number()
1673
+ .int()
1674
+ .optional()
1675
+ .describe(
1676
+ "Seed brand defaults — colors/font/CTA — from a saved brand profile (id from " +
1677
+ "list_brand_profiles). Best set at create_short time (that path also freezes a brand snapshot); " +
1678
+ "on update it sets the linked profile. Must be one of your own profiles.",
1679
+ ),
1680
+ },
1681
+ annotations: {
1682
+ title: "Update short",
1683
+ ...WRITE,
1684
+ // PATCH by slug → replaying the same fields converges to the same state.
1685
+ idempotentHint: true,
1686
+ },
1687
+ },
1688
+ tool(
1689
+ async (
1690
+ args: {
1691
+ slug: string;
1692
+ product_prompt?: string;
1693
+ language?: string;
1694
+ headline?: string;
1695
+ subheadline?: string;
1696
+ creative_format?: string;
1697
+ visual_language?: string;
1698
+ theme?: string;
1699
+ music_vibe?: string;
1700
+ music_instruments?: string[];
1701
+ text_position?: string;
1702
+ text_animation?: string;
1703
+ font_family?: string;
1704
+ headline_color?: string;
1705
+ subheadline_color?: string;
1706
+ accent_color?: string;
1707
+ offer_text?: string;
1708
+ cta_text?: string;
1709
+ badge_text?: string;
1710
+ star_rating?: number;
1711
+ text_beats?: string[];
1712
+ brand_profile_id?: number;
1713
+ },
1714
+ client,
1715
+ ) => {
1716
+ // Only forward keys the caller actually passed. A present "" / [] / 0 is
1717
+ // intentional — the API's update is present-aware, so it clears that
1718
+ // field (the four conversion graphics + beats are opt-in and this is
1719
+ // how an agent turns one back off). Field names map onto the API's
1720
+ // short_* params exactly as create_short does.
1721
+ const body: Record<string, unknown> = {};
1722
+ if (args.product_prompt !== undefined)
1723
+ body.product_prompt = args.product_prompt;
1724
+ if (args.language !== undefined) body.language = args.language;
1725
+ if (args.headline !== undefined) body.headline = args.headline;
1726
+ if (args.subheadline !== undefined) body.subheadline = args.subheadline;
1727
+ if (args.creative_format !== undefined)
1728
+ body.creative_format = args.creative_format;
1729
+ if (args.visual_language !== undefined)
1730
+ body.visual_language = args.visual_language;
1731
+ if (args.theme !== undefined) body.theme = args.theme;
1732
+ if (args.music_vibe !== undefined) body.music_vibe = args.music_vibe;
1733
+ if (args.music_instruments !== undefined)
1734
+ body.music_instruments = args.music_instruments;
1735
+ if (args.text_position !== undefined)
1736
+ body.short_text_position = args.text_position;
1737
+ if (args.text_animation !== undefined)
1738
+ body.short_text_animation = args.text_animation;
1739
+ if (args.font_family !== undefined)
1740
+ body.short_font_family = args.font_family;
1741
+ if (args.headline_color !== undefined)
1742
+ body.headline_color = args.headline_color;
1743
+ if (args.subheadline_color !== undefined)
1744
+ body.subheadline_color = args.subheadline_color;
1745
+ if (args.accent_color !== undefined)
1746
+ body.accent_color = args.accent_color;
1747
+ if (args.offer_text !== undefined) body.offer_text = args.offer_text;
1748
+ if (args.cta_text !== undefined) body.cta_text = args.cta_text;
1749
+ if (args.badge_text !== undefined) body.badge_text = args.badge_text;
1750
+ if (args.star_rating !== undefined) body.star_rating = args.star_rating;
1751
+ if (args.text_beats !== undefined) body.text_beats = args.text_beats;
1752
+ if (args.brand_profile_id !== undefined)
1753
+ body.brand_profile_id = args.brand_profile_id;
1754
+
1755
+ const res = await client.patch<{ data: unknown }>(
1756
+ `/shorts/${args.slug}`,
1757
+ body,
1758
+ );
1759
+ return ok(asRecord(res).data ?? res);
1760
+ },
1761
+ ),
1762
+ );
1763
+
1455
1764
  interface TrackingData {
1456
1765
  slug: string;
1457
1766
  status: string;
@@ -2292,11 +2601,12 @@ registerTool(
2292
2601
  "(default).",
2293
2602
  ),
2294
2603
  language: z
2295
- .string()
2604
+ .enum(LANGUAGES)
2296
2605
  .optional()
2297
2606
  .describe(
2298
- 'Language code, e.g. "en" (default), "fr", "es". Drives the WHOLE video: scenario, narration script, ' +
2299
- "narration voice, and captions. Ask the user / infer from their locale.",
2607
+ 'Language (default "en"). Bare ISO-639-1 code only — no region/script subtag (use "es", not "es-MX"). ' +
2608
+ "Drives the WHOLE video: scenario, narration script, narration voice, and captions. " +
2609
+ "Ask the user / infer from their locale.",
2300
2610
  ),
2301
2611
  creative_format: z
2302
2612
  .enum(CREATIVE_FORMATS)
@@ -2399,11 +2709,11 @@ registerTool(
2399
2709
  inputSchema: {
2400
2710
  slug: z.string().describe("Editor project slug"),
2401
2711
  language: z
2402
- .string()
2712
+ .enum(LANGUAGES)
2403
2713
  .optional()
2404
2714
  .describe(
2405
- 'Optional language override applied before launch (ISO 639-1, e.g. "fr"). Drives scenario, ' +
2406
- "narration, voice, and captions.",
2715
+ 'Optional language override applied before launch. Bare ISO-639-1 code only, e.g. "fr" no ' +
2716
+ "region/script subtag. Drives scenario, narration, voice, and captions.",
2407
2717
  ),
2408
2718
  product_subject: z
2409
2719
  .string()
@@ -2545,12 +2855,11 @@ registerTool(
2545
2855
  "invent an unrelated product. Recommended for ads.",
2546
2856
  ),
2547
2857
  language: z
2548
- .string()
2549
- .min(2)
2550
- .max(10)
2858
+ .enum(LANGUAGES)
2551
2859
  .optional()
2552
2860
  .describe(
2553
- 'Language code, e.g. "en" (default). Drives scenario, narration, voice, and captions.',
2861
+ 'Language code, e.g. "en" (default). Bare ISO-639-1 code only no region/script subtag like ' +
2862
+ '"en-US". Drives scenario, narration, voice, and captions.',
2554
2863
  ),
2555
2864
  creative_format: z
2556
2865
  .enum(CREATIVE_FORMATS)