davinci-resolve-mcp 4.7.0 → 4.7.2
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 +41 -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 +6 -6
- package/src/utils/confirm_tokens.py +12 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
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.7.2 — `overwrite="false"` no longer unlocks an install guard
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Five install actions read `overwrite` with bare truthiness, so the string
|
|
10
|
+
`"false"` opened the guard and replaced the existing file.** ([#239](https://github.com/samuelgursky/davinci-resolve-mcp/pull/239), @Dev-next-gen)
|
|
11
|
+
`fuse_plugin install`, `dctl install`, `script_plugin install`,
|
|
12
|
+
`script_plugin safe_install_extension`, and `lut install` all refuse an existing
|
|
13
|
+
file with the same words — "Pass overwrite=true to replace it." — but `"false"` is a
|
|
14
|
+
non-empty string, so `not p.get("overwrite")` was `False` and the Fuse, DCTL, script
|
|
15
|
+
or LUT was replaced. On the LUT path the response even reported `overwritten: True`
|
|
16
|
+
against a request that said `overwrite="false"`, breaking `install_lut`'s own promise
|
|
17
|
+
that an install never silently replaces something in use. Same defect and same fix
|
|
18
|
+
as `ripple="false"` (v4.6.4): all five readings go through `coerce_bool`. Real
|
|
19
|
+
booleans and `"true"`/`"yes"`/`"1"`/`"on"` are unchanged; an unrecognised string now
|
|
20
|
+
falls to refuse-and-explain instead of destroy. Guard test:
|
|
21
|
+
`tests/test_install_overwrite_string_false.py` — six false spellings across all
|
|
22
|
+
five entry points, asserting both the refusal and that the bytes on disk are
|
|
23
|
+
unchanged; 25 subtests fail on the previous code.
|
|
24
|
+
|
|
25
|
+
## What's New in v4.7.1 — a token handed back as `confirmToken` can be redeemed
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **A camelCase client could never execute a confirm-gated action.** ([#238](https://github.com/samuelgursky/davinci-resolve-mcp/pull/238), @Dev-next-gen)
|
|
30
|
+
`ConfirmTokenStore.consume` accepts the token as `confirm_token` or `confirmToken`,
|
|
31
|
+
and the twenty-eight gated compound actions treat either spelling as "the caller
|
|
32
|
+
holds a token" — but `fingerprint()` stripped only the snake_case key. A token
|
|
33
|
+
echoed back as `confirmToken` stayed in the params, the request hashed differently
|
|
34
|
+
after issuance than before, and redemption failed with
|
|
35
|
+
`CONFIRM_TOKEN_FINGERPRINT_MISMATCH`. Because the token is popped before that check
|
|
36
|
+
it was already spent, so the retry the remediation asks for reported
|
|
37
|
+
`CONFIRM_TOKEN_INVALID` instead, and re-issuing looped back to the same mismatch.
|
|
38
|
+
That was a closed loop in front of `TimelineItem.CopyGrades` and project deletion
|
|
39
|
+
on both servers, which share the store. Reproduced on `main` before merging.
|
|
40
|
+
Both spellings now live in one `TOKEN_PARAM_KEYS` tuple that `fingerprint` strips
|
|
41
|
+
and `consume` reads, so the pair cannot drift apart again. A camelCase token
|
|
42
|
+
presented against different params is still refused — stripping the key does not
|
|
43
|
+
loosen the gate. Guard tests: `tests/test_confirm_token_camel_case.py`, 5 of 6
|
|
44
|
+
failing on the previous code.
|
|
45
|
+
|
|
5
46
|
## What's New in v4.7.0 — the Resolve 21.1 transcription and timeline-item type reads are complete
|
|
6
47
|
|
|
7
48
|
The read-only pick from the 21.1 tracker ([#207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)), by @legionsound in [#237](https://github.com/samuelgursky/davinci-resolve-mcp/pull/237). Granular 387 → 389; compound unchanged at 37.
|
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
|
-
> 本翻译对应 v4.7.
|
|
15
|
+
> 本翻译对应 v4.7.2 版 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.7.
|
|
40
|
+
VERSION = "4.7.2"
|
|
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
|
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
93
93
|
handlers=[logging.StreamHandler()],
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
VERSION = "4.7.
|
|
96
|
+
VERSION = "4.7.2"
|
|
97
97
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
98
98
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
99
99
|
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.7.
|
|
14
|
+
VERSION = "4.7.2"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -31047,7 +31047,7 @@ def fuse_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
31047
31047
|
d = _fuses_dir()
|
|
31048
31048
|
os.makedirs(d, exist_ok=True)
|
|
31049
31049
|
path = _fuse_path(name)
|
|
31050
|
-
if os.path.exists(path) and not p.get("overwrite"
|
|
31050
|
+
if os.path.exists(path) and not _coerce_bool(p.get("overwrite")):
|
|
31051
31051
|
return _err(f"Fuse '{name}' already exists at {path}. "
|
|
31052
31052
|
"Pass overwrite=true to replace it.")
|
|
31053
31053
|
try:
|
|
@@ -31292,7 +31292,7 @@ def lut(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
|
31292
31292
|
return _ok(would_install=p["name"], writable_dir=lut_files.writable_dir())
|
|
31293
31293
|
return lut_files.install_lut(
|
|
31294
31294
|
p["name"], source=p.get("source"), source_path=p.get("source_path"),
|
|
31295
|
-
overwrite=
|
|
31295
|
+
overwrite=_coerce_bool(p.get("overwrite")))
|
|
31296
31296
|
if action == "remove":
|
|
31297
31297
|
if not p.get("name"):
|
|
31298
31298
|
return _err("remove requires name")
|
|
@@ -31440,7 +31440,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
|
|
|
31440
31440
|
target_dir = root if sd is None else os.path.join(root, sd)
|
|
31441
31441
|
os.makedirs(target_dir, exist_ok=True)
|
|
31442
31442
|
path = os.path.join(target_dir, f"{name}{ext}")
|
|
31443
|
-
if os.path.exists(path) and not p.get("overwrite"
|
|
31443
|
+
if os.path.exists(path) and not _coerce_bool(p.get("overwrite")):
|
|
31444
31444
|
return _err(f"DCTL '{name}{ext}' already exists at {path}. "
|
|
31445
31445
|
"Pass overwrite=true to replace it.")
|
|
31446
31446
|
try:
|
|
@@ -31879,7 +31879,7 @@ def _safe_install_extension(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
31879
31879
|
validation = _validate_script_source(source, language)
|
|
31880
31880
|
if validation.get("valid") is False:
|
|
31881
31881
|
return _err(f"Script validation failed: {validation.get('errors')}")
|
|
31882
|
-
return _script_install_source(name, source, category, language, p.get("overwrite"
|
|
31882
|
+
return _script_install_source(name, source, category, language, _coerce_bool(p.get("overwrite")))
|
|
31883
31883
|
|
|
31884
31884
|
|
|
31885
31885
|
def _safe_remove_extension(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -32250,7 +32250,7 @@ def script_plugin(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
|
|
|
32250
32250
|
return _err(str(e))
|
|
32251
32251
|
os.makedirs(target_dir, exist_ok=True)
|
|
32252
32252
|
path = os.path.join(target_dir, f"{name}{_SCRIPT_LANG_EXT[language]}")
|
|
32253
|
-
if os.path.exists(path) and not p.get("overwrite"
|
|
32253
|
+
if os.path.exists(path) and not _coerce_bool(p.get("overwrite")):
|
|
32254
32254
|
return _err(f"Script '{name}{_SCRIPT_LANG_EXT[language]}' already "
|
|
32255
32255
|
f"exists at {path}. Pass overwrite=true to replace it.")
|
|
32256
32256
|
try:
|
|
@@ -39,6 +39,13 @@ DEFAULT_TTL_SECONDS = 300
|
|
|
39
39
|
PREFERENCE_KEY = "require_confirm_token"
|
|
40
40
|
PREFERENCE_SECTION = "destructive"
|
|
41
41
|
|
|
42
|
+
#: Both spellings a caller may hand the token back under. `consume` reads either,
|
|
43
|
+
#: and every gated action in the compound server treats either as "a token is
|
|
44
|
+
#: present", so `fingerprint` has to ignore both: a spelling the fingerprint does
|
|
45
|
+
#: not strip is an ordinary param, and the request looks different after issuance
|
|
46
|
+
#: than it did before.
|
|
47
|
+
TOKEN_PARAM_KEYS = ("confirm_token", "confirmToken")
|
|
48
|
+
|
|
42
49
|
|
|
43
50
|
def gate_required_from(preferences: Optional[Dict[str, Any]]) -> bool:
|
|
44
51
|
"""Read `destructive.require_confirm_token` out of a preferences payload.
|
|
@@ -109,9 +116,10 @@ class ConfirmTokenStore:
|
|
|
109
116
|
payload = {"action": action, "params": params or {}}
|
|
110
117
|
# Strip the token itself if the caller is echoing it back to us, so the
|
|
111
118
|
# fingerprint of "the request" is the same before and after issuance.
|
|
112
|
-
|
|
119
|
+
# Both spellings, not just the snake_case one: `consume` accepts either.
|
|
120
|
+
if isinstance(payload["params"], dict):
|
|
113
121
|
payload["params"] = {
|
|
114
|
-
k: v for k, v in payload["params"].items() if k
|
|
122
|
+
k: v for k, v in payload["params"].items() if k not in TOKEN_PARAM_KEYS
|
|
115
123
|
}
|
|
116
124
|
try:
|
|
117
125
|
blob = json.dumps(payload, sort_keys=True, default=str)
|
|
@@ -181,7 +189,8 @@ class ConfirmTokenStore:
|
|
|
181
189
|
"""
|
|
182
190
|
if not self.required():
|
|
183
191
|
return None
|
|
184
|
-
|
|
192
|
+
supplied = params or {}
|
|
193
|
+
token = next((supplied.get(key) for key in TOKEN_PARAM_KEYS if supplied.get(key)), None)
|
|
185
194
|
if not token:
|
|
186
195
|
return None # Caller is expected to call issue() in this case.
|
|
187
196
|
with self.lock:
|