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 +14 -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 +66 -6
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
|
-
[](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.55.
|
|
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
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.55.
|
|
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.
|
|
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
|
-
|
|
17930
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|