davinci-resolve-mcp 4.15.3 → 4.15.4

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 v4.15.4 — add_keyframe no longer leaves a stray key
6
+
7
+ ### Fixed
8
+
9
+ - **`fusion_comp(action="add_keyframe")` removes the key Fusion adds when an
10
+ input is first animated** (#317, thanks @assassin007-hub). Attaching an
11
+ empty BezierSpline keys the comp's current time with the input's old value,
12
+ so keying frames 10 and 20 left keys `[0, 10, 20]` and the curve ramped from
13
+ the old value instead of holding until the first intended key. The first
14
+ call on an input now deletes that key after the real one is written, reads
15
+ the keys back, and returns `removed_stray_keys`, or `stray_keys_remaining`
16
+ with a `warning` pointing to `delete_keyframe` if a key survives. A key
17
+ written at the current time simply overwrites it. Text Follower range
18
+ bounds (±1e9) are not treated as keys.
19
+
20
+ ### Documentation
21
+
22
+ - Two measured `api_truth` entries: BezierSpline LH/RH handles are offsets
23
+ from their key, not absolute points; and the stray key an empty spline
24
+ attach leaves, now attached to `fusion_comp add_keyframe`.
25
+
26
+ ### Validation
27
+
28
+ - CI passed on Python 3.10 and 3.11 and on the advanced Node suite under
29
+ Node 20 and 22. Full offline suite run against main: 4,230 tests, with only
30
+ the 4 `source_media_guard` failures that always occur in a scratch worktree.
31
+ - Live behavior was measured by the contributor on Studio 21.0.2.4 (keys
32
+ `[0, 10, 20]` before, `[10, 20]` after `DeleteKeyFrames`). It was not
33
+ re-run on 19.1.3.7 for this release.
34
+
5
35
  ## What's New in v4.15.3 — CI covers the version floors
6
36
 
7
37
  No server behavior changed. Pull requests are now tested on the oldest Python
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-4.15.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.15.4-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-37%20(390%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-4.15.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.15.4-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-37%20(390%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
- > 本翻译对应 v4.15.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.15.4 版 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 = "4.15.3"
40
+ VERSION = "4.15.4"
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": "4.15.3",
3
+ "version": "4.15.4",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
91
91
  handlers=[logging.StreamHandler()],
92
92
  )
93
93
 
94
- VERSION = "4.15.3"
94
+ VERSION = "4.15.4"
95
95
  logger = logging.getLogger("davinci-resolve-mcp")
96
96
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
97
97
  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 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "4.15.3"
14
+ VERSION = "4.15.4"
15
15
 
16
16
  import base64
17
17
  import os
@@ -32535,6 +32535,32 @@ def _fusion_input_spline(inp):
32535
32535
  return None
32536
32536
 
32537
32537
 
32538
+ def _fusion_remove_stray_keys(inp, stray: List[float]) -> Dict[str, Any]:
32539
+ """Delete the keys Fusion added when add_keyframe attached a spline.
32540
+
32541
+ The result is read back rather than trusted: a key that is still there is
32542
+ reported, so a caller is never told the curve holds only what it wrote.
32543
+ """
32544
+ spline = _fusion_input_spline(inp)
32545
+ if spline is not None and _has_method(spline, "DeleteKeyFrames"):
32546
+ for frame in stray:
32547
+ try:
32548
+ spline.DeleteKeyFrames(frame)
32549
+ except Exception:
32550
+ pass
32551
+ remaining = [f for f in _fusion_keyframe_frames(inp)
32552
+ if any(abs(f - s) < 1e-6 for s in stray)]
32553
+ removed = [s for s in stray if not any(abs(s - f) < 1e-6 for f in remaining)]
32554
+ if not remaining:
32555
+ return _ok(removed_stray_keys=removed)
32556
+ return _ok(
32557
+ removed_stray_keys=removed,
32558
+ stray_keys_remaining=remaining,
32559
+ warning=(f"Attaching the spline also keyed frame(s) {remaining} with the old value "
32560
+ "and they could not be removed; remove them with delete_keyframe."),
32561
+ )
32562
+
32563
+
32538
32564
  def _fusion_delete_keyframe(tool, p: Dict[str, Any]) -> Dict[str, Any]:
32539
32565
  """Remove one keyframe from an animated Fusion input. (issue #155)
32540
32566
 
@@ -32953,6 +32979,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
32953
32979
  _already_animated = inp.GetConnectedOutput() is not None
32954
32980
  except Exception:
32955
32981
  _already_animated = False
32982
+ _stray = []
32956
32983
  if not _already_animated:
32957
32984
  _is_nest, _members = _fusion_nest_members(tool, p["input_name"])
32958
32985
  if _is_nest:
@@ -32986,8 +33013,21 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
32986
33013
  "attach it with add_modifier, then drive the "
32987
33014
  "modifier tool it returns.",
32988
33015
  )
33016
+ # Attaching an empty spline keys the comp's CURRENT time with the
33017
+ # input's old value (measured on Studio 21.0.2.4: keying 10 and 20
33018
+ # left [0, 10, 20]). Note it now; it is removed after the real key
33019
+ # lands, since DeleteKeyFrames will not remove a spline's only key.
33020
+ # Only a spline holds keys; a text Follower answers GetKeyFrames
33021
+ # with its +/-1e9 render-range bounds, which are not keys.
33022
+ _spline = _fusion_input_spline(tool[p["input_name"]])
33023
+ if _spline is not None and _has_method(_spline, "DeleteKeyFrames"):
33024
+ _stray = [f for f in _fusion_keyframe_frames(tool[p["input_name"]])
33025
+ if abs(f) < 1e8]
32989
33026
  tool[p["input_name"]][p["time"]] = p["value"]
32990
- return _ok()
33027
+ _stray = [f for f in _stray if abs(f - float(p["time"])) >= 1e-6]
33028
+ if not _stray:
33029
+ return _ok()
33030
+ return _fusion_remove_stray_keys(tool[p["input_name"]], _stray)
32991
33031
  finally:
32992
33032
  comp.EndUndo(True)
32993
33033
 
@@ -3915,6 +3915,64 @@ API_TRUTH: List[Dict[str, Any]] = [
3915
3915
  "verified_on": "DaVinci Resolve Studio 19.1.3.7",
3916
3916
  "mitigation": ["_timeline_replace_source_range_impl"],
3917
3917
  },
3918
+ {
3919
+ "symbol": "BezierSpline.SetKeyFrames (handle coordinates)",
3920
+ "object": "Fusion BezierSpline",
3921
+ "signature": "(keyframes, replace) -> None",
3922
+ "reality": "A key's LH/RH handles are OFFSETS from that key, {1: dTime, 2: dValue}, "
3923
+ "not absolute (time, value) points. GetKeyFrames() reads them back the "
3924
+ "same way: Fusion's own auto-created first key returns RH {1: 2.0, "
3925
+ "2: -0.333} for a key at frame 0 easing toward the next one. Absolute "
3926
+ "coordinates are accepted without complaint and read back verbatim, so "
3927
+ "nothing flags the mistake; the curve is simply wrong. Measured with keys "
3928
+ "0 -> 1 at frames 10 -> 20: flat handles written relative ({3, 0} / "
3929
+ "{-3, 0}) sample 0, 0.113, 0.5, 0.887, 1 at frames 10/12/15/18/20, a clean "
3930
+ "ease-in-out; the same intent written absolute ({13, 0} / {17, 1}) "
3931
+ "samples 0, 0.015, 0.089, 0.217, 1. A whip authored with absolute handles "
3932
+ "moved the wrong way (0.5 -> 0.56) before snapping to its end value.",
3933
+ "recommended": "Write handles relative to their key: RH = {segment * k, 0} and "
3934
+ "LH = {-segment * k, 0} gives a flat-tangent ease, where k is the handle "
3935
+ "length as a fraction of the segment (0.33 is a standard ease, 0.6 to "
3936
+ "0.75 an expo-like snap). Prove a curve by sampling "
3937
+ "tool.GetInput(name, frame) at a few frames, not by reading the keys "
3938
+ "back.",
3939
+ "tags": ["fusion", "keyframe", "silent-failure", "units"],
3940
+ "verified_on": "DaVinci Resolve Studio 21.0.2.4",
3941
+ "measured": "2026-10-26 on a Fusion composition clip: BezierSpline keyed 10 -> 20 "
3942
+ "with relative vs absolute flat handles, connected to Blur YBlurSize, "
3943
+ "sampled with GetInput at frames 10/12/15/18/20; whip Transform Center "
3944
+ "via XYPath confirmed in rendered frames.",
3945
+ },
3946
+ {
3947
+ "symbol": "BezierSpline attach (stray key at the current time)",
3948
+ "object": "Fusion Tool input / BezierSpline",
3949
+ "signature": "tool.AddModifier(input, 'BezierSpline') | tool[input] = comp.BezierSpline()",
3950
+ "reality": "Connecting an EMPTY spline to an input, by either route, adds a key at "
3951
+ "the comp's current time holding the input's old value. Keys set "
3952
+ "afterwards are added next to it, and SetKeyFrames(keys, True) does not "
3953
+ "remove it despite the replace flag. Measured with CurrentTime 0: "
3954
+ "AddModifier then input[10] = 5, input[20] = 9 left keys [0, 10, 20] with "
3955
+ "1.0 (the old value) at frame 0, so the value drifts from 1.0 before the "
3956
+ "first intended key instead of holding. The stray key can be deleted with "
3957
+ "spline.DeleteKeyFrames(time) once other keys exist; deleting it while it "
3958
+ "is the only key does nothing. A spline that already has keys when it is "
3959
+ "connected gets no extra key.",
3960
+ "recommended": "Build the spline first and connect it last: s = comp.BezierSpline(); "
3961
+ "s.SetKeyFrames(keys, True); tool[input] = s. When the spline is "
3962
+ "already attached, delete the stray key (DeleteKeyFrames(time)) after "
3963
+ "the intended keys are written, then confirm with GetKeyFrames(). "
3964
+ "fusion_comp add_keyframe does this itself: its first call on an "
3965
+ "input deletes the stray key, reports it as removed_stray_keys, and "
3966
+ "returns stray_keys_remaining plus a warning if it survives.",
3967
+ "tags": ["fusion", "keyframe", "silent-failure"],
3968
+ "mitigation": ["fusion_comp.add_keyframe", "_fusion_remove_stray_keys"],
3969
+ "verified_on": "DaVinci Resolve Studio 21.0.2.4",
3970
+ "measured": "2026-10-26 on a Fusion composition clip (CurrentTime 0), on a Blur: "
3971
+ "AddModifier + two indexed writes -> keys [0, 10, 20]; empty spline "
3972
+ "connected then SetKeyFrames(replace=True) -> [0, 10, 20]; "
3973
+ "DeleteKeyFrames(0.0) then -> [10, 20]; spline keyed before connect -> "
3974
+ "[10, 20].",
3975
+ },
3918
3976
 
3919
3977
  ]
3920
3978
 
@@ -3976,7 +4034,10 @@ ACTION_SYMBOLS: Dict[Tuple[str, str], List[str]] = {
3976
4034
  ("timeline", "duplicate"): ["Timeline.DuplicateTimeline"],
3977
4035
  ("project_manager", "archive"): ["ProjectManager.ArchiveProject"],
3978
4036
  ("fusion_comp", "add_modifier"): ["Tool.AddModifier", "Tool.AddModifier (NestControl inputs)"],
3979
- ("fusion_comp", "add_keyframe"): ["Tool.AddModifier (NestControl inputs)"],
4037
+ ("fusion_comp", "add_keyframe"): [
4038
+ "Tool.AddModifier (NestControl inputs)",
4039
+ "BezierSpline attach (stray key at the current time)",
4040
+ ],
3980
4041
  ("project_manager", "safe_project_archive"): ["ProjectManager.ArchiveProject"],
3981
4042
  ("render", "start"): ["Project.StartRendering"],
3982
4043
  ("resolve_control", "set_high_priority"): [