davinci-resolve-mcp 2.98.2 → 2.98.3
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 +59 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +120 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
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.98.3
|
|
6
|
+
|
|
7
|
+
**`fusion_comp` could never delete a Fusion keyframe.** Reported in
|
|
8
|
+
[#155](https://github.com/samuelgursky/davinci-resolve-mcp/issues/155) by
|
|
9
|
+
@Andrei-59, with the root cause already identified: the handler called a method
|
|
10
|
+
that does not exist. The diagnosis was correct, and the suggested replacement is
|
|
11
|
+
confirmed here against a live build.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **`delete_keyframe` called `RemoveKeyFrame()` on a Fusion Input.** No such
|
|
16
|
+
method exists there. Keyframes do not live on the Input — they live on the
|
|
17
|
+
spline modifier connected to it, which is what `add_keyframe` attaches via
|
|
18
|
+
`AddModifier(input_name, "BezierSpline")`. The action now reaches that spline
|
|
19
|
+
through `inp.GetConnectedOutput().GetTool()` and calls `DeleteKeyFrames(time)`
|
|
20
|
+
on it. Introduced with the tool in v2.1.0 and broken for every input, every
|
|
21
|
+
frame, and every tool since; there is no version of the server in which it
|
|
22
|
+
worked.
|
|
23
|
+
|
|
24
|
+
- **The failure surfaced as `'NoneType' object is not callable`.** The
|
|
25
|
+
fusionscript bridge resolves an unknown attribute to `None` instead of raising
|
|
26
|
+
`AttributeError`, so the bad lookup succeeded silently and only died at the
|
|
27
|
+
callsite — an error naming neither the method nor the object. Every branch of
|
|
28
|
+
the action now returns the normal error envelope: `FUSION_INPUT_NOT_ANIMATED`
|
|
29
|
+
when the input has no modifier, `FUSION_KEYFRAME_NOT_FOUND` when nothing is
|
|
30
|
+
keyed at that frame (the frame list is included in `state`),
|
|
31
|
+
`FUSION_DELETE_KEYFRAMES_UNSUPPORTED` when the modifier has no removal method,
|
|
32
|
+
and `INVALID_FRAME` for a non-numeric `time`. The existing `_has_method` guard
|
|
33
|
+
— which exists precisely for this silent-`None` class — is applied before the
|
|
34
|
+
call rather than after it.
|
|
35
|
+
|
|
36
|
+
- **Success is verified by readback, not by the return value.** Live testing
|
|
37
|
+
showed `DeleteKeyFrames()` returns `None` whether or not it removed anything,
|
|
38
|
+
so trusting the return would have reported failure on every successful
|
|
39
|
+
delete — and trusting the absence of an exception would have reported success
|
|
40
|
+
on every silent no-op. The handler re-reads the keyframe list and returns
|
|
41
|
+
`FUSION_DELETE_KEYFRAME_NOOP` if the frame is still there. On success it
|
|
42
|
+
returns `{success, time, remaining_keyframes}`.
|
|
43
|
+
|
|
44
|
+
### Testing
|
|
45
|
+
|
|
46
|
+
- `tests/test_fusion_comp_targeting.py` gains eleven `delete_keyframe` cases,
|
|
47
|
+
including a regression test that models the bridge's silent-`None` attribute
|
|
48
|
+
lookup and asserts the handler never reaches for `RemoveKeyFrame` on the
|
|
49
|
+
Input. The action previously had no test coverage at all.
|
|
50
|
+
- `tests/live_fusion_delete_keyframe_validation.py` is a new self-contained live
|
|
51
|
+
harness: it creates a scratch project, inserts a Fusion composition clip (no
|
|
52
|
+
media needed), and reports which keyframe methods the Input and the spline
|
|
53
|
+
actually expose before asserting the delete.
|
|
54
|
+
|
|
55
|
+
### Verified live
|
|
56
|
+
|
|
57
|
+
Validated on **DaVinci Resolve Studio 19.1.3.7** (macOS). `RemoveKeyFrame`
|
|
58
|
+
confirmed absent on the Input and resolving to `None`; `DeleteKeyFrames`
|
|
59
|
+
confirmed present on the `BezierSpline` and confirmed to remove the key. The
|
|
60
|
+
reporter's exact reproduction — `add_keyframe` then `delete_keyframe` on the
|
|
61
|
+
same tool/input/frame — was run end-to-end through the patched handler and
|
|
62
|
+
succeeds. Not re-verified on Studio 21.0.4.5, the reporter's build.
|
|
63
|
+
|
|
5
64
|
## What's New in v2.98.2
|
|
6
65
|
|
|
7
66
|
**A tool installed next to the server was invisible to it.** Reported in
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.98.
|
|
15
|
+
> 本翻译对应 v2.98.3 版 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.98.
|
|
40
|
+
VERSION = "2.98.3"
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.98.
|
|
90
|
+
VERSION = "2.98.3"
|
|
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.98.
|
|
14
|
+
VERSION = "2.98.3"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -26582,6 +26582,121 @@ def _fusion_set_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
26582
26582
|
comp.Unlock()
|
|
26583
26583
|
|
|
26584
26584
|
|
|
26585
|
+
def _fusion_keyframe_frames(inp) -> List[float]:
|
|
26586
|
+
"""Frame positions currently keyed on `inp`, as a sorted list.
|
|
26587
|
+
|
|
26588
|
+
Fusion's `GetKeyFrames()` returns {1-based index: frame_position}; the
|
|
26589
|
+
frames are the VALUES, not the keys. Frames come back as floats.
|
|
26590
|
+
"""
|
|
26591
|
+
try:
|
|
26592
|
+
kfs = inp.GetKeyFrames()
|
|
26593
|
+
except Exception:
|
|
26594
|
+
return []
|
|
26595
|
+
if not kfs:
|
|
26596
|
+
return []
|
|
26597
|
+
return sorted(float(frame) for frame in kfs.values())
|
|
26598
|
+
|
|
26599
|
+
|
|
26600
|
+
def _fusion_input_spline(inp):
|
|
26601
|
+
"""The modifier/spline tool driving `inp`, or None when it is not animated.
|
|
26602
|
+
|
|
26603
|
+
Keyframes do not live on the Input object -- they live on the spline
|
|
26604
|
+
connected to it, which is what `add_keyframe` attaches via AddModifier.
|
|
26605
|
+
"""
|
|
26606
|
+
try:
|
|
26607
|
+
connected = inp.GetConnectedOutput()
|
|
26608
|
+
except Exception:
|
|
26609
|
+
return None
|
|
26610
|
+
if connected is None:
|
|
26611
|
+
return None
|
|
26612
|
+
if not _has_method(connected, "GetTool"):
|
|
26613
|
+
return None
|
|
26614
|
+
try:
|
|
26615
|
+
return connected.GetTool()
|
|
26616
|
+
except Exception:
|
|
26617
|
+
return None
|
|
26618
|
+
|
|
26619
|
+
|
|
26620
|
+
def _fusion_delete_keyframe(tool, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
26621
|
+
"""Remove one keyframe from an animated Fusion input. (issue #155)
|
|
26622
|
+
|
|
26623
|
+
The original implementation called `inp.RemoveKeyFrame(time)`. No such
|
|
26624
|
+
method exists on a Fusion Input, and the fusionscript bridge resolves an
|
|
26625
|
+
unknown attribute to None rather than raising AttributeError -- so the
|
|
26626
|
+
lookup succeeded silently and every call died at the callsite as an opaque
|
|
26627
|
+
`'NoneType' object is not callable`. The action had never worked.
|
|
26628
|
+
|
|
26629
|
+
Deletion happens on the spline, reached the same way `add_keyframe`
|
|
26630
|
+
created it, and every step that can be absent is checked before it is
|
|
26631
|
+
called. The result is verified by reading the keyframe list back, because
|
|
26632
|
+
a Fusion call returning without error is not proof it did anything.
|
|
26633
|
+
"""
|
|
26634
|
+
tool_name = p["tool_name"]
|
|
26635
|
+
input_name = p["input_name"]
|
|
26636
|
+
inp = tool[input_name]
|
|
26637
|
+
if not inp:
|
|
26638
|
+
return _err(
|
|
26639
|
+
f"Input '{input_name}' not found on tool '{tool_name}'",
|
|
26640
|
+
code="FUSION_INPUT_NOT_FOUND", category="invalid_input",
|
|
26641
|
+
)
|
|
26642
|
+
|
|
26643
|
+
try:
|
|
26644
|
+
time = float(p["time"])
|
|
26645
|
+
except (TypeError, ValueError):
|
|
26646
|
+
return _err(
|
|
26647
|
+
f"time must be a frame number, got {p['time']!r}",
|
|
26648
|
+
code="INVALID_FRAME", category="invalid_input",
|
|
26649
|
+
)
|
|
26650
|
+
|
|
26651
|
+
spline = _fusion_input_spline(inp)
|
|
26652
|
+
if spline is None:
|
|
26653
|
+
return _err(
|
|
26654
|
+
f"Input '{input_name}' on tool '{tool_name}' is not animated, so it "
|
|
26655
|
+
"has no keyframe to delete",
|
|
26656
|
+
code="FUSION_INPUT_NOT_ANIMATED", category="precondition",
|
|
26657
|
+
remediation="Use add_keyframe first; it attaches the spline that holds keyframes.",
|
|
26658
|
+
state={"tool_name": tool_name, "input_name": input_name},
|
|
26659
|
+
)
|
|
26660
|
+
|
|
26661
|
+
if not _has_method(spline, "DeleteKeyFrames"):
|
|
26662
|
+
return _err(
|
|
26663
|
+
f"The modifier on '{tool_name}.{input_name}' has no DeleteKeyFrames method",
|
|
26664
|
+
code="FUSION_DELETE_KEYFRAMES_UNSUPPORTED", category="unsupported",
|
|
26665
|
+
reason="Only spline modifiers (e.g. BezierSpline) support keyframe removal.",
|
|
26666
|
+
state={"tool_name": tool_name, "input_name": input_name},
|
|
26667
|
+
)
|
|
26668
|
+
|
|
26669
|
+
before = _fusion_keyframe_frames(inp)
|
|
26670
|
+
if not any(abs(frame - time) < 1e-6 for frame in before):
|
|
26671
|
+
return _err(
|
|
26672
|
+
f"No keyframe at frame {time:g} on '{tool_name}.{input_name}'",
|
|
26673
|
+
code="FUSION_KEYFRAME_NOT_FOUND", category="precondition",
|
|
26674
|
+
state={"tool_name": tool_name, "input_name": input_name,
|
|
26675
|
+
"time": time, "keyframes": before},
|
|
26676
|
+
)
|
|
26677
|
+
|
|
26678
|
+
try:
|
|
26679
|
+
spline.DeleteKeyFrames(time)
|
|
26680
|
+
except Exception as exc:
|
|
26681
|
+
return _err(
|
|
26682
|
+
f"DeleteKeyFrames({time:g}) raised: {exc}",
|
|
26683
|
+
code="FUSION_DELETE_KEYFRAME_FAILED", category="resolve_api_failed",
|
|
26684
|
+
state={"tool_name": tool_name, "input_name": input_name, "time": time},
|
|
26685
|
+
)
|
|
26686
|
+
|
|
26687
|
+
after = _fusion_keyframe_frames(inp)
|
|
26688
|
+
if any(abs(frame - time) < 1e-6 for frame in after):
|
|
26689
|
+
return _err(
|
|
26690
|
+
f"DeleteKeyFrames({time:g}) returned without error but the keyframe "
|
|
26691
|
+
f"is still on '{tool_name}.{input_name}'",
|
|
26692
|
+
code="FUSION_DELETE_KEYFRAME_NOOP", category="resolve_api_failed",
|
|
26693
|
+
state={"tool_name": tool_name, "input_name": input_name,
|
|
26694
|
+
"time": time, "keyframes_before": before, "keyframes_after": after},
|
|
26695
|
+
)
|
|
26696
|
+
|
|
26697
|
+
return _ok(time=time, remaining_keyframes=after)
|
|
26698
|
+
|
|
26699
|
+
|
|
26585
26700
|
def _fusion_get_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
26586
26701
|
"""Read the text of a Fusion Text+ tool / title template. (issue #73)"""
|
|
26587
26702
|
tool, err = _fusion_find_text_tool(comp, p)
|
|
@@ -26627,7 +26742,9 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
26627
26742
|
get_attrs(tool_name) -> {attrs}
|
|
26628
26743
|
add_keyframe(tool_name, input_name, time, value) -> {success}
|
|
26629
26744
|
get_keyframes(tool_name, input_name) -> {keyframes}
|
|
26630
|
-
delete_keyframe(tool_name, input_name, time) -> {success}
|
|
26745
|
+
delete_keyframe(tool_name, input_name, time) -> {success, time, remaining_keyframes}
|
|
26746
|
+
Deletes on the spline attached to the input. Structured errors when the
|
|
26747
|
+
input is not animated or has no keyframe at that frame.
|
|
26631
26748
|
get_comp_info() -> {name, tool_count, attrs}
|
|
26632
26749
|
get_position(tool_name) -> {tool_name, x, y} — read a node's FlowView position
|
|
26633
26750
|
set_position(tool_name, x, y) -> {success, x, y, readback} — move a node
|
|
@@ -26923,11 +27040,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
26923
27040
|
return _err(f"Tool '{p['tool_name']}' not found")
|
|
26924
27041
|
comp.Lock()
|
|
26925
27042
|
try:
|
|
26926
|
-
|
|
26927
|
-
if not inp:
|
|
26928
|
-
return _err(f"Input '{p['input_name']}' not found on tool '{p['tool_name']}'")
|
|
26929
|
-
inp.RemoveKeyFrame(p["time"])
|
|
26930
|
-
return _ok()
|
|
27043
|
+
return _fusion_delete_keyframe(tool, p)
|
|
26931
27044
|
finally:
|
|
26932
27045
|
comp.Unlock()
|
|
26933
27046
|
|