davinci-resolve-mcp 2.57.4 → 2.57.5

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,37 @@
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.57.5
6
+
7
+ Fixes a silent data-loss bug on Reel Name writes (issue #77) and bundles the
8
+ community-contributed API-limitation entries from PR #76 (issue #75).
9
+
10
+ - **Fixed** (#77) `set_clip_property` / `set_metadata` reporting `success: true`
11
+ when writing the `Reel Name` clip property even though Resolve silently
12
+ dropped the value. Resolve gates `Reel Name` behind the project setting
13
+ *General Options > Assist using reel names from the:* — when that derives reel
14
+ names automatically, scripted writes are ignored but still return `True`. On a
15
+ batch ingest this meant hundreds of clips believed to have reel names assigned
16
+ when none actually stuck. The server now reads the value back after writing
17
+ known-unreliable keys and refuses to report success on mismatch, surfacing the
18
+ project-setting gate as a `hint`. Wired into `set_clip_property`,
19
+ `set_metadata` (both forms), and the bulk `normalize_metadata` path. Adds an
20
+ `api_truth` bug entry (report now lists 18 missing + 11 bugs) and 10 unit
21
+ tests.
22
+ - **Added** (PR #76, thanks @swayll) four `submit: missing` API-limitation
23
+ entries verified on Resolve 21.0.0: per-subtitle text/timing editing, subtitle
24
+ track styling/presets, speech-recognition engine selection + SRT import, and
25
+ Media Pool folder rename. These document the API ceiling behind the subtitle
26
+ feature request in #75 — direct subtitle text editing and SRT round-trip are
27
+ not exposed by the Blackmagic scripting API.
28
+
29
+ ### Validation
30
+
31
+ - Offline: full unit suite green; new `tests/test_reel_name_writeback.py` (10
32
+ tests) plus the api_truth/limitations drift guards. Live Resolve validation of
33
+ the Reel Name read-back is recommended but not performed here — forcing the
34
+ gate would require writing reel names into a live project's media metadata.
35
+
5
36
  ## What's New in v2.57.4
6
37
 
7
38
  Live mutating verification of the catalogued API gaps.
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.57.4-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.57.5-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)
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.0
14
14
 
15
- **Totals:** 14 missing capabilities, 10 bugs / unreliable behaviors.
15
+ **Totals:** 18 missing capabilities, 11 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
@@ -141,6 +141,34 @@ equivalent, blocking full automation.
141
141
  - **Workaround / current handling:** Create Smart/Power Bins in the Resolve UI; only regular bins are scriptable.
142
142
  - **Tags:** missing-method, media-pool, bins
143
143
 
144
+ ### Per-subtitle text content and timing editing
145
+
146
+ - **Object:** `TimelineItem (subtitle track)`
147
+ - **Behavior:** TimelineItem on a subtitle track exposes only 21 standard transform/composite properties (Pan, Tilt, ZoomX, Opacity, Crop, etc.). There are no methods to get or set subtitle text (GetText/SetText), start time, end time, or duration for individual subtitle items. Subtitles created via CreateSubtitlesFromAudio or imported via the Resolve UI cannot have their content or timing read or modified programmatically. Verified via dir() and GetProperty() on Resolve 21.0.0.48.
148
+ - **Workaround / current handling:** No workaround exists — subtitle text and timing are completely inaccessible from the scripting API. Must be edited in the Resolve UI.
149
+ - **Tags:** missing-method, subtitle, text, timing
150
+
151
+ ### Subtitle track styling and presets
152
+
153
+ - **Object:** `TimelineItem / Timeline / Project`
154
+ - **Behavior:** There is no API method to set or query subtitle font family, font size, text color, background color, outline, shadow, position, alignment, or to apply/query subtitle style presets. TimelineItem.GetProperty() on subtitle items returns only transform/composite keys. Timeline.GetSetting() and Project.GetSetting() return None for all probed subtitle-style keys (e.g. 'subtitleFontName', 'subtitleFontSize', 'subtitleTextColor', 'subtitleBackgroundColor', 'subtitlePosition', 'subtitleAlignment', 'subtitlePreset', 'subtitleStyle'). Verified via dir(), GetProperty(), and GetSetting() on Resolve 21.0.0.48.
155
+ - **Workaround / current handling:** No workaround exists — subtitle styling is UI-only. Burn-in overlays via Fusion titles are a visual alternative but do not produce proper subtitle tracks.
156
+ - **Tags:** missing-method, subtitle, style, preset
157
+
158
+ ### Speech recognition engine selection and SRT import
159
+
160
+ - **Object:** `Timeline`
161
+ - **Behavior:** Timeline.CreateSubtitlesFromAudio(autoCaptionSettings) always uses the built-in Resolve speech recognition engine. There is no API parameter to select an alternative provider (e.g. whisper-cli, Google Speech, AWS Transcribe). The language selection via resolve.AUTO_CAPTION_LANGUAGE_* is the only customization; the engine itself cannot be changed. Furthermore, there is no API method to import an SRT file into a subtitle track programmatically — File -> Import -> Subtitle is UI-only.
162
+ - **Workaround / current handling:** No workaround exists for provider selection or SRT import. External transcripts must be converted to SRT and imported through the Resolve UI.
163
+ - **Tags:** missing-method, subtitle, transcription, speech-recognition, asr
164
+
165
+ ### Media Pool folder rename
166
+
167
+ - **Object:** `MediaPool`
168
+ - **Behavior:** MediaPool exposes AddSubFolder(name), DeleteSubFolders([names]), and MoveFolders([names], targetFolder) but no RenameSubFolder(oldName, newName) method. Folders can be created, deleted, and moved, but their names cannot be changed through the API. Verified via dir() on Resolve 21.0.0.
169
+ - **Workaround / current handling:** Delete and recreate the folder with the desired name, or rename in the Resolve UI.
170
+ - **Tags:** missing-method, media-pool, folder
171
+
144
172
  ## Bugs / Unreliable Behavior (please fix)
145
173
 
146
174
  Methods that exist but misbehave — silent failures, unreliable return
@@ -220,3 +248,12 @@ values, or automation-hostile modal prompts.
220
248
  - **Behavior:** The Python bridge returns a callable for ANY attribute name, so hasattr(obj, 'TotallyMadeUpMethod') is always True and getattr never raises. This makes capability detection by hasattr impossible — verified on 21.0.0 (hasattr reported SetStart, Razor, AddNode, GenerateProxy, AddSmartBin etc. as present though none exist). Only dir() lists the real methods.
221
249
  - **Workaround / current handling:** Never probe method existence with hasattr/getattr; test membership against dir(obj) instead. Calling a fabricated method typically returns None/False with no error.
222
250
  - **Tags:** bridge, introspection, silent-failure
251
+
252
+ ### MediaPoolItem.SetClipProperty('Reel Name', ...)
253
+
254
+ - **Object:** `MediaPoolItem`
255
+ - **Signature:** `(propertyName, propertyValue) -> bool`
256
+ - **Behavior:** Setting the 'Reel Name' clip property returns True but the value is silently dropped on read-back when the project is configured to derive reel names automatically (General Options > 'Assist using reel names from the:' set to source clip file / embedding / filename pattern). The same True-but-unpersisted behavior occurs via SetMetadata('Reel Name', ...). Other clip properties on the same clip (e.g. 'Comments') write and persist normally, so this is field-specific, not a bridge/permission failure. Verified on Resolve 21.0.0; reported as issue #77.
257
+ - **Workaround / current handling:** After writing 'Reel Name', read it back with GetClipProperty('Reel Name') and refuse to report success on mismatch; surface the project-setting gate to the caller (server._verify_clip_property_writeback).
258
+ - **Reference:** [issue #77](https://github.com/samuelgursky/davinci-resolve-mcp/issues/77)
259
+ - **Tags:** unreliable-return, silent-failure, metadata, reel-name
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.57.4"
38
+ VERSION = "2.57.5"
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.57.4",
3
+ "version": "2.57.5",
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.57.4"
83
+ VERSION = "2.57.5"
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.57.4"
14
+ VERSION = "2.57.5"
15
15
 
16
16
  import base64
17
17
  import os
@@ -6559,6 +6559,78 @@ def _safe_clip_call(clip, method_name: str, *args):
6559
6559
  return None, str(exc)
6560
6560
 
6561
6561
 
6562
+ # Clip-property keys whose SetClipProperty/SetMetadata write returns True but is
6563
+ # silently dropped by Resolve under common project configurations (issue #77).
6564
+ # For these we read the value back and refuse to report success unless it stuck.
6565
+ _WRITEBACK_VERIFY_KEYS = frozenset({"Reel Name"})
6566
+
6567
+ # Per-key hint explaining the most common cause of a non-persisting write.
6568
+ _WRITEBACK_VERIFY_HINTS = {
6569
+ "Reel Name": (
6570
+ "Reel Name is commonly gated by the project setting "
6571
+ "'General Options > Assist using reel names from the:'. When that is "
6572
+ "set to derive reel names from the source clip / embedding / filename "
6573
+ "pattern, Resolve ignores scripted writes to 'Reel Name' (the call "
6574
+ "still returns True). Set that option to blank/manual, or assign reel "
6575
+ "names in the Resolve UI; the scripting API cannot override the gate."
6576
+ ),
6577
+ }
6578
+
6579
+
6580
+ def _normalize_clip_property_value(value):
6581
+ """Coerce a clip-property value to a comparable, trimmed string."""
6582
+ if value is None:
6583
+ return ""
6584
+ return str(value).strip()
6585
+
6586
+
6587
+ def _verify_clip_property_writeback(clip, key, value):
6588
+ """Confirm a just-written, known-unreliable clip property actually persisted.
6589
+
6590
+ Returns a structured failure dict when Resolve accepted the write (returned
6591
+ True) but the value did not stick on read-back; returns ``None`` when the
6592
+ write verified, the key is not in the watch list, or we cannot read it back
6593
+ (in which case we don't override the original success). See issue #77.
6594
+ """
6595
+ if key not in _WRITEBACK_VERIFY_KEYS:
6596
+ return None
6597
+ actual, err = _safe_clip_call(clip, "GetClipProperty", key)
6598
+ if err is not None:
6599
+ return None # can't read back — don't contradict the reported result
6600
+ if isinstance(actual, dict): # GetClipProperty('') style dict, just in case
6601
+ actual = actual.get(key)
6602
+ if _normalize_clip_property_value(actual) == _normalize_clip_property_value(value):
6603
+ return None
6604
+ return {
6605
+ "success": False,
6606
+ "verified": False,
6607
+ "key": key,
6608
+ "requested": value,
6609
+ "actual": actual,
6610
+ "error": (
6611
+ f"Resolve reported success writing {key!r} but the value did not "
6612
+ f"persist (read-back returned {actual!r})."
6613
+ ),
6614
+ "hint": _WRITEBACK_VERIFY_HINTS.get(key),
6615
+ }
6616
+
6617
+
6618
+ def _verify_writeback(clip, written):
6619
+ """Verify a batch of just-written {key: value} pairs.
6620
+
6621
+ Returns the first silent-revert failure dict, else ``None``. Only keys in
6622
+ ``_WRITEBACK_VERIFY_KEYS`` are checked, so ordinary writes pay nothing.
6623
+ """
6624
+ if not isinstance(written, dict):
6625
+ return None
6626
+ for key, value in written.items():
6627
+ if key in _WRITEBACK_VERIFY_KEYS:
6628
+ fail = _verify_clip_property_writeback(clip, key, value)
6629
+ if fail:
6630
+ return fail
6631
+ return None
6632
+
6633
+
6562
6634
  def _media_pool_item_summary(clip):
6563
6635
  if not clip:
6564
6636
  return None
@@ -10213,7 +10285,17 @@ def _normalize_metadata(root, mp, p: Dict[str, Any]):
10213
10285
  third_party_ok = True
10214
10286
  for key, value in third_party.items():
10215
10287
  third_party_ok = bool(clip.SetThirdPartyMetadata(key, value)) and third_party_ok
10216
- results.append({"clip_id": clip_id, "success": ok and third_party_ok})
10288
+ row = {"clip_id": clip_id, "success": ok and third_party_ok}
10289
+ if ok and metadata:
10290
+ silent = _verify_writeback(clip, metadata)
10291
+ if silent:
10292
+ row["success"] = False
10293
+ row["verified"] = False
10294
+ row["unpersisted"] = {silent["key"]: silent["actual"]}
10295
+ row["error"] = silent["error"]
10296
+ if silent.get("hint"):
10297
+ row["hint"] = silent["hint"]
10298
+ results.append(row)
10217
10299
  return {"success": all(row.get("success") for row in results), "count": len(results), "missing": missing, "results": results}
10218
10300
 
10219
10301
 
@@ -15696,8 +15778,18 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
15696
15778
  return {"metadata": _ser(clip.GetMetadata(p.get("key", "")))}
15697
15779
  elif action == "set_metadata":
15698
15780
  if "metadata" in p:
15699
- return {"success": bool(clip.SetMetadata(p["metadata"]))}
15700
- return {"success": bool(clip.SetMetadata(p["key"], p["value"]))}
15781
+ ok = bool(clip.SetMetadata(p["metadata"]))
15782
+ if ok:
15783
+ silent = _verify_writeback(clip, p["metadata"])
15784
+ if silent:
15785
+ return silent
15786
+ return {"success": ok}
15787
+ ok = bool(clip.SetMetadata(p["key"], p["value"]))
15788
+ if ok:
15789
+ silent = _verify_clip_property_writeback(clip, p["key"], p["value"])
15790
+ if silent:
15791
+ return silent
15792
+ return {"success": ok}
15701
15793
  elif action == "get_third_party_metadata":
15702
15794
  return {"metadata": _ser(clip.GetThirdPartyMetadata(p.get("key", "")))}
15703
15795
  elif action == "set_third_party_metadata":
@@ -15707,7 +15799,12 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
15707
15799
  elif action == "get_clip_property":
15708
15800
  return {"properties": _ser(clip.GetClipProperty(p.get("key", "")))}
15709
15801
  elif action == "set_clip_property":
15710
- return {"success": bool(clip.SetClipProperty(p["key"], p["value"]))}
15802
+ ok = bool(clip.SetClipProperty(p["key"], p["value"]))
15803
+ if ok:
15804
+ silent = _verify_clip_property_writeback(clip, p["key"], p["value"])
15805
+ if silent:
15806
+ return silent
15807
+ return {"success": ok}
15711
15808
  elif action == "get_clip_color":
15712
15809
  return {"color": clip.GetClipColor()}
15713
15810
  elif action == "set_clip_color":
@@ -366,6 +366,79 @@ API_TRUTH: List[Dict[str, Any]] = [
366
366
  "tags": ["missing-method", "media-pool", "bins"],
367
367
  "submit": "missing",
368
368
  },
369
+ {
370
+ "symbol": "Per-subtitle text content and timing editing",
371
+ "object": "TimelineItem (subtitle track)",
372
+ "reality": "TimelineItem on a subtitle track exposes only 21 standard "
373
+ "transform/composite properties (Pan, Tilt, ZoomX, Opacity, "
374
+ "Crop, etc.). There are no methods to get or set subtitle "
375
+ "text (GetText/SetText), start time, end time, or duration "
376
+ "for individual subtitle items. Subtitles created via "
377
+ "CreateSubtitlesFromAudio or imported via the Resolve UI "
378
+ "cannot have their content or timing read or modified "
379
+ "programmatically. Verified via dir() and GetProperty() on "
380
+ "Resolve 21.0.0.48.",
381
+ "recommended": "No workaround exists — subtitle text and timing are "
382
+ "completely inaccessible from the scripting API. Must "
383
+ "be edited in the Resolve UI.",
384
+ "tags": ["missing-method", "subtitle", "text", "timing"],
385
+ "submit": "missing",
386
+ },
387
+ {
388
+ "symbol": "Subtitle track styling and presets",
389
+ "object": "TimelineItem / Timeline / Project",
390
+ "reality": "There is no API method to set or query subtitle font "
391
+ "family, font size, text color, background color, outline, "
392
+ "shadow, position, alignment, or to apply/query subtitle "
393
+ "style presets. TimelineItem.GetProperty() on subtitle "
394
+ "items returns only transform/composite keys. "
395
+ "Timeline.GetSetting() and Project.GetSetting() return "
396
+ "None for all probed subtitle-style keys (e.g. "
397
+ "'subtitleFontName', 'subtitleFontSize', "
398
+ "'subtitleTextColor', 'subtitleBackgroundColor', "
399
+ "'subtitlePosition', 'subtitleAlignment', "
400
+ "'subtitlePreset', 'subtitleStyle'). Verified via dir(), "
401
+ "GetProperty(), and GetSetting() on Resolve 21.0.0.48.",
402
+ "recommended": "No workaround exists — subtitle styling is UI-only. "
403
+ "Burn-in overlays via Fusion titles are a visual "
404
+ "alternative but do not produce proper subtitle tracks.",
405
+ "tags": ["missing-method", "subtitle", "style", "preset"],
406
+ "submit": "missing",
407
+ },
408
+ {
409
+ "symbol": "Speech recognition engine selection and SRT import",
410
+ "object": "Timeline",
411
+ "reality": "Timeline.CreateSubtitlesFromAudio(autoCaptionSettings) "
412
+ "always uses the built-in Resolve speech recognition "
413
+ "engine. There is no API parameter to select an alternative "
414
+ "provider (e.g. whisper-cli, Google Speech, AWS Transcribe). "
415
+ "The language selection via resolve.AUTO_CAPTION_LANGUAGE_* "
416
+ "is the only customization; the engine itself cannot be "
417
+ "changed. Furthermore, there is no API method to import an "
418
+ "SRT file into a subtitle track programmatically — "
419
+ "File -> Import -> Subtitle is UI-only.",
420
+ "recommended": "No workaround exists for provider selection or SRT "
421
+ "import. External transcripts must be converted to SRT "
422
+ "and imported through the Resolve UI.",
423
+ "tags": ["missing-method", "subtitle", "transcription",
424
+ "speech-recognition", "asr"],
425
+ "submit": "missing",
426
+ },
427
+ {
428
+ "symbol": "Media Pool folder rename",
429
+ "object": "MediaPool",
430
+ "reality": "MediaPool exposes AddSubFolder(name), "
431
+ "DeleteSubFolders([names]), and "
432
+ "MoveFolders([names], targetFolder) but no "
433
+ "RenameSubFolder(oldName, newName) method. Folders can be "
434
+ "created, deleted, and moved, but their names cannot be "
435
+ "changed through the API. Verified via dir() on Resolve "
436
+ "21.0.0.",
437
+ "recommended": "Delete and recreate the folder with the desired name, "
438
+ "or rename in the Resolve UI.",
439
+ "tags": ["missing-method", "media-pool", "folder"],
440
+ "submit": "missing",
441
+ },
369
442
  {
370
443
  "symbol": "hasattr() / getattr() on Resolve API objects (attribute fabrication)",
371
444
  "object": "(all Resolve scripting objects)",
@@ -391,6 +464,29 @@ API_TRUTH: List[Dict[str, Any]] = [
391
464
  "run while serving over stdio.",
392
465
  "tags": ["runtime", "stdio", "subprocess"],
393
466
  },
467
+ {
468
+ "symbol": "MediaPoolItem.SetClipProperty('Reel Name', ...)",
469
+ "object": "MediaPoolItem",
470
+ "signature": "(propertyName, propertyValue) -> bool",
471
+ "reality": "Setting the 'Reel Name' clip property returns True but the "
472
+ "value is silently dropped on read-back when the project is "
473
+ "configured to derive reel names automatically "
474
+ "(General Options > 'Assist using reel names from the:' set "
475
+ "to source clip file / embedding / filename pattern). The "
476
+ "same True-but-unpersisted behavior occurs via "
477
+ "SetMetadata('Reel Name', ...). Other clip properties on the "
478
+ "same clip (e.g. 'Comments') write and persist normally, so "
479
+ "this is field-specific, not a bridge/permission failure. "
480
+ "Verified on Resolve 21.0.0; reported as issue #77.",
481
+ "recommended": "After writing 'Reel Name', read it back with "
482
+ "GetClipProperty('Reel Name') and refuse to report "
483
+ "success on mismatch; surface the project-setting gate to "
484
+ "the caller (server._verify_clip_property_writeback).",
485
+ "tags": ["unreliable-return", "silent-failure", "metadata", "reel-name"],
486
+ "submit": "bug",
487
+ "issue": 77,
488
+ "mitigation": ["_verify_clip_property_writeback", "_verify_writeback"],
489
+ },
394
490
  ]
395
491
 
396
492