davinci-resolve-mcp 2.106.0 → 2.107.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,24 @@
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.107.0
6
+
7
+ **Multi-source media authoring.** `drt.assemble`'s media support grows from
8
+ one source to many: `media` accepts an array of `{mediaFilePath, spec, cuts}`
9
+ sources, each cut landing on the shared V1/A1 with its own source's
10
+ transplanted native descriptors. New plumbing: `insertMediaElement` appends
11
+ additional native pool elements into MpFolder's MediaVec (folder-parent id
12
+ adopted), and `cutSourceIntoClips` accepts a per-cut `mediaRef` so each clone
13
+ points at ITS source. Multi-source strictly requires a captured native
14
+ template for every source (the render-verified transplant path); the refusal
15
+ names `media_pool.capture_media_template` per missing file — a repoint
16
+ fallback that renders black across N sources would be a trap, not a feature.
17
+
18
+ Live-verified end to end on Studio 19.1.3.7 with luma fingerprints: a
19
+ timeline interleaving cuts from two sources (testsrc + solid white) imported
20
+ 6/6 linked and rendered each cut's OWN pixels — YAVG 125.6 / 234 / 125.5
21
+ across the three cuts, matching each source's signature exactly.
22
+
5
23
  ## What's New in v2.106.0
6
24
 
7
25
  **Media clips in native DRT authoring — cut real footage into an importable,
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.106.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.107.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.106.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.107.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.106.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.107.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 = "2.106.0"
40
+ VERSION = "2.107.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.106.0",
3
+ "version": "2.107.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -33,7 +33,7 @@ const assembleSchema = z.object({
33
33
  spec: z
34
34
  .object({})
35
35
  .passthrough()
