davinci-resolve-mcp 2.182.0 → 2.184.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,38 @@
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.184.0 — E131: a nested Stack's audio tracks flatten onto the parent audio lanes
6
+
7
+ ### Verified
8
+
9
+ - A compound's inner Audio tracks flatten onto the parent's audio lanes
10
+ (`A`, `A2` …) tagged `fromCompound`, and the bridge places them on those
11
+ lanes — three audio placements across two lanes, none dropped. Pinned as a
12
+ test; the flatten from E120 already did this.
13
+
14
+ ## What's New in v2.183.0 — E129/E130: `import_from_drp` names from the pool, imports timelines, keeps compounds
15
+
16
+ ### Fixed
17
+
18
+ - **`timeline.import_from_drp` named containers after their first clip.**
19
+ Resolve's own compound-timeline export listed as "cut_src.mp4" /
20
+ "white_src.mp4", "by name" could not find the real timeline, and the
21
+ default (all containers) imported the inner compound containers as
22
+ separate, hollow timelines. The lister now names each container from the
23
+ pool (`Sm2MpTimelineClip` / `Sm2MpCompoundClip` embed the `Sm2Sequence`
24
+ the container's `<Sequence>` names) and reports `kind`; the default
25
+ imports every TIMELINE; explicit names and indexes are unchanged (E129).
26
+ - **The Python extractor dropped a timeline's compound containers.** A
27
+ compound is a pool `Sm2MpCompoundClip` whose embedded sequence lives in
28
+ its own container (the E45 law `drt.extract_from_drp` already honours), so
29
+ a timeline with compounds imported with hollow compounds. The extractor now
30
+ keeps them recursively (MediaRefs → compound pool elements → embedded
31
+ sequence ids → containers) and `metadata.json` lists `keptSeqContainers`.
32
+ Verified on the fixture: E57_NESTED keeps E57_OUT and E57_IN; E57_OUT keeps
33
+ E57_IN; the bundled template keeps its one container (E130). The
34
+ bundled template's sequence now lists as `MediaTemplate` on this route
35
+ too, where it used to read as its clip `sample.mp4`.
36
+
5
37
  ## What's New in v2.182.0 — E128: `extract_from_drp` defaults to the pool's timeline, not the first-sorted compound
6
38
 
7
39
  ### 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.182.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.184.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.182.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.184.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.182.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.184.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.182.0"
40
+ VERSION = "2.184.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.182.0",
3
+ "version": "2.184.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.182.0"
90
+ VERSION = "2.184.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.182.0"
14
+ VERSION = "2.184.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -7574,17 +7574,58 @@ def _import_timeline_checked(proj, mp, p: Dict[str, Any]):
7574
7574
  _SEQ_CONTAINER_RE = re.compile(r"(^|/)SeqContainer(\d*\.xml|/[^/]+\.xml)$")
7575
7575
 
7576
7576
 
7577
+ def _drp_pool_sequence_names(zf) -> Dict[str, Dict[str, str]]:
7578
+ """Map an embedded Sm2Sequence DbId → {name, kind} from every MpFolder*.xml.
7579
+
7580
+ A SeqContainer XML carries NO timeline name — its first <Name> is the first
7581
+ CLIP's (measured on Resolve 19.1.3.7's EXPORT_DRT of a compound timeline,
7582
+ E127/E129) — while the pool's Sm2MpTimelineClip (a timeline) and
7583
+ Sm2MpCompoundClip (a compound) embed the Sm2Sequence whose DbId the
7584
+ container's track-level <Sequence> names. That is where names and kinds live.
7585
+ """
7586
+ out: Dict[str, Dict[str, str]] = {}
7587
+ for entry in zf.namelist():
7588
+ if entry.endswith("/") or not re.search(r"(^|/)MpFolder[^/]*\.xml$", entry):
7589
+ continue
7590
+ try:
7591
+ xml = zf.read(entry).decode("utf-8", "replace")
7592
+ except Exception:
7593
+ continue
7594
+ for m in re.finditer(r'<(Sm2MpTimelineClip|Sm2MpCompoundClip) DbId="([^"]+)">([\s\S]*?)</\1>', xml):
7595
+ body = m.group(3)
7596
+ nm = re.search(r"<Name>([\s\S]*?)</Name>", body)
7597
+ name = nm.group(1).strip() if nm else None
7598
+ kind = "compound" if m.group(1) == "Sm2MpCompoundClip" else "timeline"
7599
+ for sm in re.finditer(r'<Sm2Sequence DbId="([^"]+)">', body):
7600
+ out.setdefault(sm.group(1), {"name": name, "kind": kind})
7601
+ return out
7602
+
7603
+
7577
7604
  def _drp_seq_containers(zf) -> List[Dict[str, Any]]:
7578
- """List a .drp/.drt zip's SeqContainers as [{entry, name, index}] (0-based, sorted)."""
7605
+ """List a .drp/.drt zip's SeqContainers as [{entry, name, kind, index}] (0-based, sorted).
7606
+
7607
+ `name` is the POOL's name for the sequence when the archive carries one
7608
+ (the container's own first <Name> is a clip's — E129); `kind` is
7609
+ 'timeline' | 'compound' | None.
7610
+ """
7579
7611
  entries = sorted(n for n in zf.namelist() if not n.endswith("/") and _SEQ_CONTAINER_RE.search(n))
7612
+ pool = _drp_pool_sequence_names(zf)
7580
7613
  out = []
7581
7614
  for index, entry in enumerate(entries):
7582
7615
  try:
7583
7616
  xml = zf.read(entry).decode("utf-8", "replace")
7584
7617
  except Exception:
7585
7618
  xml = ""
7619
+ sid = re.search(r"<Sequence>([0-9a-f-]{36})</Sequence>", xml)
7620
+ info = pool.get(sid.group(1)) if sid else None
7586
7621
  m = re.search(r"<Name>([\s\S]*?)</Name>", xml)
7587
- out.append({"entry": entry, "name": (m.group(1).strip() if m else None), "index": index})
7622
+ fallback = m.group(1).strip() if m else None
7623
+ out.append({
7624
+ "entry": entry,
7625
+ "name": (info["name"] if info and info.get("name") else fallback),
7626
+ "kind": (info["kind"] if info else None),
7627
+ "index": index,
7628
+ })
7588
7629
  return out
7589
7630
 
7590
7631
 
@@ -7608,11 +7649,49 @@ def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str)
7608
7649
  seq_entries = [n for n in names if _SEQ_CONTAINER_RE.search(n)]
