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 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
- [![Version](https://img.shields.io/badge/version-2.34.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.35.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-32%20(341%20full)-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.34.1"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.34.1",
3
+ "version": "2.35.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.34.1"
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.34.1"
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
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -70,6 +70,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
70
70
  "duplicate_range",
71
71
  "overwrite_range",
72
72
  "lift_range",
73
+ "apply_cuts",
73
74
  "create_stereo_clip",
74
75
  "auto_sync_audio",
75
76
  "create_compound_clip",