36
- .describe("assembleTimeline spec: { timelineName?, elements: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
36
+ .describe("assembleTimeline spec: { timelineName?, media?: {mediaFilePath, spec:{width,height,frameCount,fps}, cuts:[{startFrame,durationFrames,srcIn?}]} | [same, ...] (multi-source needs media_pool.capture_media_template run once per file), elements?: [{type:'title'|'generator', track, startFrame, durationFrames?, text?, ...}], transitions? }. startFrame is timeline-absolute (origin 86400)."),
37
37
  outputPath: z.string().describe('Absolute path where the importable .drt will be written'),
38
38
  targetAppVersion: z
39
39
  .union([z.string(), z.number()])
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  const { createEmptyProject, addMediaClip, DEFAULT_START_FRAME } = require('./author-project');
25
- const { loadMediaTemplate, transplantMediaElement } = require('./media-template-cache');
25
+ const { loadMediaTemplate, transplantMediaElement, insertMediaElement } = require('./media-template-cache');
26
26
  const JSZip = require('jszip');
27
27
  const { cutSourceIntoClips } = require('./cut-media');
28
28
  const { placeFusionTitle } = require('./place-fusion-title');
@@ -35,24 +35,31 @@ async function assembleTimeline(spec = {}) {
35
35
  if (!Array.isArray(transitions)) throw new TypeError('assembleTimeline: transitions must be an array');
36
36
 
37
37
  let base;
38
+ let mediaDescriptorState = 'none';
38
39
  if (media) {
39
- // Media authoring: ONE source file, cut into N placements — the template
40
- // media pool holds one media entry, so multi-source is refused honestly
41
- // rather than half-built.
42
- if (Array.isArray(media)) {
43
- throw new TypeError('assembleTimeline: media must be a single {mediaFilePath, spec, cuts} object — multi-source authoring is not supported yet');
40
+ // Media authoring: cut one or MORE sources into placements. Every source
41
+ // carries {mediaFilePath, spec, cuts}. Multi-source requires a native
42
+ // media template CAPTURED for every source (capture-once transplant is
43
+ // the only measured way authored media renders); single-source may fall
44
+ // back to descriptor repoint, which imports but usually will not render.
45
+ const sources = Array.isArray(media) ? media : [media];
46
+ if (!sources.length) throw new TypeError('assembleTimeline: media must not be empty');
47
+ const caches = sources.map((src) => loadMediaTemplate(src.mediaFilePath));
48
+ if (sources.length > 1) {
49
+ const missing = sources.filter((src, i) => !caches[i]).map((src) => src.mediaFilePath);
50
+ if (missing.length) {
51
+ throw new Error(
52
+ 'assembleTimeline: multi-source authoring requires a captured native media template for EVERY source — ' +
53
+ `missing for: ${missing.join(', ')}. Run media_pool.capture_media_template(path) with Resolve open for each.`,
54
+ );
55
+ }
44
56
  }
45
- const { mediaFilePath, spec: mediaSpec, cuts } = media;
46
- base = await addMediaClip({ mediaFile: mediaFilePath, spec: mediaSpec, timelineName, templateVersion });
47
- base.startFrame = DEFAULT_START_FRAME;
48
- if (Array.isArray(cuts) && cuts.length) {
49
- // Placement guards, loudly: clips before the timeline origin are
50
- // DROPPED by Resolve on import with no error, and a source range past
51
- // the media's end reads back as a truncated clip.
52
- cuts.forEach((cut, i) => {
57
+ const validateCuts = (src, label) => {
58
+ const mediaSpec = src.spec;
59
+ (src.cuts || []).forEach((cut, i) => {
53
60
  if (cut.startFrame < DEFAULT_START_FRAME) {
54
61
  throw new RangeError(
55
- `assembleTimeline: media.cuts[${i}].startFrame ${cut.startFrame} is before the timeline origin ${DEFAULT_START_FRAME} — Resolve silently drops it on import`,
62
+ `assembleTimeline: ${label}.cuts[${i}].startFrame ${cut.startFrame} is before the timeline origin ${DEFAULT_START_FRAME} — Resolve silently drops it on import`,
56
63
  );
57
64
  }
58
65
  if (mediaSpec && Number.isFinite(mediaSpec.frameCount) && Number.isFinite(mediaSpec.fps)) {
@@ -61,12 +68,55 @@ async function assembleTimeline(spec = {}) {
61
68
  const maxTimelineFrames = Math.floor((mediaSpec.frameCount / mediaSpec.fps) * 24);
62
69
  if ((cut.srcIn ?? 0) + cut.durationFrames > maxTimelineFrames) {
63
70
  throw new RangeError(
64
- `assembleTimeline: media.cuts[${i}] reads past the media's end — (srcIn ?? 0) + durationFrames exceeds ${maxTimelineFrames} timeline frames (media ${mediaSpec.frameCount} frames @ ${mediaSpec.fps} fps on the 24fps template timeline)`,
71
+ `assembleTimeline: ${label}.cuts[${i}] reads past the media's end — (srcIn ?? 0) + durationFrames exceeds ${maxTimelineFrames} timeline frames (media ${mediaSpec.frameCount} frames @ ${mediaSpec.fps} fps on the 24fps template timeline)`,
65
72
  );
66
73
  }
67
74
  }
68
75
  });
69
- const cutRes = await cutSourceIntoClips(base.buffer, { cuts });
76
+ };
77
+ sources.forEach((src, i) => validateCuts(src, sources.length > 1 ? `media[${i}]` : 'media'));
78
+
79
+ base = await addMediaClip({
80
+ mediaFile: sources[0].mediaFilePath, spec: sources[0].spec, timelineName, templateVersion,
81
+ });
82
+ base.startFrame = DEFAULT_START_FRAME;
83
+
84
+ // Transplant/insert native pool elements BEFORE cutting, so each cut can
85
+ // reference its source's MediaRef.
86
+ let zip = await JSZip.loadAsync(base.buffer);
87
+ const mpPath = 'MediaPool/Master/MpFolder.xml';
88
+ let mpXml = await zip.file(mpPath).async('string');
89
+ const seqNames = Object.keys(zip.files).filter((n) => /SeqContainer\/.+\.xml$/.test(n) || /\/SeqContainer\d*\.xml$/.test(n));
90
+ const seqXmls = [];
91
+ for (const n of seqNames) seqXmls.push(await zip.file(n).async('string'));
92
+ const mediaRefs = [];
93
+ if (caches[0]) {
94
+ const res = transplantMediaElement(mpXml, seqXmls, caches[0]);
95
+ mpXml = res.mpXml;
96
+ for (let i = 0; i < seqNames.length; i += 1) seqXmls[i] = res.seqXmls[i];
97
+ mediaRefs[0] = caches[0].mediaRef;
98
+ mediaDescriptorState = 'native-transplant';
99
+ } else {
100
+ mediaRefs[0] = null; // donor already points at the repointed entry
101
+ mediaDescriptorState = 'repoint-fallback';
102
+ }
103
+ for (let i = 1; i < sources.length; i += 1) {
104
+ mpXml = insertMediaElement(mpXml, caches[i].poolElement);
105
+ mediaRefs[i] = caches[i].mediaRef;
106
+ }
107
+ zip.file(mpPath, mpXml);
108
+ seqNames.forEach((n, i) => zip.file(n, seqXmls[i]));
109
+ base.buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
110
+
111
+ const allCuts = [];
112
+ sources.forEach((src, i) => {
113
+ (src.cuts || []).forEach((cut) => {
114
+ allCuts.push(mediaRefs[i] ? { ...cut, mediaRef: mediaRefs[i] } : { ...cut });
115
+ });
116
+ });
117
+ allCuts.sort((a, b) => a.startFrame - b.startFrame);
118
+ if (allCuts.length) {
119
+ const cutRes = await cutSourceIntoClips(base.buffer, { cuts: allCuts });
70
120
  base.buffer = cutRes.buffer;
71
121
  }
72
122
  } else {
@@ -100,32 +150,7 @@ async function assembleTimeline(spec = {}) {
100
150
  }));
101
151
  }
102
152
 
103
- // Native-descriptor transplant: when a live-captured media template exists
104
- // for this file, swap the pool media element and rewire MediaRefs — the
105
- // only measured way an authored timeline's media actually RENDERS (the
106
- // repoint fallback imports and reads back fine but the render engine
107
- // refuses or paints black; see media-template-cache).
108
- let mediaDescriptor = 'none';
109
- if (media && media.mediaFilePath) {
110
- const cached = loadMediaTemplate(media.mediaFilePath);
111
- if (cached) {
112
- const zip = await JSZip.loadAsync(buffer);
113
- const mpPath = 'MediaPool/Master/MpFolder.xml';
114
- const seqNames = Object.keys(zip.files).filter((n) => /SeqContainer\/.+\.xml$/.test(n) || /\/SeqContainer\d*\.xml$/.test(n));
115
- const mpXml = await zip.file(mpPath).async('string');
116
- const seqXmls = [];
117
- for (const n of seqNames) seqXmls.push(await zip.file(n).async('string'));
118
- const res = transplantMediaElement(mpXml, seqXmls, cached);
119
- zip.file(mpPath, res.mpXml);
120
- seqNames.forEach((n, i) => zip.file(n, res.seqXmls[i]));
121
- buffer = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
122
- mediaDescriptor = 'native-transplant';
123
- } else {
124
- mediaDescriptor = 'repoint-fallback';
125
- }
126
- }
127
-
128
- return { buffer, timelineName: tlName, startFrame, mediaDescriptor };
153
+ return { buffer, timelineName: tlName, startFrame, mediaDescriptor: mediaDescriptorState };
129
154
  }
130
155
 
131
156
  module.exports = { assembleTimeline };
@@ -49,6 +49,9 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
49
49
  if (cut.srcIn !== undefined && (!Number.isInteger(cut.srcIn) || cut.srcIn < 0)) {
50
50
  throw new TypeError(`cutSourceIntoClips: cuts[${i}].srcIn must be a non-negative integer`);
51
51
  }
52
+ if (cut.mediaRef !== undefined && !/^[0-9a-f-]{36}$/.test(cut.mediaRef)) {
53
+ throw new TypeError(`cutSourceIntoClips: cuts[${i}].mediaRef must be a uuid`);
54
+ }
52
55
  });
53
56
 
54
57
  const zip = await loadDrpZip(drpInput);
@@ -68,6 +71,11 @@ async function cutSourceIntoClips(drpInput, opts = {}) {
68
71
  c = setClipStart(c, cut.startFrame);
69
72
  c = setClipDuration(c, cut.durationFrames);
70
73
  c = setClipIn(c, cut.srcIn ?? 0);
74
+ if (cut.mediaRef) {
75
+ // Multi-source: point this cut at ITS source's transplanted pool
76
+ // element instead of the donor's.
77
+ c = c.replace(/<MediaRef>[0-9a-f-]{36}<\/MediaRef>/, `<MediaRef>${cut.mediaRef}</MediaRef>`);
78
+ }
71
79
  return c;
72
80
  });
73
81
  if (trackType === 'video') {
@@ -68,4 +68,16 @@ function transplantMediaElement(mpXml, seqXmls, { poolElement, mediaRef }) {
68
68
  return { mpXml: outMp, seqXmls: outSeqs };
69
69
  }
70
70
 
71
- module.exports = { CACHE_DIR, cachePathFor, loadMediaTemplate, transplantMediaElement };
71
+ /** Insert an ADDITIONAL native media <Element> into MpFolder's MediaVec. */
72
+ function insertMediaElement(mpXml, poolElement) {
73
+ const close = mpXml.indexOf('</MediaVec>');
74
+ if (close < 0) throw new Error('insertMediaElement: no MediaVec in MpFolder');
75
+ const folderId = (mpXml.match(/<Sm2MpVideoClip[^>]*>[\s\S]*?<MpFolder>([0-9a-f-]{36})<\/MpFolder>/) || [])[1];
76
+ let element = poolElement;
77
+ if (folderId) {
78
+ element = element.replace(/<MpFolder>[0-9a-f-]{36}<\/MpFolder>/, `<MpFolder>${folderId}</MpFolder>`);
79
+ }
80
+ return mpXml.slice(0, close) + element + mpXml.slice(close);
81
+ }
82
+
83
+ module.exports = { CACHE_DIR, cachePathFor, loadMediaTemplate, transplantMediaElement, insertMediaElement };
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.106.0"
90
+ VERSION = "2.107.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.106.0"
14
+ VERSION = "2.107.0"
15
15
 
16
16
  import base64
17
17
  import os