davinci-resolve-mcp 2.122.0 → 2.124.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.
@@ -54,7 +54,10 @@ window. `render.verify_output` covers the container-level checks.
54
54
  | Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
55
55
 
56
56
  `assemble_from_interchange` drives the same engine from an EDL / OTIO /
57
- FCP7-XML / AAF plus a `sourceMap`, and returns an honesty ledger
57
+ FCP7-XML / AAF / **.prproj** (Premiere, read offline no Premiere needed)
58
+ plus a `sourceMap` — all five formats are route-proven end-to-end
59
+ (parse → assemble → import → measured frames and RMS) — and
60
+ returns an honesty ledger
58
61
  (`authoredTransitions`, `droppedTransitions` with reasons, `authoredRetimes`,
59
62
  `flattenedRetimes`, `authoredAudioEvents`, `upperTrackCutsVideoOnly`).
60
63
 
@@ -99,7 +102,10 @@ Everything else stays in `droppedTransitions` with the reason.
99
102
  ## Verification checklist for a delivered .drt
100
103
 
101
104
  1. `timeline.import_timeline_checked` — expect `linked == total` for media
102
- (generators legitimately count as offline).
105
+ (generators legitimately count as offline). For `.drt`/`.drp` it also
106
+ cross-checks the files items ACTUALLY link against the archive's
107
+ `<MediaFilePath>` set and warns on a coarse-identity cross-link
108
+ (`cross_link_warning`) — `linked == total` alone cannot see one.
103
109
  2. Render a probe range; check frame luma at cut boundaries, dissolve
104
110
  midpoints (expect the blend average), and retime windows.
105
111
  3. For audio: RMS per window (silence = -inf is a failed placement).
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.122.0"
40
+ VERSION = "2.124.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.122.0",
3
+ "version": "2.124.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -30,7 +30,7 @@ const authorSchema = z.object({
30
30
  });
31
31
  const validateSchema = z.object({ drtPath: z.string().describe('Absolute path to a .drt file') });
