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 +27 -0
- package/README.md +1 -1
- package/docs/SKILL.md +2 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +38 -14
- package/src/utils/api_truth.py +132 -0
- package/src/utils/readback.py +70 -0
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-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.
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.33.
|
|
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.
|
|
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
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
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":
|
|
5548
|
-
"
|
|
5549
|
-
"
|
|
5550
|
-
"
|
|
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
|