@vibeframe/mcp-server 0.108.0 → 0.109.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.
Files changed (3) hide show
  1. package/README.md +24 -1
  2. package/dist/index.js +111 -12
  3. package/package.json +4 -3
package/README.md CHANGED
@@ -41,7 +41,26 @@ args = ["-y", "@vibeframe/mcp-server"]
41
41
  enabled = true
42
42
  ```
43
43
 
44
- ### Claude Desktop
44
+ ### Claude Desktop (recommended: extension)
45
+
46
+ Install the prebuilt Desktop Extension — no Node, npx, or JSON editing needed:
47
+
48
+ 1. Download `vibeframe-<version>.mcpb` from the
49
+ [latest GitHub Release](https://github.com/vericontext/vibeframe/releases/latest).
50
+ 2. In Claude Desktop open **Settings → Extensions** and drag the `.mcpb` file in
51
+ (or double-click the file).
52
+ 3. In the extension's settings pick your **Workspace folder** (projects are
53
+ created there; a `.env` file in that folder is loaded for API keys) and
54
+ optionally paste provider keys.
55
+
56
+ Rendering still needs Google Chrome and `ffmpeg` on the machine. For free local
57
+ Kokoro TTS, run `npm i kokoro-js` once inside the workspace folder. To update,
58
+ download the new release's `.mcpb` and install it over the old one.
59
+
60
+ To build the bundle from source: `pnpm -F @vibeframe/mcp-server build:mcpb`
61
+ (output in `packages/mcp-server/dist-mcpb/`).
62
+
63
+ #### Claude Desktop (manual JSON config)
45
64
 
46
65
  Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
47
66
 
@@ -64,6 +83,10 @@ config and may not preserve a raw `cwd` field. It anchors relative project paths
64
83
  so prompts like "create a project named launch" create `launch/` under your
65
84
  workspace instead of a temporary directory.
66
85
 
86
+ Note: some Claude Desktop builds rewrite this file from memory and can mangle
87
+ hand-edited entries (the entry comes back as `"command": "custom"`). The
88
+ extension install above avoids that entirely and is the recommended path.
89
+
67
90
  ### Cursor
68
91
 
69
92
  Add to `.cursor/mcp.json`:
package/dist/index.js CHANGED
@@ -4730,13 +4730,32 @@ async function loadKokoroFactory() {
4730
4730
  const mod = await import("kokoro-js");
4731
4731
  return mod.KokoroTTS;
4732
4732
  } catch (err) {
4733
+ const fallback2 = await loadKokoroFromWorkspace();
4734
+ if (fallback2)
4735
+ return fallback2;
4733
4736
  throw mapKokoroImportError(err);
4734
4737
  }
4735
4738
  }
4739
+ async function loadKokoroFromWorkspace(baseDir = process.cwd()) {
4740
+ try {
4741
+ const { readFile: readFile45 } = await import("node:fs/promises");
4742
+ const { join: join44, resolve: resolve79 } = await import("node:path");
4743
+ const { pathToFileURL } = await import("node:url");
4744
+ const pkgDir = resolve79(baseDir, "node_modules", "kokoro-js");
4745
+ const pkg = JSON.parse(await readFile45(join44(pkgDir, "package.json"), "utf-8"));
4746
+ const entry = pkg.exports?.node?.import ?? pkg.exports?.default ?? pkg.module ?? pkg.main;
4747
+ if (!entry)
4748
+ return null;
4749
+ const mod = await import(pathToFileURL(join44(pkgDir, entry)).href);
4750
+ return mod.KokoroTTS ?? null;
4751
+ } catch {
4752
+ return null;
4753
+ }
4754
+ }
4736
4755
  function mapKokoroImportError(err) {
4737
4756
  const code = err instanceof Error && "code" in err ? err.code : void 0;
4738
4757
  if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
4739
- return new Error("Local Kokoro TTS is unavailable: optional dependency kokoro-js is not installed (its native dependencies may have failed to build on this platform). Install it with `npm i kokoro-js`, or set ELEVENLABS_API_KEY to use the elevenlabs TTS provider instead.");
4758
+ return new Error("Local Kokoro TTS is unavailable: optional dependency kokoro-js is not installed (its native dependencies may have failed to build on this platform). Install it with `npm i kokoro-js` \u2014 run it inside your workspace folder when using the Claude Desktop extension \u2014 or set ELEVENLABS_API_KEY to use the elevenlabs TTS provider instead.");
4740
4759
  }
4741
4760
  return err instanceof Error ? err : new Error(String(err));
4742
4761
  }
@@ -25267,6 +25286,8 @@ imageProvider: openai
25267
25286
 
25268
25287
  Edit these beats before running \`vibe build\`. Each beat starts with
25269
25288
  YAML cues that drive narration, backdrop generation, and timing.
25289
+ Pacing: keep beats 6-15 seconds (split longer ones); a 90s video should
25290
+ have 6-8 beats, not 3 long ones.
25270
25291
 
25271
25292
  ## Beat hook \u2014 Hook
25272
25293
 
@@ -450075,6 +450096,29 @@ function filterSubCompFalsePositives(findings, isSubComposition) {
450075
450096
  if (!isSubComposition) return noEpsilon;
450076
450097
  return noEpsilon.filter((f2) => !SUB_COMP_FALSE_POSITIVES.has(f2.code));
450077
450098
  }
450099
+ function findInternalPhaseClipFindings(html) {
450100
+ const findings = [];
450101
+ for (const tagMatch of html.matchAll(/<[a-z][a-z0-9-]*\s[^>]*>/gi)) {
450102
+ const tag = tagMatch[0];
450103
+ if (!/class="[^"]*\bclip\b[^"]*"/i.test(tag)) continue;
450104
+ const startAttr = tag.match(/data-start="([^"]+)"/i);
450105
+ if (!startAttr) continue;
450106
+ const start = Number(startAttr[1]);
450107
+ if (!Number.isFinite(start) || start === 0) continue;
450108
+ findings.push({
450109
+ code: "internal_phase_clip_unsupported",
450110
+ severity: "error",
450111
+ message: `Inner .clip has data-start="${startAttr[1]}". The renderer does not toggle internal clip visibility inside sub-compositions, so phased clips render ALL phases at once (overlapping text).`,
450112
+ fixHint: 'Give every inner .clip data-start="0" spanning the full beat, and drive phase changes with GSAP autoAlpha instead (animate the previous phase out, the next one in).',
450113
+ snippet: tag.slice(0, 120)
450114
+ });
450115
+ }
450116
+ return findings;
450117
+ }
450118
+ function withVibeframeSubCompFindings(findings, html, isSubComposition) {
450119
+ if (!isSubComposition) return findings;
450120
+ return [...findings, ...findInternalPhaseClipFindings(html)];
450121
+ }
450078
450122
  function applyMechanicalFixes(html, findings) {
450079
450123
  const fixedCodes = /* @__PURE__ */ new Set();
450080
450124
  let updated = html;
@@ -450120,7 +450164,11 @@ async function runProjectLint(opts) {
450120
450164
  source: "projectDir"
450121
450165
  };
450122
450166
  const raw2 = runHyperframeLint(prepared);
450123
- const findings = filterSubCompFalsePositives(raw2.findings, isSub);
450167
+ const findings = withVibeframeSubCompFindings(
450168
+ filterSubCompFalsePositives(raw2.findings, isSub),
450169
+ html,
450170
+ isSub
450171
+ );
450124
450172
  if (opts.fix && findings.length > 0) {
450125
450173
  const { html: nextHtml, fixedCodes } = applyMechanicalFixes(html, findings);
450126
450174
  if (fixedCodes.length > 0) {
@@ -450130,7 +450178,11 @@ async function runProjectLint(opts) {
450130
450178
  files.push({
450131
450179
  file: rel,
450132
450180
  isSubComposition: isSub,
450133
- findings: filterSubCompFalsePositives(reLinted.findings, isSub)
450181
+ findings: withVibeframeSubCompFindings(
450182
+ filterSubCompFalsePositives(reLinted.findings, isSub),
450183
+ nextHtml,
450184
+ isSub
450185
+ )
450134
450186
  });
450135
450187
  continue;
450136
450188
  }
@@ -469633,8 +469685,14 @@ ${finalDurationBullet}
469633
469685
  Animate the backdrop/media plane instead; let text enter briefly, then hold
469634
469686
  still at its final CSS position. Continuous transforms on text ancestors can
469635
469687
  create subpixel shimmer in screenshot-captured renders.
469636
- - Timed children inside the composition have \`class="clip"\` plus
469637
- \`data-start\`, \`data-duration\`, \`data-track-index\`.
469688
+ - Inner \`.clip\` elements carry \`data-start\`, \`data-duration\`,
469689
+ \`data-track-index\` \u2014 but **every inner clip MUST be full-window:
469690
+ \`data-start="0"\` spanning the whole beat.** The renderer does NOT toggle
469691
+ internal clip visibility inside sub-compositions, so "phase clips" with a
469692
+ non-zero \`data-start\` render ALL phases at once and text stacks on top of
469693
+ itself. For multi-phase beats, drive phase changes with GSAP \`autoAlpha\`
469694
+ inside full-window clips: animate phase A out (\`autoAlpha: 0\`), then
469695
+ phase B in, on the same timeline.
469638
469696
  - If \`assets/backdrop-${ctx.beat.id}.png\` exists, use that local file as the
469639
469697
  full-frame visual backdrop. The exact path string is
469640
469698
  \`assets/backdrop-${ctx.beat.id}.png\`; do NOT prefix it with \`../\` or \`./\`
@@ -469648,7 +469706,9 @@ ${finalDurationBullet}
469648
469706
  narration.
469649
469707
  - Keep a meaningful visual progression across the full beat duration: line
469650
469708
  tracing, gentle camera drift, staged labels, value cards, or parallax. Avoid
469651
- a completed static frame holding silently for most of the beat.
469709
+ a completed static frame holding silently for most of the beat. For longer
469710
+ beats, progress through content phases with GSAP autoAlpha (old phase out,
469711
+ new phase in) \u2014 never with inner clip windows.
469652
469712
  - Do not import external font or image URLs. Use the project DESIGN.md font
469653
469713
  choice when explicit; otherwise use \`Inter\`, which is available in the
469654
469714
  deterministic render font map.
@@ -469823,7 +469883,11 @@ function lintBeatHtml(html, beatId) {
469823
469883
  source: "projectDir"
469824
469884
  };
469825
469885
  const raw2 = runHyperframeLint(prepared);
469826
- const findings = filterSubCompFalsePositives(raw2.findings, true);
469886
+ const findings = withVibeframeSubCompFindings(
469887
+ filterSubCompFalsePositives(raw2.findings, true),
469888
+ html,
469889
+ true
469890
+ );
469827
469891
  return {
469828
469892
  errorCount: findings.filter((f2) => f2.severity === "error").length,
469829
469893
  warningCount: findings.filter((f2) => f2.severity === "warning").length,
@@ -470262,7 +470326,8 @@ function buildInstructions(args) {
470262
470326
  lines.push(`2. Read \`DESIGN.md\` for project-specific palette, typography, motion signature.`);