7609
7650
  keep_xml = zf.read(seq_entry).decode("utf-8", "replace")
7610
7651
  keep_seq_ids = re.findall(r"<Sequence>([0-9a-f-]{36})</Sequence>", keep_xml)
7652
+ # COMPOUND CLIPS (E130, the E45 law ported from drt.extract_from_drp): a
7653
+ # compound is a pool Sm2MpCompoundClip with an EMBEDDED Sm2Sequence
7654
+ # whose tracks live in their OWN SeqContainer — dropping that container
7655
+ # ships a compound that imports and reads back but is hollow. Walk the
7656
+ # kept container's MediaRefs → compound pool elements → embedded
7657
+ # sequence ids → keep those containers too, recursively.
7658
+ mp_texts = {}
7659
+ for name in names:
7660
+ if name.endswith("MpFolder.xml"):
7661
+ mp_texts[name] = zf.read(name).decode("utf-8", "replace")
7662
+ seq_xml_by_entry = {seq_entry: keep_xml}
7663
+ keep_containers = {seq_entry}
7664
+ seen_refs = set()
7665
+ queue = [keep_xml]
7666
+ while queue:
7667
+ xml = queue.pop()
7668
+ for ref in re.findall(r"<MediaRef>([0-9a-f-]{36})</MediaRef>", xml):
7669
+ if ref in seen_refs:
7670
+ continue
7671
+ for mp_xml in mp_texts.values():
7672
+ cm = re.search(r'<Sm2MpCompoundClip DbId="%s">[\s\S]*?</Sm2MpCompoundClip>' % re.escape(ref), mp_xml)
7673
+ if not cm:
7674
+ continue
7675
+ seen_refs.add(ref)
7676
+ im = re.search(r'<Sm2Sequence DbId="([0-9a-f-]{36})">', cm.group(0))
7677
+ if not im:
7678
+ continue
7679
+ inner = im.group(1)
7680
+ for entry_name in seq_entries:
7681
+ if entry_name in keep_containers:
7682
+ continue
7683
+ sx = seq_xml_by_entry.get(entry_name)
7684
+ if sx is None:
7685
+ sx = zf.read(entry_name).decode("utf-8", "replace")
7686
+ seq_xml_by_entry[entry_name] = sx
7687
+ if ("<Sequence>%s</Sequence>" % inner) in sx:
7688
+ keep_containers.add(entry_name)
7689
+ queue.append(sx)
7611
7690
  with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as out:
