davinci-resolve-mcp 2.136.1 → 2.137.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/CHANGELOG.md CHANGED
@@ -2,6 +2,44 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v2.137.0 — the sweep reaches the database tier
6
+
7
+ ### Fixed
8
+
9
+ - **Fairlight DB row selection** (adapted the v2.133 container-pin lesson to
10
+ the vendor layer): `readFromDatabase` took the FIRST `Sm2Sequence` row — in
11
+ any project holding a compound clip that can be a compound's embedded
12
+ sequence with no `FLStudioModelBA`, so bus reads failed while the model sat
13
+ in the next row (measured live). Worse, `applyTemplate` wrote its new blob
14
+ into EVERY blob-bearing sequence row, clobbering compound `SeqRef` links
15
+ project-wide. Reads now pick the model-bearing row; writes scope to exactly
16
+ that `Sm2Sequence_id` and refuse ambiguous multi-timeline projects without
17
+ an explicit target. `read_buses_from_db` also stops dumping the ~430KB
18
+ decompressed model into the tool response.
19
+ - **`provenance.cdl_diff` silently identity-defaulted array-shaped CDLs**:
20
+ `[r,g,b]` slope/offset/power (the common interchange form) read as unity
21
+ through the `{r,g,b}` accessor, so two different grades diffed as
22
+ saturation-only (measured). Both shapes are accepted now; unrecognizable
23
+ shapes refuse loudly.
24
+ - **Node-floor enforcement**: the advanced server refuses to start below
25
+ Node 20.9 with the exact fix named (measured live: a client config's node
26
+ resolved to an nvm v18, where pure-JS tools limp and better-sqlite3 dies
27
+ with a cryptic ABI mismatch). `install.py` now writes an absolute,
28
+ version-checked node path into client configs, and the better-sqlite3
29
+ loader distinguishes "not installed" from "built for a different Node".
30
+ - **Windows UTF-8, rounds two and three** — adapted from
31
+ [PR #175](https://github.com/samuelgursky/davinci-resolve-mcp/pull/175) and
32
+ [PR #176](https://github.com/samuelgursky/davinci-resolve-mcp/pull/176) by
33
+ @Chosen-3: the six unencoded call sites in `src/` (brain-edits registry,
34
+ page lock) and `install.py`'s client-config `read_json`/`write_json`. The
35
+ UTF-8 discipline guard now covers `src/` and `install.py` too.
36
+
37
+ ### Verified through the MCP tool layer (E60)
38
+
39
+ `project_read` (introspect/report/audit/timeline_clips), `offline_ref.list_in_project`,
40
+ `color_trace.plan` (6/6 exact-name matches), and both fairlight bus readers,
41
+ all against a live scratch project database.
42
+
5
43
  ## What's New in v2.136.1 — the changelog catches up
6
44
 
7
45
  ### Fixed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.136.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.137.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.136.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.137.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.136.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.137.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -13,6 +13,27 @@
13
13
  import { fileURLToPath, pathToFileURL } from 'node:url';
14
14
  import path from 'node:path';
15
15
 
16
+ // Node floor (package.json engines: >=20.9), enforced at STARTUP rather than
17
+ // discovered per-feature: under an old Node the pure-JS tools limp along
18
+ // while native-dep paths (better-sqlite3: project_read, fairlight DB actions,
19
+ // offline_ref live linking) die with a cryptic NODE_MODULE_VERSION mismatch —
20
+ // measured live under nvm's v18.20.8, which is exactly what a bare "node"
21
+ // command in an MCP registration resolves to on a machine whose shell
22
+ // default lags. Silent degradation is this repo's least favorite failure
23
+ // mode; refuse loudly with the fix instead.
24
+ const [major, minor] = process.versions.node.split('.').map(Number);
25
+ if (major < 20 || (major === 20 && minor < 9)) {
26
+ process.stderr.write(
27
+ `[davinci-resolve-advanced-mcp] Node ${process.versions.node} is below the ` +
28
+ `supported floor (>=20.9). This process was started by: ${process.execPath}\n` +
29
+ `Fix: point the MCP registration's command at a Node >=20.9 binary ` +
30
+ `(e.g. the absolute path from \`nvm which 20\`), or update the default ` +
31
+ `node on PATH. Re-running install.py also rewrites client configs with ` +
32
+ `an absolute, version-checked node path.\n`,
33
+ );
34
+ process.exit(1);
35
+ }
36
+
16
37
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
38
  const serverEntry = path.resolve(__dirname, '..', 'resolve-advanced', 'server', 'index.mjs');
18
39
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.136.1"
40
+ VERSION = "2.137.0"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
@@ -1091,7 +1091,7 @@ def read_json(path):
1091
1091
  existing settings (issue #71: Zed's settings.json ships with comments).
1092
1092
  """
1093
1093
  try:
1094
- with open(path, "r") as f:
1094
+ with open(path, "r", encoding="utf-8") as f:
1095
1095
  raw = f.read()
1096
1096
  except FileNotFoundError:
1097
1097
  return {}
@@ -1120,7 +1120,7 @@ def write_json(path, data):
1120
1120
  backup = path.with_suffix(path.suffix + ".backup")
1121
1121
  shutil.copy2(path, backup)
1122
1122
 
1123
- with open(path, "w") as f:
1123
+ with open(path, "w", encoding="utf-8") as f:
1124
1124
  json.dump(data, f, indent=2)
1125
1125
  f.write("\n")
1126
1126
 
@@ -1183,12 +1183,61 @@ def build_advanced_entry(server_path, python_path=None):
1183
1183
  """
1184
1184
  project_dir = Path(server_path).resolve().parents[1] # .../src/server.py -> repo root
1185
1185
  advanced_bin = project_dir / "bin" / "davinci-resolve-advanced-mcp.mjs"
1186
- entry = {"command": "node", "args": [str(advanced_bin)]}
1186
+ entry = {"command": resolve_node_command(), "args": [str(advanced_bin)]}
1187
1187
  if python_path:
1188
1188
  entry["env"] = {"AAF_PROBE_PYTHON": str(python_path)}
1189
1189
  return entry
1190
1190
 
1191
1191
 
1192
+ # Node floor for the advanced server (package.json engines). Below it the
1193
+ # pure-JS tools limp along while native-dep paths (better-sqlite3) die with a
1194
+ # cryptic NODE_MODULE_VERSION mismatch — measured live when a client config's
1195
+ # bare "node" resolved to an nvm v18 that a GUI app had on PATH.
1196
+ NODE_MIN = (20, 9)
1197
+
1198
+
1199
+ def _node_version(cmd):
1200
+ try:
1201
+ out = subprocess.run([cmd, "--version"], capture_output=True, text=True,
1202
+ encoding="utf-8", timeout=10, check=False).stdout.strip()
1203
+ parts = out.lstrip("v").split(".")
1204
+ return (int(parts[0]), int(parts[1]))
1205
+ except Exception:
1206
+ return None
1207
+
1208
+
1209
+ def resolve_node_command():
1210
+ """Absolute path to a Node >= the floor, or bare "node" as a last resort.
1211
+
1212
+ A bare "node" in a client config resolves against WHATEVER PATH the
1213
+ launching GUI app has — which on this machine meant an nvm v18 while the
1214
+ repo floor is 20.9. Pin the binary at install time instead: prefer the
1215
+ node on the installer's PATH if it meets the floor, else the newest
1216
+ nvm-managed node that does.
1217
+ """
1218
+ on_path = shutil.which("node")
1219
+ if on_path:
1220
+ ver = _node_version(on_path)
1221
+ if ver and ver >= NODE_MIN:
1222
+ return str(Path(on_path).resolve())
1223
+ nvm_dir = Path.home() / ".nvm" / "versions" / "node"
1224
+ if nvm_dir.is_dir():
1225
+ candidates = []
1226
+ for d in nvm_dir.iterdir():
1227
+ node_bin = d / "bin" / "node"
1228
+ if node_bin.exists():
1229
+ ver = _node_version(str(node_bin))
1230
+ if ver and ver >= NODE_MIN:
1231
+ candidates.append((ver, str(node_bin)))
1232
+ if candidates:
1233
+ return max(candidates)[1]
1234
+ print(
1235
+ f" {yellow('Node:')} no Node >= {NODE_MIN[0]}.{NODE_MIN[1]} found; writing a bare 'node' command. "
1236
+ "The advanced server will refuse to start under an older Node and name this fix."
1237
+ )
1238
+ return "node"
1239
+
1240
+
1192
1241
  def generate_manual_config(python_path, server_path, api_path, lib_path):
1193
1242
  """Generate config snippets for manual setup."""
1194
1243
  entry = build_server_entry(python_path, server_path, api_path, lib_path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.136.1",
3
+ "version": "2.137.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -49,6 +49,31 @@ export const hasFfprobe = () => onPath('ffprobe');
49
49
  export const hasSharp = () => nodeModuleAvailable('sharp');
50
50
  export const hasBetterSqlite3 = () => nodeModuleAvailable('better-sqlite3');
51
51
 
52
+ /**
53
+ * Load better-sqlite3 or throw the RIGHT actionable error. Two distinct
54
+ * failures hide behind one require: the module is not installed (install it),
55
+ * or it IS installed but compiled against a different Node ABI — which is
56
+ * what a client config pointing at a different Node than the one that ran
57
+ * `npm install` produces (measured live: server under nvm v18, module built
58
+ * under v20 → cryptic NODE_MODULE_VERSION mismatch). Name the real fix.
59
+ */
60
+ export function requireBetterSqlite3(featureLabel = 'This feature') {
61
+ try {
62
+ return createRequire(import.meta.url)('better-sqlite3');
63
+ } catch (err) {
64
+ const msg = String(err && err.message);
65
+ if (/NODE_MODULE_VERSION/.test(msg)) {
66
+ throw new Error(
67
+ `${featureLabel} needs better-sqlite3, which is installed but compiled for a DIFFERENT Node ` +
68
+ `than this server is running under (${process.version} at ${process.execPath}). ` +
69
+ `Fix: run \`npm rebuild better-sqlite3\` in resolve-advanced/ using THIS node, ` +
70
+ `or point the MCP registration at the node that built it.`,
71
+ );
72
+ }
73
+ throw new Error(`${featureLabel} needs the optional native dep 'better-sqlite3'. Install: npm i better-sqlite3`);
74
+ }
75
+ }
76
+
52
77
  /** Throw a clear, actionable error if ffmpeg/ffprobe aren't on PATH. */
53
78
  export function requireFfmpeg() {
54
79
  if (!hasFfmpeg() || !hasFfprobe()) {
@@ -15,6 +15,7 @@ import os from 'node:os';
15
15
  import { createRequire } from 'node:module';
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
+ import { requireBetterSqlite3 } from './capabilities.mjs';
18
19
 
19
20
  export const DISK_DB_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Disk Database/Resolve Projects');
20
21
 
@@ -91,11 +92,7 @@ export async function responsiveRoots(roots = DB_ROOTS, deadlineMs = 3000) {
91
92
  }
92
93
 
93
94
  export function loadSqlite() {
94
- try {
95
- return require('better-sqlite3');
96
- } catch {
97
- throw new Error("Project.db patching needs the optional native dep 'better-sqlite3'. Install: npm i better-sqlite3");
98
- }
95
+ return requireBetterSqlite3('Project.db patching');
99
96
  }
100
97
 
101
98
  /**
@@ -15,6 +15,7 @@ import crypto from 'node:crypto';
15
15
  import { createRequire } from 'node:module';
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
+ import { requireBetterSqlite3 } from './capabilities.mjs';
18
19
  const V = '../vendor/conform-qc';
19
20
  const parse = require(`${V}/parse/index.js`);
20
21
  const resolveTarget = require(`${V}/oracle/resolve.js`);
@@ -22,11 +23,7 @@ const resolveTarget = require(`${V}/oracle/resolve.js`);
22
23
  const TICKS_PER_SEC = 254016000000;
23
24
 
24
25
  function loadSqlite() {
25
- try {
26
- return require('better-sqlite3');
27
- } catch {
28
- throw new Error("lineage store needs the optional native dep 'better-sqlite3'. Install: npm i better-sqlite3");
29
- }
26
+ return requireBetterSqlite3('The lineage DB');
30
27
  }
31
28
 
32
29
  const SCHEMA = `
@@ -31,6 +31,7 @@ import os from 'node:os';
31
31
  import { createRequire } from 'node:module';
32
32
 
33
33
  const require = createRequire(import.meta.url);
34
+ import { requireBetterSqlite3 } from './capabilities.mjs';
34
35
 
35
36
  const DISK_DB_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Disk Database/Resolve Projects');
36
37
  // Modern Studio installs keep the local library under "Resolve Project
@@ -38,11 +39,7 @@ const DISK_DB_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackm
38
39
  const PROJECT_LIBRARY_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Project Library/Resolve Projects');
39
40
 
40
41
  function loadSqlite() {
41
- try {
42
- return require('better-sqlite3');
43
- } catch {
44
- throw new Error("offline_ref DB path needs the optional native dep 'better-sqlite3'. Install: npm i better-sqlite3");
45
- }
42
+ return requireBetterSqlite3('The live offline-reference DB path');
46
43
  }
47
44
 
48
45
  /** Recursively locate <projectName>/Project.db under the Resolve Disk Database. */
@@ -14,12 +14,9 @@ import crypto from 'node:crypto';
14
14
  import { createRequire } from 'node:module';
15
15
 
16
16
  const require = createRequire(import.meta.url);
17
+ import { requireBetterSqlite3 } from './capabilities.mjs';
17
18
  function loadSqlite() {
18
- try {
19
- return require('better-sqlite3');
20
- } catch {
21
- throw new Error("project DB needs the optional native dep 'better-sqlite3'. Install: npm i better-sqlite3");
22
- }
19
+ return requireBetterSqlite3('Project.db access');
23
20
  }
24
21
 
25
22
  export const ENTITY_KINDS = ['type', 'series', 'episode', 'sequence', 'group', 'deliverable'];
@@ -120,13 +120,34 @@ export async function cdlExport(ref, opts = {}) {
120
120
  }
121
121
 
122
122
  /** Diff two ASC CDL objects (slope/offset/power/saturation) → per-param deltas. */
123
+ // Accept BOTH common CDL shapes — {r,g,b} objects (this repo's internal
124
+ // vocabulary) and [r,g,b] arrays (the common interchange form) — and REFUSE
125
+ // anything else. The old reader indexed .r/.g/.b unconditionally, so an
126
+ // array-shaped CDL silently decayed to identity and cdl_diff reported two
127
+ // different grades as matching everywhere but saturation (measured, E60).
128
+ function cdlChannels(value, grp, side) {
129
+ if (value == null) return { r: undefined, g: undefined, b: undefined };
130
+ if (Array.isArray(value)) {
131
+ if (value.length !== 3 || value.some((v) => typeof v !== 'number')) {
132
+ throw new Error(`cdlDiff: ${side}.${grp} array must be exactly [r, g, b] numbers`);
133
+ }
134
+ return { r: value[0], g: value[1], b: value[2] };
135
+ }
136
+ if (typeof value === 'object' && ['r', 'g', 'b'].every((k) => value[k] === undefined || typeof value[k] === 'number')) {
137
+ return value;
138
+ }
139
+ throw new Error(`cdlDiff: ${side}.${grp} must be {r,g,b} or [r,g,b] — refusing to silently treat it as identity`);
140
+ }
141
+
123
142
  export function cdlDiff(a, b, opts = {}) {
124
143
  const tol = opts.tol ?? 1e-4;
125
144
  const deltas = [];
126
145
  for (const grp of ['slope', 'offset', 'power']) {
146
+ const ac = cdlChannels(a[grp], grp, 'a');
147
+ const bc = cdlChannels(b[grp], grp, 'b');
127
148
  for (const k of ['r', 'g', 'b']) {
128
- const av = (a[grp] && a[grp][k]) ?? (grp === 'offset' ? 0 : 1);
129
- const bv = (b[grp] && b[grp][k]) ?? (grp === 'offset' ? 0 : 1);
149
+ const av = ac[k] ?? (grp === 'offset' ? 0 : 1);
150
+ const bv = bc[k] ?? (grp === 'offset' ? 0 : 1);
130
151
  if (Math.abs(av - bv) > tol) deltas.push({ param: `${grp}.${k}`, a: +av.toFixed(5), b: +bv.toFixed(5), delta: +(bv - av).toFixed(5) });
131
152
  }
132
153
  }
@@ -31,13 +31,10 @@ import path from 'node:path';
31
31
  import { createRequire } from 'node:module';
32
32
 
33
33
  const require = createRequire(import.meta.url);
34
+ import { requireBetterSqlite3 } from './capabilities.mjs';
34
35
 
35
36
  function loadSqlite() {
36
- try {
37
- return require('better-sqlite3');
38
- } catch {
39
- throw new Error("reverse-clip DB path needs the optional native dep 'better-sqlite3'. Install: npm i better-sqlite3");
40
- }
37
+ return requireBetterSqlite3('Reversed-clip DB repair');
41
38
  }
42
39
  function loadPg() {
43
40
  try {
@@ -53,7 +53,18 @@ export const fairlightTool = {
53
53
  const p = dbReadSchema.parse(args);
54
54
  const db = openDb(p.dbPath, false);
55
55
  try {
56
- return patcher.readFromDatabase(db);
56
+ // Summarize: the full readFromDatabase result carries the multi-
57
+ // hundred-KB decompressed model + raw blobs — dumped into the tool
58
+ // response as JSON byte arrays (measured E60). Return the facts.
59
+ const m = patcher.readFromDatabase(db);
60
+ return {
61
+ busFormat: m.busFormat,
62
+ buses: m.buses,
63
+ sequenceId: m.sequenceId,
64
+ modelRowCount: m.modelRowCount,
65
+ modelBytes: m.data.length,
66
+ compressedBytes: m.rawBlob.length,
67
+ };
57
68
  } finally {
58
69
  db.close();
59
70
  }
@@ -216,27 +216,46 @@ function readBusConfig(data) {
216
216
  * @returns {{ busFormat, buses, data, rawBlob, fieldsBlob, entries }}
217
217
  */
218
218
  function readFromDatabase(db) {
219
- const row = db.prepare('SELECT FieldsBlob FROM Sm2Sequence WHERE FieldsBlob IS NOT NULL').get();
220
- if (!row) throw new Error('No Sm2Sequence with FieldsBlob found');
221
-
222
- const fieldsBlobBuf = Buffer.from(row.FieldsBlob);
223
- const { entries } = parseFieldsBlob(fieldsBlobBuf);
219
+ // Iterate EVERY blob-bearing sequence row and pick one that actually
220
+ // carries the Fairlight model. "First row wins" is wrong the moment a
221
+ // project holds a COMPOUND CLIP: a compound's embedded Sm2Sequence has a
222
+ // FieldsBlob (SeqRef/SequenceSetup/thumbnail) but no FLStudioModelBA, and
223
+ // whichever row the engine returns first decided whether this function
224
+ // worked at all (measured: a one-timeline project with compounds reported
225
+ // "No FLStudioModelBA found" while the model sat in the next row).
226
+ const rows = db.prepare('SELECT Sm2Sequence_id, FieldsBlob FROM Sm2Sequence WHERE FieldsBlob IS NOT NULL').all();
227
+ if (!rows.length) throw new Error('No Sm2Sequence with FieldsBlob found');
224
228
 
225
229
  let busFormat = null;
226
230
  let rawBlob = null;
227
231
  let data = null;
228
-
229
- for (const e of entries) {
230
- if (e.key === 'FirstMainBusFormat') {
231
- busFormat = e.rawValue.readUInt32BE(0);
232
- }
233
- if (e.key === 'FLStudioModelBA') {
234
- rawBlob = e.rawValue.slice(4); // skip size prefix
235
- data = decompressFLModel(rawBlob);
232
+ let fieldsBlobBuf = null;
233
+ let entries = null;
234
+ let sequenceId = null;
235
+ let modelRowCount = 0;
236
+
237
+ for (const row of rows) {
238
+ const buf = Buffer.from(row.FieldsBlob);
239
+ let parsed;
240
+ try {
241
+ parsed = parseFieldsBlob(buf);
242
+ } catch {
243
+ continue; // not a keyed FieldsBlob — not a candidate
236
244
  }
245
+ const model = parsed.entries.find(e => e.key === 'FLStudioModelBA');
246
+ if (!model) continue;
247
+ modelRowCount += 1;
248
+ if (data) continue; // keep the first model-bearing row; count the rest
249
+ fieldsBlobBuf = buf;
250
+ entries = parsed.entries;
251
+ sequenceId = row.Sm2Sequence_id;
252
+ const fmt = parsed.entries.find(e => e.key === 'FirstMainBusFormat');
253
+ if (fmt) busFormat = fmt.rawValue.readUInt32BE(0);
254
+ rawBlob = model.rawValue.slice(4); // skip size prefix
255
+ data = decompressFLModel(rawBlob);
237
256
  }
238
257
 
239
- if (!data) throw new Error('No FLStudioModelBA found in FieldsBlob');
258
+ if (!data) throw new Error('No FLStudioModelBA found in any Sm2Sequence FieldsBlob');
240
259
 
241
260
  const busConfig = readBusConfig(data);
242
261
 
@@ -247,6 +266,8 @@ function readFromDatabase(db) {
247
266
  rawBlob,
248
267
  fieldsBlobBuf,
249
268
  entries,
269
+ sequenceId,
270
+ modelRowCount,
250
271
  busNameOffset: busConfig.busNameOffset,
251
272
  };
252
273
  }
@@ -269,12 +290,22 @@ function readFromDatabase(db) {
269
290
  * @returns {{ success: boolean, details: string }}
270
291
  */
271
292
  function applyTemplate(db, templateData, options = {}) {
272
- // Read existing FieldsBlob
273
- const row = db.prepare('SELECT FieldsBlob FROM Sm2Sequence WHERE FieldsBlob IS NOT NULL').get();
274
- if (!row) throw new Error('No Sm2Sequence with FieldsBlob found');
275
-
276
- const fieldsBlobBuf = Buffer.from(row.FieldsBlob);
277
- const { entries } = parseFieldsBlob(fieldsBlobBuf);
293
+ // Read the MODEL-BEARING FieldsBlob and remember which row it came from —
294
+ // the write below must target exactly that row. The old blanket
295
+ // `UPDATE ... WHERE FieldsBlob IS NOT NULL` stamped one timeline's blob
296
+ // into EVERY sequence row: compound clips' embedded sequences (whose blob
297
+ // carries their SeqRef container link), other timelines, everything —
298
+ // cross-wiring the project. Scoped by Sm2Sequence_id instead, and refused
299
+ // when several timelines carry models and no target was named.
300
+ const model = readFromDatabase(db);
301
+ if (model.modelRowCount > 1 && options.sequenceId === undefined) {
302
+ throw new Error(
303
+ `applyTemplate: ${model.modelRowCount} sequences carry a Fairlight model (multi-timeline project) — ` +
304
+ 'pass options.sequenceId (Sm2Sequence_id) to name the target, or use the timeline-scoped actions.',
305
+ );
306
+ }
307
+ const targetSequenceId = options.sequenceId !== undefined ? options.sequenceId : model.sequenceId;
308
+ const { entries } = model;
278
309
 
279
310
  let patchedData = Buffer.from(templateData);
280
311
 
@@ -313,8 +344,8 @@ function applyTemplate(db, templateData, options = {}) {
313
344
  // Encode new FieldsBlob
314
345
  const newFieldsBlob = encodeFieldsBlob(newEntries);
315
346
 
316
- // Write to database
317
- db.prepare('UPDATE Sm2Sequence SET FieldsBlob = ? WHERE FieldsBlob IS NOT NULL').run(newFieldsBlob);
347
+ // Write to database — ONLY the row the model came from.
348
+ db.prepare('UPDATE Sm2Sequence SET FieldsBlob = ? WHERE Sm2Sequence_id = ?').run(newFieldsBlob, targetSequenceId);
318
349
 
319
350
  return {
320
351
  success: true,
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.136.1"
90
+ VERSION = "2.137.0"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.136.1"
14
+ VERSION = "2.137.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -348,7 +348,7 @@ def update_brain_edits_registry(
348
348
  payload: Dict[str, Any]
349
349
  if os.path.isfile(path):
350
350
  try:
351
- with open(path, "r") as fh:
351
+ with open(path, "r", encoding="utf-8") as fh:
352
352
  payload = json.load(fh)
353
353
  except (OSError, json.JSONDecodeError):
354
354
  payload = {"entries": []}
@@ -368,7 +368,7 @@ def update_brain_edits_registry(
368
368
  payload["updated_at"] = _now_iso()
369
369
 
370
370
  tmp_path = path + ".tmp"
371
- with open(tmp_path, "w") as fh:
371
+ with open(tmp_path, "w", encoding="utf-8") as fh:
372
372
  json.dump(payload, fh, indent=2, default=str)
373
373
  os.replace(tmp_path, path)
374
374
  return {"success": True, "registry_path": path, "entry_count": len(entries)}
@@ -379,7 +379,7 @@ def read_brain_edits_registry(project_root: str) -> Dict[str, Any]:
379
379
  if not os.path.isfile(path):
380
380
  return {"entries": [], "registry_path": path}
381
381
  try:
382
- with open(path, "r") as fh:
382
+ with open(path, "r", encoding="utf-8") as fh:
383
383
  payload = json.load(fh)
384
384
  except (OSError, json.JSONDecodeError):
385
385
  return {"entries": [], "registry_path": path}
@@ -49,7 +49,7 @@ def page_lock():
49
49
  try:
50
50
  if _depth == 1 and _HAS_FCNTL:
51
51
  try:
52
- _fh = open(_LOCKFILE, "w")
52
+ _fh = open(_LOCKFILE, "w", encoding="utf-8")
53
53
  fcntl.flock(_fh, fcntl.LOCK_EX)
54
54
  except OSError:
55
55
  # Advisory lock is best-effort; never block real work on it.