davinci-resolve-mcp 2.34.1 → 2.35.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 +13 -0
- package/README.md +1 -1
- package/docs/SKILL.md +3 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +62 -2
- package/src/utils/destructive_hook.py +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
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.35.0
|
|
6
|
+
|
|
7
|
+
The Cut-IR executor — transcript-driven editing now closes the loop onto the
|
|
8
|
+
timeline.
|
|
9
|
+
|
|
10
|
+
- **Added** `timeline(action="apply_cuts", cuts, dry_run?, confirm_token?)` applies
|
|
11
|
+
a CutList (from `propose_cuts`) to the timeline as lift / ripple deletes. It is
|
|
12
|
+
**DRY-RUN by default**; applying is destructive and fully governed: confirm-token
|
|
13
|
+
gated, and a timeline version is archived first (so it is reversible), through
|
|
14
|
+
the existing destructive hook. Cuts apply latest-first so ripple deletes do not
|
|
15
|
+
invalidate earlier spans. Live-validated end-to-end (propose -> token -> apply ->
|
|
16
|
+
version archived).
|
|
17
|
+
|
|
5
18
|
## What's New in v2.34.1
|
|
6
19
|
|
|
7
20
|
Page-switch serialization — the concurrency primitive for safe multi-agent use.
|
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/docs/SKILL.md
CHANGED
|
@@ -731,6 +731,9 @@ Key actions:
|
|
|
731
731
|
text `{text, cue_count, has_subtitles, cues}`
|
|
732
732
|
- `propose_cuts(cues?, long_pause_frames?)` — DRY-RUN: mechanically detect
|
|
733
733
|
candidate cuts (fillers, long pauses, repeats) from the transcript; proposes only
|
|
734
|
+
- `apply_cuts(cuts, dry_run?, confirm_token?)` — apply a CutList as lift/ripple
|
|
735
|
+
deletes. DRY-RUN by default; applying is destructive (confirm-token gated, a
|
|
736
|
+
timeline version is archived first). Cuts apply latest-first
|
|
734
737
|
- `add_track(track_type, sub_type?)` / `delete_track(track_type, index)`
|
|
735
738
|
- `get_items(track_type, index)` — items on a track
|
|
736
739
|
- `clip_where(track_type?, track_index?, name_contains?, duration_lt?, duration_gt?)` —
|
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.35.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.35.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.35.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -729,6 +729,7 @@ _destructive_hook.register_preference_provider(_destructive_preference_provider)
|
|
|
729
729
|
# a version. Keep in sync with the _issue_confirm_token call sites.
|
|
730
730
|
_TOKEN_GATED_DESTRUCTIVE_ACTIONS = frozenset({
|
|
731
731
|
("timeline", "delete_track"),
|
|
732
|
+
("timeline", "apply_cuts"),
|
|
732
733
|
("graph", "apply_grade_from_drx"),
|
|
733
734
|
("graph", "reset_all_grades"),
|
|
734
735
|
# 21.0 AI ops that render/generate NEW media files (additive, but expensive
|
|
@@ -15209,6 +15210,10 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
15209
15210
|
propose_cuts(cues?, long_pause_frames?) -> {cuts, cut_count, basis_cue_count, pass, note}
|
|
15210
15211
|
DRY-RUN. Mechanically detect candidate cuts (filler words, long pauses,
|
|
15211
15212
|
repeated lines) from the subtitle transcript. Proposes only; applies nothing.
|
|
15213
|
+
apply_cuts(cuts, dry_run?, confirm_token?, allow_partial_item_delete?) -> {applied, total, results}
|
|
15214
|
+
Apply a CutList (from propose_cuts) as lift/ripple deletes. DRY-RUN by
|
|
15215
|
+
default; applying is DESTRUCTIVE — confirm-token gated and a timeline
|
|
15216
|
+
version is archived first. Cuts apply latest-first.
|
|
15212
15217
|
get_mark_in_out() -> {mark}
|
|
15213
15218
|
set_mark_in_out(mark_in, mark_out, type?) -> {success}
|
|
15214
15219
|
clear_mark_in_out(type?) -> {success}
|
|
@@ -15503,6 +15508,61 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
15503
15508
|
if cues is None:
|
|
15504
15509
|
cues = _timeline_transcript(tl, with_timecodes=True)["cues"]
|
|
15505
15510
|
return _build_cut_list(cues, long_pause_frames=int(p.get("long_pause_frames", 48)))
|
|
15511
|
+
elif action == "apply_cuts":
|
|
15512
|
+
# Apply a CutList (from propose_cuts) to the timeline. DRY-RUN by default;
|
|
15513
|
+
# applying is destructive (confirm-token gated, a version is archived
|
|
15514
|
+
# first by the destructive hook). Cuts are applied latest-first so ripple
|
|
15515
|
+
# deletes don't invalidate earlier spans.
|
|
15516
|
+
cuts = p.get("cuts")
|
|
15517
|
+
if not isinstance(cuts, list):
|
|
15518
|
+
return _err("apply_cuts requires 'cuts' (a list, e.g. from propose_cuts)")
|
|
15519
|
+
applicable = [
|
|
15520
|
+
c for c in cuts
|
|
15521
|
+
if isinstance(c, dict) and c.get("action") in ("lift", "ripple_delete")
|
|
15522
|
+
and isinstance(c.get("span"), dict)
|
|
15523
|
+
and c["span"].get("start") is not None and c["span"].get("end") is not None
|
|
15524
|
+
]
|
|
15525
|
+
applicable.sort(key=lambda c: c["span"]["start"], reverse=True)
|
|
15526
|
+
plan = [{"action": c["action"], "span": c["span"], "kind": c.get("kind")} for c in applicable]
|
|
15527
|
+
|
|
15528
|
+
if p.get("dry_run", True):
|
|
15529
|
+
return {
|
|
15530
|
+
"dry_run": True,
|
|
15531
|
+
"would_apply": len(applicable),
|
|
15532
|
+
"plan": plan,
|
|
15533
|
+
"note": "No edits made. Re-run with dry_run=false (and a confirm_token) to apply.",
|
|
15534
|
+
}
|
|
15535
|
+
|
|
15536
|
+
if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
|
|
15537
|
+
return _issue_confirm_token(
|
|
15538
|
+
action="timeline.apply_cuts", params=p,
|
|
15539
|
+
preview={
|
|
15540
|
+
"operation": "timeline.apply_cuts",
|
|
15541
|
+
"warning": "Applies lift/ripple deletes to the timeline; ripple closes "
|
|
15542
|
+
"gaps and cannot be selectively undone. A timeline version is "
|
|
15543
|
+
"archived first.",
|
|
15544
|
+
"would_apply": len(applicable),
|
|
15545
|
+
"plan": plan,
|
|
15546
|
+
},
|
|
15547
|
+
)
|
|
15548
|
+
blocked = _consume_confirm_token(action="timeline.apply_cuts", params=p)
|
|
15549
|
+
if blocked:
|
|
15550
|
+
return blocked
|
|
15551
|
+
|
|
15552
|
+
allow_partial = bool(p.get("allow_partial_item_delete", True))
|
|
15553
|
+
results = []
|
|
15554
|
+
for c in applicable:
|
|
15555
|
+
sp = c["span"]
|
|
15556
|
+
res = _timeline_lift_range_impl(tl, {
|
|
15557
|
+
"start_frame": sp["start"],
|
|
15558
|
+
"end_frame": sp["end"],
|
|
15559
|
+
"ripple": c["action"] == "ripple_delete",
|
|
15560
|
+
"allow_partial_item_delete": allow_partial,
|
|
15561
|
+
})
|
|
15562
|
+
results.append({"action": c["action"], "span": sp, "result": res})
|
|
15563
|
+
applied = sum(1 for r in results
|
|
15564
|
+
if isinstance(r["result"], dict) and r["result"].get("success"))
|
|
15565
|
+
return {"success": True, "applied": applied, "total": len(applicable), "results": results}
|
|
15506
15566
|
elif action == "get_mark_in_out":
|
|
15507
15567
|
return _ser(tl.GetMarkInOut())
|
|
15508
15568
|
elif action == "set_mark_in_out":
|
|
@@ -15712,7 +15772,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
15712
15772
|
if mp_err:
|
|
15713
15773
|
return mp_err
|
|
15714
15774
|
return _fairlight_boundary_report(proj, mp, tl, p)
|
|
15715
|
-
return _unknown(action, ["list","get_current","set_current","get_name","set_name","get_start_frame","get_end_frame","get_start_timecode","set_start_timecode","get_track_count","add_track","delete_track","get_track_sub_type","set_track_enable","get_track_enabled","set_track_lock","get_track_locked","get_track_name","set_track_name","get_items","delete_clips","set_clips_linked","duplicate","duplicate_clips","copy_clips","move_clips","copy_range","duplicate_range","overwrite_range","lift_range","story_spine_report","create_variant_from_ranges","bulk_set_item_properties","apply_look_to_items","thumbnail_contact_sheet","marker_thumbnail_review","edit_kernel_capabilities","probe_edit_kernel_item","title_property_scan","set_title_text","bulk_set_title_text","create_compound_clip","create_fusion_clip","import_into_timeline","export","get_setting","set_setting","insert_generator","insert_fusion_generator","insert_fusion_composition","insert_ofx_generator","insert_title","insert_fusion_title","get_unique_id","get_node_graph","get_media_pool_item","get_transcript","propose_cuts","get_mark_in_out","set_mark_in_out","clear_mark_in_out","convert_to_stereo","get_items_in_track","get_voice_isolation_state","set_voice_isolation_state","extract_source_frame_ranges","audio_mix_capability_report",*_TIMELINE_CONFORM_KERNEL_ACTIONS,*_TIMELINE_AUDIO_KERNEL_ACTIONS])
|
|
15775
|
+
return _unknown(action, ["list","get_current","set_current","get_name","set_name","get_start_frame","get_end_frame","get_start_timecode","set_start_timecode","get_track_count","add_track","delete_track","get_track_sub_type","set_track_enable","get_track_enabled","set_track_lock","get_track_locked","get_track_name","set_track_name","get_items","delete_clips","set_clips_linked","duplicate","duplicate_clips","copy_clips","move_clips","copy_range","duplicate_range","overwrite_range","lift_range","story_spine_report","create_variant_from_ranges","bulk_set_item_properties","apply_look_to_items","thumbnail_contact_sheet","marker_thumbnail_review","edit_kernel_capabilities","probe_edit_kernel_item","title_property_scan","set_title_text","bulk_set_title_text","create_compound_clip","create_fusion_clip","import_into_timeline","export","get_setting","set_setting","insert_generator","insert_fusion_generator","insert_fusion_composition","insert_ofx_generator","insert_title","insert_fusion_title","get_unique_id","get_node_graph","get_media_pool_item","get_transcript","propose_cuts","apply_cuts","get_mark_in_out","set_mark_in_out","clear_mark_in_out","convert_to_stereo","get_items_in_track","get_voice_isolation_state","set_voice_isolation_state","extract_source_frame_ranges","audio_mix_capability_report",*_TIMELINE_CONFORM_KERNEL_ACTIONS,*_TIMELINE_AUDIO_KERNEL_ACTIONS])
|
|
15716
15776
|
|
|
15717
15777
|
|
|
15718
15778
|
# ═══════════════════════════════════════════════════════════════════════════════
|