32
32
  const assembleFromInterchangeSchema = z.object({
33
- format: z.enum(['edl', 'otio', 'xml', 'aaf']).describe('Interchange format of the input'),
33
+ format: z.enum(['edl', 'otio', 'xml', 'aaf', 'prproj']).describe('Interchange format of the input'),
34
34
  path: z.string().optional().describe('Path to the interchange file (aaf REQUIRES a path)'),
35
35
  content: z.string().optional().describe('Inline interchange text (edl/otio/xml)'),
36
36
  fps: z.number().optional().describe('Event frame rate for parsing (default 24; use e.g. 29.97 for NTSC EDLs)'),
@@ -211,14 +211,23 @@ export const drtTool = {
211
211
  const { parseInterchange } = await import('../editorial.mjs');
212
212
  const { eventsToAssembleSpec } = await import('../author-interchange.mjs');
213
213
  let content = p.content;
214
+ let events;
214
215
  if (p.format === 'aaf') {
215
216
  if (!p.path) return { error: 'aaf input requires path' };
216
217
  content = p.path;
218
+ } else if (p.format === 'prproj') {
219
+ // Premiere: offline gunzip+graph read (no Premiere, no Resolve import
220
+ // path). parsePrproj flattens EVERY sequence's events; a multi-sequence
221
+ // .prproj with overlapping record ranges refuses naturally in the
222
+ // overlap check — pre-split via editorial.list_sequences if needed.
223
+ if (!p.path) return { error: 'prproj input requires path' };
224
+ const { parsePrproj } = await import('../prproj.mjs');
225
+ events = parsePrproj(p.path);
217
226
  } else if (!content) {
218
227
  if (!p.path) return { error: 'provide content or path' };
219
228
  content = await fs.readFile(p.path, 'utf8');
220
229
  }
221
- const events = parseInterchange(p.format, content, { fps: p.fps ?? 24 });
230
+ if (!events) events = parseInterchange(p.format, content, { fps: p.fps ?? 24 });
222
231
  if (!events || !events.length) return { error: 'no events parsed from the interchange input' };
223
232
  const { spec, report } = eventsToAssembleSpec(events, {
224
233
  sourceMap: p.sourceMap, timelineName: p.timelineName,
@@ -248,7 +257,7 @@ export const drtTool = {
248
257
  stamped,
249
258
  conform: report,
250
259
  note:
251
- 'Import with timeline.import_timeline_checked (timeline is named after the FILE). ' +
260
+ 'Import with timeline.import_timeline_checked (timeline is named after the FILE). Dissolves/cross-fades, forward+reverse retimes, multi-track video, audio events and markers are AUTHORED when geometry allows; everything else drops WITH a reason — see `conform` for the ledger.' +
252
261
  'Retimes are flattened and transitions become cuts — see `conform` for the ledger.',
253
262
  };
254
263
  }
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.122.0"
90
+ VERSION = "2.124.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.122.0"
14
+ VERSION = "2.124.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -6966,6 +6966,49 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
6966
6966
  return _run_maybe_background("timeline.export_timeline_checked", p, _work)
6967
6967
 
6968
6968
 
6969
+ def _drt_expected_media_paths(path: str):
6970
+ """Unique <MediaFilePath> values referenced by a .drt/.drp's timeline clips.
6971
+
6972
+ Feeds the cross-link check: Resolve merges pool media by a COARSE identity
6973
+ across imports, so a clip can silently relink to a DIFFERENT pre-existing
6974
+ file (measured: item readback even shows the wrong clip name). The archive
6975
+ is the ground truth for which files the timeline meant.
6976
+ """
6977
+ import zipfile as _zf
6978
+ paths = set()
6979
+ try:
6980
+ with _zf.ZipFile(path) as zf:
6981
+ for n in zf.namelist():
6982
+ if re.search(r"SeqContainer/.+\.xml$", n) or re.search(r"/SeqContainer\d*\.xml$", n):
6983
+ xml = zf.read(n).decode("utf-8", "replace")
6984
+ for m in re.finditer(r"<MediaFilePath>([^<]+)</MediaFilePath>", xml):
6985
+ paths.add(m.group(1))
6986
+ except Exception:
6987
+ return None
6988
+ return paths or None
6989
+
6990
+
6991
+ def _timeline_cross_link_check(tl, expected_paths):
6992
+ """Compare the media files a timeline ACTUALLY links against the expected set."""
6993
+ actual = set()
6994
+ for track_type in ("video", "audio"):
6995
+ try:
6996
+ count = int(tl.GetTrackCount(track_type) or 0)
6997
+ except Exception:
6998
+ count = 0
6999
+ for i in range(1, count + 1):
7000
+ for item in (tl.GetItemListInTrack(track_type, i) or []):
7001
+ try:
7002
+ mpi = item.GetMediaPoolItem()
7003
+ fp = mpi.GetClipProperty("File Path") if mpi else None
7004
+ if fp:
7005
+ actual.add(str(fp))
7006
+ except Exception:
7007
+ pass
7008
+ missing = sorted(x for x in expected_paths if x not in actual)
7009
+ return {"expected": sorted(expected_paths), "actual": sorted(actual), "missing": missing}
7010
+
7011
+
6969
7012
  def _timeline_media_coverage(tl) -> Dict[str, Any]:
6970
7013
  """Count how many timeline items are linked to a Media Pool Item vs. offline.
6971
7014
 
@@ -7427,6 +7470,20 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
7427
7470
  out["relink"] = relink_result
7428
7471
  if binary_relink_note:
7429
7472
  out["note"] = binary_relink_note
7473
+ if ext in {".drt", ".drp"}:
7474
+ expected_paths = _drt_expected_media_paths(path)
7475
+ if expected_paths:
7476
+ xcheck = _timeline_cross_link_check(imported, expected_paths)
7477
+ if xcheck["missing"]:
7478
+ out["cross_link_warning"] = (
7479
+ "Media files this timeline references are NOT among the files its "
7480
+ f"items actually link to: {', '.join(xcheck['missing'])}. Resolve "
7481
+ "merges pool media by a coarse identity across imports, so clips "
7482
+ "can silently relink to a DIFFERENT pre-existing file (item names "
7483
+ "follow the wrong file too). Import into a fresh project, or "
7484
+ "render-probe before trusting this conform."
7485
+ )
7486
+ out["cross_link_check"] = xcheck
7430
7487
  if media["total"] and media["offline"]:
7431
7488
  msg = f"{media['offline']} of {media['total']} timeline items are offline (no linked media)."
7432
7489
  if is_binary: