davinci-resolve-mcp 4.10.5 → 4.10.7

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,83 @@
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.10.7 — `retargetDRT` moves the `<ProjectVersion>` import gate, not only the comment stamp
6
+
7
+ ### Fixes
8
+
9
+ - **`retargetDRT` / `retargetDRP` produced files an older Resolve still
10
+ refused.** Resolve decides "A newer version of DaVinci Resolve is needed to
11
+ import" on project.xml's `<ProjectVersion>` element; it never reads the
12
+ `DbAppVer="…" DbPrjVer="…"` comment for that decision. The vendored
13
+ `drt-format/resolve-versions.js` rewrote the comment on every XML member and
14
+ left the element alone, so a DRP "retargeted" to 19 still carried 17 inside.
15
+ - **The bundled project shell had the same hole.** It is a 20.3.2 export
16
+ with `<ProjectVersion>15</ProjectVersion>`, so a tool-built DRT retargeted
17
+ to 19 shipped a shell Resolve 19 cannot open.
18
+ - `restampVersions` now also moves any `<ProjectVersion>` element to the
19
+ target's DbPrjVer, whatever its current value. A DRP's kept project.xml
20
+ and the added shell both land on the target schema. DRX and
21
+ SeqContainer/MpFolder XML carry no such element and are unchanged;
22
+ unrelated numbers in the body are untouched.
23
+ - The `drt` tool's `downgrade` and `assemble` actions patched the element
24
+ themselves and were never affected; the library on its own was.
25
+
26
+ ### Validation
27
+
28
+ - Tests: gate moved for a DRP's project.xml and for the shell at every
29
+ registry target (19, 20, 21); null control that XML without the element
30
+ gains none and keeps its other numbers; the pre-fix module leaves the gate
31
+ at 17 on the same input.
32
+ - Vendored drt-format suites: 51 pass, 0 fail, 1 pre-existing skip (missing
33
+ fixture). Server drt suites (downgrade, assemble, events): 32/32.
34
+ - Manual import, 2026-10-13: a Resolve 21.0.3 timeline (schema 17) imported
35
+ into Resolve 19.1.3 once the element read 14. The `.drp` round-trip harness
36
+ was not run: it needs a Resolve 21.x Studio host, and this machine runs
37
+ 19.1.3.7.
38
+
39
+ ## What's New in v4.10.6 — names that need xml escaping reach their bin and their color group
40
+
41
+ ### Fixes
42
+
43
+ - **`drt(action="assemble_project")` failed outright when a timeline placed
44
+ in a bin had `&`, `"` or `'` in its name.** The pool clip stores the name
45
+ escaped (`Reel 1 &amp; 2`), but the step that moves it into its bin searched
46
+ for the raw `Reel 1 & 2`, found nothing, and threw
47
+ `could not locate pool clip for timeline Reel 1 & 2 to move into folder Reels`.
48
+ No archive was written. The lookup now matches the escaped name.
49
+ - **The bin's own name was written unescaped**, so `folder: "R&D"` produced
50
+ a `MpFolder.xml` that does not parse. It is escaped now.
51
+ - A name without those characters escapes to itself, so existing archives
52
+ come out byte-for-byte unchanged.
53
+ - Lands #283 from @Dev-next-gen, as submitted, with two tests that fail on
54
+ the old code.
55
+ - **The same mistake in reverse affected color-group grade reads.** The
56
+ helpers behind `drp(action="extract_group_grades")` and
57
+ `drx(action="grade_transfer")` found a group by searching `project.xml` for
58
+ `<Name>` plus the raw name:
59
+ - asking for a group called `Night & Day` returned `group not found`, because
60
+ the file stores `Night &amp; Day`;
61
+ - the unfiltered listing reported the stored form, `Night &amp; Day`, as the
62
+ group's name;
63
+ - a clip or other element with the same name as a group, placed before it in
64
+ the file, made the lookup return a segment that spanned two groups and
65
+ both of their grades.
66
+
67
+ Group names are now decoded when read and compared as plain text. An
68
+ apostrophe resolves whether the file spells it `'` or `&apos;`. The lookup
69
+ only looks inside `<Sm2Group>` elements, and the label passed to the
70
+ decoder is escaped. A caller that passed the escaped form back
71
+ (`Night &amp; Day`) must now pass the plain name.
72
+
73
+ ### Validation
74
+
75
+ - Offline file handling only; nothing calls Resolve. Advanced suite green,
76
+ with two new group-name tests that fail on the old code; Python suite green.
77
+ - `assemble_project` changes the offline `.drp` authoring tier. The
78
+ round-trip harness was not run: it needs a Resolve 21.x Studio host, and
79
+ this machine runs 19.1.3.7. For a name without `&`, `"` or `'`, the archive
80
+ is byte-identical to before.
81
+
5
82
  ## What's New in v4.10.5 — `rename_plan` sees a name the plan keeps
