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 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
- [![Version](https://img.shields.io/badge/version-2.54.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.55.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-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.54.5"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.54.5",
3
+ "version": "2.55.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.54.5"
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.54.5"
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
- return {"success": bool(mp.DeleteFolders(folders))} if folders else _err("No folders found")
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
- return {"success": bool(mp.DeleteTimelines(timelines))} if timelines else _err("No timelines found")
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
- return {"success": bool(mp.DeleteClips(clips))} if clips else _err("No clips found")
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
- "delete_media_pool_clips",
46
- "delete_media_pool_folders",
51
+ "delete_clips",
52
+ "delete_folders",
47
53
  "move_clips",
48
- "move_media_pool_folders",
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
  }),