davinci-resolve-mcp 2.71.0 → 2.71.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 CHANGED
@@ -2,6 +2,48 @@
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.71.1
6
+
7
+ `Timeline.DeleteClips` can lie about whether it worked. #111 recorded four
8
+ behaviours from a live edit session; #114 mitigates the first of them. Both by
9
+ @billcarroll.
10
+
11
+ ### DeleteClips readback-and-retry
12
+
13
+ `Timeline.DeleteClips` can return `False` on a first call even when every item
14
+ passed is a valid, present `TimelineItem`, with an identical retry succeeding.
15
+ `_timeline_delete_clips_verified` reads the tracks back on a `False` and retries
16
+ once if the items are still there. All four timeline call sites route through
17
+ it: the `delete_clips` action, `lift_range`, `duplicate_clips` and `copy_range`.
18
+
19
+ The readback is deliberately **tri-state**. A walk that raised, enumerated no
20
+ track at all, or covered items whose unique ID cannot be read is `unknown`, not
21
+ `absent` — so an unverifiable delete is never reported as success, and never
22
+ spends a second destructive call buying information it cannot read. An earlier
23
+ draft collapsed unknown into absent, which turned a failed delete into a
24
+ reported success; that is the exact silent-lie class this series exists to
25
+ remove, so it is worth naming.
26
+
27
+ The `ripple=True` non-idempotence of a retry is recorded in the docstring rather
28
+ than claimed to be solved: if the first call deleted some items and left others,
29
+ the retry passes the original list back in, stale handles included. It could not
30
+ be made to misbehave against a fake.
31
+
32
+ ### Four edit-session behaviours recorded
33
+
34
+ - **`DeleteClips` flaky first attempt.** The entry states plainly that the cause
35
+ is **unknown**, and specifically that this is *not* the
36
+ `ProjectManager.DeleteProject` shape — that one has an identified mechanism
37
+ which retrying does not clear, whereas a single retry cleared this in the one
38
+ instance seen. One observation is not a mechanism.
39
+ - **`DeleteClips` leaves linked audio.** The API deletes exactly the items
40
+ passed; the UI's linked-selection behaviour does not apply, so orphaned audio
41
+ collides with later appends.
42
+ - **`AppendToTimeline` mixed-fps duration floor.** Source-to-timeline frame
43
+ conversion rounds down, landing a planned range one frame short.
44
+ - **`ImportMedia` current-folder only.** No destination parameter; imports land
45
+ in the current bin.
46
+
5
47
  ## What's New in v2.71.0
6
48
 
7
49
  Keyed metadata getters honor a list of keys, and `delete_timelines` names the
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.71.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.71.1-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/docs/SKILL.md CHANGED
@@ -1307,7 +1307,12 @@ Key actions:
1307
1307
  instead of walking tracks by hand. Filters may be passed inline or as a
1308
1308
  `filters` dict; a mistyped filter name is rejected rather than silently
1309
1309
  matching everything. Returns `{clips, match_count, total_clips}`.
1310
- - `delete_clips(clip_ids, ripple?)` — IDs are unique IDs from `get_items`
1310
+ - `delete_clips(clip_ids, ripple?)` — IDs are unique IDs from `get_items`.
1311
+ Two verified quirks (see `api_truth`): the call can return `success: false`
1312
+ on the first attempt with valid IDs — re-list and retry once before failing;
1313
+ and deleting a video item does NOT delete its linked audio — pass the linked
1314
+ audio item IDs explicitly, then `detect_gaps_overlaps` across both track
1315
+ types.
1311
1316
  - `duplicate_clips(clip_ids?, selected?, target_track_index?, track_offset?, placement?, record_frame?, record_frame_offset?, copy_properties?, include_linked?)` —
1312
1317
  duplicate existing video timeline items by re-appending the same Media Pool
1313
1318
  item with the same source trim; `selected=True` uses Resolve's selected/current
@@ -1723,6 +1728,14 @@ media_pool(action="append_to_timeline", params={"clip_infos": [
1723
1728
  ]})
