davinci-resolve-mcp 2.55.1 → 2.55.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 CHANGED
@@ -2,6 +2,20 @@
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.2
6
+
7
+ Deep-QC P1 1b — required-param validation. Mutating/destructive actions that
8
+ hard-indexed required params (`p["name"]`, `p["index"]`, `p["color"]`, …) now
9
+ return a structured error instead of crashing with an unhandled `KeyError` (or,
10
+ for `set_clip_enabled`/`set_current`, coercing the value). Guarded actions:
11
+
12
+ - `timeline`: `set_current`, `set_name`, `set_start_timecode`, `add_track`
13
+ - `timeline_item`: `set_property`, `set_clip_enabled`
14
+ - `project_manager`: `create`, `load`, `import_project`, `export_project`,
15
+ `archive`, `restore`
16
+ - marker `delete_by_color` (clip / timeline / item — destructive)
17
+ - `layout_presets`: `save`, `export`; `project_settings`: `set_name`, `set_setting`
18
+
5
19
  ## What's New in v2.55.1
6
20
 
7
21
  Deep-QC P1 — settings/options whitelisting + DeleteProject reliability.
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.55.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.55.2-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.55.1"
38
+ VERSION = "2.55.2"
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.55.1",
3
+ "version": "2.55.2",
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.55.1"
83
+ VERSION = "2.55.2"
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.55.1"
14
+ VERSION = "2.55.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -12961,12 +12961,20 @@ def layout_presets(action: str, params: Optional[Dict[str, Any]] = None) -> Dict
12961
12961
  return _err("Could not connect to DaVinci Resolve. It was not running and auto-launch failed. Check that Resolve Studio is installed.")
12962
12962
 
12963
12963
  if action == "save":
12964
+ if not p.get("name"):
12965
+ return _err("save requires name")
12964
12966
  return {"success": bool(r.SaveLayoutPreset(p["name"]))}
12965
12967
  elif action == "load":
12966
12968
  return {"success": bool(r.LoadLayoutPreset(p["name"]))}
12967
12969
  elif action == "update":
12968
12970
  return {"success": bool(r.UpdateLayoutPreset(p["name"]))}
12969
12971
  elif action == "export":
12972
+ err, _clean = _validate_params(p, {
12973
+ "name": {"type": str, "required": True, "non_empty": True},
12974
+ "path": {"type": str, "required": True, "non_empty": True},
12975
+ })
12976
+ if err:
12977
+ return _err(err)
12970
12978
  return {"success": bool(r.ExportLayoutPreset(p["name"], p["path"]))}
12971
12979
  elif action == "import_preset":
12972
12980
  if "name" in p:
@@ -13982,6 +13990,8 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
13982
13990
  proj = pm.GetCurrentProject()
13983
13991
  return {"name": proj.GetName(), "id": proj.GetUniqueId()} if proj else _err("No project open")
13984
13992
  elif action == "create":
13993
+ if not p.get("name"):
13994
+ return _err("create requires name")
13985
13995
  media_location_path = p.get("media_location_path") or p.get("mediaLocationPath")
13986
13996
  if media_location_path:
13987
13997
  version = r.GetVersion() or [0]
@@ -13990,6 +14000,8 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
13990
14000
  proj = pm.CreateProject(p["name"], media_location_path) if media_location_path else pm.CreateProject(p["name"])
13991
14001
  return _ok(name=proj.GetName()) if proj else _err(f"Failed to create '{p['name']}'")
13992
14002
  elif action == "load":
14003
+ if not p.get("name"):
14004
+ return _err("load requires name")
13993
14005
  proj = pm.LoadProject(p["name"])
13994
14006
  return _ok() if proj else _err(f"Failed to load '{p['name']}'")
13995
14007
  elif action == "save":
@@ -14004,13 +14016,29 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
14004
14016
  deleted = delete_project_safely(pm, p["name"])
14005
14017
  return {"success": bool(deleted.get("success")), "delete_detail": deleted}
14006
14018
  elif action == "import_project":
14019
+ if not p.get("path"):
14020
+ return _err("import_project requires path")
14007
14021
  return {"success": bool(pm.ImportProject(p["path"], p.get("name")))}