6
83
 
7
84
  ### Fixes
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.10.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.10.7-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.10.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.10.7-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.10.5 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.10.7 版 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.10.5"
40
+ VERSION = "4.10.7"
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.10.5",
3
+ "version": "4.10.7",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -54,26 +54,36 @@ export function readProjectXml(drpPath) {
54
54
  return xml;
55
55
  }
56
56
 
57
+ // project.xml stores a name as xml text (`Night &amp; Day`); callers name a group as
58
+ // plain text. Read and compare the decoded form so both spellings agree.
59
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
60
+ const unescapeXml = (s) =>
61
+ s.replace(/&(?:#(\d+)|#x([0-9a-fA-F]+)|(amp|lt|gt|quot|apos));/g, (m, dec, hex, name) =>
62
+ dec ? String.fromCodePoint(+dec) : hex ? String.fromCodePoint(parseInt(hex, 16)) : ENTITIES[name]);
63
+ const xmlEscape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
64
+
65
+ /** Each <Sm2Group> element with its (decoded) name. */
66
+ function* groups(xml) {
67
+ for (const m of xml.matchAll(/<Sm2Group\b[\s\S]*?<\/Sm2Group>/g)) {
68
+ const n = m[0].match(/<Name>([^<]*)<\/Name>/);
69
+ if (n) yield { name: unescapeXml(n[1]), seg: m[0] };
70
+ }
71
+ }
72
+
57
73
  /** All color-group names present in the project (from <Sm2Group><Name>…). */
58
74
  export function listGroupNames(xml) {
59
- const names = [];
60
- for (const m of xml.matchAll(/<Sm2Group\b[\s\S]*?<Name>([^<]+)<\/Name>/g)) names.push(m[1]);
61
- return [...new Set(names)];
75
+ return [...new Set([...groups(xml)].map((g) => g.name))];
62
76
  }
63
77
 
64
78
  export function groupSegment(xml, name) {
65
- const j = xml.indexOf(`<Name>${name}</Name>`);
66
- if (j < 0) return null;
67
- const start = xml.lastIndexOf('<Sm2Group', j);
68
- const end = xml.indexOf('</Sm2Group>', j);
69
- if (start < 0 || end < 0) return null;
70
- return xml.slice(start, end + '</Sm2Group>'.length);
79
+ for (const g of groups(xml)) if (g.name === name) return g.seg;
80
+ return null;
71
81
  }
72
82
 
73
83
  export const groupBodies = (seg) => [...seg.matchAll(/<Body>([0-9a-fA-F]+)<\/Body>/g)].map((m) => m[1]);
74
84
 
75
85
  async function decodeBody(bodyHex, label) {
76
- const content = `<?xml version="1.0" encoding="UTF-8"?>\n<Resolve_Color_Exchange><Label>${label}</Label><Width>1920</Width><Height>1080</Height><Body>${bodyHex}</Body></Resolve_Color_Exchange>`;
86
+ const content = `<?xml version="1.0" encoding="UTF-8"?>\n<Resolve_Color_Exchange><Label>${xmlEscape(label)}</Label><Width>1920</Width><Height>1080</Height><Body>${bodyHex}</Body></Resolve_Color_Exchange>`;
77
87
  let r;
78
88
  try {
79
89
  r = await drxTool.handler({ action: 'parse', args: { content } });
@@ -316,7 +316,7 @@ export const drtTool = {
316
316
  if (!f || /[\/\\]/.test(f)) throw new Error(`assemble_project: folder must be a plain bin name (no path separators): ${JSON.stringify(t.folder)}`);
317
317
  return f;
318
318
  });
319
- const { assembleTimeline } = drp();
319
+ const { assembleTimeline, escapeXml } = drp();
320
320
  const buffers = [];
321
321
  for (const [i, spec] of p.timelines.entries()) {
322
322
  const s = { ...spec, timelineName: names[i] };
@@ -459,7 +459,10 @@ export const drtTool = {
459
459
  bins.set(folder, bin);
460
460
  }
461
461
  const bin = bins.get(folder);
462
- const tlRe = new RegExp(`<Element>\\s*<Sm2MpTimelineClip DbId="[^"]+">(?:(?!<\\/Element>\\s*<Element>)[\\s\\S])*?<Name>${names[i].replace(/[.*+?^$()|[\]{}]/g, '\\$&')}<\\/Name>[\\s\\S]*?<\\/Sm2MpTimelineClip>\\s*<\\/Element>`);
462
+ // The name reached the pool clip through escapeXml: `Reel 1 & 2` is stored
463
+ // `Reel 1 &amp; 2`, and the raw name matched nothing.
464
+ const nameXml = escapeXml(names[i]).replace(/[.*+?^$()|[\]{}]/g, '\\$&');
465
+ const tlRe = new RegExp(`<Element>\\s*<Sm2MpTimelineClip DbId="[^"]+">(?:(?!<\\/Element>\\s*<Element>)[\\s\\S])*?<Name>${nameXml}<\\/Name>[\\s\\S]*?<\\/Sm2MpTimelineClip>\\s*<\\/Element>`);
463
466
  const hit = mpXml.match(tlRe);
464
467
  if (!hit) throw new Error(`assemble_project: could not locate pool clip for timeline ${names[i]} to move into folder ${folder}`);
465
468
  mpXml = mpXml.replace(hit[0], '');
@@ -495,7 +498,7 @@ export const drtTool = {
495
498
  for (const [folder, bin] of bins) {
496
499
  base.file(bin.entry,
497
500
  `<?xml version="1.0" encoding="UTF-8"?>\n` +
498
- `<Sm2MpFolder DbId="${bin.id}">\n <FieldsBlob/>\n <Name>${folder}</Name>\n` +
501
+ `<Sm2MpFolder DbId="${bin.id}">\n <FieldsBlob/>\n <Name>${escapeXml(folder)}</Name>\n` +
499
502
  ` <MpFolder>${masterFolderId}</MpFolder>\n <UniqueMediaPoolItemId>${randomUUID()}</UniqueMediaPoolItemId>\n` +
500
503
  ` <MediaVec>\n${bin.clips.join('\n')}\n </MediaVec>\n` +
501
504
  ` <MediaPool>${poolId}</MediaPool>\n <Folded>false</Folded>\n <ColorTag>FOLDER_COLOR_NONE</ColorTag>\n` +
@@ -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); adds a stamped generic shell only if the source
69
- * has no project.xml (a tool-built DRT).
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
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
91
91
  handlers=[logging.StreamHandler()],
92
92
  )
93
93
 
94
- VERSION = "4.10.5"
94
+ VERSION = "4.10.7"
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()}")
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.10.5"
14
+ VERSION = "4.10.7"
15
15
 
16
16
  import base64
17
17
  import os