davinci-resolve-mcp 2.104.0 → 2.104.1

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,36 @@
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.104.1
6
+
7
+ **The job metadata lies too — verify_output now cross-checks the timeline.**
8
+ Finishing v2.104.0's pending live validation exposed a hole in the new
9
+ `render.verify_output`: in the issue #164 case (content before the timeline
10
+ start) Resolve rewrites the render job's own MarkIn/MarkOut down to the
11
+ collapsed extent. Measured live on Studio 19.1.3.7: 96 frames of content
12
+ placed before the start turned an explicit 96-frame mark range into a 1-frame
13
+ job — Complete at 100%, a 1-frame black stub, and a clean duration ratio,
14
+ because the expected duration was computed from the job's own lying range.
15
+
16
+ The only truthful readback in that state is the timeline items themselves,
17
+ which report their real positions. `verify_output` now finds the job's
18
+ timeline and cross-checks it: video items starting before the timeline's
19
+ start frame warn (the #164 signature by direct evidence), and a mark range
20
+ under half the items' extent warns (the collapse signature). Callers can also
21
+ pass `expected_frames` or `expected_duration_seconds` outright. Live-verified
22
+ both ways on 19.1.3.7: the healthy render verifies clean, the stub now fails
23
+ with both warnings.
24
+
25
+ ### Fixed
26
+
27
+ - `render.verify_output` no longer trusts the job's MarkIn/MarkOut as the
28
+ expected duration — the #164 stub previously verified clean at ratio 1.0.
29
+
30
+ ### Changed
31
+
32
+ - The api_truth recordFrame entry now documents the mark-range rewrite and
33
+ the item-readback discriminator; the Blackmagic-facing report regenerated.
34
+
5
35
  ## What's New in v2.104.0
6
36
 
7
37
  The read/write symmetry audit's worklist, worked. PR #162's AST rewrite left
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.104.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.104.1-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.104.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.104.1-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.104.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.104.1 版 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.104.0"
40
+ VERSION = "2.104.1"
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.104.0",
3
+ "version": "2.104.1",
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.104.0"
90
+ VERSION = "2.104.1"
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.104.0"
14
+ VERSION = "2.104.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -18580,11 +18580,16 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
18580
18580
  delete_all_jobs() -> {success}
18581
18581
  list_jobs() -> {jobs}
18582
18582
  get_job_status(job_id) -> {status}
18583
- verify_output(job_id) -> {status, output_path, output_exists, output_size_bytes, output_probe, expected_duration_seconds, duration_ratio, warnings, verified}
18583
+ verify_output(job_id, expected_frames?, expected_duration_seconds?) -> {status, output_path, output_exists, output_size_bytes, output_probe, mark_range_frames, timeline_item_extent_frames, expected_duration_seconds, duration_ratio, warnings, verified}
18584
18584
  JobStatus reports Complete even when the render produced a near-empty
18585
- stub (content before the timeline start, issue #164) this checks the
18586
- actual output file against the job's mark range. Verify BEFORE deleting
18587
- the job; deleted jobs carry no TargetDir to check.
18585
+ stub (content before the timeline start, issue #164), and Resolve
18586
+ rewrites the job's own MarkIn/MarkOut down to the collapsed extent, so
18587
+ the job metadata lies too. This checks the actual output file, and
18588
+ cross-checks the job's timeline items (which read back their real
18589
+ positions): items before the timeline start and a mark range far
18590
+ smaller than the item extent both warn. Pass expected_frames or
18591
+ expected_duration_seconds when you know what the render should hold.
18592
+ Verify BEFORE deleting the job; deleted jobs carry no TargetDir.
18588
18593
  start(job_ids?, interactive?) -> {success}
18589
18594
  stop() -> {success}
18590
18595
  is_rendering() -> {rendering}
@@ -18675,6 +18680,71 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
18675
18680
  else:
18676
18681
  warnings.append("Job carries no TargetDir/OutputFilename to verify against.")
18677
18682
  result["output_path"] = output_path
18683
+ # The job's own MarkIn/MarkOut cannot be the only truth source: when
18684
+ # timeline content sits before the timeline's start frame (issue #164)
18685
+ # Resolve rewrites the job's mark range down to the collapsed extent,
18686
+ # so the job metadata lies right along with JobStatus (measured live on
18687
+ # Studio 19.1.3.7: 96 frames of content before start -> job range of 1
18688
+ # frame, Complete at 100%, a 1-frame black stub). Cross-check the
18689
+ # job's timeline items, which DO read back their real positions, and
18690
+ # let the caller override expected duration outright.
18691
+ try:
18692
+ fps = float(str(job.get("FrameRate")))
18693
+ except (TypeError, ValueError):
18694
+ fps = None
18695
+ try:
18696
+ mark_frames = int(job.get("MarkOut")) - int(job.get("MarkIn")) + 1
18697
+ except (TypeError, ValueError):
18698
+ mark_frames = None
18699
+ result["mark_range_frames"] = mark_frames
18700
+ tl_for_job = None
18701
+ try:
18702
+ for i in range(1, int(proj.GetTimelineCount() or 0) + 1):
18703
+ cand = proj.GetTimelineByIndex(i)
18704
+ if cand and cand.GetName() == job.get("TimelineName"):
18705
+ tl_for_job = cand
18706
+ break
18707
+ except Exception:
18708
+ tl_for_job = None
18709
+ if tl_for_job is not None:
18710
+ try:
18711
+ tl_start = int(tl_for_job.GetStartFrame())
18712
+ extent_lo, extent_hi, before_start = None, None, 0
18713
+ for ti in range(1, int(tl_for_job.GetTrackCount("video") or 0) + 1):
18714
+ for it in tl_for_job.GetItemListInTrack("video", ti) or []:
18715
+ it_start, it_end = int(it.GetStart()), int(it.GetEnd())
18716
+ extent_lo = it_start if extent_lo is None else min(extent_lo, it_start)
18717
+ extent_hi = it_end if extent_hi is None else max(extent_hi, it_end)
18718
+ if it_start < tl_start:
18719
+ before_start += 1
18720
+ if before_start:
18721
+ warnings.append(
18722
+ f"{before_start} video item(s) on timeline "
18723
+ f"{job.get('TimelineName')!r} start before the timeline's "
18724
+ f"start frame {tl_start} — the render engine never visits "
18725
+ "that content (recordFrame counts from absolute frame "
18726
+ "zero, issue #164). The items read back as placed; the "
18727
+ "render is a stub."
18728
+ )
18729
+ if (extent_lo is not None and mark_frames is not None
18730
+ and mark_frames < (extent_hi - extent_lo) * 0.5):
18731
+ warnings.append(
18732
+ f"The job's mark range ({mark_frames} frame(s)) is under "
18733
+ f"half the timeline's item extent "
18734
+ f"({extent_hi - extent_lo} frames) — Resolve rewrote the "
18735
+ "range down to a collapsed extent, which is how a stub "
18736
+ "render reports a clean duration ratio."
18737
+ )
18738
+ result["timeline_item_extent_frames"] = (
18739
+ None if extent_lo is None else extent_hi - extent_lo
18740
+ )
18741
+ except Exception:
18742
+ pass
18743
+ else:
18744
+ warnings.append(
18745
+ f"Timeline {job.get('TimelineName')!r} was not found in the "
18746
+ "project, so item-extent cross-checks were skipped."
18747
+ )
18678
18748
  if output_path:
18679
18749
  exists = os.path.exists(output_path)
18680
18750
  result["output_exists"] = exists
@@ -18684,13 +18754,21 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
18684
18754
  if probe:
18685
18755
  result["output_probe"] = probe
18686
18756
  duration = probe.get("duration_seconds")
18687
- # Expected duration from the job's own mark range and rate.
18688
- try:
18689
- frames = int(job.get("MarkOut")) - int(job.get("MarkIn")) + 1
18690
- fps = float(str(job.get("FrameRate")))
18691
- expected = frames / fps if fps > 0 else None
18692
- except (TypeError, ValueError):
18693
- expected = None
18757
+ # Expected duration: caller-supplied truth wins; the job's
18758
+ # mark range is the fallback (see collapse caveat above).
18759
+ expected = None
18760
+ if p.get("expected_duration_seconds") is not None:
18761
+ try:
18762
+ expected = float(p["expected_duration_seconds"])
18763
+ except (TypeError, ValueError):
18764
+ expected = None
18765
+ elif p.get("expected_frames") is not None and fps:
18766
+ try:
18767
+ expected = int(p["expected_frames"]) / fps
18768
+ except (TypeError, ValueError):
18769
+ expected = None
18770
+ elif mark_frames is not None and fps:
18771
+ expected = mark_frames / fps
18694
18772
  result["expected_duration_seconds"] = expected
18695
18773
  if duration is not None and expected and expected > 0:
18696
18774
  result["duration_ratio"] = round(duration / expected, 4)
@@ -1875,10 +1875,20 @@ API_TRUTH: List[Dict[str, Any]] = [
1875
1875
  "(frame 0 == first frame), while recordFrame and "
1876
1876
  "TimelineItem.GetStart/GetEnd are absolute. Distinct from "
1877
1877
  "the null-id entry below, which is a recordFrame landing in "
1878
- "an OCCUPIED span rather than before the start. Verified on "
1878
+ "an OCCUPIED span rather than before the start. Worse, the "
1879
+ "render JOB's own metadata lies too: Resolve rewrites the "
1880
+ "job's MarkIn/MarkOut down to the collapsed extent (measured "
1881
+ "live on 19.1.3.7: 96 frames of content before start -> an "
1882
+ "explicit 96-frame mark range became a 1-frame job, Complete "
1883
+ "at 100%, a 1-frame black stub), so output-duration vs "
1884
+ "job-mark-range checks read clean. The only truthful "
1885
+ "readback is the timeline items themselves, which report "
1886
+ "their real GetStart/GetEnd — before the start frame. "
1887
+ "Verified on "
1879
1888
  "Studio 20.3.2.9 (v2.17.1 probe: relative record_frame 12 "
1880
- "landed at 86400 + 12 = 86412, absolute preserved 86484) and "
1881
- "independently on Studio 21.0.4.5 (issue #164).",
1889
+ "landed at 86400 + 12 = 86412, absolute preserved 86484), "
1890
+ "independently on Studio 21.0.4.5 (issue #164), and the "
1891
+ "mark-range rewrite on Studio 19.1.3.7 (2026-08-29).",
1882
1892
  "recommended": "Offset every recordFrame by timeline.GetStartFrame(). "
1883
1893
  "This server already does it: "
1884
1894
  "media_pool.append_to_timeline defaults to "
@@ -1888,8 +1898,10 @@ API_TRUTH: List[Dict[str, Any]] = [
1888
1898
  "value below the timeline start is refused outright. "
1889
1899
  "When driving the API directly, "
1890
1900
  "never treat JobStatus Complete as proof a render "
1891
- "worked — check the output file's duration, not just "
1892
- "that the job finished.",
1901
+ "worked — and do not trust the job's MarkIn/MarkOut as "
1902
+ "the expected duration either. render.verify_output "
1903
+ "(v2.104.1) cross-checks the job against its timeline's "
1904
+ "item extents and flags items before the start frame.",
1893
1905
  "tags": ["timeline", "edit", "render", "silent-failure", "media-pool"],
1894
1906
  "issue": 164,
1895
1907
  },