davinci-resolve-mcp 2.33.3 → 2.33.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,33 @@
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.5
6
+
7
+ A queryable ledger of verified Resolve API behavior.
8
+
9
+ - **Added** `resolve_control(action="api_truth", query?)` returns
10
+ behaviorally-verified facts about quirky or unreliable Resolve scripting-API
11
+ behavior — methods that live on unexpected objects, return values that lie,
12
+ silently-rejected string keys, and calls that don't exist. Each fact records
13
+ the reality, a recommended approach, and the Resolve build it was verified on.
14
+ No connection required. Backed by `src/utils/api_truth.py`, seeded from
15
+ hard-won findings (AutoSyncAudio, Fusion Paste, FlowView positions,
16
+ GetTimelineByName, project render methods, transcription truncation, the
17
+ CreateProject modal, stdio subprocess hygiene) and meant to grow over time.
18
+
19
+ ## What's New in v2.33.4
20
+
21
+ Internal reliability framework.
22
+
23
+ - **Added** `verify_by_readback` (`src/utils/readback.py`) — a primitive for
24
+ mutating Resolve ops that verifies an action by reading the real post-state
25
+ back instead of trusting the API's frequently-unreliable return value. A
26
+ contradiction (reported success but a failing readback) is logged as a
27
+ reliability signal.
28
+ - **Changed** Auto-sync audio now runs through `verify_by_readback` as its first
29
+ user and reports a `verified` field alongside `linked`/`newly_linked`. Behavior
30
+ is unchanged; the bespoke readback loop is replaced by the shared primitive.
31
+
5
32
  ## What's New in v2.33.3
6
33
 