470263
470327
  lines.push(`3. For each beat in the \`beats\` array below, author HTML at \`outputPath\` matching the \`userPrompt\`. The beat \`body\` carries the narrative + visual + animation intent; \`cues\` carries machine-readable per-beat overrides (narration, duration, backdrop, voice).`);
470264
470328
  lines.push(`3b. Use each beat's \`finalDurationSec\` (narration-synced) for \`data-duration\` and timeline anchors when present \u2014 NOT the storyboard \`duration\`, which is only the minimum. Scenes composed at the storyboard duration end early and render black tails.`);
470265
- lines.push(`3c. If your environment cannot write files (e.g. Claude Desktop / MCP-only hosts), author each beat's HTML and submit it with the \`scene_submit\` tool (beat id + html). It validates with the same Hyperframes lint and writes the file for you; on lint errors it returns the findings without writing \u2014 fix and resubmit.`);
470329
+ lines.push(`3c. Never give inner \`.clip\` elements a non-zero \`data-start\` \u2014 the renderer does not toggle internal clip visibility inside sub-compositions, so phased clips render all phases at once (overlapping text). Use full-window clips and GSAP autoAlpha phase transitions instead. Also keep beats 6-15s; split anything longer in the storyboard first.`);
470330
+ lines.push(`3d. If your environment cannot write files (e.g. Claude Desktop / MCP-only hosts), author each beat's HTML and submit it with the \`scene_submit\` tool (beat id + html). It validates with the same Hyperframes lint and writes the file for you; on lint errors it returns the findings without writing \u2014 fix and resubmit.`);
470266
470331
  if (args.beatCount > 1) {
470267
470332
  lines.push(`4. After authoring all ${args.beatCount} beat(s), run \`vibe scene lint --fix\` to validate. Fix any remaining errors by editing the HTML directly.`);
470268
470333
  } else if (args.filtered) {
@@ -470584,7 +470649,11 @@ function lintHtml(html, rel, isSub) {
470584
470649
  source: "projectDir"
470585
470650
  };
470586
470651
  const raw2 = runHyperframeLint(prepared);
470587
- return filterSubCompFalsePositives(raw2.findings, isSub);
470652
+ return withVibeframeSubCompFindings(
470653
+ filterSubCompFalsePositives(raw2.findings, isSub),
470654
+ html,
470655
+ isSub
470656
+ );
470588
470657
  }