7612
7691
  for name in names:
7613
7692
  if name == "Gallery.xml":
7614
7693
  continue
7615
- if name in seq_entries and name != seq_entry:
7694
+ if name in seq_entries and name not in keep_containers:
7616
7695
  continue
7617
7696
  data = zf.read(name)
7618
7697
  if name.endswith("MpFolder.xml") and len(seq_entries) > 1:
@@ -7640,6 +7719,7 @@ def _extract_seqcontainer_from_drp(drp_path: str, seq_entry: str, out_path: str)
7640
7719
  "source": "import_from_drp",
7641
7720
  "sourceDrp": drp_path,
7642
7721
  "sourceSeqContainer": seq_entry,
7722
+ "keptSeqContainers": sorted(keep_containers),
7643
7723
  "exportedFrom": "davinci-resolve-mcp timeline.import_from_drp",
7644
7724
  },
7645
7725
  indent=2,
@@ -7681,7 +7761,7 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
7681
7761
  want_indexes = [want_indexes]
7682
7762
 
7683
7763
  selected: List[Dict[str, Any]] = []
7684
- available = [{"name": c["name"], "index": c["index"]} for c in containers]
7764
+ available = [{"name": c["name"], "kind": c.get("kind"), "index": c["index"]} for c in containers]
7685
7765
  if want_names:
7686
7766
  by_name = {}
7687
7767
  for c in containers:
@@ -7703,7 +7783,11 @@ def _import_from_drp(proj, mp, p: Dict[str, Any]):
7703
7783
  return _err(f"timelineIndex {i} out of range (0..{len(containers) - 1})", category="invalid_input")
7704
7784
  selected.append(match)
7705
7785
  else:
7706
- selected = list(containers)
7786
+ # ALL means every TIMELINE (E129): a compound's inner container is not a
7787
+ # timeline of its own — extraction keeps it with the timeline that
7788
+ # places it — so importing it separately lands a hollow duplicate.
7789
+ timelines_only = [c for c in containers if c.get("kind") == "timeline"]
7790
+ selected = timelines_only if timelines_only else list(containers)
7707
7791
 
7708
7792
  # Per-timeline options passthrough (importSourceClips etc.) minus selection
7709
7793
  # keys; background is a top-level option, so each sub-step import runs
@@ -24255,6 +24339,11 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
24255
24339
  `sanitize.flagged` for human review, never silently applied. verify_threshold
24256
24340
  (default 0.90) sets the structural-match bar.
24257
24341
  import_from_drp(drpPath, timelineNames?|timelineIndexes?, import_source_clips?, dry_run?) -> {success, selected, imported, available, results, partial?}
24342
+ timelineNames are the POOL's names (a SeqContainer's own first <Name>
24343
+ is a clip's — measured E129); `available` rows carry kind
24344
+ (timeline|compound). Omitted selection = every TIMELINE: a compound's
24345
+ inner container is not a timeline of its own, it travels with the
24346
+ timeline that places it (kept recursively in the extracted .drt, E130).
24258
24347
  CAVEAT (measured 19.1.3.7): Resolve refuses extracted containers even
24259
24348
  repacked with the source's project.xml — the sufficient import set
24260
24349
  beyond Resolve's own .drt exports is unmapped, so expect failures on