davinci-resolve-mcp 2.104.3 → 2.104.5

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,76 @@
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.5
6
+
7
+ The recent bug classes, generalized into guards — and the sweeps found the
8
+ kwarg bug a second time.
9
+
10
+ **PR #165's bug existed twice.** The positional-only bridge rule was guarded
11
+ for src/server.py alone; sweeping ALL of src/ found
12
+ `StartRendering(isInteractiveMode=...)` again in the render-deliver probe
13
+ catalogue. Fixed, and the guard is rebuilt properly: it parses the Resolve
14
+ method names out of the shipped API reference and flags keyword arguments on
15
+ exactly those calls across the whole tree — which is what separates
16
+ StartRendering from Popen without drowning in stdlib false positives.
17
+
18
+ **Closing a project mid-render is now unreachable through this server.** The
19
+ wedge documented in v2.104.0 (orphaned render, stuck IsRenderingInProgress,
20
+ 0% jobs, refused Quit) could still be triggered via project_manager.close or
21
+ a disposable-project delete. `close` now refuses while a render is in
22
+ progress — with the wedge named in the remediation — and accepts
23
+ stop_render=true to stop, wait for the flag to clear, and close.
24
+ delete_project_safely auto-stops first (deleting kills the render anyway;
25
+ stopping is strictly better) and refuses when the flag will not clear, which
26
+ is the already-wedged state where no delete ends well. Live-verified both
27
+ paths on Studio 19.1.3.7: mid-render close refused, stop_render=true stopped
28
+ and closed cleanly, no wedge.
29
+
30
+ **Audits that came back clean, on the record:** the remaining default-ON
31
+ analysis gates (marker plan is built unconditionally, vision is default-OFF
32
+ behind a capability gate) cannot reproduce the cache-poisoning shape, and the
33
+ Python tree carries no numeric-keyed hex tables of the kind that rotted in
34
+ the Node encoders.
35
+
36
+ PR #166's discarded-return guard fired on this release's own
37
+ StopRendering call — third catch in three releases; the allowlist entry
38
+ records that the helper verifies by polling the flag, stronger than the None
39
+ the API returns.
40
+
41
+ ## What's New in v2.104.4
42
+
43
+ Hardening pass over the classes the v2.104.2 batch exposed, live-verified on
44
+ Studio 19.1.3.7.
45
+
46
+ **set_title_text works on builds where SetProperty cannot.** On Studio 19.1.3
47
+ a Text+ item rejects every title property key, so set_title_text failed while
48
+ the item's Fusion comp accepted the same text all along. The setter now falls
49
+ back to writing StyledText on the TextPlus tool — deliberately UNLOCKED, per
50
+ the comp-lock render bug — and reports success only after reading the input
51
+ back. Live-verified end to end: set via fallback, read via get_title_text,
52
+ and a rendered frame confirms the text reaches the output (mean luma above
53
+ black). bulk_set_title_text inherits the fallback. PR #166's discarded-return
54
+ guard caught the fallback's bare SetInput during development — the allowlist
55
+ entry records that the write is verified by readback, which is stronger than
56
+ the bool Fusion doesn't return.
57
+
58
+ **verify_output no longer flags deliberate short renders.** A single-frame
59
+ capture tripped the mark-range-collapse warning, because the checker cannot
60
+ distinguish a caller-chosen short range from a Resolve-rewritten one. Passing
61
+ expected_frames / expected_duration_seconds matching the mark range now
62
+ suppresses the collapse warning; an unstated short range still warns.
63
+
64
+ **#171's scope measured: the internal-name override is FCP7-specific.** An
65
+ OTIO export re-imported under a new timelineName creates a new timeline
66
+ (measured 19.1.3.7), so the api_truth entry now says the override is an FCP7
67
+ XML behavior, not a general import rule.
68
+
69
+ **One more #167-class constant found and removed.** effect-encoder's exported
70
+ "common double values" hex table — consumed by nothing — carried a '0.9'
71
+ entry that decoded to 0.8. Deleted; the sweep found the remaining converters
72
+ (editorial, media-inventory, the Python timecode helpers) already round
73
+ correctly.
74
+
5
75
  ## What's New in v2.104.3
6
76
 
