davinci-resolve-mcp 2.162.0 → 2.163.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,45 @@
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.163.0 — E105: QC through every export format
6
+
7
+ One faded, dissolved, retimed conform exported from Resolve three ways — OTIO,
8
+ FCP7 XML, and CMX EDL — and pushed through `editorial.verify_roundtrip`. All
9
+ three now close `pass: true`; getting there measured four laws of Resolve's own
10
+ writers and fixed the parsers to match.
11
+
12
+ ### Fixed
13
+
14
+ - **Every FCP7-XML retime read as a FREEZE**: Resolve's timeremap effect
15
+ writes `speed` 50 followed by `variablespeed` 0, and a loose `/speed/`
16
+ parameter match let the second overwrite the first. Exact parameter
17
+ matching now, with the `reverse` flag read alongside.
18
+ - **FCP7 `-1` clip edges** (transition-adjacent) are resolved to the
19
+ transition's junction — the span center for `center` alignment — with the
20
+ clip's `in` advanced by the overlap offset so source stays record-aligned;
21
+ `out - in` is the record duration even under a retime. Solid Color
22
+ `generatoritem`s walk as black legs, and fade transitions attach to the
23
+ picture rather than the black.
24
+ - **CMX EDL exports name every file source reel `AX`** with the real names in
25
+ `* FROM/TO CLIP NAME` comments; `parseEDL` now applies them (TO = incoming,
26
+ FROM = outgoing of a dissolve pair), keeping specific reels intact.
27
+ - **`timeline.export_timeline_checked` refuses unresolved export constants
28
+ loudly.** A made-up `EXPORT_CMX_3600` reached `Timeline.Export` as a string
29
+ and came back as a bare `success: false`; the real constant is `EXPORT_EDL`,
30
+ and the refusal now lists the vocabulary.
31
+
32
+ ### Added
33
+
34
+ - `verify_roundtrip` reports `audioNotInExport` when the export carries no
35
+ audio at all (Resolve's EDL writer is video-only — measured) instead of
36
+ failing, mirroring `markersNotInExport`.
37
+
38
+ ### Measured (filed in api-limitations)
39
+
40
+ - `EXPORT_EDL` is video-only, writes reel `AX` + clip-name comments, places
41
+ dissolve junctions at the CMX start-at-cut position, and writes the BL
42
+ fades its own importer drops.
43
+
5
44
  ## What's New in v2.162.0 — freeze parity: Avid 0% motion effects are freezes
6
45
 
7
46
  ### Fixed
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.162.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.163.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.162.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.163.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.162.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.163.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 35 missing capabilities, 50 bugs / unreliable behaviors.
15
+ **Totals:** 36 missing capabilities, 50 bugs / unreliable behaviors.
16
16
 
17
17
  The authoritative source is the runtime-queryable `api_truth` ledger
18
18
  (`resolve_control api_truth "<query>"`); this document is generated from
@@ -301,6 +301,14 @@ equivalent, blocking full automation.
301
301
  - **Workaround / current handling:** Read item.GetNodeGraph().GetNumNodes() before SetCDL and diagnose a False against the node count and clip type. Prove the applied look with a rendered frame (gallery_stills grab_and_export or Project.ExportCurrentFrameAsStill), not the return value.
302
302
  - **Tags:** color, cdl, readback, silent-failure
303
303
 
304
+ ### Timeline.Export EXPORT_EDL (video-only, reel AX, clip-name comments)
305
+
306
+ - **Object:** `Timeline`
307
+ - **Signature:** `(filePath, EXPORT_EDL, EXPORT_NONE) -> bool`
308
+ - **Behavior:** Resolve's CMX EDL writer (measured on Studio 19.1.3.7, E105) emits VIDEO events only — audio legs never appear; names every file source by the generic reel AX and carries the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments; writes black legs as reel BL; places dissolve junctions at the CMX start-at-cut position (the overlap start, not the centered junction the timeline holds); and WRITES BL fades that its own EDL importer then drops. The FCP7 XML writer, by contrast, carries audio, writes transition-adjacent clip edges as -1 (the junction), and emits `speed` followed by `variablespeed` 0 in the same timeremap effect.
309
+ - **Workaround / current handling:** For round-trip QC prefer EXPORT_OTIO (carries audio, retimes as LinearTimeWarp/FreezeFrame, exact spans). When an EDL is the required deliverable, expect no audio (editorial.verify_roundtrip reports audioNotInExport) and resolve AX reels through the clip-name comments (parseEDL does). Use EXPORT_EDL — there is no EXPORT_CMX_3600 constant, and an unknown name reaches Export as a string that returns a bare False (timeline.export_timeline_checked now refuses it loudly).
310
+ - **Tags:** timeline, export, edl, audio, silent-failure
311
+
304
312
  ## Bugs / Unreliable Behavior (please fix)
305
313
 
306
314
  Methods that exist but misbehave — silent failures, unreliable return
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.162.0"
40
+ VERSION = "2.163.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.162.0",
3
+ "version": "2.163.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -990,7 +990,14 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
990
990
  const aa = dedupeAud(norm(inputEvents, audsOf).filter((e) => !isBlackSeg(e)));
991
991
  const ba = dedupeAud(norm(exportedEvents, audsOf).filter((e) => !isBlackSeg(e)));
992
992
  let audio;
993
- if (aa.length) {
993
+ let audioNotInExport = false;
994
+ if (aa.length && !ba.length) {
995
+ // The export carries NO audio at all while the input declares some: a
996
+ // capability gap (Resolve's EXPORT_EDL is video-only, measured E105),
997
+ // reported as audioNotInExport rather than failed — like markers.
998
+ audio = { input: aa.length, exported: 0, compared: false, note: 'export carries no audio events (format capability, e.g. EDL is video-only) — not compared' };
999
+ audioNotInExport = true;
1000
+ } else if (aa.length) {
994
1001
  audio = { input: aa.length, exported: ba.length, compared: true };
995
1002
  comparePairs(aa, ba, junctionsFor(/^A\d*$/), 'audio');
996
1003
  } else if (ba.length) {
@@ -1033,5 +1040,6 @@ export function verifyRoundtrip(inputEvents, exportedEvents, opts = {}) {
1033
1040
  ...(blackSegments.input || blackSegments.exported ? { blackSegments } : {}),
1034
1041
  ...(fadeReshapedBoundaries.length ? { fadeReshapedBoundaries } : {}),
1035
1042
  ...(audio ? { audio } : {}),
1043
+ ...(audioNotInExport ? { audioNotInExport } : {}),
1036
1044
  };
1037
1045
  }
@@ -90,6 +90,29 @@ export function parseEDL(text, opts = {}) {
90
90
  // at an absolute record timecode. Emitted as track 'MARKER' pseudo-events
91
91
  // (recIn only) so the assemble bridge can author them; consumers that
92
92
  // filter video/audio by track shape ignore them.
93
+ // `* FROM CLIP NAME:` / `* TO CLIP NAME:` comments (E105): Resolve's own
94
+ // EDL writer names every file source by the generic reel AX and carries
95
+ // the real clip name here. TO names the incoming (the last event, a D
96
+ // line's), FROM the outgoing (the event before a transition pair, else
97
+ // the last event). A generic reel (AX/BL-less) takes the clip name as
98
+ // its source so sourceMap and QC key on something real; a specific reel
99
+ // keeps it, with clipName carried alongside.
100
+ const cn = /^\*\s*(FROM|TO)\s+CLIP\s+NAME:\s*(.+?)\s*$/i.exec(line);
101
+ if (cn) {
102
+ const which = cn[1].toUpperCase();
103
+ const clipName = cn[2];
104
+ const last = [...events].reverse().find((e) => e.track !== 'MARKER');
105
+ let target = last;
106
+ if (which === 'FROM' && last && last.transition) {
107
+ const prev = [...events].reverse().find((e) => e !== last && e.track === last.track && e.recOut === last.recIn);
108
+ if (prev) target = prev;
109
+ }
110
+ if (target && !/^(BL|BLACK)$/i.test(String(target.source))) {
111
+ target.clipName = clipName;
112
+ if (/^AX$/i.test(String(target.source))) target.source = clipName;
113
+ }
114
+ continue;
115
+ }
93
116
  const loc = /^\*\s*LOC:\s*(\d{2}:\d{2}:\d{2}[:;]\d{2})\s+(\S+)\s*(.*)$/i.exec(line);
94
117
  if (loc) {
95
118
  events.push(evt({
@@ -273,36 +296,69 @@ export function parseXMEMLEvents(xml, opts = {}) {
273
296
  const events = [];
274
297
  let idx = 1;
275
298
  const seqRate = opts.fps || 24;
299
+ let currentJunctions = [];
276
300
  const walk = (node, track) => {
277
301
  if (!node) return;
278
302
  const items = Array.isArray(node) ? node : [node];
279
303
  for (const it of items) {
280
304
  const name = it.name || (it.file && it.file.name) || 'UNKNOWN';
281
- const start = Number(it.start);
282
- const end = Number(it.end);
305
+ const start0 = Number(it.start);
306
+ const end0 = Number(it.end);
283
307
  const inF = Number(it.in);
284
308
  const outF = Number(it.out);
285
309
  let speed = 100,
286
310
  reverse = false;
287
- // speed via a timeremap/motion filter (best-effort)
311
+ // speed via a timeremap/motion filter. EXACT parameter match (E105):
312
+ // Resolve's own FCP7 writer emits `speed` 50 followed by
313
+ // `variablespeed` 0 in the same effect, and a loose /speed/ match let
314
+ // the second overwrite the first — every retime read as a FREEZE.
288
315
  const filters = it.filter ? (Array.isArray(it.filter) ? it.filter : [it.filter]) : [];
289
316
  for (const fl of filters) {
290
317
  const params = fl.effect && fl.effect.parameter ? (Array.isArray(fl.effect.parameter) ? fl.effect.parameter : [fl.effect.parameter]) : [];
291
318
  for (const pm of params) {
292
- if (/speed/i.test(pm.name || '') && pm.value != null) {
293
- speed = Math.abs(Number(pm.value));
294
- reverse = Number(pm.value) < 0;
319
+ const pid = String(pm.parameterid || pm.name || '').trim().toLowerCase();
320
+ if (pid === 'speed' && pm.value != null && typeof pm.value !== 'object') {
321
+ const v = Number(pm.value);
322
+ if (Number.isFinite(v)) { speed = Math.abs(v); reverse = v < 0; }
323
+ } else if (pid === 'reverse' && /^true$/i.test(String(pm.value))) {
324
+ reverse = true;
295
325
  }
296
326
  }
297
327
  }
298
- if (Number.isFinite(start) && Number.isFinite(end)) {
328
+ // FCP7 `-1` EDGES (E105, measured against Resolve's own FCP7 writer):
329
+ // a clipitem edge under a transitionitem is written as -1 and means
330
+ // "this edge is the adjacent transition's JUNCTION" — the span center
331
+ // for alignment center, its start/end for start-black/end-black.
332
+ // `out - in` is the RECORD duration even under a retime (Resolve
333
+ // writes out = in + record length), which anchors the missing edge.
334
+ let start = start0;
335
+ let end = end0;
336
+ let inAdj = 0;
337
+ const dur = Number.isFinite(inF) && Number.isFinite(outF) ? outF - inF : null;
338
+ const junctions = currentJunctions;
339
+ if (start === -1 && end !== -1 && dur != null) start = end - dur;
340
+ else if (end === -1 && start !== -1 && dur != null) end = start + dur;
341
+ else if (start === -1 && end === -1 && dur != null && junctions.length) {
342
+ const js = junctions.map((j) => j.frame).sort((a, b) => a - b);
343
+ const pair = js.find((a) => js.includes(a + dur));
344
+ if (pair != null) { start = pair; end = pair + dur; }
345
+ }
346
+ // A -1 START edge's `in` is the source at the OVERLAP start (the
347
+ // clip's material begins under the transition), while the resolved
348
+ // start is the junction — so `in` advances by the same offset to stay
349
+ // record-aligned (measured: srcIn read 12 short without this).
350
+ if (start0 === -1 && Number.isFinite(start)) {
351
+ const j = junctions.find((jj) => jj.frame === start);
352
+ if (j) inAdj = start - j.start;
353
+ }
354
+ if (Number.isFinite(start) && Number.isFinite(end) && start >= 0 && end >= 0) {
299
355
  // FCP7 clipitem <marker> children are CLIP markers (frames relative
300
356
  // to the clip's own <in>) — routed to item markers like OTIO's (E81).
301
357
  const mks = it.marker ? (Array.isArray(it.marker) ? it.marker : [it.marker]) : [];
302
358
  const itemMarkers = mks
303
359
  .filter((mk) => Number.isFinite(Number(mk.in)))
304
360
  .map((mk) => ({ frame: Number(mk.in) - inF, name: mk.name != null ? String(mk.name) : undefined, note: mk.comment != null ? String(mk.comment) : undefined }));
305
- events.push(evt({ index: idx++, track, source: name, srcIn: inF, srcOut: outF, recIn: start, recOut: end, speed, reverse, itemMarkers: itemMarkers.length ? itemMarkers : undefined, fps: seqRate }));
361
+ events.push(evt({ index: idx++, track, source: name, srcIn: Number.isFinite(inF) ? inF + inAdj : inF, srcOut: Number.isFinite(outF) ? outF + inAdj : outF, recIn: start, recOut: end, speed, reverse, itemMarkers: itemMarkers.length ? itemMarkers : undefined, fps: seqRate }));
306
362
  }
307
363
  }
308
364
  };
@@ -313,7 +369,24 @@ export function parseXMEMLEvents(xml, opts = {}) {
313
369
  vtracks.forEach((t, vi) => {
314
370
  const label = vi === 0 ? 'V' : `V${vi + 1}`;
315
371
  const before = events.length;
372
+ // Junctions of this track's transitionitems, for -1 edge resolution.
373
+ const tlist = t.transitionitem ? (Array.isArray(t.transitionitem) ? t.transitionitem : [t.transitionitem]) : [];
374
+ currentJunctions = tlist.map((tr) => {
375
+ const s0 = Number(tr.start), e0 = Number(tr.end);
376
+ if (!Number.isFinite(s0) || !Number.isFinite(e0)) return null;
377
+ const al = String(tr.alignment || 'center').toLowerCase();
378
+ const frame = al === 'start-black' || al === 'start' ? s0 : al === 'end-black' || al === 'end' ? e0 : Math.round((s0 + e0) / 2);
379
+ return { frame, start: s0, end: e0 };
380
+ }).filter(Boolean);
381
+ // Solid Color generatoritems are BLACK legs (what Resolve writes for a
382
+ // fade's black side); they walk as BL events so fades round-trip.
383
+ const gens = t.generatoritem ? (Array.isArray(t.generatoritem) ? t.generatoritem : [t.generatoritem]) : [];
384
+ for (const g of gens) {
385
+ if (!/solid color|black/i.test(String(g.name || ''))) continue;
386
+ walk([{ ...g, name: 'BL', filter: undefined, marker: undefined }], label);
387
+ }
316
388
  if (t.clipitem) walk(t.clipitem, label);
389
+ currentJunctions = [];
317
390
  // <transitionitem> siblings: attach each to the INCOMING clip event —
318
391
  // the one whose record-in falls inside the transition's span. Resolve's
319
392
  // OWN XMEML importer writes these as elements that render INERT on
@@ -326,7 +399,11 @@ export function parseXMEMLEvents(xml, opts = {}) {
326
399
  const s = Number(tr.start), e2 = Number(tr.end);
327
400
  if (!Number.isFinite(s) || !Number.isFinite(e2)) continue;
328
401
  const effectId = (tr.effect && (tr.effect.effectid || tr.effect.name)) || 'Cross Dissolve';
329
- const incoming = events.slice(before).find((ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2);
402
+ const isBLev = (ev) => /^(BL|BLACK)$/i.test(String(ev.source || '').trim());
403
+ const inSpan = (ev) => ev.track === label && ev.recIn > s - 1 && ev.recIn <= e2;
404
+ // Prefer the PICTURE as the incoming: a Solid Color generatoritem in
405
+ // the same span is the fade's black side, not the clip fading in.
406
+ const incoming = events.slice(before).find((ev) => inSpan(ev) && !isBLev(ev)) || events.slice(before).find(inSpan);
330
407
  // recStart carries the transitionitem's EXPLICIT record span start
331
408
  // (sequence-relative) so the bridge reproduces the editor's actual
332
409
  // alignment instead of assuming centered.
@@ -86,7 +86,7 @@ const markerSchema = z.object({
86
86
  export const editorialTool = {
87
87
  name: 'editorial',
88
88
  description:
89
- 'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and freezes as zero-speed events (OTIO FreezeFrame, XMEML timeremap 0, PrProj in==out, AAF 0% motion effects); for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source; the EDL target writes CMX transition pairs incl. BL fades — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base; BL-aware — black legs need no source, fades no black-side handles, and a fade-out tail requirement lands on the picture source), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets + marker compare w/ markersNotInExport honesty flag; FADE-AWARE: BL/Solid-Color legs merge out as blackSegments and fade-window boundary reshapes are excused into fadeReshapedBoundaries instead of failing; RETIME-AWARE: speed/reverse compare pairwise — EXPORT_OTIO carries an authored Sm2TimeMap back as LinearTimeWarp (measured), so a flattened/lost retime fails as drift geometry alone cannot catch; AUDIO-AWARE: declared audio events compare (channel legs deduped, mismatches tagged trackType audio) while the mirrored-A1 export of a video-only turnover stays informational; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
89
+ 'Editorial integrity (Cluster E) — turnover interchange → normalized events → changelist + conform manifest with TIMING silent-lie guards (flattened retime / dropped J/L-cut audio / framerate-pulldown slip / reverse dropped / transition-handle starvation → flag, skip-not-fake). Report-only (gate: review). Actions: parse_interchange (EDL/OTIO/XMEML natively — incl. Resolve-written FCP7 -1 junction edges + Solid Color generators, and CMX FROM/TO CLIP NAME comments over the generic AX reel — + AAF via pyaaf2 + PRPROJ via gunzip+XML → normalized events incl. span-explicit transitions, BL fade legs, and freezes as zero-speed events (OTIO FreezeFrame, XMEML timeremap 0, PrProj in==out, AAF 0% motion effects); for AAF/PRPROJ pass the file PATH as content; AAF also returns per-sequence startTimecode/startFrame — build the timeline at THAT start, not the Resolve 01:00:00:00 default — and per-clip `geometry` for Avid transform effects), list_sequences (ONE offline picker entry point across xml/edl/otio/drt/drp/aaf/prproj → [{id,name,eventCount}], plus startTimecode/startFrame for AAF), convert_to_interchange (author OTIO/EDL/DRT Resolve CAN import from events or a parsed source; the EDL target writes CMX transition pairs incl. BL fades — the .prproj→Resolve conform bridge, no Premiere needed; editorial timing/transitions survive and per-clip effects/color do not. SPEED/REVERSE survive on the otio (LinearTimeWarp) and edl (M2) targets ONLY — this FLAT drt target flattens every retime to 100% forward and returns `flattened`/`flattenedCount` naming each event that lost one (`flattened` is always present on `drt`, empty when there were none); for a .drt that AUTHORS retimes/dissolves/multi-track/audio, use drt.assemble_from_interchange), turnover_changelist (diff old vs new → moved/retimed/replaced/new/gone + timing flags), conform_manifest (per-event assert: source resolved/handles/retime/reverse/TC-base; BL-aware — black legs need no source, fades no black-side handles, and a fade-out tail requirement lands on the picture source), marker_roundtrip (markers with provenance tags), verify_roundtrip (input events vs re-export events -> pass/mismatches + fitted per-source TC offsets + marker compare w/ markersNotInExport honesty flag; FADE-AWARE: BL/Solid-Color legs merge out as blackSegments and fade-window boundary reshapes are excused into fadeReshapedBoundaries instead of failing; RETIME-AWARE: speed/reverse compare pairwise — EXPORT_OTIO carries an authored Sm2TimeMap back as LinearTimeWarp (measured), so a flattened/lost retime fails as drift geometry alone cannot catch; AUDIO-AWARE: declared audio events compare (a video-only export such as EXPORT_EDL flags audioNotInExport instead of failing) (channel legs deduped, mismatches tagged trackType audio) while the mirrored-A1 export of a video-only turnover stays informational; the conform QC loop-closer). Offline (AAF needs pyaaf2; live AAF/DRP import is on the Python davinci-resolve MCP).',
90
90
  async handler({ action, args }) {
91
91
  if (action === 'parse_interchange') {
92
92
  const p = parseSchema.parse(args);
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.162.0"
90
+ VERSION = "2.163.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.162.0"
14
+ VERSION = "2.163.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -6927,6 +6927,26 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
6927
6927
  # (which can be None and silently degrade the EXPORT_* args to strings — the
6928
6928
  # same failure class as issue #70).
6929
6929
  spec = _timeline_export_spec(p, get_resolve())
6930
+ # An export type that did not resolve to a Resolve constant reaches
6931
+ # Timeline.Export as a STRING, which returns a bare False — reported as
6932
+ # success:false with no reason (measured E105 with a made-up
6933
+ # EXPORT_CMX_3600: the real constant is EXPORT_EDL). Refuse loudly with
6934
+ # the vocabulary instead of letting a typo read as an export failure.
6935
+ # Only when this build exposes the export vocabulary at all (a bare stub
6936
+ # or a degraded connection resolves nothing, and the old passthrough
6937
+ # stays the honest behavior there).
6938
+ _probe_const, _ = _timeline_export_value("EXPORT_OTIO", get_resolve())
6939
+ vocab_known = not isinstance(_probe_const, str)
6940
+ for field in ("export_type", "export_subtype"):
6941
+ val = spec.get(field)
6942
+ if vocab_known and isinstance(val, str) and val.startswith("EXPORT_"):
6943
+ return _err(
6944
+ f"{field} {val!r} is not a Resolve export constant on this build. "
6945
+ f"Known aliases: {', '.join(sorted(_TIMELINE_EXPORT_ALIASES))}; "
6946
+ "constants: EXPORT_AAF, EXPORT_DRT, EXPORT_EDL, EXPORT_FCP_7_XML, "
6947
+ "EXPORT_FCPXML_1_8/1_9/1_10, EXPORT_OTIO (subtypes EXPORT_NONE, "
6948
+ "EXPORT_AAF_NEW, EXPORT_AAF_EXISTING, EXPORT_CDL, EXPORT_SDL, EXPORT_MISSING_CLIPS)."
6949
+ )
6930
6950
  if p.get("dry_run"):
6931
6951
  return _ok(path=path, would_export=True, spec={k: v for k, v in spec.items() if k != "export_type"})
6932
6952
 
@@ -2764,6 +2764,36 @@ API_TRUTH: List[Dict[str, Any]] = [
2764
2764
  "submit": "bug",
2765
2765
  "mitigation": ["drt.assemble_from_interchange black-leg authoring"],
2766
2766
  },
2767
+ {
2768
+ "symbol": "Timeline.Export EXPORT_EDL (video-only, reel AX, clip-name comments)",
2769
+ "object": "Timeline",
2770
+ "signature": "(filePath, EXPORT_EDL, EXPORT_NONE) -> bool",
2771
+ "reality": "Resolve's CMX EDL writer (measured on Studio 19.1.3.7, "
2772
+ "E105) emits VIDEO events only — audio legs never appear; "
2773
+ "names every file source by the generic reel AX and carries "
2774
+ "the real names in `* FROM CLIP NAME:` / `* TO CLIP NAME:` "
2775
+ "comments; writes black legs as reel BL; places dissolve "
2776
+ "junctions at the CMX start-at-cut position (the overlap "
2777
+ "start, not the centered junction the timeline holds); and "
2778
+ "WRITES BL fades that its own EDL importer then drops. The "
2779
+ "FCP7 XML writer, by contrast, carries audio, writes "
2780
+ "transition-adjacent clip edges as -1 (the junction), and "
2781
+ "emits `speed` followed by `variablespeed` 0 in the same "
2782
+ "timeremap effect.",
2783
+ "recommended": "For round-trip QC prefer EXPORT_OTIO (carries audio, "
2784
+ "retimes as LinearTimeWarp/FreezeFrame, exact spans). "
2785
+ "When an EDL is the required deliverable, expect no "
2786
+ "audio (editorial.verify_roundtrip reports "
2787
+ "audioNotInExport) and resolve AX reels through the "
2788
+ "clip-name comments (parseEDL does). Use EXPORT_EDL — "
2789
+ "there is no EXPORT_CMX_3600 constant, and an unknown "
2790
+ "name reaches Export as a string that returns a bare "
2791
+ "False (timeline.export_timeline_checked now refuses "
2792
+ "it loudly).",
2793
+ "tags": ["timeline", "export", "edl", "audio", "silent-failure"],
2794
+ "submit": "missing",
2795
+ "mitigation": ["editorial.verify_roundtrip audioNotInExport", "timeline.export_timeline_checked"],
2796
+ },
2767
2797
  {
2768
2798
  "symbol": "Project.SetRenderSettings ExportSubtitle/SubtitleFormat (inert on 19.x)",
2769
2799
  "object": "Project",