davinci-resolve-mcp 4.10.6 → 4.11.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 +62 -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/server/media-ops.mjs +9 -4
- package/resolve-advanced/server/tools/media.mjs +5 -1
- package/resolve-advanced/vendor/drt-format/__tests__/multi-format.test.js +14 -0
- package/resolve-advanced/vendor/drt-format/__tests__/resolve-versions.test.js +42 -0
- package/resolve-advanced/vendor/drt-format/resolve-versions.js +15 -3
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,68 @@
|
|
|
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.11.0 — `rename_plan` collides names a case-insensitive volume treats as one
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **`media(action="rename_plan")` missed collisions that only differ in case
|
|
10
|
+
or Unicode form.** macOS (default APFS) and Windows volumes are
|
|
11
|
+
case-insensitive, and APFS also ignores Unicode normalization, so renaming
|
|
12
|
+
`take_A.mov` to `A.mov` overwrites an existing `a.mov`. The plan compared
|
|
13
|
+
names byte-for-byte and reported that rename as free (`collisions: 0`).
|
|
14
|
+
- Targets are now compared after NFC normalization and lowercasing, so
|
|
15
|
+
`A.mov` / `a.mov` and `café` spelled precomposed or with a combining
|
|
16
|
+
accent each count as one file.
|
|
17
|
+
- A case-only rename of a single file (`clip.MOV` → `clip.mov`) is still a
|
|
18
|
+
`rename`, not a collision.
|
|
19
|
+
|
|
20
|
+
### New parameter
|
|
21
|
+
|
|
22
|
+
- `rename_plan` takes `caseSensitive` (default `false`). Pass `true` only for
|
|
23
|
+
a case-sensitive volume to restore exact comparison. The result reports the
|
|
24
|
+
mode used as `caseSensitive`.
|
|
25
|
+
|
|
26
|
+
### Validation
|
|
27
|
+
|
|
28
|
+
- Offline planning only; nothing calls Resolve or touches files. New
|
|
29
|
+
`media-ops` test fails on the old code and passes now; `group-grade-read`
|
|
30
|
+
gains a case for a group name with `"`, `<` and `>`, as Studio 19.1.3.7
|
|
31
|
+
escapes them.
|
|
32
|
+
|
|
33
|
+
## What's New in v4.10.7 — `retargetDRT` moves the `<ProjectVersion>` import gate, not only the comment stamp
|
|
34
|
+
|
|
35
|
+
### Fixes
|
|
36
|
+
|
|
37
|
+
- **`retargetDRT` / `retargetDRP` produced files an older Resolve still
|
|
38
|
+
refused.** Resolve decides "A newer version of DaVinci Resolve is needed to
|
|
39
|
+
import" on project.xml's `<ProjectVersion>` element; it never reads the
|
|
40
|
+
`DbAppVer="…" DbPrjVer="…"` comment for that decision. The vendored
|
|
41
|
+
`drt-format/resolve-versions.js` rewrote the comment on every XML member and
|
|
42
|
+
left the element alone, so a DRP "retargeted" to 19 still carried 17 inside.
|
|
43
|
+
- **The bundled project shell had the same hole.** It is a 20.3.2 export
|
|
44
|
+
with `<ProjectVersion>15</ProjectVersion>`, so a tool-built DRT retargeted
|
|
45
|
+
to 19 shipped a shell Resolve 19 cannot open.
|
|
46
|
+
- `restampVersions` now also moves any `<ProjectVersion>` element to the
|
|
47
|
+
target's DbPrjVer, whatever its current value. A DRP's kept project.xml
|
|
48
|
+
and the added shell both land on the target schema. DRX and
|
|
49
|
+
SeqContainer/MpFolder XML carry no such element and are unchanged;
|
|
50
|
+
unrelated numbers in the body are untouched.
|
|
51
|
+
- The `drt` tool's `downgrade` and `assemble` actions patched the element
|
|
52
|
+
themselves and were never affected; the library on its own was.
|
|
53
|
+
|
|
54
|
+
### Validation
|
|
55
|
+
|
|
56
|
+
- Tests: gate moved for a DRP's project.xml and for the shell at every
|
|
57
|
+
registry target (19, 20, 21); null control that XML without the element
|
|
58
|
+
gains none and keeps its other numbers; the pre-fix module leaves the gate
|
|
59
|
+
at 17 on the same input.
|
|
60
|
+
- Vendored drt-format suites: 51 pass, 0 fail, 1 pre-existing skip (missing
|
|
61
|
+
fixture). Server drt suites (downgrade, assemble, events): 32/32.
|
|
62
|
+
- Manual import, 2026-10-13: a Resolve 21.0.3 timeline (schema 17) imported
|
|
63
|
+
into Resolve 19.1.3 once the element read 14. The `.drp` round-trip harness
|
|
64
|
+
was not run: it needs a Resolve 21.x Studio host, and this machine runs
|
|
65
|
+
19.1.3.7.
|
|
66
|
+
|
|
5
67
|
## What's New in v4.10.6 — names that need xml escaping reach their bin and their color group
|
|
6
68
|
|
|
7
69
|
### Fixes
|
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.
|
|
15
|
+
> 本翻译对应 v4.11.0 版 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.
|
|
40
|
+
VERSION = "4.11.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
|
package/package.json
CHANGED
|
@@ -121,7 +121,9 @@ const CAMERA_ORIGINAL_RE = /^([A-Z]\d{3}[_ ]?[CR]\d{3,4}|[A-Z]\d{3}_\d{8}_C\d+|M
|
|
|
121
121
|
/**
|
|
122
122
|
* Dry-run rename plan (regex find/replace). REFUSES camera originals (sidecar/DB only).
|
|
123
123
|
* @param {string[]} names basenames to rename
|
|
124
|
-
* @param {{find:string, replace:string, allowCameraOriginals?:boolean, cameraOriginalRe?:RegExp}} rule
|
|
124
|
+
* @param {{find:string, replace:string, allowCameraOriginals?:boolean, cameraOriginalRe?:RegExp, caseSensitive?:boolean}} rule
|
|
125
|
+
* caseSensitive defaults to false: macOS (APFS) and Windows volumes treat names that
|
|
126
|
+
* differ only in case or Unicode normalization as one file, so those collide.
|
|
125
127
|
*/
|
|
126
128
|
export function renamePlan(names, rule = {}) {
|
|
127
129
|
let re;
|
|
@@ -133,27 +135,30 @@ export function renamePlan(names, rule = {}) {
|
|
|
133
135
|
const camRe = rule.cameraOriginalRe || CAMERA_ORIGINAL_RE;
|
|
134
136
|
const plan = [];
|
|
135
137
|
const targets = new Map();
|
|
138
|
+
const caseSensitive = rule.caseSensitive === true;
|
|
139
|
+
const key = (n) => (caseSensitive ? n : n.normalize('NFC').toLowerCase());
|
|
136
140
|
// A name the plan leaves in place still occupies it — a refused camera original
|
|
137
141
|
// above all — so it counts as a target too, or a rename onto it reads as free.
|
|
138
142
|
for (const name of names) {
|
|
139
143
|
const isCam = camRe.test(name);
|
|
140
144
|
if (isCam && !rule.allowCameraOriginals) {
|
|
141
145
|
plan.push({ from: name, action: 'refuse-camera-original', note: 'camera original — rename via sidecar/DB only, never the file' });
|
|
142
|
-
targets.set(name, (targets.get(name) || 0) + 1);
|
|
146
|
+
targets.set(key(name), (targets.get(key(name)) || 0) + 1);
|
|
143
147
|
continue;
|
|
144
148
|
}
|
|
145
149
|
const to = name.replace(re, rule.replace ?? '');
|
|
146
|
-
targets.set(to, (targets.get(to) || 0) + 1);
|
|
150
|
+
targets.set(key(to), (targets.get(key(to)) || 0) + 1);
|
|
147
151
|
plan.push({ from: name, to, action: to === name ? 'noop' : 'rename' });
|
|
148
152
|
}
|
|
149
153
|
// Flag collisions (two sources → one target).
|
|
150
|
-
for (const p of plan) if (p.action === 'rename' && targets.get(p.to) > 1) p.action = 'collision';
|
|
154
|
+
for (const p of plan) if (p.action === 'rename' && targets.get(key(p.to)) > 1) p.action = 'collision';
|
|
151
155
|
const collisions = plan.filter((p) => p.action === 'collision');
|
|
152
156
|
return {
|
|
153
157
|
plan,
|
|
154
158
|
renameCount: plan.filter((p) => p.action === 'rename').length,
|
|
155
159
|
refused: plan.filter((p) => p.action === 'refuse-camera-original').length,
|
|
156
160
|
collisions: collisions.length,
|
|
161
|
+
caseSensitive,
|
|
157
162
|
dryRun: true,
|
|
158
163
|
};
|
|
159
164
|
}
|
|
@@ -47,6 +47,10 @@ const renameSchema = z.object({
|
|
|
47
47
|
find: z.string(),
|
|
48
48
|
replace: z.string().optional(),
|
|
49
49
|
allowCameraOriginals: z.boolean().optional(),
|
|
50
|
+
caseSensitive: z
|
|
51
|
+
.boolean()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Default false: names differing only in case/Unicode form collide, as on macOS and Windows volumes. Pass true only for a case-sensitive volume.'),
|
|
50
54
|
});
|
|
51
55
|
|
|
52
56
|
const reelSchema = z.object({ reels: z.array(z.string()), pad: z.number().optional() });
|
|
@@ -83,7 +87,7 @@ export const mediaTool = {
|
|
|
83
87
|
}
|
|
84
88
|
if (action === 'rename_plan') {
|
|
85
89
|
const p = renameSchema.parse(args);
|
|
86
|
-
return renamePlan(p.names, { find: p.find, replace: p.replace, allowCameraOriginals: p.allowCameraOriginals });
|
|
90
|
+
return renamePlan(p.names, { find: p.find, replace: p.replace, allowCameraOriginals: p.allowCameraOriginals, caseSensitive: p.caseSensitive });
|
|
87
91
|
}
|
|
88
92
|
if (action === 'reel_normalize') {
|
|
89
93
|
const p = reelSchema.parse(args);
|
|
@@ -91,3 +91,17 @@ test('index re-exports the multi-format surface', () => {
|
|
|
91
91
|
assert.equal(typeof drt.capabilityDomains.capabilitiesForFormat, 'function');
|
|
92
92
|
assert.equal(typeof drt.schemaFingerprint.fingerprintFile, 'function');
|
|
93
93
|
});
|
|
94
|
+
|
|
95
|
+
test('retargetDRP moves the kept project.xml\'s <ProjectVersion> gate, not only its stamp', async () => {
|
|
96
|
+
// A real Resolve 21 DRP: full project.xml with the gate at 17.
|
|
97
|
+
const zip = new JSZip();
|
|
98
|
+
zip.file('project.xml', `${STAMP('21.0.3.0007', '17')}\n<SM_Project><FullProjectSentinel>keep</FullProjectSentinel><ProjectVersion>17</ProjectVersion></SM_Project>`);
|
|
99
|
+
zip.file('SeqContainer/uuid-1.xml', `${STAMP('21.0.3.0007', '17')}\n<Sm2SequenceContainer><Sm2TiVideoClip><MediaRef>m</MediaRef></Sm2TiVideoClip></Sm2SequenceContainer>`);
|
|
100
|
+
zip.file('MediaPool/Master/MpFolder.xml', `${STAMP('21.0.3.0007', '17')}\n<MpFolder/>`);
|
|
101
|
+
const out = await retargetDRP(await zip.generateAsync({ type: 'nodebuffer' }), 19);
|
|
102
|
+
const proj = await (await JSZip.loadAsync(out)).file('project.xml').async('string');
|
|
103
|
+
assert.ok(proj.includes('FullProjectSentinel'), 'project kept');
|
|
104
|
+
assert.ok(proj.includes('<ProjectVersion>14</ProjectVersion>'), 'gate moved to 14');
|
|
105
|
+
assert.ok(!proj.includes('<ProjectVersion>17</ProjectVersion>'), 'old gate gone');
|
|
106
|
+
assert.deepEqual(readVersionStamp(proj), { dbAppVer: '19.1.3.0007', dbPrjVer: '14' });
|
|
107
|
+
});
|
|
@@ -118,3 +118,45 @@ test('index re-exports retargetDRT + capabilities surface', () => {
|
|
|
118
118
|
assert.equal(typeof drt.resolveVersions.resolveVersion, 'function');
|
|
119
119
|
assert.equal(typeof drt.capabilities.collectCapabilities, 'function');
|
|
120
120
|
});
|
|
121
|
+
|
|
122
|
+
// ---- the import gate: <ProjectVersion> in project.xml ----
|
|
123
|
+
// Resolve decides "a newer version is needed" on this element, not on the comment
|
|
124
|
+
// stamp. Verified live 2026-10-13: a 21.0.3 DRT imported into 19.1.3 only once the
|
|
125
|
+
// element read 14. Before this fix retargetDRT moved the comment and left the gate.
|
|
126
|
+
|
|
127
|
+
const { restampVersions } = require('../resolve-versions');
|
|
128
|
+
|
|
129
|
+
test('restampVersions moves the <ProjectVersion> gate to the target schema, whatever it was', () => {
|
|
130
|
+
const src = { dbAppVer: '21.0.3.0007', dbPrjVer: '17' };
|
|
131
|
+
const tgt = VERSIONS[19];
|
|
132
|
+
const proj = `<!--DbAppVer="21.0.3.0007" DbPrjVer="17"-->\n<SM_Project><ProjectVersion>17</ProjectVersion><Name>T</Name></SM_Project>`;
|
|
133
|
+
const out = restampVersions(proj, src, tgt);
|
|
134
|
+
assert.ok(out.includes('<ProjectVersion>14</ProjectVersion>'), 'gate moved to 14');
|
|
135
|
+
assert.ok(!out.includes('17'), 'no residual 17 in stamp or gate');
|
|
136
|
+
// a gate that disagrees with the comment stamp (the bundled shell: comment 15, element 15,
|
|
137
|
+
// but a hand-edited file could differ) still lands on the target
|
|
138
|
+
const skew = `<!--DbAppVer="21.0.3.0007" DbPrjVer="17"-->\n<SM_Project><ProjectVersion>15</ProjectVersion></SM_Project>`;
|
|
139
|
+
assert.ok(restampVersions(skew, src, tgt).includes('<ProjectVersion>14</ProjectVersion>'));
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('restampVersions: null control — XML without the element gains none, other numbers untouched', () => {
|
|
143
|
+
const seq = `<!--DbAppVer="21.0.3.0007" DbPrjVer="17"-->\n<Sm2SequenceContainer><Version>17</Version><Start>17</Start></Sm2SequenceContainer>`;
|
|
144
|
+
const out = restampVersions(seq, { dbAppVer: '21.0.3.0007', dbPrjVer: '17' }, VERSIONS[19]);
|
|
145
|
+
assert.ok(!out.includes('<ProjectVersion>'), 'no element invented');
|
|
146
|
+
assert.ok(out.includes('<Version>17</Version><Start>17</Start>'), 'unrelated 17s kept');
|
|
147
|
+
assert.deepEqual(readVersionStamp(out), { dbAppVer: '19.1.3.0007', dbPrjVer: '14' });
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('retargetDRT: the added shell carries the TARGET gate, not the template\'s own', async () => {
|
|
151
|
+
// The bundled shell is a 20.3.2 export (<ProjectVersion>15</ProjectVersion>); a DRT
|
|
152
|
+
// retargeted to 19 must not ship a shell Resolve 19 refuses.
|
|
153
|
+
const src = await makeDrt({ dbAppVer: '21.0.0.0048', dbPrjVer: '17', body: MEDIA_CLIP });
|
|
154
|
+
for (const tgt of [19, 20, 21]) {
|
|
155
|
+
const z = await JSZip.loadAsync(await retargetDRT(src, tgt));
|
|
156
|
+
const proj = await z.file('project.xml').async('string');
|
|
157
|
+
const gates = proj.match(/<ProjectVersion>(\d+)<\/ProjectVersion>/g) || [];
|
|
158
|
+
assert.equal(gates.length, 1, `shell has exactly one gate (target ${tgt})`);
|
|
159
|
+
assert.equal(gates[0], `<ProjectVersion>${VERSIONS[tgt].dbPrjVer}</ProjectVersion>`);
|
|
160
|
+
assert.deepEqual(readVersionStamp(proj), { dbAppVer: VERSIONS[tgt].dbAppVer, dbPrjVer: VERSIONS[tgt].dbPrjVer });
|
|
161
|
+
}
|
|
162
|
+
});
|
|
@@ -54,19 +54,31 @@ function readVersionStamp(seqXml) {
|
|
|
54
54
|
|
|
55
55
|
const SEQ_RE = /(^|\/)SeqContainer(\d*\.xml|\/[^/]+\.xml)$/;
|
|
56
56
|
|
|
57
|
+
// The import GATE: project.xml's <ProjectVersion> element. Resolve refuses a file whose
|
|
58
|
+
// value is above its own ("A newer version of DaVinci Resolve is needed to import") and
|
|
59
|
+
// never reads the comment stamp for that decision — verified live 2026-10-13 (a 21.0.3
|
|
60
|
+
// DRT with the element at 14 imported into 19.1.3; the same file with only the comment
|
|
61
|
+
// re-stamped did not). DRX and SeqContainer/MpFolder XML carry no such element.
|
|
62
|
+
const PROJECT_VERSION_RE = /<ProjectVersion>\d+<\/ProjectVersion>/g;
|
|
63
|
+
|
|
57
64
|
// Value-based version re-stamp (not length-dependent → safe even if stamp widths differ).
|
|
58
65
|
// Shared by every format (DRT, DRP, DRX) — the DbAppVer/DbPrjVer header is universal.
|
|
66
|
+
// Also moves the <ProjectVersion> gate (whatever its current value) to the target schema,
|
|
67
|
+
// so a project.xml — a DRP's own or the bundled shell — is importable by the target, not
|
|
68
|
+
// just labelled for it.
|
|
59
69
|
function restampVersions(xmlStr, src, target) {
|
|
60
70
|
return xmlStr
|
|
61
71
|
.split(`DbAppVer="${src.dbAppVer}"`).join(`DbAppVer="${target.dbAppVer}"`)
|
|
62
|
-
.split(`DbPrjVer="${src.dbPrjVer}"`).join(`DbPrjVer="${target.dbPrjVer}"`)
|
|
72
|
+
.split(`DbPrjVer="${src.dbPrjVer}"`).join(`DbPrjVer="${target.dbPrjVer}"`)
|
|
73
|
+
.replace(PROJECT_VERSION_RE, `<ProjectVersion>${target.dbPrjVer}</ProjectVersion>`);
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
/**
|
|
66
77
|
* Re-stamp a zip-based Resolve file (DRT or DRP) to a target version. Value-based,
|
|
67
78
|
* so it's safe even if stamp widths differ. Re-stamps every XML entry in place
|
|
68
|
-
* (keeping a DRP's full project.xml
|
|
69
|
-
* has no project.xml
|
|
79
|
+
* (keeping a DRP's full project.xml, with its <ProjectVersion> gate moved to the
|
|
80
|
+
* target schema); adds a stamped generic shell only if the source has no project.xml
|
|
81
|
+
* (a tool-built DRT).
|
|
70
82
|
*
|
|
71
83
|
* @param {Buffer} drtBuffer a real-schema .drt/.drp (SeqContainer/<uuid>.xml + MpFolder)
|
|
72
84
|
* @param {number|string} targetKey e.g. 19, 20, 21
|
package/src/granular/common.py
CHANGED
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.
|
|
94
|
+
VERSION = "4.11.0"
|
|
95
95
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
96
96
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
97
97
|
logger.info(f"Detected platform: {get_platform()}")
|