1724
1729
  ```
1725
1730
 
1731
+ Mixed-fps caution: `start_frame`/`end_frame` are SOURCE frames, and a source
1732
+ whose fps differs from the timeline's rounds DOWN on conversion — a 24.0 or
1733
+ 29.97 clip appended into a 23.976 timeline can land one frame short of its
1734
+ slot. Plan durations in timeline frames, extend `end_frame` by a source frame
1735
+ when the floor misses, and finish with `detect_gaps_overlaps` (see
1736
+ `api_truth`). `import_media` always lands in the CURRENT bin — call
1737
+ `set_current_folder` first; there is no destination parameter.
1738
+
1726
1739
  ### 4. Inspect and annotate timeline items
1727
1740
 
1728
1741
  ```
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.0
14
14
 
15
- **Totals:** 22 missing capabilities, 20 bugs / unreliable behaviors.
15
+ **Totals:** 23 missing capabilities, 22 bugs / unreliable behaviors.
16
16
 
17
17
  The authoritative source is the runtime-queryable `api_truth` ledger
18
18
  (`resolve_control api_truth "<query>"`); this document is generated from
@@ -192,6 +192,14 @@ equivalent, blocking full automation.
192
192
  - **Workaround / current handling:** Delete and recreate the folder with the desired name, or rename in the Resolve UI.
193
193
  - **Tags:** missing-method, media-pool, folder
194
194
 
195
+ ### MediaPool.ImportMedia (current-folder destination only)
196
+
197
+ - **Object:** `MediaPool`
198
+ - **Signature:** `([paths] | [clipInfos]) -> [MediaPoolItem]`
199
+ - **Behavior:** Imports always land in the CURRENT media pool folder; the call has no destination-folder parameter, and passing an unrecognized one to the MCP tool is silently ignored.
200
+ - **Workaround / current handling:** SetCurrentFolder to the target bin first (media_pool set_current_folder), import, then restore the previous current folder if it matters.
201
+ - **Tags:** media-pool, import
202
+
195
203
  ### Project.SetCurrentRenderFormatAndCodec
196
204
 
197
205
  - **Object:** `Project`