14008
14022
  elif action == "export_project":
14023
+ err, _clean = _validate_params(p, {
14024
+ "name": {"type": str, "required": True, "non_empty": True},
14025
+ "path": {"type": str, "required": True, "non_empty": True},
14026
+ })
14027
+ if err:
14028
+ return _err(err)
14009
14029
  return {"success": bool(pm.ExportProject(p["name"], p["path"], p.get("with_stills_and_luts", True)))}
14010
14030
  elif action == "archive":
14031
+ err, _clean = _validate_params(p, {
14032
+ "name": {"type": str, "required": True, "non_empty": True},
14033
+ "path": {"type": str, "required": True, "non_empty": True},
14034
+ })
14035
+ if err:
14036
+ return _err(err)
14011
14037
  return {"success": bool(pm.ArchiveProject(p["name"], p["path"],
14012
14038
  p.get("src_media", True), p.get("render_cache", True), p.get("proxy_media", False)))}
14013
14039
  elif action == "restore":
14040
+ if not p.get("path"):
14041
+ return _err("restore requires path")
14014
14042
  return {"success": bool(pm.RestoreProject(p["path"], p.get("name")))}
14015
14043
  return _unknown(action, ["list","get_current","create","load","save","close","delete","import_project","export_project","archive","restore","lint","diff_to_spec","plan_spec","apply_spec", *_PROJECT_KERNEL_ACTIONS])
14016
14044
 
@@ -14165,10 +14193,16 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
14165
14193
  if action == "get_name":
14166
14194
  return {"name": proj.GetName()}
14167
14195
  elif action == "set_name":
14196
+ if not p.get("name"):
14197
+ return _err("set_name requires name")
14168
14198
  return {"success": bool(proj.SetName(p["name"]))}
14169
14199
  elif action == "get_setting":
14170
14200
  return {"settings": _ser(proj.GetSetting(p.get("name", "")))}
14171
14201
  elif action == "set_setting":
14202
+ if not p.get("name"):
14203
+ return _err("set_setting requires name")
14204
+ if "value" not in p:
14205
+ return _err("set_setting requires value")
14172
14206
  return {"success": bool(proj.SetSetting(p["name"], p["value"]))}
14173
14207
  elif action == "get_unique_id":
14174
14208
  return {"id": proj.GetUniqueId()}
