davinci-resolve-mcp 2.33.3 → 2.33.4

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.33.4
6
+
7
+ Internal reliability framework.
8
+
9
+ - **Added** `verify_by_readback` (`src/utils/readback.py`) — a primitive for
10
+ mutating Resolve ops that verifies an action by reading the real post-state
11
+ back instead of trusting the API's frequently-unreliable return value. A
12
+ contradiction (reported success but a failing readback) is logged as a
13
+ reliability signal.
14
+ - **Changed** Auto-sync audio now runs through `verify_by_readback` as its first
15
+ user and reports a `verified` field alongside `linked`/`newly_linked`. Behavior
16
+ is unchanged; the bespoke readback loop is replaced by the shared primitive.
17
+
5
18
  ## What's New in v2.33.3
6
19
 
7
20
  Two read tools that surface existing project state.
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.33.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.33.4-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/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.33.3"
38
+ VERSION = "2.33.4"
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.33.3",
3
+ "version": "2.33.4",
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.33.3"
83
+ VERSION = "2.33.4"
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.33.3"
14
+ VERSION = "2.33.4"
15
15
 
16
16
  import base64
17
17
  import os
@@ -43,6 +43,7 @@ for p in [current_dir, project_dir]:
43
43
  # Platform-specific Resolve paths
44
44
  from src.utils.cdl import normalize_cdl_payload
45
45
  from src.utils.mcp_stdio import run_fastmcp_stdio
46
+ from src.utils.readback import verify_by_readback
46
47
  from src.utils.update_check import (
47
48
  check_for_updates,
48
49
  clear_update_prompt_preferences,
@@ -5535,19 +5536,34 @@ def _safe_auto_sync_audio(mp, p: Dict[str, Any]):
5535
5536
  # Read-back verification: AutoSyncAudio's boolean return is unreliable, so
5536
5537
  # capture each clip's "Synced Audio" linkage before and after and report the
5537
5538
  # delta. Trust `linked`/`newly_linked`, not `success`.
5538
- before = [_synced_audio(c) for c in clips]
5539
- ok = bool(mp.AutoSyncAudio(clips, settings))
5540
- linked, newly_linked, already_linked = [], [], []
5541
- for c, was in zip(clips, before):
5542
- name = _clip_name(c)
5543
- if _synced_audio(c):
5544
- linked.append(name)
5545
- (already_linked if was else newly_linked).append(name)
5539
+ def _categorize(before, observed):
5540
+ linked, newly, already = [], [], []
5541
+ for c, was, now in zip(clips, before, observed):
5542
+ if now:
5543
+ name = _clip_name(c)
5544
+ linked.append(name)
5545
+ (already if was else newly).append(name)
5546
+ return {
5547
+ "linked": linked,
5548
+ "newly_linked": newly,
5549
+ "already_linked": already,
5550
+ "verified": bool(linked),
5551
+ }
5552
+
5553
+ res = verify_by_readback(
5554
+ mutate=lambda: mp.AutoSyncAudio(clips, settings),
5555
+ observe=lambda: [_synced_audio(c) for c in clips],
5556
+ snapshot=lambda: [_synced_audio(c) for c in clips],
5557
+ compare=_categorize,
5558
+ label="auto_sync_audio",
5559
+ intent={"clip_count": len(clips)},
5560
+ )
5546
5561
  return {
5547
- "success": ok,
5548
- "linked": linked,
5549
- "newly_linked": newly_linked,
5550
- "already_linked": already_linked,
5562
+ "success": res["success_raw"],
5563
+ "verified": res["verified"],
5564
+ "linked": res["linked"],
5565
+ "newly_linked": res["newly_linked"],
5566
+ "already_linked": res["already_linked"],
5551
5567
  "count": len(clips),
5552
5568
  "missing": missing,
5553
5569
  "settings": settings,
@@ -0,0 +1,70 @@
1
+ """Readback verification for unreliable Resolve API mutations.
2
+
3
+ The Resolve scripting API frequently returns a success value that does not
4
+ reflect what actually happened: ``AutoSyncAudio`` returns a boolean unrelated to
5
+ whether clips linked, ``AppendToTimeline`` may not hand back the new item id,
6
+ many setters return ``True`` regardless of effect, and Fusion ``Paste()`` can
7
+ report nothing while creating nothing. The defense is to **verify by reading the
8
+ real post-state back** instead of trusting the return value.
9
+
10
+ ``verify_by_readback`` is the small primitive every mutating op can use. A
11
+ mutation that reports success while the readback disagrees is a *contradiction*
12
+ — the single most valuable reliability signal — and is logged.
13
+ """
14
+ import logging
15
+ from typing import Any, Callable, Dict, Optional
16
+
17
+ logger = logging.getLogger("davinci-resolve-mcp")
18
+
19
+
20
+ def verify_by_readback(
21
+ mutate: Callable[[], Any],
22
+ observe: Callable[[], Any],
23
+ *,
24
+ snapshot: Optional[Callable[[], Any]] = None,
25
+ compare: Optional[Callable[[Any, Any], Dict[str, Any]]] = None,
26
+ intent: Optional[Dict[str, Any]] = None,
27
+ label: Optional[str] = None,
28
+ ) -> Dict[str, Any]:
29
+ """Run a mutation and verify it by reading actual state back.
30
+
31
+ Args:
32
+ mutate: performs the Resolve call; its return value is the unreliable
33
+ ``success_raw``.
34
+ observe: reads the relevant state AFTER the mutation.
35
+ snapshot: optional — captures pre-state for delta comparisons (passed to
36
+ ``compare`` as ``before``).
37
+ compare: ``(before, observed) -> dict`` merged into the result. It should
38
+ set ``verified: bool``. Defaults to ``verified = truthy(observed)``.
39
+ intent: optional description of what we meant to do (for the ledger).
40
+ label: optional op name, used in the contradiction log line.
41
+
42
+ Returns a dict with at least ``success_raw`` and ``verified``, plus whatever
43
+ ``compare`` contributed and (if given) ``intent``.
44
+ """
45
+ before = snapshot() if snapshot is not None else None
46
+ raw = mutate()
47
+ observed = observe()
48
+
49
+ result: Dict[str, Any] = {"success_raw": bool(raw), "observed": observed}
50
+ if compare is not None:
51
+ result.update(compare(before, observed))
52
+ else:
53
+ result["verified"] = bool(observed)
54
+ if intent is not None:
55
+ result["intent"] = intent
56
+
57
+ # A contradiction — reported success but the readback disagrees — is the
58
+ # signal worth surfacing loudly.
59
+ if result.get("success_raw") and not result.get("verified"):
60
+ logger.warning(
61
+ "readback contradiction%s: API reported success but post-state "
62
+ "verification failed%s",
63
+ f" [{label}]" if label else "",
64
+ f" (intent={intent})" if intent else "",
65
+ )
66
+ result["contradiction"] = True
67
+ else:
68
+ result["contradiction"] = False
69
+
70
+ return result