davinci-resolve-mcp 4.7.1 → 4.7.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 +45 -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 +14 -14
- package/src/utils/destructive_hook.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
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.3 — a false spelling no longer grants permission on six opt-in flags
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Six opt-in permission flags were read with bare truthiness, so a client sending
|
|
10
|
+
`"false"` was granted the permission it was declining.** ([#240](https://github.com/samuelgursky/davinci-resolve-mcp/pull/240), @Dev-next-gen)
|
|
11
|
+
These open in the opposite direction from `overwrite` (v4.7.2): they default to off,
|
|
12
|
+
so the string does not perform the act, it *grants* it. On the previous code:
|
|
13
|
+
`allow_media_archive="false"` let `ArchiveProject` run with source media on (the
|
|
14
|
+
21.1.0.14 crash `archive_guard` measures); `acknowledge_trap="false"` stood the crash
|
|
15
|
+
refusal down on both archive paths and stood the trap guard in
|
|
16
|
+
`destructive_hook._trap_acknowledged` down in front of `TimelineItem.CopyGrades`;
|
|
17
|
+
`close_current="false"` closed and deleted the open project; `allow_generate`,
|
|
18
|
+
`allow_render` and `allow_switch` ran `CreateSubtitlesFromAudio`,
|
|
19
|
+
`RenderWithQuickExport` and `SetCurrentDatabase`. Nine readings in all now go through
|
|
20
|
+
`coerce_bool`. Real booleans and the true spellings are unchanged; a false or
|
|
21
|
+
unrecognised string now yields the documented refusal with its `retry_with` payload.
|
|
22
|
+
The neighbouring `dry_run` reads are deliberately untouched (a stringified `"false"`
|
|
23
|
+
there keeps the call in preview, which fails safe), and the granular tools already
|
|
24
|
+
type these as booleans. Guard test: `tests/test_permission_flags_string_false.py` —
|
|
25
|
+
one test per flag over six false spellings, asserting the effect did not happen
|
|
26
|
+
before checking the envelope; 54 of 54 subtests fail on the previous code, every one
|
|
27
|
+
on the effect. One bare opt-in read remains by choice, `allow_timeline_mismatch` on
|
|
28
|
+
`apply_trace_plan`, which gates a name mismatch rather than a destructive act.
|
|
29
|
+
|
|
30
|
+
## What's New in v4.7.2 — `overwrite="false"` no longer unlocks an install guard
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- **Five install actions read `overwrite` with bare truthiness, so the string
|
|
35
|
+
`"false"` opened the guard and replaced the existing file.** ([#239](https://github.com/samuelgursky/davinci-resolve-mcp/pull/239), @Dev-next-gen)
|
|
36
|
+
`fuse_plugin install`, `dctl install`, `script_plugin install`,
|
|
37
|
+
`script_plugin safe_install_extension`, and `lut install` all refuse an existing
|
|
38
|
+
file with the same words — "Pass overwrite=true to replace it." — but `"false"` is a
|
|
39
|
+
non-empty string, so `not p.get("overwrite")` was `False` and the Fuse, DCTL, script
|
|
40
|
+
or LUT was replaced. On the LUT path the response even reported `overwritten: True`
|
|
41
|
+
against a request that said `overwrite="false"`, breaking `install_lut`'s own promise
|
|
42
|
+
that an install never silently replaces something in use. Same defect and same fix
|
|
43
|
+
as `ripple="false"` (v4.6.4): all five readings go through `coerce_bool`. Real
|
|
44
|
+
booleans and `"true"`/`"yes"`/`"1"`/`"on"` are unchanged; an unrecognised string now
|
|
45
|
+
falls to refuse-and-explain instead of destroy. Guard test:
|
|
46
|
+
`tests/test_install_overwrite_string_false.py` — six false spellings across all
|
|
47
|
+
five entry points, asserting both the refusal and that the bytes on disk are
|
|
48
|
+
unchanged; 25 subtests fail on the previous code.
|
|
49
|
+
|
|
5
50
|
## What's New in v4.7.1 — a token handed back as `confirmToken` can be redeemed
|
|
6
51
|
|
|
7
52
|
### Fixed
|
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.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 = "4.7.
|
|
40
|
+
VERSION = "4.7.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
|
@@ -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.3"
|
|
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.3"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -9298,7 +9298,7 @@ def _transcription_capabilities(mp, p: Dict[str, Any]):
|
|
|
9298
9298
|
|
|
9299
9299
|
def _subtitle_generation_probe(tl, p: Dict[str, Any]):
|
|
9300
9300
|
settings, ignored = _normalize_auto_caption_settings(p.get("settings"), get_resolve())
|
|
9301
|
-
if not p.get("allow_generate"
|
|
9301
|
+
if not _coerce_bool(p.get("allow_generate")):
|
|
9302
9302
|
return _ok(would_generate=True, settings=settings, ignored_settings=ignored,
|
|
9303
9303
|
note="Pass allow_generate=True to call CreateSubtitlesFromAudio.")
|
|
9304
9304
|
if not _has_method(tl, "CreateSubtitlesFromAudio"):
|
|
@@ -18550,10 +18550,10 @@ def _safe_project_archive(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18550
18550
|
return _err(flag_err, code="INVALID_ARCHIVE_FLAG", category="invalid_input")
|
|
18551
18551
|
src_media, render_cache, proxy_media = (flags["src_media"], flags["render_cache"],
|
|
18552
18552
|
flags["proxy_media"])
|
|
18553
|
-
if (src_media or render_cache or proxy_media) and not p.get("allow_media_archive"
|
|
18553
|
+
if (src_media or render_cache or proxy_media) and not _coerce_bool(p.get("allow_media_archive")):
|
|
18554
18554
|
return _err("Archive media/cache/proxy flags must stay false unless allow_media_archive=True")
|
|
18555
18555
|
# allow_media_archive guards size; this guards the crash. Both are required.
|
|
18556
|
-
if not p.get("acknowledge_trap"):
|
|
18556
|
+
if not _coerce_bool(p.get("acknowledge_trap")):
|
|
18557
18557
|
refused = archive_guard.crash_refusal("project_manager", "safe_project_archive", flags)
|
|
18558
18558
|
if refused:
|
|
18559
18559
|
return refused
|
|
@@ -18600,7 +18600,7 @@ def _safe_project_delete(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18600
18600
|
from src.utils.project_cleanup import delete_project_safely
|
|
18601
18601
|
current_name = current.GetName() if current and _has_method(current, "GetName") else None
|
|
18602
18602
|
if current_name == name:
|
|
18603
|
-
if not p.get("close_current"
|
|
18603
|
+
if not _coerce_bool(p.get("close_current")):
|
|
18604
18604
|
return _err("Refusing to delete the currently open project; pass close_current=True")
|
|
18605
18605
|
saved = bool(pm.SaveProject()) if p.get("save_current", True) else None
|
|
18606
18606
|
closed = bool(pm.CloseProject(current))
|
|
@@ -18646,7 +18646,7 @@ def _safe_set_current_database(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
18646
18646
|
if not isinstance(db_info, dict) or not db_info.get("DbType") or not db_info.get("DbName"):
|
|
18647
18647
|
return _err("db_info must include DbType and DbName")
|
|
18648
18648
|
current = _ser(pm.GetCurrentDatabase()) if _has_method(pm, "GetCurrentDatabase") else None
|
|
18649
|
-
dry_run = p.get("dry_run", True) or not p.get("allow_switch"
|
|
18649
|
+
dry_run = p.get("dry_run", True) or not _coerce_bool(p.get("allow_switch"))
|
|
18650
18650
|
if dry_run:
|
|
18651
18651
|
return _ok(
|
|
18652
18652
|
would_switch=True,
|
|
@@ -19233,7 +19233,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19233
19233
|
_open_name = _open.GetName() if _open else None
|
|
19234
19234
|
except Exception:
|
|
19235
19235
|
_open_name = None
|
|
19236
|
-
if _open_name == p["name"] and not p.get("close_current"
|
|
19236
|
+
if _open_name == p["name"] and not _coerce_bool(p.get("close_current")):
|
|
19237
19237
|
return _err("Refusing to delete the currently open project; pass close_current=True")
|
|
19238
19238
|
from src.utils.project_cleanup import delete_project_safely
|
|
19239
19239
|
deleted = delete_project_safely(pm, p["name"])
|
|
@@ -19262,7 +19262,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19262
19262
|
flags, flag_err = archive_guard.read_flags(p)
|
|
19263
19263
|
if flag_err:
|
|
19264
19264
|
return _err(flag_err, code="INVALID_ARCHIVE_FLAG", category="invalid_input")
|
|
19265
|
-
if not p.get("acknowledge_trap"):
|
|
19265
|
+
if not _coerce_bool(p.get("acknowledge_trap")):
|
|
19266
19266
|
refused = archive_guard.crash_refusal("project_manager", "archive", flags)
|
|
19267
19267
|
if refused:
|
|
19268
19268
|
return refused
|
|
@@ -20354,7 +20354,7 @@ def _safe_quick_export(proj, p: Dict[str, Any]):
|
|
|
20354
20354
|
return err
|
|
20355
20355
|
if not validation["valid"]:
|
|
20356
20356
|
return {"success": False, "validation": validation}
|
|
20357
|
-
if p.get("dry_run") or not p.get("allow_render"
|
|
20357
|
+
if p.get("dry_run") or not _coerce_bool(p.get("allow_render")):
|
|
20358
20358
|
return _ok(would_render=False, preset=preset, params=params, validation=validation)
|
|
20359
20359
|
before = set()
|
|
20360
20360
|
if target_dir and os.path.isdir(target_dir):
|
|
@@ -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:
|
|
@@ -833,7 +833,7 @@ TRAP_REFUSAL_EXEMPT_ACTIONS: FrozenSet[Tuple[str, str]] = frozenset({
|
|
|
833
833
|
|
|
834
834
|
def _trap_acknowledged(params: Optional[Dict[str, Any]]) -> bool:
|
|
835
835
|
"""Did the caller explicitly accept a known-destructive behaviour?"""
|
|
836
|
-
return
|
|
836
|
+
return isinstance(params, dict) and coerce_bool(params.get("acknowledge_trap"))
|
|
837
837
|
|
|
838
838
|
|
|
839
839
|
def _trap_block_response(
|