7
34
  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.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-32%20(341%20full)-blue.svg)](#server-modes)
package/docs/SKILL.md CHANGED
@@ -318,6 +318,8 @@ Key actions:
318
318
  - `launch` — connect to or start Resolve; call this first if any tool returns a
319
319
  "Not connected" error
320
320
  - `get_version` — returns `{product, version, version_string}`
321
+ - `api_truth(query?)` — look up behaviorally-verified facts about quirky/unreliable
322
+ Resolve API behavior (no connection needed); filter by substring
321
323
  - `get_page` / `open_page(page)` — read or switch the active page
322
324
  - `get_keyframe_mode` / `set_keyframe_mode(mode)`
323
325
  - `get_fairlight_presets` — Resolve 20.2.2+; returns available Fairlight
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.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.33.3",
3
+ "version": "2.33.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.33.3"
83
+ VERSION = "2.33.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.33.3"
14
+ VERSION = "2.33.5"
15
15
 
16
16
  import base64
17
17
  import os
@@ -43,6 +43,8 @@ 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.api_truth import lookup_api_truth, VERIFIED_ON as _API_TRUTH_VERIFIED_ON
47
+ from src.utils.readback import verify_by_readback
46
48
  from src.utils.update_check import (
47
49
  check_for_updates,
48
50
  clear_update_prompt_preferences,
@@ -5535,19 +5537,34 @@ def _safe_auto_sync_audio(mp, p: Dict[str, Any]):
5535
5537
  # Read-back verification: AutoSyncAudio's boolean return is unreliable, so
5536
5538
  # capture each clip's "Synced Audio" linkage before and after and report the
5537
5539
  # 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)
5540
+ def _categorize(before, observed):
5541
+ linked, newly, already = [], [], []
5542
+ for c, was, now in zip(clips, before, observed):
5543
+ if now:
5544
+ name = _clip_name(c)
5545
+ linked.append(name)
5546
+ (already if was else newly).append(name)
5547
+ return {
5548
+ "linked": linked,
5549
+ "newly_linked": newly,
5550
+ "already_linked": already,
5551
+ "verified": bool(linked),
5552
+ }
5553
+
5554
+ res = verify_by_readback(
5555
+ mutate=lambda: mp.AutoSyncAudio(clips, settings),
5556
+ observe=lambda: [_synced_audio(c) for c in clips],
5557
+ snapshot=lambda: [_synced_audio(c) for c in clips],
5558
+ compare=_categorize,
5559
+ label="auto_sync_audio",
5560
+ intent={"clip_count": len(clips)},
5561
+ )
5546
5562
  return {
5547
- "success": ok,
5548
- "linked": linked,
5549
- "newly_linked": newly_linked,
5550
- "already_linked": already_linked,
5563
+ "success": res["success_raw"],
5564
+ "verified": res["verified"],
5565
+ "linked": res["linked"],
5566
+ "newly_linked": res["newly_linked"],
5567
+ "already_linked": res["already_linked"],
5551
5568
  "count": len(clips),
5552
5569
  "missing": missing,
5553
5570
  "settings": settings,
@@ -10559,6 +10576,8 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
10559
10576
  ignore_mcp_update() -> {success, version, update, decision}
10560
10577
  snooze_mcp_update(hours?) -> {success, version, update, decision}
10561
10578
  clear_mcp_update_preferences() -> {success, version, update, decision}
10579
+ api_truth(query?) -> {verified_on, count, facts} — look up behaviorally-verified
10580
+ facts about quirky/unreliable Resolve API behavior (no connection needed).
10562
10581
  get_page() -> {page}
10563
10582
  open_page(page) -> {success} — page: edit, cut, color, fusion, fairlight, deliver
10564
10583
  get_keyframe_mode() -> {mode}
@@ -10579,6 +10598,11 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
10579
10598
  """
10580
10599
  p = params or {}
10581
10600
 
10601
+ # api_truth is a static knowledge lookup — no Resolve connection needed.
10602
+ if action == "api_truth":
10603
+ facts = lookup_api_truth(p.get("query"))
10604
+ return {"verified_on": _API_TRUTH_VERIFIED_ON, "count": len(facts), "facts": facts}
10605
+
10582
10606
  # Control-panel actions don't require Resolve to be running.
10583
10607
  if action == "open_control_panel":
10584
10608
  return _open_control_panel(p)
@@ -10666,7 +10690,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
10666
10690
  return missing
10667
10691
  r.DisableBackgroundTasksForCurrentResolveSession()
10668
10692
  return _ok()
10669
- return _unknown(action, ["launch","get_version","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
10693
+ return _unknown(action, ["launch","get_version","api_truth","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
10670
10694
 
10671
10695
 
10672
10696
  # ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
@@ -0,0 +1,132 @@
1
+ """Behaviorally-verified facts about the DaVinci Resolve scripting API.
2
+
3
+ The Resolve scripting API is under-documented and frequently behaves differently
4
+ from its apparent signature: methods live on objects you wouldn't expect, return
5
+ values lie, string keys are silently rejected, and some documented-looking calls
6
+ don't exist. This module records facts we have *verified against live Resolve* so
7
+ agents and code can look the reality up instead of rediscovering it the hard way.
8
+
9
+ Each entry is a small dict. Grow it opportunistically — every readback recipe and
10
+ every fix that uncovers a surprising behavior should add an entry. Facts are
11
+ stamped with the Resolve build they were verified on so drift is visible when a
12
+ new version ships.
13
+ """
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ VERIFIED_ON = "DaVinci Resolve Studio 21.0.0"
17
+
18
+ # Each entry: symbol, object, reality, recommended, tags. `signature` optional.
19
+ API_TRUTH: List[Dict[str, Any]] = [
20
+ {
21
+ "symbol": "MediaPool.AutoSyncAudio",
22
+ "object": "MediaPool",
23
+ "signature": "(clips, settings) -> bool",
24
+ "reality": "The boolean return does not reflect whether clips actually "
25
+ "linked, and string enum keys in `settings` are silently "
26
+ "rejected (the call returns False).",
27
+ "recommended": "Resolve the AUDIO_SYNC_* enum constants via the live "
28
+ "resolve handle, and verify by reading each clip's "
29
+ "'Synced Audio' property (see verify_by_readback).",
30
+ "tags": ["unreliable-return", "silent-failure", "audio", "enum"],
31
+ },
32
+ {
33
+ "symbol": "Composition.Paste",
34
+ "object": "Fusion Composition",
35
+ "reality": "Passing tool.SaveSettings()'s in-memory table to Paste() / "
36
+ "LoadSettings() fails across the Python bridge with an "
37
+ "OrderedDict/null-argument error and creates no node, while "
38
+ "reporting nothing useful.",
39
+ "recommended": "Duplicate via AddTool(RegID) + SaveSettings(path)/"
40
+ "LoadSettings(path) through a temp .setting FILE, which "
41
+ "round-trips reliably. Identify the new node by name diff.",
42
+ "tags": ["fusion", "bridge", "silent-failure"],
43
+ },
44
+ {
45
+ "symbol": "FlowView.SetPos / FlowView.GetPosTable",
46
+ "object": "Fusion FlowView (comp.CurrentFrame.FlowView)",
47
+ "reality": "Node positions are read/written through the FlowView, not the "
48
+ "tool. SetPos returns nothing reliable; GetPosTable returns a "
49
+ "1-indexed table (or dict/tuple depending on bridge).",
50
+ "recommended": "Use comp.CurrentFrame.FlowView.SetPos(tool, x, y); confirm "
51
+ "with GetPosTable and a liberal position parser.",
52
+ "tags": ["fusion", "unreliable-return"],
53
+ },
54
+ {
55
+ "symbol": "Timeline.GetTimelineByName",
56
+ "object": "Project",
57
+ "reality": "Does not exist. Timelines are looked up by index.",
58
+ "recommended": "Iterate GetTimelineByIndex(1..GetTimelineCount()).",
59
+ "tags": ["missing-method", "timeline"],
60
+ },
61
+ {
62
+ "symbol": "Project render methods (AddRenderJob, SetRenderSettings, ...)",
63
+ "object": "Project",
64
+ "reality": "Render methods live on the Project object, not on a separate "
65
+ "render-settings interface.",
66
+ "recommended": "Call proj.AddRenderJob(), proj.SetRenderSettings(), "
67
+ "proj.LoadRenderPreset() directly on the project.",
68
+ "tags": ["render"],
69
+ },
70
+ {
71
+ "symbol": "MediaPoolItem.GetClipProperty('Transcription')",
72
+ "object": "MediaPoolItem",
73
+ "reality": "Returns a PREVIEW of the transcription that ends in an "
74
+ "ellipsis when the full transcript is longer than the property "
75
+ "exposes.",
76
+ "recommended": "Treat a trailing ellipsis as truncation (see "
77
+ "media_pool_item get_transcription's `truncated` flag).",
78
+ "tags": ["transcription", "truncation"],
79
+ },
80
+ {
81
+ "symbol": "ProjectManager.CreateProject (with a dirty Untitled project)",
82
+ "object": "ProjectManager",
83
+ "reality": "Returns None and pops a modal 'Save Current Project' dialog "
84
+ "when the current unsaved/Untitled project blocks the switch. "
85
+ "SaveProject() on an Untitled project re-triggers the same modal.",
86
+ "recommended": "CloseProject(current) to discard the untitled project "
87
+ "without a prompt, then CreateProject; restore with "
88
+ "LoadProject afterward.",
89
+ "tags": ["project", "modal", "silent-failure"],
90
+ },
91
+ {
92
+ "symbol": "Timeline.InsertFusionCompositionIntoTimeline",
93
+ "object": "Timeline",
94
+ "reality": "Reliable way to obtain a Fusion comp on an otherwise empty "
95
+ "timeline: it inserts a Fusion composition clip whose comp is "
96
+ "then reachable via GetFusionCompByIndex(1).",
97
+ "recommended": "Use it (rather than InsertGeneratorIntoTimeline) when you "
98
+ "need a comp to operate on.",
99
+ "tags": ["fusion", "timeline"],
100
+ },
101
+ {
102
+ "symbol": "subprocess inheriting stdin under the MCP stdio server",
103
+ "object": "(server runtime)",
104
+ "reality": "A child process that inherits stdin can race-read bytes off "
105
+ "the JSON-RPC protocol stream and corrupt it; capture_output "
106
+ "redirects only stdout/stderr.",
107
+ "recommended": "Pass stdin=subprocess.DEVNULL on every subprocess that can "
108
+ "run while serving over stdio.",
109
+ "tags": ["runtime", "stdio", "subprocess"],
110
+ },
111
+ ]
112
+
113
+
114
+ def lookup_api_truth(query: Optional[str] = None) -> List[Dict[str, Any]]:
115
+ """Return verified facts matching `query`, or all facts if no query.
116
+
117
+ Matches a case-insensitive substring against the symbol, tags, and reality.
118
+ """
119
+ if not query:
120
+ return list(API_TRUTH)
121
+ q = query.lower()
122
+ out = []
123
+ for e in API_TRUTH:
124
+ hay = " ".join([
125
+ e.get("symbol", ""),
126
+ e.get("object", ""),
127
+ e.get("reality", ""),
128
+ " ".join(e.get("tags", [])),
129
+ ]).lower()
130
+ if q in hay:
131
+ out.append(e)
132
+ return out
@@ -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