davinci-resolve-mcp 2.54.5 → 2.55.0
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 +23 -0
- package/README.md +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +48 -4
- package/src/utils/destructive_hook.py +9 -10
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
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.55.0
|
|
6
|
+
|
|
7
|
+
Governance for catastrophic Media Pool deletes (exhaustive audit EX2/EX3).
|
|
8
|
+
|
|
9
|
+
**Behavior change:** `media_pool` `delete_clips`, `delete_folders`, and
|
|
10
|
+
`delete_timelines` now require a confirm token, the same two-step handshake
|
|
11
|
+
`timeline.delete_track` already uses — the first call returns
|
|
12
|
+
`status: "confirmation_required"` with a `confirm_token` and a preview (count +
|
|
13
|
+
names); re-call with `params.confirm_token` to execute. Callers that disable
|
|
14
|
+
gating via the `destructive.require_confirm_token=false` preference are
|
|
15
|
+
unaffected.
|
|
16
|
+
|
|
17
|
+
- **Fixed** the destructive-action registry listed granular function names
|
|
18
|
+
(`delete_media_pool_clips`, `move_media_pool_folders`, …) that the compound
|
|
19
|
+
`media_pool` tool never dispatches, so `is_destructive()` returned False and
|
|
20
|
+
these catastrophic deletes silently skipped version-on-mutate archiving and
|
|
21
|
+
change logging. The registry now uses the real compound action strings, so the
|
|
22
|
+
deletes are archived/logged as intended.
|
|
23
|
+
- **Added** confirm-token gating + previews to the three deletes (EX3). The
|
|
24
|
+
registry fix also re-enables the existing media-pool change log for them.
|
|
25
|
+
- Live-validated against DaVinci Resolve Studio 21.0.0 (non-destructively): the
|
|
26
|
+
no-token call returns `confirmation_required` and deletes nothing.
|
|
27
|
+
|
|
5
28
|
## What's New in v2.54.5
|
|
6
29
|
|
|
7
30
|
Reliability + security hardening from the exhaustive reliability audit (Wave A).
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
package/install.py
CHANGED
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.
|
|
38
|
+
VERSION = "2.55.0"
|
|
39
39
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
40
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
41
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.55.0"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
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 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.55.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -784,6 +784,11 @@ _destructive_hook.register_preference_provider(_destructive_preference_provider)
|
|
|
784
784
|
_TOKEN_GATED_DESTRUCTIVE_ACTIONS = frozenset({
|
|
785
785
|
("timeline", "delete_track"),
|
|
786
786
|
("timeline", "apply_cuts"),
|
|
787
|
+
# Catastrophic media-pool deletes (EX3): irreversibly destroy clips/folders/
|
|
788
|
+
# timelines. Gated like delete_track; also archive-on-mutate via the registry.
|
|
789
|
+
("media_pool", "delete_clips"),
|
|
790
|
+
("media_pool", "delete_folders"),
|
|
791
|
+
("media_pool", "delete_timelines"),
|
|
787
792
|
# Phase E edit-engine loops: plan → confirm → execute.
|
|
788
793
|
("edit_engine", "execute_selects"),
|
|
789
794
|
("edit_engine", "execute_tighten"),
|
|
@@ -14935,7 +14940,20 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
14935
14940
|
for sub in (root.GetSubFolderList() or []):
|
|
14936
14941
|
if sub.GetUniqueId() == fid:
|
|
14937
14942
|
folders.append(sub)
|
|
14938
|
-
|
|
14943
|
+
if not folders:
|
|
14944
|
+
return _err("No folders found")
|
|
14945
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
14946
|
+
return _issue_confirm_token(
|
|
14947
|
+
action="media_pool.delete_folders", params=p,
|
|
14948
|
+
preview={"operation": "media_pool.delete_folders",
|
|
14949
|
+
"warning": "Permanently deletes folders AND every clip they contain.",
|
|
14950
|
+
"folders_lost": len(folders),
|
|
14951
|
+
"names": [_clip_name(f) for f in folders][:25]},
|
|
14952
|
+
)
|
|
14953
|
+
blocked = _consume_confirm_token(action="media_pool.delete_folders", params=p)
|
|
14954
|
+
if blocked:
|
|
14955
|
+
return blocked
|
|
14956
|
+
return {"success": bool(mp.DeleteFolders(folders))}
|
|
14939
14957
|
elif action == "move_folders":
|
|
14940
14958
|
target = _navigate_folder(mp, p["target_path"])
|
|
14941
14959
|
if not target:
|
|
@@ -15028,7 +15046,20 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
15028
15046
|
tl = proj.GetTimelineByIndex(i)
|
|
15029
15047
|
if tl and tl.GetUniqueId() in p["timeline_ids"]:
|
|
15030
15048
|
timelines.append(tl)
|
|
15031
|
-
|
|
15049
|
+
if not timelines:
|
|
15050
|
+
return _err("No timelines found")
|
|
15051
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
15052
|
+
return _issue_confirm_token(
|
|
15053
|
+
action="media_pool.delete_timelines", params=p,
|
|
15054
|
+
preview={"operation": "media_pool.delete_timelines",
|
|
15055
|
+
"warning": "Permanently deletes timelines.",
|
|
15056
|
+
"timelines_lost": len(timelines),
|
|
15057
|
+
"names": [t.GetName() for t in timelines][:25]},
|
|
15058
|
+
)
|
|
15059
|
+
blocked = _consume_confirm_token(action="media_pool.delete_timelines", params=p)
|
|
15060
|
+
if blocked:
|
|
15061
|
+
return blocked
|
|
15062
|
+
return {"success": bool(mp.DeleteTimelines(timelines))}
|
|
15032
15063
|
elif action == "append_to_timeline":
|
|
15033
15064
|
if p.get("clip_infos") is not None:
|
|
15034
15065
|
raw = p["clip_infos"]
|
|
@@ -15115,7 +15146,20 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
15115
15146
|
elif action == "delete_clips":
|
|
15116
15147
|
clips = [_find_clip(root, cid) for cid in p["clip_ids"]]
|
|
15117
15148
|
clips = [c for c in clips if c]
|
|
15118
|
-
|
|
15149
|
+
if not clips:
|
|
15150
|
+
return _err("No clips found")
|
|
15151
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
15152
|
+
return _issue_confirm_token(
|
|
15153
|
+
action="media_pool.delete_clips", params=p,
|
|
15154
|
+
preview={"operation": "media_pool.delete_clips",
|
|
15155
|
+
"warning": "Permanently deletes Media Pool clips.",
|
|
15156
|
+
"clips_lost": len(clips),
|
|
15157
|
+
"names": [_clip_name(c) for c in clips][:25]},
|
|
15158
|
+
)
|
|
15159
|
+
blocked = _consume_confirm_token(action="media_pool.delete_clips", params=p)
|
|
15160
|
+
if blocked:
|
|
15161
|
+
return blocked
|
|
15162
|
+
return {"success": bool(mp.DeleteClips(clips))}
|
|
15119
15163
|
elif action == "move_clips":
|
|
15120
15164
|
target = _navigate_folder(mp, p["target_path"])
|
|
15121
15165
|
if not target:
|
|
@@ -41,18 +41,17 @@ logger = logging.getLogger("resolve-mcp.destructive-hook")
|
|
|
41
41
|
# default metric.
|
|
42
42
|
|
|
43
43
|
DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
44
|
+
# Real COMPOUND media_pool action strings (the @destructive_op("media_pool")
|
|
45
|
+
# wrapper queries is_destructive("media_pool", <compound action>)). The prior
|
|
46
|
+
# entries used granular function names (delete_media_pool_clips, …) and
|
|
47
|
+
# media_pool_item names that the compound tool never dispatches, so catastrophic
|
|
48
|
+
# deletes silently bypassed version-on-mutate archiving (EX2). media_pool_item
|
|
49
|
+
# link/replace actions are governed separately (its own tool is not yet wrapped).
|
|
44
50
|
"media_pool": frozenset({
|
|
45
|
-
"
|
|
46
|
-
"
|
|
51
|
+
"delete_clips",
|
|
52
|
+
"delete_folders",
|
|
47
53
|
"move_clips",
|
|
48
|
-
"
|
|
49
|
-
"replace_clip",
|
|
50
|
-
"replace_clip_preserve_sub_clip",
|
|
51
|
-
"link_clip_proxy_media",
|
|
52
|
-
"unlink_clip_proxy_media",
|
|
53
|
-
"link_proxy_media",
|
|
54
|
-
"unlink_proxy_media",
|
|
55
|
-
"link_clip_full_resolution_media",
|
|
54
|
+
"move_folders",
|
|
56
55
|
"delete_clip_mattes",
|
|
57
56
|
"delete_timelines",
|
|
58
57
|
}),
|