@@ -15868,6 +15902,8 @@ def media_pool_item_markers(action: str, params: Optional[Dict[str, Any]] = None
15868
15902
  return frame_err
15869
15903
  return {"data": clip.GetMarkerCustomData(frame)}
15870
15904
  elif action == "delete_by_color":
15905
+ if not p.get("color"):
15906
+ return _err("delete_by_color requires color")
15871
15907
  return {"success": bool(clip.DeleteMarkersByColor(p["color"]))}
15872
15908
  elif action == "delete_at_frame":
15873
15909
  frame, frame_err = _marker_frame_from_params(p)
@@ -17926,8 +17962,11 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
17926
17962
  timelines.append({"name": tl.GetName(), "id": tl.GetUniqueId(), "index": i})
17927
17963
  return {"timelines": timelines}
17928
17964
  elif action == "set_current":
17929
- tl = proj.GetTimelineByIndex(p["index"])
17930
- return {"success": bool(proj.SetCurrentTimeline(tl))} if tl else _err(f"No timeline at index {p['index']}")
17965
+ err, clean = _validate_params(p, {"index": {"type": int, "required": True, "min": 1}})
17966
+ if err:
17967
+ return _err(err)
17968
+ tl = proj.GetTimelineByIndex(clean["index"])
17969
+ return {"success": bool(proj.SetCurrentTimeline(tl))} if tl else _err(f"No timeline at index {clean['index']}")
17931
17970
  elif action == "edit_kernel_capabilities":
17932
17971
  return _timeline_edit_kernel_capabilities()
17933
17972
  elif action == "conform_capabilities":
@@ -17964,7 +18003,10 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
17964
18003
  elif action == "get_name":
17965
18004
  return {"name": tl.GetName()}
17966
18005
  elif action == "set_name":
17967
- return {"success": bool(tl.SetName(p["name"]))}
18006
+ err, clean = _validate_params(p, {"name": {"type": str, "required": True, "non_empty": True}})
18007
+ if err:
18008
+ return _err(err)
18009
+ return {"success": bool(tl.SetName(clean["name"]))}
17968
18010
  elif action == "get_start_frame":
17969
18011
  return {"frame": tl.GetStartFrame()}
17970
18012
  elif action == "get_end_frame":
@@ -17972,10 +18014,16 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
17972
18014
  elif action == "get_start_timecode":
17973
18015
  return {"timecode": tl.GetStartTimecode()}
17974
18016
  elif action == "set_start_timecode":
17975
- return {"success": bool(tl.SetStartTimecode(p["timecode"]))}
18017
+ err, clean = _validate_params(p, {"timecode": {"type": str, "required": True, "non_empty": True}})
18018
+ if err:
18019
+ return _err(err)
18020
+ return {"success": bool(tl.SetStartTimecode(clean["timecode"]))}
17976
18021
  elif action == "get_track_count":
17977
18022
  return {"count": tl.GetTrackCount(p["track_type"])}
17978
18023
  elif action == "add_track":
18024
+ err, _clean = _validate_params(p, {"track_type": {"enum": ["video", "audio", "subtitle"], "required": True}})
18025
+ if err:
18026
+ return _err(err)
17979
18027
  opts_in = p.get("options") or {}
17980
18028
  new_track_options: Dict[str, Any] = {}
17981
18029
  if "audio_type" in opts_in:
@@ -18515,6 +18563,8 @@ def timeline_markers(action: str, params: Optional[Dict[str, Any]] = None) -> An
18515
18563
  return frame_err
18516
18564
  return {"data": tl.GetMarkerCustomData(frame)}
18517
18565
  elif action == "delete_by_color":
18566
+ if not p.get("color"):
18567
+ return _err("delete_by_color requires color")
18518
18568
  return {"success": bool(tl.DeleteMarkersByColor(p["color"]))}
18519
18569
  elif action == "delete_at_frame":
18520
18570
  frame, frame_err = _marker_frame_from_params(p, tl=tl)
@@ -18682,6 +18732,11 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
18682
18732
  elif action == "get_property":
18683
18733
  return {"properties": _ser(item.GetProperty(p.get("key", "")))}
18684
18734
  elif action == "set_property":
18735
+ err, _clean = _validate_params(p, {"key": {"type": str, "required": True, "non_empty": True}})
18736
+ if err:
18737
+ return _err(err)
18738
+ if "value" not in p:
18739
+ return _err("'value' is required")
18685
18740
  return {"success": bool(item.SetProperty(p["key"], p["value"]))}
18686
18741
  elif action == "get_duration":
18687
18742
  return {"duration": item.GetDuration()}
@@ -18702,7 +18757,10 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
18702
18757
  elif action == "get_right_offset":
18703
18758
  return {"offset": item.GetRightOffset()}
18704
18759
  elif action == "set_clip_enabled":
18705
- return {"success": bool(item.SetClipEnabled(p["enabled"]))}
18760
+ err, clean = _validate_params(p, {"enabled": {"type": bool, "required": True}})
18761
+ if err:
18762
+ return _err(err)
18763
+ return {"success": bool(item.SetClipEnabled(clean["enabled"]))}
18706
18764
  elif action == "get_clip_enabled":
18707
18765
  return {"enabled": bool(item.GetClipEnabled())}
18708
18766
  elif action == "update_sidecar":
@@ -18899,6 +18957,8 @@ def timeline_item_markers(action: str, params: Optional[Dict[str, Any]] = None)
18899
18957
  return frame_err
18900
18958
  return {"data": item.GetMarkerCustomData(frame)}
18901
18959
  elif action == "delete_by_color":
18960
+ if not p.get("color"):
18961
+ return _err("delete_by_color requires color")
18902
18962
  return {"success": bool(item.DeleteMarkersByColor(p["color"]))}
18903
18963
  elif action == "delete_at_frame":
18904
18964
  frame, frame_err = _marker_frame_from_params(p)