7
77
  Documentation follow-through on the v2.104.2 batch.
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.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.104.5-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.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.104.5-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.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.104.5 版 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.3"
40
+ VERSION = "2.104.5"
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.3",
3
+ "version": "2.104.5",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -180,16 +180,10 @@ function encodeFloat(value) {
180
180
  return buffer.toString('hex');
181
181
  }
182
182
 
183
- /**
184
- * Common double values in hex for quick reference
185
- */
186
- const DOUBLE_VALUES = {
187
- '1.0': '000000000000f03f',
188
- '1.1': '9a9999999999f13f',
189
- '0.9': '9a9999999999e93f',
190
- '2.0': '0000000000000040',
191
- '0.5': '000000000000e03f',
192
- };
183
+ // A "common double values in hex" reference table used to sit here. It was
184
+ // consumed by nothing, and its '0.9' entry actually decoded to 0.8 — the same
185
+ // hand-typed-hex rot that put 30000/1001 in drt.author's 23.976 slot
186
+ // (issue #167). Encode with encodeDouble()/writeDoubleLE; never copy hex.
193
187
 
194
188
  /**
195
189
  * Build a protobuf field with double value
@@ -636,5 +630,4 @@ module.exports = {
636
630
  // Constants
637
631
  EFFECT_PARAMS,
638
632
  WIRE_TYPES,
639
- DOUBLE_VALUES,
640
633
  };
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.104.3"
90
+ VERSION = "2.104.5"
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.3"
14
+ VERSION = "2.104.5"
15
15
 
16
16
  import base64
17
17
  import os
@@ -5745,9 +5745,41 @@ def _timeline_set_title_text(tl, p: Dict[str, Any]) -> Dict[str, Any]:
5745
5745
  "attempts": attempts,
5746
5746
  }
5747
5747
 
5748
+ # SetProperty exposes no title keys on every build (Text+ on Studio 19.1.3
5749
+ # rejects all of them), while the item's Fusion comp carries the same text
5750
+ # as the TextPlus tool's StyledText input and accepts writes there — the
5751
+ # route get_title_text already reads. Deliberately a bare, UNLOCKED
5752
+ # SetInput: the comp-lock render bug (api_truth / v2.98.5) eats writes
5753
+ # wrapped in Comp.Lock(), and unlocked writes are the safe ones.
5754
+ if not bool(p.get("as_styled_xml", p.get("styled", False))):
5755
+ try:
5756
+ if int(item.GetFusionCompCount() or 0) > 0:
5757
+ comp = item.GetFusionCompByIndex(1)
5758
+ tools = comp.GetToolList(False, "TextPlus") if comp else None
5759
+ for tool_key in (tools or {}):
5760
+ tool = tools[tool_key]
5761
+ tool.SetInput("StyledText", text)
5762
+ confirmed = tool.GetInput("StyledText")
5763
+ rec = {"mode": "fusion_comp", "property_key": "StyledText",
5764
+ "success": confirmed == text}
5765
+ if confirmed != text:
5766
+ rec["readback"] = _ser(confirmed)
5767
+ attempts.append(rec)
5768
+ if confirmed == text:
5769
+ return {
5770
+ "success": True,
5771
+ "timeline_item_id": _safe_timeline_item_id(item),
5772
+ "property_key": "StyledText",
5773
+ "mode": "fusion_comp",
5774
+ "attempts": attempts,
5775
+ }
5776
+ except Exception as exc:
5777
+ attempts.append({"mode": "fusion_comp", "success": False, "error": str(exc)})
5778
+
5748
5779
  return {
5749
5780
  "success": False,
5750
- "error": "SetProperty did not succeed; run title_property_scan, copy a real key from `properties`, "
5781
+ "error": "SetProperty did not succeed and the Fusion-comp StyledText fallback found no "
5782
+ "TextPlus tool to write; run title_property_scan, copy a real key from `properties`, "
5751
5783
  "and pass `property_key` (see `attempts` for diagnostics).",
5752
5784
  "attempts": attempts,
5753
5785
  }
@@ -17598,7 +17630,11 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17598
17630
  create(name, media_location_path?) -> {success, name}
17599
17631
  load(name) -> {success}
17600
17632
  save() -> {success}
17601
- close() -> {success}
17633
+ close(stop_render?) -> {success}
17634
+ Refuses while a render is in progress — closing mid-render orphans the
17635
+ render and wedges Resolve's pipeline until restart (stuck
17636
+ IsRenderingInProgress, 0% jobs, refused Quit). stop_render=true stops
17637
+ the render, waits for the flag to clear, then closes.
17602
17638
  delete(name) -> {success}
17603
17639
  import_project(path, name?) -> {success}
17604
17640
  export_project(name, path, with_stills_and_luts?) -> {success}
@@ -17702,7 +17738,33 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17702
17738
  return {"success": bool(pm.SaveProject())}
17703
17739
  elif action == "close":
17704
17740
  proj = pm.GetCurrentProject()
17705
- return {"success": bool(pm.CloseProject(proj))} if proj else _err("No project open")
17741
+ if not proj:
17742
+ return _err("No project open")
17743
+ # Closing mid-render orphans the render and wedges Resolve's pipeline
17744
+ # (api_truth IsRenderingInProgress entry, measured live). Refuse by
17745
+ # default; stop_render=true stops the render and waits before closing.
17746
+ try:
17747
+ rendering = bool(proj.IsRenderingInProgress())
17748
+ except Exception:
17749
+ rendering = False
17750
+ if rendering:
17751
+ if not p.get("stop_render"):
17752
+ return _err(
17753
+ "A render is in progress on this project. Closing now would "
17754
+ "orphan the render and wedge Resolve's render pipeline — the "
17755
+ "stuck IsRenderingInProgress flag blocks every later render "
17756
+ "and refuses Quit until Resolve is restarted.",
17757
+ category="invalid_input",
17758
+ remediation=(
17759
+ "Wait for the render (poll render.get_job_status), or pass "
17760
+ "stop_render=true to stop it and close once the flag clears."
17761
+ ),
17762
+ )
17763
+ from src.utils.project_cleanup import stop_render_before_close
17764
+ render_state = stop_render_before_close(proj)
17765
+ if not render_state.get("safe"):
17766
+ return _err(render_state["detail"], category="api_error")
17767
+ return {"success": bool(pm.CloseProject(proj))}
17706
17768
  elif action == "delete":
17707
17769
  if not p.get("name"):
17708
17770
  return _err("delete requires name")
@@ -18833,7 +18895,9 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
18833
18895
  cross-checks the job's timeline items (which read back their real
18834
18896
  positions): items before the timeline start and a mark range far
18835
18897
  smaller than the item extent both warn. Pass expected_frames or
18836
- expected_duration_seconds when you know what the render should hold.
18898
+ expected_duration_seconds when you know what the render should hold
18899
+ a DELIBERATE short render (single-frame capture, excerpt) reads as a
18900
+ collapse warning unless the stated expectation matches the mark range.
18837
18901
  Verify BEFORE deleting the job; deleted jobs carry no TargetDir.
18838
18902
  start(job_ids?, interactive?) -> {success}
18839
18903
  stop() -> {success}
@@ -18971,7 +19035,28 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
18971
19035
  "zero, issue #164). The items read back as placed; the "
18972
19036
  "render is a stub."
18973
19037
  )
19038
+ caller_expected_frames = None
19039
+ if p.get("expected_frames") is not None:
19040
+ try:
19041
+ caller_expected_frames = int(p["expected_frames"])
19042
+ except (TypeError, ValueError):
19043
+ caller_expected_frames = None
19044
+ elif p.get("expected_duration_seconds") is not None and fps:
19045
+ try:
19046
+ caller_expected_frames = int(round(float(p["expected_duration_seconds"]) * fps))
19047
+ except (TypeError, ValueError):
19048
+ caller_expected_frames = None
19049
+ # A caller who states the render SHOULD be this short (a
19050
+ # deliberate single-frame or excerpt render) is not a collapse
19051
+ # victim — the warning is for ranges Resolve rewrote, which the
19052
+ # caller by definition did not expect.
19053
+ intentional_short_range = (
19054
+ caller_expected_frames is not None
19055
+ and mark_frames is not None
19056
+ and abs(caller_expected_frames - mark_frames) <= 1
19057
+ )
18974
19058
  if (extent_lo is not None and mark_frames is not None
19059
+ and not intentional_short_range
18975
19060
  and mark_frames < (extent_hi - extent_lo) * 0.5):
18976
19061
  warnings.append(
18977
19062
  f"The job's mark range ({mark_frames} frame(s)) is under "
@@ -504,7 +504,9 @@ def _probe_render_to_disk(ctx):
504
504
  job = ctx.project.AddRenderJob()
505
505
  if not job:
506
506
  return False
507
- ctx.project.StartRendering(job, isInteractiveMode=False)
507
+ # Positional: the free-edition bridge proxies Resolve calls positionally,
508
+ # and a keyword argument dies inside _BoundMethod (PR #165's bug class).
509
+ ctx.project.StartRendering(job, False)
508
510
  deadline = _t.monotonic() + 420
509
511
  while ctx.project.IsRenderingInProgress() and _t.monotonic() < deadline:
510
512
  _t.sleep(1)
@@ -64,6 +64,52 @@ def save_project_if_safe(pm: Any) -> Dict[str, Any]:
64
64
  return {"saved": False, "skipped": False, "project": name, "reason": repr(exc)}
65
65
 
66
66
 
67
+ def stop_render_before_close(project: Any, *, wait_seconds: float = 8.0) -> Dict[str, Any]:
68
+ """Stop any in-progress render and wait for the flag to clear.
69
+
70
+ Closing or deleting a project while its render runs orphans the render and
71
+ wedges Resolve's whole pipeline: IsRenderingInProgress sticks True on every
72
+ subsequent project, new jobs sit at 0%, StartRendering starts returning
73
+ False, and Quit() is refused behind a modal (api_truth, measured live on
74
+ Studio 19.1.3.7). Returns {safe, was_rendering, waited_seconds, detail}.
75
+ A flag that stays True after StopRendering at idle is the stuck state —
76
+ only restarting Resolve clears it, so `safe` comes back False.
77
+ """
78
+ import time as _time
79
+
80
+ try:
81
+ rendering = bool(project.IsRenderingInProgress())
82
+ except Exception:
83
+ return {"safe": True, "was_rendering": False, "waited_seconds": 0.0,
84
+ "detail": "IsRenderingInProgress unavailable; proceeding"}
85
+ if not rendering:
86
+ return {"safe": True, "was_rendering": False, "waited_seconds": 0.0, "detail": ""}
87
+ try:
88
+ project.StopRendering()
89
+ except Exception:
90
+ pass
91
+ waited = 0.0
92
+ while waited < wait_seconds:
93
+ _time.sleep(0.5)
94
+ waited += 0.5
95
+ try:
96
+ if not project.IsRenderingInProgress():
97
+ return {"safe": True, "was_rendering": True,
98
+ "waited_seconds": waited, "detail": "render stopped"}
99
+ except Exception:
100
+ break
101
+ return {
102
+ "safe": False, "was_rendering": True, "waited_seconds": waited,
103
+ "detail": (
104
+ "IsRenderingInProgress is still True after StopRendering and "
105
+ f"{waited:.0f}s — closing now would orphan the render and wedge "
106
+ "Resolve's render pipeline (stuck flag, 0% jobs, refused Quit). "
107
+ "If Resolve is idle at 0% CPU this is the already-stuck state and "
108
+ "only restarting Resolve clears it."
109
+ ),
110
+ }
111
+
112
+
67
113
  def delete_project_safely(
68
114
  pm: Any,
69
115
  name: str,
@@ -89,6 +135,20 @@ def delete_project_safely(
89
135
  except Exception:
90
136
  current = None
91
137
  if current == name:
138
+ # Deleting the current project kills its render anyway — stopping
139
+ # first is strictly better than orphaning it (the wedge documented
140
+ # in stop_render_before_close). Refuse only when the flag will not
141
+ # clear, which is the already-wedged state where the delete cannot
142
+ # end well either.
143
+ try:
144
+ project = pm.GetCurrentProject()
145
+ except Exception:
146
+ project = None
147
+ if project is not None:
148
+ render_state = stop_render_before_close(project)
149
+ if not render_state.get("safe"):
150
+ return {"success": False, "attempts": 0, "leftover": name,
151
+ "detail": render_state["detail"]}
92
152
  # CloseProject ALWAYS, and first. It is what releases the session's
93
153
  # lock on the project; switching away with LoadProject does not, and
94
154
  # DeleteProject then returns False permanently — retries never help.