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 +23 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/vendor/drx-codec/__tests__/ofx-instance-key.test.js +96 -0
- package/resolve-advanced/vendor/drx-codec/drx-generator.js +27 -4
- package/resolve-advanced/vendor/drx-codec/extract-ofx-params.js +7 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v4.8.
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|
package/src/granular/common.py
CHANGED
|
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
93
93
|
handlers=[logging.StreamHandler()],
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
VERSION = "4.8.
|
|
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()}")
|