davinci-resolve-mcp 4.8.18 → 4.8.19

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,29 @@
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 v4.8.19 — generated OFX nodes bind their params on Resolve 21.0
6
+
7
+ ### Fixed
8
+
9
+ - **Generated OFX nodes could apply but run on the plugin's defaults.** ([#267](https://github.com/samuelgursky/davinci-resolve-mcp/pull/267), @noah1234j)
10
+ In a native Resolve capture the tool-list instance entry carries
11
+ `<context>_<clip-version DbId>_<node id>`, while the OFX container keeps the bare
12
+ context name. The DRX generator wrote the bare name in both slots. On Studio
13
+ 21.0.0.48 the contributor measured that the plugin's stored params then do not
14
+ bind, so a Color Space Transform ignores its input/output spaces.
15
+ `generateMultiNodeDRX` now creates the clip-version id before the nodes and emits
16
+ the keyed form in the tool-list slot. An explicit `options.instanceKey` still wins.
17
+ Reported fix: the generated CST's 33-pt LUT is bit-identical to a hand-built CST
18
+ on 21.0.0.48.
19
+ Measured here on Studio 19.1.3.7: both forms already bind on that build, and the
20
+ new generator's LUTs are bit-identical to the old one's for every variant tested
21
+ (CC only, CST with default params, two different CST parameter sets), so nothing
22
+ changes on 19.1.3.
23
+ - **Integer/choice OFX params round-trip.** `buildOFXToolEntry` encodes `{int: n}`
24
+ and booleans as varint F3 (e.g. CST `doFwdOOTF` / `doInvOOTF`), and
25
+ `extract-ofx-params` now decodes F3 to `{int: n}` instead of `null`, so parse →
26
+ generate keeps them. Guard test: `vendor/drx-codec/__tests__/ofx-instance-key.test.js`.
27
+
5
28
  ## What's New in v4.8.18 — `copy_clip_annotations` honours `include_*="false"`
6
29
 
7
30
  ### 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-4.8.18-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.19-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-37%20(389%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-4.8.18-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.19-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-37%20(389%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
