davinci-resolve-mcp 2.55.2 → 2.56.1
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 +40 -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 +25 -6
- package/src/utils/destructive_hook.py +49 -63
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,46 @@
|
|
|
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.56.1
|
|
6
|
+
|
|
7
|
+
Final reliability batch from the exhaustive audit (Wave B + P2/P3 selections).
|
|
8
|
+
|
|
9
|
+
- **Fixed** (EX11) `audio_track_probe` reported `available: true` for track index 0
|
|
10
|
+
(and negatives); audio tracks are 1-indexed, so it now requires
|
|
11
|
+
`1 <= index <= track_count`.
|
|
12
|
+
- **Fixed** (EX10) `_find_timeline_item_by_id` no longer silently skips a whole
|
|
13
|
+
track type when `GetTrackCount` errors — it logs the failure so a real API error
|
|
14
|
+
isn't mistaken for "item not found".
|
|
15
|
+
- **Fixed** (P3) the raw `timeline_item_color.set_cdl` now validates the ASC-CDL
|
|
16
|
+
payload (shape/ranges) before `SetCDL`, matching its safe twin — malformed CDL
|
|
17
|
+
returns a structured error instead of being silently rejected by Resolve.
|
|
18
|
+
- **Fixed** (P2) `media_pool.delete_timelines` is read-back verified: it reports
|
|
19
|
+
`verified` from the project's timeline count dropping, not the unreliable boolean.
|
|
20
|
+
`import_folder` now validates its required `path`.
|
|
21
|
+
|
|
22
|
+
## What's New in v2.56.0
|
|
23
|
+
|
|
24
|
+
Destructive-action registry audit (EX-REG) — a systemic version of the EX2 bug.
|
|
25
|
+
|
|
26
|
+
Many destructive actions were registered under the **wrong tool key**, so
|
|
27
|
+
`is_destructive()` returned False and **version-on-mutate archiving / change
|
|
28
|
+
logging silently never fired** for them. Examples: `create_timeline`,
|
|
29
|
+
`auto_sync_audio`, `create_stereo_clip`, `append_to_timeline` were filed under
|
|
30
|
+
`timeline` though `media_pool` dispatches them; `set_cdl`, `copy_grades`,
|
|
31
|
+
`export_lut`, the version ops were under `timeline_item` though
|
|
32
|
+
`timeline_item_color` dispatches them; the Fusion-comp and take actions used stale
|
|
33
|
+
names (`add_fusion_comp`, `delete_take`) that matched no real handler.
|
|
34
|
+
|
|
35
|
+
- **Fixed** rebuilt `DESTRUCTIVE_ACTIONS_BY_TOOL` so every action is keyed under
|
|
36
|
+
the tool that actually dispatches it (stale names mapped to real ones:
|
|
37
|
+
`*_fusion_comp` → `*_comp`, `*_take` → `add`/`delete`/`select`/`finalize`,
|
|
38
|
+
`import_into` → `import_into_timeline`, `create_subtitles_from_audio` →
|
|
39
|
+
`create_subtitles`). Inert entries whose tool isn't governed (media_pool_item
|
|
40
|
+
`replace_clip`/`link_*`) were dropped. Catastrophic take/Fusion-comp deletes and
|
|
41
|
+
the media-pool create/sync ops are now archived as intended.
|
|
42
|
+
- **Added** the registry-drift guard now asserts *every* registry action is a real
|
|
43
|
+
handler of its tool (broad check enabled), so this class can't regress.
|
|
44
|
+
|
|
5
45
|
## What's New in v2.55.2
|
|
6
46
|
|
|
7
47
|
Deep-QC P1 1b — required-param validation. Mutating/destructive actions that
|
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.56.1"
|
|
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.56.1"
|
|
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.56.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -1891,7 +1891,10 @@ def _find_timeline_item_by_id(tl, timeline_item_id) -> Optional[Any]:
|
|
|
1891
1891
|
for track_type in ("video", "audio", "subtitle"):
|
|
1892
1892
|
try:
|
|
1893
1893
|
track_count = int(tl.GetTrackCount(track_type) or 0)
|
|
1894
|
-
except Exception:
|
|
1894
|
+
except Exception as exc:
|
|
1895
|
+
# Don't silently skip a whole track type on API error — log it so a
|
|
1896
|
+
# genuine API failure isn't mistaken for "item not found" (EX10).
|
|
1897
|
+
logger.warning("_find_timeline_item_by_id: GetTrackCount(%s) failed, skipping: %s", track_type, exc)
|
|
1895
1898
|
continue
|
|
1896
1899
|
for track_index in range(1, track_count + 1):
|
|
1897
1900
|
for item in (tl.GetItemListInTrack(track_type, track_index) or []):
|
|
@@ -5691,8 +5694,11 @@ def _audio_capabilities():
|
|
|
5691
5694
|
def _audio_track_probe(tl, p: Dict[str, Any]):
|
|
5692
5695
|
track_index = int(p.get("track_index", 1))
|
|
5693
5696
|
track_count = int(tl.GetTrackCount("audio") or 0)
|
|
5694
|
-
|
|
5695
|
-
|
|
5697
|
+
# Resolve audio tracks are 1-indexed; track_index < 1 is invalid (EX11 — the
|
|
5698
|
+
# old `track_index <= track_count` reported available:true for index 0).
|
|
5699
|
+
out = {"track_index": track_index, "track_count": track_count,
|
|
5700
|
+
"available": 1 <= track_index <= track_count}
|
|
5701
|
+
if not (1 <= track_index <= track_count):
|
|
5696
5702
|
return out
|
|
5697
5703
|
for key, getter, args in (
|
|
5698
5704
|
("sub_type", "GetTrackSubType", ("audio", track_index)),
|
|
@@ -15158,7 +15164,13 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
15158
15164
|
blocked = _consume_confirm_token(action="media_pool.delete_timelines", params=p)
|
|
15159
15165
|
if blocked:
|
|
15160
15166
|
return blocked
|
|
15161
|
-
|
|
15167
|
+
# Read-back: DeleteTimelines' bool is unreliable; verify by the project's
|
|
15168
|
+
# timeline count dropping (P2).
|
|
15169
|
+
before_n = proj.GetTimelineCount()
|
|
15170
|
+
raw = bool(mp.DeleteTimelines(timelines))
|
|
15171
|
+
after_n = proj.GetTimelineCount()
|
|
15172
|
+
return {"success": raw, "verified": after_n < before_n,
|
|
15173
|
+
"timelines_before": before_n, "timelines_after": after_n}
|
|
15162
15174
|
elif action == "append_to_timeline":
|
|
15163
15175
|
if p.get("clip_infos") is not None:
|
|
15164
15176
|
raw = p["clip_infos"]
|
|
@@ -15321,6 +15333,8 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
15321
15333
|
return _err("Clip not found")
|
|
15322
15334
|
return {"success": bool(mp.DeleteClipMattes(clip, p["paths"]))}
|
|
15323
15335
|
elif action == "import_folder":
|
|
15336
|
+
if not p.get("path"):
|
|
15337
|
+
return _err("import_folder requires path")
|
|
15324
15338
|
return {"success": bool(mp.ImportFolderFromFile(p["path"], p.get("source_clips_path", "")))}
|
|
15325
15339
|
elif action == "ingest_capabilities":
|
|
15326
15340
|
return _media_pool_ingest_capabilities()
|
|
@@ -20377,7 +20391,12 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
20377
20391
|
elif action == "grade_boundary_report":
|
|
20378
20392
|
return _grade_boundary_report(proj, item, p)
|
|
20379
20393
|
elif action == "set_cdl":
|
|
20380
|
-
|
|
20394
|
+
# Validate ASC-CDL shape before SetCDL (malformed CDL is silently rejected
|
|
20395
|
+
# by Resolve); the raw action shares the safe twin's validator now (P3 #22).
|
|
20396
|
+
validation, err = _validate_cdl_payload(p.get("cdl"))
|
|
20397
|
+
if err:
|
|
20398
|
+
return err
|
|
20399
|
+
return {"success": bool(item.SetCDL(_normalize_cdl(validation["cdl"])))}
|
|
20381
20400
|
elif action == "copy_grades":
|
|
20382
20401
|
# Find target items by IDs
|
|
20383
20402
|
_, tl, _ = _get_tl()
|
|
@@ -40,13 +40,17 @@ logger = logging.getLogger("resolve-mcp.destructive-hook")
|
|
|
40
40
|
# here; (b) optionally add a metric-capture entry below if it has a sensible
|
|
41
41
|
# default metric.
|
|
42
42
|
|
|
43
|
+
# Each action is keyed under the tool whose @_destructive_op wrapper actually
|
|
44
|
+
# DISPATCHES it. Historically many entries were filed under the wrong tool key
|
|
45
|
+
# (e.g. create_timeline/auto_sync_audio under "timeline" though they are media_pool
|
|
46
|
+
# actions; set_cdl/copy_grades under "timeline_item" though timeline_item_color
|
|
47
|
+
# dispatches them; *_fusion_comp / *_take stale names that never matched), so
|
|
48
|
+
# is_destructive() returned False and version-on-mutate archiving silently did not
|
|
49
|
+
# fire. EX-REG re-filed every action under its real dispatcher and dropped inert
|
|
50
|
+
# entries whose tool is not @_destructive_op-wrapped (e.g. media_pool_item
|
|
51
|
+
# replace_clip/link_*). The test_destructive_registry_drift guard asserts every
|
|
52
|
+
# string here is a real handler so this can't regress.
|
|
43
53
|
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).
|
|
50
54
|
"media_pool": frozenset({
|
|
51
55
|
"delete_clips",
|
|
52
56
|
"delete_folders",
|
|
@@ -54,6 +58,14 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
54
58
|
"move_folders",
|
|
55
59
|
"delete_clip_mattes",
|
|
56
60
|
"delete_timelines",
|
|
61
|
+
"create_timeline",
|
|
62
|
+
"create_timeline_from_clips",
|
|
63
|
+
"append_to_timeline",
|
|
64
|
+
"setup_multicam_timeline",
|
|
65
|
+
"create_stereo_clip",
|
|
66
|
+
"auto_sync_audio",
|
|
67
|
+
"set_clip_marks",
|
|
68
|
+
"clear_clip_marks",
|
|
57
69
|
}),
|
|
58
70
|
"edit_engine": frozenset({
|
|
59
71
|
"execute_selects",
|
|
@@ -61,11 +73,6 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
61
73
|
"execute_swap",
|
|
62
74
|
}),
|
|
63
75
|
"timeline": frozenset({
|
|
64
|
-
"create_timeline",
|
|
65
|
-
"create_timeline_from_clips",
|
|
66
|
-
"setup_multicam_timeline",
|
|
67
|
-
"delete_timelines",
|
|
68
|
-
"append_to_timeline",
|
|
69
76
|
"delete_clips",
|
|
70
77
|
"move_clips",
|
|
71
78
|
"duplicate_clips",
|
|
@@ -75,19 +82,9 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
75
82
|
"overwrite_range",
|
|
76
83
|
"lift_range",
|
|
77
84
|
"apply_cuts",
|
|
78
|
-
"create_stereo_clip",
|
|
79
|
-
"auto_sync_audio",
|
|
80
85
|
"create_compound_clip",
|
|
81
86
|
"create_fusion_clip",
|
|
82
87
|
"convert_to_stereo",
|
|
83
|
-
"create_subtitles_from_audio",
|
|
84
|
-
"set_clip_marks",
|
|
85
|
-
"clear_clip_marks",
|
|
86
|
-
"set_clip_property",
|
|
87
|
-
"set_clip_color",
|
|
88
|
-
"clear_clip_color",
|
|
89
|
-
"replace_clip",
|
|
90
|
-
"replace_clip_preserve_sub_clip",
|
|
91
88
|
"set_clips_linked",
|
|
92
89
|
"duplicate",
|
|
93
90
|
"insert_generator",
|
|
@@ -108,7 +105,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
108
105
|
"set_mark_in_out",
|
|
109
106
|
"clear_mark_in_out",
|
|
110
107
|
"set_title_text",
|
|
111
|
-
"
|
|
108
|
+
"import_into_timeline",
|
|
112
109
|
}),
|
|
113
110
|
"timeline_markers": frozenset({
|
|
114
111
|
"add",
|
|
@@ -120,77 +117,64 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
|
|
|
120
117
|
"timeline_ai": frozenset({
|
|
121
118
|
"detect_scene_cuts",
|
|
122
119
|
"analyze_dolby_vision",
|
|
120
|
+
"create_subtitles",
|
|
123
121
|
}),
|
|
124
122
|
"timeline_item": frozenset({
|
|
125
123
|
"set_clip_enabled",
|
|
126
|
-
"set_clip_color",
|
|
127
|
-
"clear_clip_color",
|
|
128
|
-
"add_flag",
|
|
129
|
-
"clear_flags",
|
|
130
124
|
"set_property",
|
|
131
125
|
"set_name",
|
|
132
|
-
"assign_to_color_group",
|
|
133
|
-
"remove_from_color_group",
|
|
134
|
-
"create_magic_mask",
|
|
135
|
-
"regenerate_magic_mask",
|
|
136
|
-
"smart_reframe",
|
|
137
|
-
"stabilize",
|
|
138
126
|
"set_voice_isolation_state",
|
|
139
|
-
"set_cdl",
|
|
140
|
-
"copy_grades",
|
|
141
|
-
"reset_all_node_colors",
|
|
142
|
-
"set_color_output_cache",
|
|
143
|
-
"set_fusion_output_cache",
|
|
144
127
|
"update_sidecar",
|
|
145
|
-
"export_lut",
|
|
146
|
-
"add_take",
|
|
147
|
-
"delete_take",
|
|
148
|
-
"select_take",
|
|
149
|
-
"finalize_take",
|
|
150
|
-
"add_version",
|
|
151
|
-
"delete_version",
|
|
152
|
-
"load_version",
|
|
153
|
-
"rename_version",
|
|
154
128
|
"set_transform",
|
|
155
129
|
"set_crop",
|
|
156
130
|
"set_retime",
|
|
157
131
|
"set_composite",
|
|
158
132
|
"set_audio",
|
|
159
|
-
"set_stabilization",
|
|
160
133
|
}),
|
|
161
134
|
"timeline_item_markers": frozenset({
|
|
162
135
|
"add",
|
|
136
|
+
"add_flag",
|
|
137
|
+
"clear_flags",
|
|
138
|
+
"set_clip_color",
|
|
139
|
+
"clear_clip_color",
|
|
163
140
|
"delete_at_frame",
|
|
164
141
|
"delete_by_custom_data",
|
|
165
142
|
"delete_by_color",
|
|
166
143
|
"update_custom_data",
|
|
167
144
|
}),
|
|
168
145
|
"timeline_item_fusion": frozenset({
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
146
|
+
"add_comp",
|
|
147
|
+
"delete_comp",
|
|
148
|
+
"import_comp",
|
|
149
|
+
"load_comp",
|
|
150
|
+
"rename_comp",
|
|
174
151
|
}),
|
|
175
152
|
"timeline_item_color": frozenset({
|
|
176
153
|
"set_cdl",
|
|
177
154
|
"copy_grades",
|
|
178
155
|
"reset_all_node_colors",
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
156
|
+
"assign_color_group",
|
|
157
|
+
"remove_from_color_group",
|
|
158
|
+
"create_magic_mask",
|
|
159
|
+
"regenerate_magic_mask",
|
|
160
|
+
"smart_reframe",
|
|
161
|
+
"stabilize",
|
|
162
|
+
"export_lut",
|
|
163
|
+
"add_version",
|
|
164
|
+
"delete_version",
|
|
165
|
+
"load_version",
|
|
166
|
+
"rename_version",
|
|
167
|
+
"set_color_cache",
|
|
168
|
+
"set_fusion_cache",
|
|
184
169
|
}),
|
|
185
170
|
"timeline_item_takes": frozenset({
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
171
|
+
"add",
|
|
172
|
+
"delete",
|
|
173
|
+
"select",
|
|
174
|
+
"finalize",
|
|
190
175
|
}),
|
|
191
176
|
"graph": frozenset({
|
|
192
177
|
"set_lut",
|
|
193
|
-
"set_node_cache_mode",
|
|
194
178
|
"set_node_enabled",
|
|
195
179
|
"apply_arri_cdl_lut",
|
|
196
180
|
"apply_grade_from_drx",
|
|
@@ -227,8 +211,10 @@ NO_ARCHIVE_ON_KEYS: Dict[Tuple[str, str], frozenset] = {
|
|
|
227
211
|
# first, the user almost certainly wants to know before losing data.
|
|
228
212
|
|
|
229
213
|
STRICT_DEFAULT_ACTIONS: frozenset = frozenset({
|
|
230
|
-
("timeline", "delete_timelines"),
|
|
231
214
|
("timeline", "delete_track"),
|
|
215
|
+
# NOTE: delete_timelines is a media_pool action (EX-REG re-keyed it); it is
|
|
216
|
+
# archive+confirm-token gated (EX3) rather than strict, per the decision that
|
|
217
|
+
# strict over-blocks deletes when there is nothing to archive.
|
|
232
218
|
# delete_clips with ripple=True is destructive in a way single-clip delete
|
|
233
219
|
# isn't — handled separately in is_strict_required() because it depends on
|
|
234
220
|
# the params payload, not just the action name.
|