470589
470658
  function lintFilesToIssues(files) {
470590
470659
  const issues = [];
@@ -471655,6 +471724,16 @@ function validateStoryboardMarkdown(markdown) {
471655
471724
  }
471656
471725
  }
471657
471726
  }
471727
+ for (const beat of parsed.beats) {
471728
+ if (beat.duration !== void 0 && beat.duration > MAX_RECOMMENDED_BEAT_SEC) {
471729
+ issues.push({
471730
+ severity: "warning",
471731
+ code: "BEAT_DURATION_TOO_LONG",
471732
+ beatId: beat.id,
471733
+ message: `Beat "${beat.id}" is ${beat.duration}s \u2014 beats longer than ${MAX_RECOMMENDED_BEAT_SEC}s render static and overstuffed. Split it into 6-15s beats (a 90s video should have 6-8 beats).`
471734
+ });
471735
+ }
471736
+ }
471658
471737
  return {
471659
471738
  ok: !issues.some((i2) => i2.severity === "error"),
471660
471739
  beats: parsed.beats,
@@ -471753,7 +471832,7 @@ function readLeadingCueBlock(body) {
471753
471832
  return { full: match2[0], error: error instanceof Error ? error.message : String(error) };
471754
471833
  }
471755
471834
  }
471756
- var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS;
471835
+ var import_yaml5, STORYBOARD_CUE_KEYS, HEADING_RE2, LEADING_CUE_RE, ALLOWED_CUE_KEYS, STRING_CUE_KEYS, MAX_RECOMMENDED_BEAT_SEC;
471757
471836
  var init_storyboard_edit = __esm({
471758
471837
  "../cli/src/commands/_shared/storyboard-edit.ts"() {
471759
471838
  "use strict";
@@ -471773,6 +471852,7 @@ var init_storyboard_edit = __esm({
471773
471852
  LEADING_CUE_RE = /^(\s*)```ya?ml\s*\n([\s\S]*?)\n```\s*(?:\n|$)/;
471774
471853
  ALLOWED_CUE_KEYS = new Set(STORYBOARD_CUE_KEYS);
471775
471854
  STRING_CUE_KEYS = /* @__PURE__ */ new Set(["narration", "backdrop", "video", "motion", "voice", "music", "asset"]);
471855
+ MAX_RECOMMENDED_BEAT_SEC = 15;
471776
471856
  }
471777
471857
  });
471778
471858
 
@@ -491942,7 +492022,8 @@ function revisionMessages(opts) {
491942
492022
  "The JSON object must have: storyboardMd string, summary string, changedBeats string[], warnings string[].",
491943
492023
  "Preserve existing frontmatter, beat ids, cue YAML keys, and useful prose unless the user asks to change structure.",
491944
492024
  "Keep cue YAML valid. Allowed cue keys are duration, narration, backdrop, video, motion, voice, music, asset.",
491945
- "If a target duration is supplied, every beat must have a positive duration cue and durations must sum to that target."
492025
+ "If a target duration is supplied, every beat must have a positive duration cue and durations must sum to that target.",
492026
+ "Aim for 6-15 seconds per beat and never exceed ~15s \u2014 long beats render as static, overstuffed scenes. A 90-second video should have 6-8 beats."
491946
492027
  ].join("\n");
491947
492028
  const repairBlock = opts.invalidStoryboard ? [
491948
492029
  "The previous revision failed validation. Repair it.",
@@ -497625,7 +497706,23 @@ This creates a music video-style edit where cuts happen in rhythm with the music
497625
497706
  }
497626
497707
 
497627
497708
  // src/instructions.ts
497709
+ import { mkdirSync as mkdirSync13 } from "node:fs";
497628
497710
  import { isAbsolute as isAbsolute9, resolve as resolve78 } from "node:path";
497711
+ function applyWorkspaceEnv(env4 = process.env, chdir = (dir) => process.chdir(dir)) {
497712
+ const raw2 = env4.VIBE_MCP_WORKSPACE?.trim();
497713
+ if (!raw2) return null;
497714
+ const dir = resolve78(raw2);
497715
+ try {
497716
+ mkdirSync13(dir, { recursive: true });
497717
+ chdir(dir);
497718
+ return dir;
497719
+ } catch (error) {
497720
+ console.error(
497721
+ `[vibeframe] Could not use VIBE_MCP_WORKSPACE=${raw2} (${error instanceof Error ? error.message : String(error)}); staying in ${process.cwd()}`
497722
+ );
497723
+ return null;
497724
+ }
497725
+ }
497629
497726
  function resolveServerWorkspaceRoot(env4 = process.env, cwd = process.cwd()) {
497630
497727
  const initCwd = env4.INIT_CWD?.trim();
497631
497728
  if (!initCwd) return cwd;
@@ -497645,10 +497742,12 @@ function buildServerInstructions(cwd = resolveServerWorkspaceRoot()) {
497645
497742
  console.log = (...args) => console.error(...args);
497646
497743
  console.info = (...args) => console.error(...args);
497647
497744
  console.debug = (...args) => console.error(...args);
497745
+ applyWorkspaceEnv();
497648
497746
  var server2 = new Server(
497649
497747
  {
497650
497748
  name: "vibeframe",
497651
- version: "0.1.0"
497749
+ // Stamped with the package version by build.js at bundle time.
497750
+ version: "0.109.0"
497652
497751
  },
497653
497752
  {
497654
497753
  capabilities: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibeframe/mcp-server",
3
- "version": "0.108.0",
3
+ "version": "0.109.0",
4
4
  "description": "VibeFrame MCP Server - AI-native video editing via Model Context Protocol",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,14 +60,15 @@
60
60
  "tsx": "^4.21.0",
61
61
  "typescript": "^5.3.3",
62
62
  "vitest": "^1.2.2",
63
- "@vibeframe/cli": "0.108.0",
64
- "@vibeframe/core": "0.108.0"
63
+ "@vibeframe/core": "0.109.0",
64
+ "@vibeframe/cli": "0.109.0"
65
65
  },
66
66
  "engines": {
67
67
  "node": ">=20"
68
68
  },
69
69
  "scripts": {
70
70
  "build": "node build.js",
71
+ "build:mcpb": "node build-mcpb.js",
71
72
  "build:types": "tsc --emitDeclarationOnly",
72
73
  "dev": "tsx src/index.ts",
73
74
  "start": "node dist/index.js",