- > 本翻译对应 v4.8.18 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.8.19 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.8.18"
40
+ VERSION = "4.8.19"
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "4.8.18",
3
+ "version": "4.8.19",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -0,0 +1,96 @@
1
+ /**
2
+ * OFX instance binding + integer params in generated DRX.
3
+ *
4
+ * Native Resolve (Studio 21.0 CST capture) writes the tool-list instance entry
5
+ * (0xC000005E) as "<context>_<clip-version DbId>_<node id>" while the OFX
6
+ * container's F3 keeps the bare context. With the bare context in both slots
7
+ * the node applies but the plugin's stored params never bind — verified live by
8
+ * exporting a 33-pt LUT: the generated CST only matched a hand-built CST
9
+ * (bit-identical, 35,937/35,937 points) once the keyed form was emitted.
10
+ *
11
+ * Integer params (doFwdOOTF / doInvOOTF) are varint F3 on the wire.
12
+ */
13
+
14
+ const test = require('node:test');
15
+ const assert = require('node:assert/strict');
16
+ const zlib = require('node:zlib');
17
+
18
+ const { drxGenerator, drxParser } = require('..');
19
+
20
+ const CST = 'com.blackmagicdesign.resolvefx.colorspacetransformv2';
21
+
22
+ function decompressClipBody(xml) {
23
+ const hex = xml.match(/<pClipFullVer>[\s\S]*?<Body>([0-9a-f]+)<\/Body>/)[1];
24
+ const body = Buffer.from(hex, 'hex');
25
+ assert.equal(body[0], 0x81);
26
+ const decompress = zlib.zstdDecompressSync
27
+ ? (b) => zlib.zstdDecompressSync(b)
28
+ : (b) => Buffer.from(require('fzstd').decompress(b));
29
+ return decompress(body.subarray(1));
30
+ }
31
+
32
+ async function generateCstGraph() {
33
+ const nodes = [
34
+ { label: 'CC' },
35
+ {
36
+ label: 'CST 709',
37
+ params: {
38
+ ofx: {
39
+ pluginId: CST,
40
+ params: {
41
+ inputColorSpace: 'DWG_COLORSPACE',
42
+ inputGamma: 'DAV_INTER_OETF_GAMMA',
43
+ outputColorSpace: 'REC709_COLORSPACE',
44
+ outputGamma: 'TWOPOINTFOUR_GAMMA',
45
+ doFwdOOTF: { int: 1 },
46
+ doInvOOTF: { int: 0 },
47
+ },
48
+ options: { version: '1.4' },
49
+ },
50
+ },
51
+ },
52
+ ];
53
+ return drxGenerator.generateMultiNodeDRX(nodes, [{ from: 1, to: 2 }], { label: 'test' });
54
+ }
55
+
56
+ test('tool-list instance entry is keyed by clip version DbId and node id', async () => {
57
+ const xml = await generateCstGraph();
58
+ const versionId = xml.match(/<pClipFullVer>\s*<ListMgt::LmVersion DbId="([0-9a-f-]+)"/)[1];
59
+ const raw = decompressClipBody(xml);
60
+ // Fresh mode numbers nodes from 2, so the CST (second node) has id 3.
61
+ const keyed = Buffer.from(`OfxImageEffectContextFilter_${versionId}_3`, 'utf-8');
62
+ assert.ok(raw.includes(keyed), 'keyed instance id present in tool list');
63
+ });
64
+
65
+ test('OFX container keeps the bare context name', async () => {
66
+ const xml = await generateCstGraph();
67
+ const parsed = await drxParser.parseDRXContent(xml);
68
+ const tool = parsed.nodes[1].ofxTools[0];
69
+ assert.equal(tool.pluginId, CST);
70
+ assert.equal(tool.instanceId, 'OfxImageEffectContextFilter');
71
+ });
72
+
73
+ test('integer params round-trip as {int} via varint F3', async () => {
74
+ const xml = await generateCstGraph();
75
+ const parsed = await drxParser.parseDRXContent(xml);
76
+ const params = parsed.nodes[1].ofxTools[0].params;
77
+ assert.deepEqual(params.doFwdOOTF, { int: 1 });
78
+ assert.deepEqual(params.doInvOOTF, { int: 0 });
79
+ assert.equal(params.inputColorSpace, 'DWG_COLORSPACE');
80
+ assert.equal(params.outputGamma, 'TWOPOINTFOUR_GAMMA');
81
+
82
+ // wire check: name entry followed by F2{F3 varint 1}
83
+ const raw = decompressClipBody(xml);
84
+ const name = Buffer.from('doFwdOOTF', 'utf-8');
85
+ const expected = Buffer.concat([Buffer.from([0x0a, name.length]), name, Buffer.from([0x12, 0x02, 0x18, 0x01])]);
86
+ assert.ok(raw.includes(expected), 'doFwdOOTF encoded as varint F3');
87
+ });
88
+
89
+ test('explicit instanceKey option is honoured', async () => {
90
+ const xml = await drxGenerator.generateMultiNodeDRX(
91
+ [{ label: 'X', params: { ofx: { pluginId: CST, params: {}, options: { instanceKey: 'Custom_key_9' } } } }],
92
+ [],
93
+ { label: 'test' },
94
+ );
95
+ assert.ok(decompressClipBody(xml).includes(Buffer.from('Custom_key_9')));
96
+ });
@@ -900,7 +900,12 @@ function createNode(nodeId, xPos, yPos, colorParams = null, options = {}) {
900
900
  // OFX spec ({ofx:{pluginId, params, options?}}), emit the full OFX container instead
901
901
  // (params are self-describing name/value pairs on the wire — see extract-ofx-params).
902
902
  if (colorParams && colorParams.ofx && colorParams.ofx.pluginId) {
903
- parts.push(buildOFXToolEntry(colorParams.ofx.pluginId, colorParams.ofx.params || {}, colorParams.ofx.options || {}));
903
+ const ofxOptions = { ...(colorParams.ofx.options || {}) };
904
+ if (options.clipVersionId && !ofxOptions.instanceKey) {
905
+ const ctx = ofxOptions.instanceId || 'OfxImageEffectContextFilter';
906
+ ofxOptions.instanceKey = `${ctx}_${options.clipVersionId}_${nodeId}`;
907
+ }
908
+ parts.push(buildOFXToolEntry(colorParams.ofx.pluginId, colorParams.ofx.params || {}, ofxOptions));
904
909
  } else {
905
910
  // Structure: F10 = {F1 = {F1=0xC0000001, F2={F2=2}}}
906
911
  const f10InnerInner = Buffer.concat([
@@ -1750,6 +1755,8 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
1750
1755
  } = metadata;
1751
1756
 
1752
1757
  const timestamp = generateTimestamp();
1758
+ // The clip version DbId is also the key Resolve embeds in each OFX node's instance id.
1759
+ const clipVersionId = require('node:crypto').randomUUID();
1753
1760
 
1754
1761
  // Determine node ID scheme
1755
1762
  let baseNodeId, firstNodeId, lastNodeId;
@@ -1798,6 +1805,7 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
1798
1805
  label: nodeConfig.label || `Node ${nodeIndex}`,
1799
1806
  enabled: nodeConfig.enabled !== false,
1800
1807
  nodeIndex: nodeIndex, // Pass separate index for F2 field
1808
+ clipVersionId,
1801
1809
  }
1802
1810
  );
1803
1811
  containerParts.push(node);
@@ -1861,7 +1869,7 @@ async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
1861
1869
  // Generate UUIDs
1862
1870
  const { randomUUID: uuidv4 } = require('node:crypto');
1863
1871
  const stillId = uuidv4();
1864
- const clipVersionId = uuidv4();
1872
+ // clipVersionId is generated before the nodes (it keys OFX instances).
1865
1873
 
1866
1874
  const now = new Date().toISOString().replace('Z', '');
1867
1875
 
@@ -3981,6 +3989,16 @@ function buildOFXToolEntry(pluginId, params, options = {}) {
3981
3989
  // plugin un-instantiated: node applies but the effect never engages — found live
3982
3990
  // 2026-07-03). Callers can still override for generator/transition contexts.
3983
3991
  const instanceId = options.instanceId || 'OfxImageEffectContextFilter';
3992
+ // The tool-list instance entry (0xC000005E) is NOT the bare context name in native
3993
+ // captures: Resolve writes "<context>_<clip-version DbId>_<node id>" there (e.g.
3994
+ // "OfxImageEffectContextFilter_06aff833-..._5", Resolve Studio 21.0 CST capture),
3995
+ // while the container's F3 keeps the bare context. With the bare name in both
3996
+ // slots the node applies but the plugin's stored params do not bind, so it runs on
3997
+ // defaults. generateMultiNodeDRX supplies the keyed form via options.instanceKey.
3998
+ // Version scope: the unbound-params failure was reported on Studio 21.0.0.48
3999
+ // (PR #267). On Studio 19.1.3.7 both forms bind: generated CSTs exported
4000
+ // bit-identical 33-pt LUTs with the bare and the keyed id (measured 2026-09-23).
4001
+ const instanceKey = options.instanceKey || instanceId;
3984
4002
 
3985
4003
  // Build F5 repeated param entries. Native containers always carry resolvefxVersion
3986
4004
  // and serialize params in name order — mirror both.
@@ -3993,6 +4011,11 @@ function buildOFXToolEntry(pluginId, params, options = {}) {
3993
4011
  if (typeof value === 'string') {
3994
4012
  const strBuf = Buffer.from(value, 'utf-8');
3995
4013
  valueBuf = protoBytes(5, strBuf);
4014
+ } else if (typeof value === 'boolean') {
4015
+ valueBuf = protoVarint(3, value ? 1 : 0);
4016
+ } else if (value && typeof value === 'object' && Number.isInteger(value.int)) {
4017
+ // Integer/choice params (e.g. CST doFwdOOTF) are varint F3 on the wire, not F2 doubles.
4018
+ valueBuf = protoVarint(3, value.int);
3996
4019
  } else {
3997
4020
  valueBuf = protoFloat64(2, value);
3998
4021
  }
@@ -4023,10 +4046,10 @@ function buildOFXToolEntry(pluginId, params, options = {}) {
4023
4046
  protoVarint(1, ID_PLUGIN),
4024
4047
  protoBytes(2, protoBytes(5, Buffer.from(pluginId, 'utf-8'))),
4025
4048
  ])),
4026
- // Instance ID
4049
+ // Instance ID (keyed form — see instanceKey above)
4027
4050
  protoBytes(1, Buffer.concat([
4028
4051
  protoVarint(1, ID_INSTANCE),
4029
- protoBytes(2, protoBytes(5, Buffer.from(instanceId, 'utf-8'))),
4052
+ protoBytes(2, protoBytes(5, Buffer.from(instanceKey, 'utf-8'))),
4030
4053
  ])),
4031
4054
  // Enable flag (native captures carry 0 here for enabled plugins)
4032
4055
  protoBytes(1, Buffer.concat([
@@ -88,7 +88,11 @@ const OFY_MARKER = 0x4F4659;
88
88
  /**
89
89
  * Decode an F5 param-entry buffer to {name, value}.
90
90
  * F1 = name (length-delimited UTF-8)
91
- * F2 = wrapped value (F2 = float64 OR F5 = UTF-8 string)
91
+ * F2 = wrapped value (F2 = float64, F3 = varint int/choice, OR F5 = UTF-8 string)
92
+ *
93
+ * Integer params (e.g. Color Space Transform doFwdOOTF / doInvOOTF) come back as
94
+ * {int: n} — the same shape buildOFXToolEntry accepts — so a parse → generate
95
+ * round-trip re-emits them as varints instead of dropping or float-encoding them.
92
96
  */
93
97
  function decodeParamEntry(entryBuf) {
94
98
  let name = '';
@@ -103,6 +107,8 @@ function decodeParamEntry(entryBuf) {
103
107
  for (const inn of inner) {
104
108
  if (inn.fieldNum === 2 && inn.wireType === 1) {
105
109
  value = inn.value; // float64
110
+ } else if (inn.fieldNum === 3 && inn.wireType === 0) {
111
+ value = { int: Number(inn.value) };
106
112
  } else if (inn.fieldNum === 5 && inn.wireType === 2) {
107
113
  value = utf8(inn.value);
108
114
  }
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
93
93
  handlers=[logging.StreamHandler()],
94
94
  )
95
95
 
96
- VERSION = "4.8.18"
96
+ VERSION = "4.8.19"
97
97
  logger = logging.getLogger("davinci-resolve-mcp")
98
98
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
99
99
  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 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "4.8.18"
14
+ VERSION = "4.8.19"
15
15
 
16
16
  import base64
17
17
  import os