@@ -327,6 +335,22 @@ values, or automation-hostile modal prompts.
327
335
  - **Reference:** [issue #77](https://github.com/samuelgursky/davinci-resolve-mcp/issues/77)
328
336
  - **Tags:** unreliable-return, silent-failure, metadata, reel-name
329
337
 
338
+ ### Timeline.DeleteClips (flaky first attempt)
339
+
340
+ - **Object:** `Timeline`
341
+ - **Signature:** `([TimelineItem], ripple) -> bool`
342
+ - **Behavior:** Can return False on the first call even when every item in the list is a valid, present TimelineItem; an identical immediate retry succeeds. Observed once, on Studio 21.0 during a cut-video edit session (items confirmed still present after the False, deleted cleanly on retry). Cause unknown — do NOT read this as the ProjectManager.DeleteProject shape: that one has an identified mechanism (the project being, or recently having been, current) that retrying does not clear, whereas a single retry cleared this in the one instance seen. One observation is not a mechanism; if a retry is ever seen to fail repeatedly here, this entry needs revisiting.
343
+ - **Workaround / current handling:** Treat a False return as advisory: re-list the track and check whether the items are actually gone; if still present, retry the identical call once before failing. A readback that raised, enumerated nothing, or covered items whose unique ID cannot be read is UNKNOWN, not gone — never report an unverifiable delete as success, and do not spend a second destructive call on an outcome you equally cannot read.
344
+ - **Tags:** unreliable-return, flaky, timeline, edit
345
+
346
+ ### MediaPool.AppendToTimeline with mixed-fps sources (duration floor)
347
+
348
+ - **Object:** `MediaPool`
349
+ - **Signature:** `([{mediaPoolItem, startFrame, endFrame, recordFrame, ...}]) -> [TimelineItem]`
350
+ - **Behavior:** start/endFrame are in SOURCE frames. When the source fps differs from the timeline fps (e.g. 24.0 or 29.97 source in a 23.976 timeline), Resolve converts the source range to timeline frames by flooring — so a range planned to fill an exact record slot lands one frame short, leaving a 1-frame gap before the next clip.
351
+ - **Workaround / current handling:** Plan durations in timeline frames (floor(src_frames * timeline_fps / source_fps)); if the floored duration misses the slot, extend endFrame by a source frame and re-check. Always finish with detect_gaps_overlaps.
352
+ - **Tags:** timeline, edit, off-by-one, mixed-fps
353
+
330
354
  ### Graph.SetLUT (master-LUT-dir-only resolution)
331
355
 
332
356
  - **Object:** `Graph`
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.71.0"
39
+ VERSION = "2.71.1"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (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.71.0",
3
+ "version": "2.71.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.71.0"
88
+ VERSION = "2.71.1"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  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.71.0"
14
+ VERSION = "2.71.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -3795,6 +3795,81 @@ def _timeline_item_ids(items):
3795
3795
  return ids
3796
3796
 
3797
3797
 
3798
+ def _timeline_items_presence(tl, items):
3799
+ """Are these timeline items still on the timeline? present/absent/unknown.
3800
+
3801
+ 'absent' is a positive finding: every item was identifiable and a
3802
+ completed track walk did not see any of them. A walk that raised, that
3803
+ could not enumerate a single track, or items whose unique ID cannot be
3804
+ read all yield 'unknown' — the readback saw nothing, which is not the
3805
+ same as nothing being there. Callers must never treat 'unknown' as
3806
+ verified-gone.
3807
+ """
3808
+ target_ids = []
3809
+ unreadable_item = False
3810
+ for item in items:
3811
+ item_id = _safe_timeline_item_id(item)
3812
+ if item_id:
3813
+ target_ids.append(item_id)
3814
+ else:
3815
+ unreadable_item = True
3816
+ target_ids = set(target_ids)
3817
+
3818
+ tracks_walked = 0
3819
+ walk_failed = False
3820
+ for track_type in ("video", "audio", "subtitle"):
3821
+ try:
3822
+ track_count = int(tl.GetTrackCount(track_type) or 0)
3823
+ except Exception:
3824
+ walk_failed = True
3825
+ continue
3826
+ for index in range(1, track_count + 1):
3827
+ try:
3828
+ track_items = tl.GetItemListInTrack(track_type, index) or []
3829
+ except Exception:
3830
+ walk_failed = True
3831
+ continue
3832
+ tracks_walked += 1
3833
+ for track_item in track_items:
3834
+ # A sighting is definitive even if another track failed.
3835
+ if _safe_timeline_item_id(track_item) in target_ids:
3836
+ return "present"
3837
+
3838
+ if walk_failed or tracks_walked == 0 or unreadable_item or not target_ids:
3839
+ return "unknown"
3840
+ return "absent"
3841
+
3842
+
3843
+ def _timeline_delete_clips_verified(tl, items, ripple):
3844
+ """Timeline.DeleteClips with readback-and-retry.
3845
+
3846
+ api_truth 'Timeline.DeleteClips (flaky first attempt)': the call can
3847
+ return False while every item is still present, and an identical retry
3848
+ then succeeds. On a False, read the tracks back:
3849
+
3850
+ absent -> the delete landed despite the False; report success.
3851
+ present -> retry the identical call once, then read back again.
3852
+ unknown -> report failure and do NOT retry. An unverifiable delete must
3853
+ not be claimed as success, and a retry whose outcome we
3854
+ equally cannot read is a second destructive call bought with
3855
+ no information.
3856
+
3857
+ ripple=True caveat: a retry is not idempotent in principle. If the first
3858
+ call deleted some items and left others, the readback reports 'present'
3859
+ for the survivors and the retry passes the original list back in — stale
3860
+ handles to already-deleted items included. That could not be made to
3861
+ misbehave against a fake; it is recorded, not resolved.
3862
+ """
3863
+ if bool(tl.DeleteClips(items, ripple)):
3864
+ return True
3865
+ presence = _timeline_items_presence(tl, items)
3866
+ if presence != "present":
3867
+ return presence == "absent"
3868
+ if bool(tl.DeleteClips(items, ripple)):
3869
+ return True
3870
+ return _timeline_items_presence(tl, items) == "absent"
3871
+
3872
+
3798
3873
  def _timeline_items_by_ids(tl, ids, track_types=("video", "audio", "subtitle")):
3799
3874
  ids_set = {str(item_id) for item_id in ids if item_id is not None}
3800
3875
  found = []
@@ -4168,7 +4243,7 @@ def _timeline_duplicate_clips_impl(proj, tl, p: Dict[str, Any], *, delete_source
4168
4243
  seen_delete_ids.add(item_id)
4169
4244
  if delete_items:
4170
4245
  try:
4171
- out["deleted_sources"] = bool(tl.DeleteClips(delete_items, bool(p.get("ripple", False))))
4246
+ out["deleted_sources"] = _timeline_delete_clips_verified(tl, delete_items, bool(p.get("ripple", False)))
4172
4247
  out["deleted_source_ids"] = _timeline_item_ids(delete_items)
4173
4248
  except Exception as exc:
4174
4249
  out["deleted_sources"] = False
@@ -4283,7 +4358,7 @@ def _timeline_copy_range_impl(proj, tl, p: Dict[str, Any], *, overwrite: bool =
4283
4358
  if existing_start < dest_end and existing_end > dest_start:
4284
4359
  delete_targets.append(existing)
4285
4360
  if delete_targets:
4286
- deleted = bool(tl.DeleteClips(delete_targets, False))
4361
+ deleted = _timeline_delete_clips_verified(tl, delete_targets, False)
4287
4362
 
4288
4363
  results = []
4289
4364
  for track_type, source_track, item, overlap_start, overlap_end in items:
@@ -4376,7 +4451,7 @@ def _timeline_lift_range_impl(tl, p: Dict[str, Any]):
4376
4451
  return {"success": True, "deleted": 0, "range": {"start": start, "end": end}}
4377
4452
  deleted_ids = _timeline_item_ids(delete_items)
4378
4453
  return {
4379
- "success": bool(tl.DeleteClips(delete_items, bool(p.get("ripple", False)))),
4454
+ "success": _timeline_delete_clips_verified(tl, delete_items, bool(p.get("ripple", False))),
4380
4455
  "deleted": len(delete_items),
4381
4456
  "deleted_ids": deleted_ids,
4382
4457
  "range": {"start": start, "end": end},
@@ -20773,7 +20848,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
20773
20848
  blocked = _consume_confirm_token(action="timeline.delete_clips_ripple", params=p)
20774
20849
  if blocked:
20775
20850
  return blocked
20776
- return {"success": bool(tl.DeleteClips(found, ripple))}
20851
+ return {"success": _timeline_delete_clips_verified(tl, found, ripple)}
20777
20852
  elif action == "set_clips_linked":
20778
20853
  ids_set = set(p["clip_ids"])
20779
20854
  found = []
@@ -807,6 +807,81 @@ API_TRUTH: List[Dict[str, Any]] = [
807
807
  "when mirroring keep-ranges into clipInfos.",
808
808
  "tags": ["timeline", "edit", "off-by-one", "readback"],
809
809
  },
810
+ {
811
+ "symbol": "Timeline.DeleteClips (flaky first attempt)",
812
+ "object": "Timeline",
813
+ "signature": "([TimelineItem], ripple) -> bool",
814
+ "reality": "Can return False on the first call even when every item in "
815
+ "the list is a valid, present TimelineItem; an identical "
816
+ "immediate retry succeeds. Observed once, on Studio 21.0 "
817
+ "during a cut-video edit session (items confirmed still "
818
+ "present after the False, deleted cleanly on retry). Cause "
819
+ "unknown — do NOT read this as the ProjectManager."
820
+ "DeleteProject shape: that one has an identified mechanism "
821
+ "(the project being, or recently having been, current) that "
822
+ "retrying does not clear, whereas a single retry cleared "
823
+ "this in the one instance seen. One observation is not a "
824
+ "mechanism; if a retry is ever seen to fail repeatedly here, "
825
+ "this entry needs revisiting.",
826
+ "recommended": "Treat a False return as advisory: re-list the track and "
827
+ "check whether the items are actually gone; if still "
828
+ "present, retry the identical call once before failing. "
829
+ "A readback that raised, enumerated nothing, or covered "
830
+ "items whose unique ID cannot be read is UNKNOWN, not "
831
+ "gone — never report an unverifiable delete as success, "
832
+ "and do not spend a second destructive call on an "
833
+ "outcome you equally cannot read.",
834
+ "tags": ["unreliable-return", "flaky", "timeline", "edit"],
835
+ "submit": "bug",
836
+ "mitigation": ["_timeline_delete_clips_verified", "_timeline_items_presence"],
837
+ },
838
+ {
839
+ "symbol": "Timeline.DeleteClips (linked audio not deleted)",
840
+ "object": "Timeline",
841
+ "signature": "([TimelineItem], ripple) -> bool",
842
+ "reality": "Deleting video items does NOT delete their linked audio "
843
+ "items — the UI's linked-selection behavior does not apply "
844
+ "to the API, which deletes exactly the items passed. The "
845
+ "orphaned audio stays on its track and collides with any "
846
+ "later append into the same record range.",
847
+ "recommended": "When deleting a video item that has linked audio, list "
848
+ "the audio track(s) (timeline get_items, track_type "
849
+ "'audio'), find the overlapping linked items, and pass "
850
+ "their IDs in the same delete. Verify with "
851
+ "detect_gaps_overlaps across both track types.",
852
+ "tags": ["timeline", "edit", "audio", "silent-failure"],
853
+ },
854
+ {
855
+ "symbol": "MediaPool.AppendToTimeline with mixed-fps sources (duration floor)",
856
+ "object": "MediaPool",
857
+ "signature": "([{mediaPoolItem, startFrame, endFrame, recordFrame, ...}]) -> [TimelineItem]",
858
+ "reality": "start/endFrame are in SOURCE frames. When the source fps "
859
+ "differs from the timeline fps (e.g. 24.0 or 29.97 source in "
860
+ "a 23.976 timeline), Resolve converts the source range to "
861
+ "timeline frames by flooring — so a range planned to fill an "
862
+ "exact record slot lands one frame short, leaving a 1-frame "
863
+ "gap before the next clip.",
864
+ "recommended": "Plan durations in timeline frames "
865
+ "(floor(src_frames * timeline_fps / source_fps)); if the "
866
+ "floored duration misses the slot, extend endFrame by a "
867
+ "source frame and re-check. Always finish with "
868
+ "detect_gaps_overlaps.",
869
+ "tags": ["timeline", "edit", "off-by-one", "mixed-fps"],
870
+ "submit": "bug",
871
+ },
872
+ {
873
+ "symbol": "MediaPool.ImportMedia (current-folder destination only)",
874
+ "object": "MediaPool",
875
+ "signature": "([paths] | [clipInfos]) -> [MediaPoolItem]",
876
+ "reality": "Imports always land in the CURRENT media pool folder; the "
877
+ "call has no destination-folder parameter, and passing an "
878
+ "unrecognized one to the MCP tool is silently ignored.",
879
+ "recommended": "SetCurrentFolder to the target bin first (media_pool "
880
+ "set_current_folder), import, then restore the previous "
881
+ "current folder if it matters.",
882
+ "tags": ["media-pool", "import"],
883
+ "submit": "missing",
884
+ },
810
885
  {
811
886
  "symbol": "Graph.SetLUT (master-LUT-dir-only resolution)",
812
887
  "object": "Graph",