davinci-resolve-mcp 2.70.0 → 2.70.2
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 +139 -0
- package/README.md +16 -10
- package/docs/SKILL.md +14 -5
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/doctor.py +87 -17
- package/scripts/install_resolve_bridge.py +174 -56
- package/src/analysis_dashboard.py +49 -4
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,145 @@
|
|
|
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.70.2
|
|
6
|
+
|
|
7
|
+
The control panel could never reach the free edition, even with a perfectly
|
|
8
|
+
healthy in-app bridge. Reported in issue #109 by @alpaolo.
|
|
9
|
+
|
|
10
|
+
### The bug
|
|
11
|
+
|
|
12
|
+
The panel runs as a separate process from the MCP server and has its own Resolve
|
|
13
|
+
connector. That connector returned as soon as `import DaVinciResolveScript`
|
|
14
|
+
failed:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
try:
|
|
18
|
+
import DaVinciResolveScript as dvr_script
|
|
19
|
+
except Exception as exc:
|
|
20
|
+
return None, f"Resolve scripting API unavailable: {exc}"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On the free edition that import is exactly what fails — Blackmagic's module ships
|
|
24
|
+
with the installer, not the App Store build, so there is no
|
|
25
|
+
`Developer/Scripting/Modules` tree to import from. The panel returned there,
|
|
26
|
+
before ever calling `connect_resolve`, whose entire purpose is that it accepts
|
|
27
|
+
`None` in bridge mode:
|
|
28
|
+
|
|
29
|
+
> `dvr_script` may be None in bridge mode: the bridge does not need Blackmagic's
|
|
30
|
+
> module at all, which is precisely why it reaches editions the module cannot.
|
|
31
|
+
|
|
32
|
+
So the reporter saw the bridge listening, the MCP server connected, and the panel
|
|
33
|
+
insisting "Resolve unavailable" — all at the same time, all correct.
|
|
34
|
+
|
|
35
|
+
`_try_connect` in `src/server.py` already carried this guard, with a comment
|
|
36
|
+
recording the same diagnosis from when it bit the server. The panel's connector
|
|
37
|
+
was missed. That makes it the third connector overlooked when a transport was
|
|
38
|
+
added, after the network-scripting one in v2.64.0.
|
|
39
|
+
|
|
40
|
+
### The fix
|
|
41
|
+
|
|
42
|
+
The panel now consults the bridge first and treats both the environment setup and
|
|
43
|
+
the module import as optional when it is enabled — the same ordering the MCP
|
|
44
|
+
server uses.
|
|
45
|
+
|
|
46
|
+
The not-connected message no longer assumes Studio. It used to send every reader
|
|
47
|
+
to "open Resolve Studio with a project loaded", which is poor advice for a
|
|
48
|
+
free-edition user, since free is the one edition external scripting refuses by
|
|
49
|
+
design. It now names the fix that fits the situation, and says something
|
|
50
|
+
different depending on whether the bridge is enabled.
|
|
51
|
+
|
|
52
|
+
### Windows bridge: `%PROGRAMDATA%` now confirmed
|
|
53
|
+
|
|
54
|
+
v2.70.1 shipped Windows script paths unverified. The #109 report was made on free
|
|
55
|
+
21.0.1.11 with the bridge installed, listed and serving from `%PROGRAMDATA%`, so
|
|
56
|
+
that half is now confirmed rather than assumed. `%APPDATA%` remains untested.
|
|
57
|
+
|
|
58
|
+
## What's New in v2.70.1
|
|
59
|
+
|
|
60
|
+
Windows support for the free-edition in-app bridge, and the end of doctor.py's
|
|
61
|
+
macOS-only path assumptions. Reported in issue #106 by @kacemmosbah8-afk.
|
|
62
|
+
|
|
63
|
+
### The bug
|
|
64
|
+
|
|
65
|
+
`script_targets()` in `scripts/install_resolve_bridge.py` enumerated macOS and
|
|
66
|
+
Linux candidates only. There was no Windows branch at all, so the candidate list
|
|
67
|
+
came back empty on every Windows machine and the installer exited with:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
No writable DaVinci Resolve Scripts/Utility folder found. Is Resolve installed?
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
on installs where Resolve was demonstrably present. Since the in-app bridge is
|
|
74
|
+
the *only* route to the free edition — external scripting is Studio-gated by
|
|
75
|
+
Blackmagic — Windows free-edition users had no working path to this server at
|
|
76
|
+
all. The bridge shipped in v2.68.0; it has been macOS/Linux-only that whole time.
|
|
77
|
+
|
|
78
|
+
Windows now targets the two Scripts/Utility trees Blackmagic documents, per-user
|
|
79
|
+
first because `%PROGRAMDATA%` typically needs an elevated prompt:
|
|
80
|
+
|
|
81
|
+
- `%APPDATA%\Blackmagic Design\DaVinci Resolve\Support\Fusion\Scripts\Utility`
|
|
82
|
+
- `%PROGRAMDATA%\Blackmagic Design\DaVinci Resolve\Fusion\Scripts\Utility`
|
|
83
|
+
|
|
84
|
+
Three traps came out of implementing that, each now pinned by a test:
|
|
85
|
+
|
|
86
|
+
- **The per-user tree carries a `Support` segment the all-users tree does not.**
|
|
87
|
+
They are not one layout under two roots; deriving either from the other lands
|
|
88
|
+
in a folder Resolve never scans.
|
|
89
|
+
- **Blackmagic's own README writes the per-user root as `%APPDATA%\Roaming\...`,
|
|
90
|
+
which is wrong** — `%APPDATA%` already *is* `...\AppData\Roaming`. Transcribing
|
|
91
|
+
it verbatim yields `AppData\Roaming\Roaming\...`: a real, creatable folder that
|
|
92
|
+
is silently never read.
|
|
93
|
+
- **Resolve does not create its `Fusion\Scripts` tree until a script is
|
|
94
|
+
installed**, so gating on that tree existing — or on its parent being writable
|
|
95
|
+
— skips a fresh free-edition install, the exact build the bridge exists for.
|
|
96
|
+
The candidates are gated on Resolve's product folder instead, mirroring how the
|
|
97
|
+
macOS sandbox container is handled.
|
|
98
|
+
|
|
99
|
+
`install()` no longer aborts when one target is unwritable. The `%PROGRAMDATA%`
|
|
100
|
+
tree needs elevation, and Windows `os.access(W_OK)` reports the read-only flag
|
|
101
|
+
rather than the ACL, so it cannot be screened out in advance — without this, the
|
|
102
|
+
newly added candidate would have crashed the installer on every non-elevated
|
|
103
|
+
Windows account *after* it had already succeeded into the per-user tree. Skips
|
|
104
|
+
are reported in `warnings`; only a clean sweep is fatal, and that error now names
|
|
105
|
+
the folders and the reason instead of asking whether Resolve is installed.
|
|
106
|
+
|
|
107
|
+
### The macOS framework-Python alarm was firing off macOS
|
|
108
|
+
|
|
109
|
+
`python_preflight()` looked for a framework Python under `/Library/Frameworks`,
|
|
110
|
+
a path that cannot exist on Windows or Linux — so it always found nothing and
|
|
111
|
+
emitted the macOS remediation, telling the reporter (running a working python.org
|
|
112
|
+
3.12.9) to install the Python they already had. Linux had the same false alarm.
|
|
113
|
+
The check is now macOS-only. The Lua canary still ships everywhere, so a genuine
|
|
114
|
+
enumeration failure stays diagnosable.
|
|
115
|
+
|
|
116
|
+
### doctor.py was macOS-only too
|
|
117
|
+
|
|
118
|
+
`scripts/doctor.py` hardcoded macOS defaults for `RESOLVE_APP`,
|
|
119
|
+
`RESOLVE_SCRIPT_API` and `RESOLVE_SCRIPT_LIB`. Run standalone — that is, without
|
|
120
|
+
the environment variables the npm/`install.py` flow injects — it reported four
|
|
121
|
+
`[FAIL]` lines naming a `.app` bundle and `fusionscript.so` on a Windows 11
|
|
122
|
+
machine that had neither, while `install.py` detected that same install
|
|
123
|
+
correctly. Two tables describing one thing, one of them platform-blind. Linux was
|
|
124
|
+
equally affected.
|
|
125
|
+
|
|
126
|
+
doctor now selects per-platform candidates, and picks the first that exists so
|
|
127
|
+
the reported path matches reality. macOS values are byte-identical to before.
|
|
128
|
+
A new `tests/test_doctor_paths.py` drift guard asserts every path doctor names
|
|
129
|
+
also appears in `install.py`'s `RESOLVE_PATHS`, so the two cannot diverge again.
|
|
130
|
+
Claude Desktop's config path is now MSIX-aware on Windows as well, matching the
|
|
131
|
+
issue #93 fix that `install.py` already had.
|
|
132
|
+
|
|
133
|
+
### Scope — what is and is not verified
|
|
134
|
+
|
|
135
|
+
The path construction is unit-tested, and a simulated Windows run exercises the
|
|
136
|
+
installer end to end. **No Windows hardware has confirmed that Resolve actually
|
|
137
|
+
lists the bridge from these folders.** The reporter verified the folders exist
|
|
138
|
+
and are writable; issue #104 is precedent that "the folder exists" and "Resolve
|
|
139
|
+
reads it" are different claims. The README and `docs/SKILL.md` say so plainly
|
|
140
|
+
rather than implying Windows is a supported, tested tier.
|
|
141
|
+
|
|
142
|
+
Suite: 2312 → 2336.
|
|
143
|
+
|
|
5
144
|
## What's New in v2.70.0
|
|
6
145
|
|
|
7
146
|
Headless (`-nogui`) Resolve, measured rather than assumed — and the finding
|
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)
|
|
@@ -51,15 +51,21 @@ python scripts/install_resolve_bridge.py
|
|
|
51
51
|
export DAVINCI_RESOLVE_BRIDGE=1 # opt-in; unset changes nothing
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
only when it finds one — Homebrew, pyenv and conda
|
|
56
|
-
detected, and the script silently never appears in the
|
|
57
|
-
installed alongside so you can tell that apart from a
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
54
|
+
On **macOS**, this requires a **framework Python** (python.org). Resolve
|
|
55
|
+
enumerates `.py` scripts only when it finds one — Homebrew, pyenv and conda
|
|
56
|
+
interpreters are not detected, and the script silently never appears in the
|
|
57
|
+
menu. A Lua canary is installed alongside so you can tell that apart from a
|
|
58
|
+
wrong folder.
|
|
59
|
+
|
|
60
|
+
Validated on free 21.0.3.7 and Studio 19.1.3.7, both macOS. The Windows paths
|
|
61
|
+
added in v2.70.1 (issue #106) shipped unverified; a report on free 21.0.1.11
|
|
62
|
+
(issue #109) has since shown the bridge installing, listing and serving from
|
|
63
|
+
`%PROGRAMDATA%` on Windows, so that path is now confirmed rather than assumed.
|
|
64
|
+
`%APPDATA%` remains untested. Reports welcome.
|
|
65
|
+
|
|
66
|
+
This is the documented in-app path, not a licence circumvention, but Blackmagic
|
|
67
|
+
could close it — treat it as a supported-until-it-is-not tier. Loopback only,
|
|
68
|
+
HMAC-signed requests, one-use nonces.
|
|
63
69
|
|
|
64
70
|
## Local Control Panel
|
|
65
71
|
|
package/docs/SKILL.md
CHANGED
|
@@ -27,11 +27,20 @@ loopback listener. Install with `python scripts/install_resolve_bridge.py`, star
|
|
|
27
27
|
it from that menu, and set `DAVINCI_RESOLVE_BRIDGE=1`. Existing tool call sites
|
|
28
28
|
work unchanged. Two things to know when diagnosing it:
|
|
29
29
|
|
|
30
|
-
- Resolve lists `.py` scripts only when it can find a **framework
|
|
31
|
-
(python.org). Homebrew/pyenv/conda are not detected and the script
|
|
32
|
-
appears, with no error. The installer preflights this and ships a
|
|
33
|
-
which always lists, so "Python not detected" is distinguishable
|
|
34
|
-
folder".
|
|
30
|
+
- On macOS, Resolve lists `.py` scripts only when it can find a **framework
|
|
31
|
+
Python** (python.org). Homebrew/pyenv/conda are not detected and the script
|
|
32
|
+
simply never appears, with no error. The installer preflights this and ships a
|
|
33
|
+
Lua canary, which always lists, so "Python not detected" is distinguishable
|
|
34
|
+
from "wrong folder". The preflight is macOS-only — off macOS Resolve finds
|
|
35
|
+
Python by other means, and running the check there was a false alarm (#106).
|
|
36
|
+
- **Windows: `%PROGRAMDATA%` confirmed, `%APPDATA%` still not.** Both script
|
|
37
|
+
folders have been targeted since v2.70.1; a free 21.0.1.11 report (#109) has
|
|
38
|
+
since shown Resolve listing and serving the bridge from `%PROGRAMDATA%`. If a
|
|
39
|
+
user reports the menu entry missing on Windows, ask whether the Lua canary
|
|
40
|
+
lists — that separates "wrong folder" from "Python not detected" there too.
|
|
41
|
+
- The **control panel connects over the bridge too** (fixed in v2.70.2). It runs
|
|
42
|
+
as a separate process with its own connector, so a panel that reports "Resolve
|
|
43
|
+
unavailable" while tool calls work is a panel-side bug, not a broken bridge.
|
|
35
44
|
- The in-Resolve runtime is a **copy taken at install time**. After changing the
|
|
36
45
|
repository, re-run the installer and then ask the running bridge to reload —
|
|
37
46
|
it re-imports from disk in place, so Resolve does not need restarting.
|
package/install.py
CHANGED
|
@@ -36,7 +36,7 @@ from src.utils.update_check import (
|
|
|
36
36
|
|
|
37
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
VERSION = "2.70.
|
|
39
|
+
VERSION = "2.70.2"
|
|
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
package/scripts/doctor.py
CHANGED
|
@@ -48,26 +48,96 @@ if not PYTHON.exists():
|
|
|
48
48
|
SERVER = REPO / "src" / "server.py"
|
|
49
49
|
CODEX_HOME = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
|
50
50
|
CODEX_CONFIG = CODEX_HOME / "config.toml"
|
|
51
|
+
#: Where Resolve installs itself, per platform. Mirrors install.py's
|
|
52
|
+
#: RESOLVE_PATHS — kept in step by tests/test_doctor_paths.py, because these
|
|
53
|
+
#: being macOS-only is exactly the bug that made doctor useless off macOS.
|
|
54
|
+
#:
|
|
55
|
+
#: doctor used to hardcode the Darwin values. Run standalone on Windows (i.e.
|
|
56
|
+
#: without the env vars the npm/install.py flow injects) it then reported four
|
|
57
|
+
#: FAILs naming a `.app` bundle and `fusionscript.so` on a machine that had
|
|
58
|
+
#: neither — while install.py detected the very same install correctly, which is
|
|
59
|
+
#: what made it so confusing to diagnose (issue #106). Linux had the same bug.
|
|
60
|
+
_RESOLVE_PATH_CANDIDATES: dict[str, dict[str, tuple[str, ...]]] = {
|
|
61
|
+
"darwin": {
|
|
62
|
+
"app": ("/Applications/DaVinci Resolve/DaVinci Resolve.app",),
|
|
63
|
+
"api": ("/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",),
|
|
64
|
+
"lib": ("/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",),
|
|
65
|
+
},
|
|
66
|
+
"win32": {
|
|
67
|
+
"app": (r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Resolve.exe",),
|
|
68
|
+
"api": (
|
|
69
|
+
r"C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting",
|
|
70
|
+
r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Developer\Scripting",
|
|
71
|
+
),
|
|
72
|
+
"lib": (r"C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll",),
|
|
73
|
+
},
|
|
74
|
+
"linux": {
|
|
75
|
+
"app": ("/opt/resolve/bin/resolve",),
|
|
76
|
+
"api": (
|
|
77
|
+
"/opt/resolve/Developer/Scripting",
|
|
78
|
+
"/opt/resolve/libs/Fusion/Developer/Scripting",
|
|
79
|
+
),
|
|
80
|
+
"lib": (
|
|
81
|
+
"/opt/resolve/libs/Fusion/fusionscript.so",
|
|
82
|
+
"/opt/resolve/bin/fusionscript.so",
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _platform_key(platform: str | None = None) -> str:
|
|
89
|
+
"""Which candidate set applies. Unknown platforms fall back to Linux."""
|
|
90
|
+
platform = platform if platform is not None else sys.platform
|
|
91
|
+
if platform == "darwin":
|
|
92
|
+
return "darwin"
|
|
93
|
+
if platform == "win32":
|
|
94
|
+
return "win32"
|
|
95
|
+
return "linux"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_default(kind: str, platform: str | None = None) -> str:
|
|
99
|
+
"""First candidate of `kind` that exists, else the first (so the FAIL line
|
|
100
|
+
names the canonical location rather than an arbitrary miss)."""
|
|
101
|
+
candidates = _RESOLVE_PATH_CANDIDATES[_platform_key(platform)][kind]
|
|
102
|
+
for candidate in candidates:
|
|
103
|
+
if Path(candidate).exists():
|
|
104
|
+
return candidate
|
|
105
|
+
return candidates[0]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _default_claude_config() -> Path:
|
|
109
|
+
"""Claude Desktop's config path, MSIX-aware on Windows (issue #93).
|
|
110
|
+
|
|
111
|
+
Claude Desktop for Windows ships as an MSIX package whose %APPDATA% writes
|
|
112
|
+
are virtualized into a per-package container, so the documented
|
|
113
|
+
%APPDATA%\\Claude path is a decoy the app never reads. Mirrors
|
|
114
|
+
install.py's windows_claude_desktop_config().
|
|
115
|
+
"""
|
|
116
|
+
if sys.platform == "win32":
|
|
117
|
+
local = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData/Local"))
|
|
118
|
+
try:
|
|
119
|
+
packages = sorted((local / "Packages").glob("Claude_*"))
|
|
120
|
+
except OSError:
|
|
121
|
+
packages = []
|
|
122
|
+
for pkg in packages:
|
|
123
|
+
virtual = pkg / "LocalCache" / "Roaming" / "Claude"
|
|
124
|
+
if virtual.is_dir():
|
|
125
|
+
return virtual / "claude_desktop_config.json"
|
|
126
|
+
appdata = Path(os.environ.get("APPDATA", Path.home() / "AppData/Roaming"))
|
|
127
|
+
return appdata / "Claude" / "claude_desktop_config.json"
|
|
128
|
+
if sys.platform == "darwin":
|
|
129
|
+
return Path("~/Library/Application Support/Claude/claude_desktop_config.json").expanduser()
|
|
130
|
+
xdg = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"))
|
|
131
|
+
return xdg / "Claude" / "claude_desktop_config.json"
|
|
132
|
+
|
|
133
|
+
|
|
51
134
|
CLAUDE_CONFIG = Path(
|
|
52
|
-
os.environ.get(
|
|
53
|
-
"CLAUDE_DESKTOP_CONFIG",
|
|
54
|
-
"~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
55
|
-
)
|
|
135
|
+
os.environ.get("CLAUDE_DESKTOP_CONFIG", _default_claude_config())
|
|
56
136
|
).expanduser()
|
|
57
|
-
RESOLVE_APP = Path(os.environ.get("RESOLVE_APP", "
|
|
58
|
-
RESOLVE_API = Path(
|
|
59
|
-
os.environ.get(
|
|
60
|
-
"RESOLVE_SCRIPT_API",
|
|
61
|
-
"/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
|
|
62
|
-
)
|
|
63
|
-
)
|
|
137
|
+
RESOLVE_APP = Path(os.environ.get("RESOLVE_APP", _resolve_default("app")))
|
|
138
|
+
RESOLVE_API = Path(os.environ.get("RESOLVE_SCRIPT_API", _resolve_default("api")))
|
|
64
139
|
RESOLVE_MODULES = Path(os.environ.get("RESOLVE_SCRIPT_MODULES", RESOLVE_API / "Modules"))
|
|
65
|
-
RESOLVE_LIB = Path(
|
|
66
|
-
os.environ.get(
|
|
67
|
-
"RESOLVE_SCRIPT_LIB",
|
|
68
|
-
"/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
|
|
69
|
-
)
|
|
70
|
-
)
|
|
140
|
+
RESOLVE_LIB = Path(os.environ.get("RESOLVE_SCRIPT_LIB", _resolve_default("lib")))
|
|
71
141
|
|
|
72
142
|
|
|
73
143
|
def run(cmd: list[str], timeout: int = 12) -> dict[str, Any]:
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
python scripts/install_resolve_bridge.py --probe-only # settle the host model first
|
|
4
4
|
python scripts/install_resolve_bridge.py # probe + bridge launcher
|
|
5
5
|
|
|
6
|
-
Deploys to every applicable macOS/Linux location, including the sandboxed
|
|
7
|
-
Store container used by the free edition — which is why the free edition can
|
|
8
|
-
installed alongside a direct-download Studio without either disturbing the other.
|
|
6
|
+
Deploys to every applicable macOS/Windows/Linux location, including the sandboxed
|
|
7
|
+
App Store container used by the free edition — which is why the free edition can
|
|
8
|
+
be installed alongside a direct-download Studio without either disturbing the other.
|
|
9
9
|
|
|
10
10
|
Nothing is started here. Resolve must be restarted after installing so it
|
|
11
11
|
re-scans the Scripts folders, and the script is then run from
|
|
@@ -76,6 +76,56 @@ _SANDBOX_FALLBACK_SUFFIX = "Library/Application Support/Fusion/Scripts/Utility"
|
|
|
76
76
|
#: Containers that are not Resolve (RAW Player, Speed Test, the IO XPC helper).
|
|
77
77
|
_NON_RESOLVE_CONTAINERS = ("BlackmagicRaw", "IOXPC")
|
|
78
78
|
|
|
79
|
+
#: Windows Scripts/Utility trees, per Blackmagic's own README
|
|
80
|
+
#: (docs/reference/resolve_scripting_api.txt, "Specific user"/"All users").
|
|
81
|
+
#: Two traps, both live in those two lines:
|
|
82
|
+
#:
|
|
83
|
+
#: 1. The per-user tree carries a `Support` segment that the all-users tree does
|
|
84
|
+
#: NOT. They are not one layout under two roots, and deriving one from the
|
|
85
|
+
#: other lands in a folder Resolve never scans.
|
|
86
|
+
#: 2. The README writes the per-user root as `%APPDATA%\Roaming\...`, which is
|
|
87
|
+
#: wrong — `%APPDATA%` already IS `...\AppData\Roaming`, so following it
|
|
88
|
+
#: literally yields `AppData\Roaming\Roaming\...`. The paths below drop the
|
|
89
|
+
#: doubled segment; both were confirmed present and writable on the Windows 11
|
|
90
|
+
#: machine in issue #106.
|
|
91
|
+
_WINDOWS_USER_SCRIPTS_SUFFIX = (
|
|
92
|
+
"Blackmagic Design/DaVinci Resolve/Support/Fusion/Scripts/Utility"
|
|
93
|
+
)
|
|
94
|
+
_WINDOWS_ALL_USERS_SCRIPTS_SUFFIX = "Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
#: The product folder both Windows trees hang off. Its existence is the "Resolve
|
|
98
|
+
#: is installed" signal — see `_windows_script_candidates`.
|
|
99
|
+
_WINDOWS_PRODUCT_ROOT = "Blackmagic Design/DaVinci Resolve"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _windows_script_candidates() -> list[Path]:
|
|
103
|
+
"""Scripts/Utility trees Resolve scans on Windows (issue #106).
|
|
104
|
+
|
|
105
|
+
Gated on the *product* folder, not the Scripts/Utility tree, for the same
|
|
106
|
+
reason the sandbox branch is: Resolve does not create its `Fusion/Scripts`
|
|
107
|
+
tree until a script is installed, so requiring the target to pre-exist skips
|
|
108
|
+
a fresh free-edition install — the one build the bridge exists for. The
|
|
109
|
+
product folder is created on first launch, so it answers "is Resolve
|
|
110
|
+
installed" without demanding a folder only we will ever create.
|
|
111
|
+
|
|
112
|
+
Per-user first: it is the one a non-elevated account can actually write,
|
|
113
|
+
while `%PROGRAMDATA%` typically needs an elevated prompt. Ordering it first
|
|
114
|
+
means the target that will succeed is also the one reported first.
|
|
115
|
+
"""
|
|
116
|
+
candidates: list[Path] = []
|
|
117
|
+
for env_var, suffix in (
|
|
118
|
+
("APPDATA", _WINDOWS_USER_SCRIPTS_SUFFIX),
|
|
119
|
+
("PROGRAMDATA", _WINDOWS_ALL_USERS_SCRIPTS_SUFFIX),
|
|
120
|
+
):
|
|
121
|
+
root = os.environ.get(env_var)
|
|
122
|
+
if not root:
|
|
123
|
+
continue
|
|
124
|
+
target = Path(root) / suffix
|
|
125
|
+
if target.is_dir() or (Path(root) / _WINDOWS_PRODUCT_ROOT).is_dir():
|
|
126
|
+
candidates.append(target)
|
|
127
|
+
return candidates
|
|
128
|
+
|
|
79
129
|
|
|
80
130
|
def _is_resolve_container(path: Path) -> bool:
|
|
81
131
|
if not path.name.startswith(_SANDBOX_MARKER):
|
|
@@ -91,28 +141,40 @@ def script_targets() -> list[Path]:
|
|
|
91
141
|
coexist with a direct-download Studio install. Each container contributes
|
|
92
142
|
TWO targets (see the module comment on issue #104): the documented tree
|
|
93
143
|
first, then the Fusion standalone tree that Lite also scans.
|
|
144
|
+
|
|
145
|
+
Windows has no sandbox and no `~/Library`, so it gets its own candidate list
|
|
146
|
+
(issue #106) — without one this returned empty on every Windows machine and
|
|
147
|
+
the installer exited with "Is Resolve installed?" on a working install.
|
|
148
|
+
Those candidates arrive pre-vetted, like the sandboxed ones: they are gated
|
|
149
|
+
on Resolve's product folder rather than on the Scripts tree, which Resolve
|
|
150
|
+
does not create until a script is installed.
|
|
94
151
|
"""
|
|
95
152
|
home = Path.home()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
153
|
+
# Pre-vetted targets, whose tree gets created rather than being required.
|
|
154
|
+
vetted: list[Path] = []
|
|
155
|
+
candidates: list[Path] = []
|
|
156
|
+
if sys.platform == "win32":
|
|
157
|
+
vetted.extend(_windows_script_candidates())
|
|
158
|
+
else:
|
|
159
|
+
candidates = [
|
|
160
|
+
home / _SCRIPTS_SUFFIX,
|
|
161
|
+
Path("/") / _SCRIPTS_SUFFIX,
|
|
162
|
+
home / ".local/share/DaVinciResolve/Fusion/Scripts/Utility",
|
|
163
|
+
]
|
|
164
|
+
containers = home / "Library/Containers"
|
|
165
|
+
if containers.is_dir():
|
|
166
|
+
for entry in sorted(containers.iterdir()):
|
|
167
|
+
if _is_resolve_container(entry) and (entry / "Data").is_dir():
|
|
168
|
+
# The container's existence is the signal; the tree gets created.
|
|
169
|
+
vetted.append(entry / "Data" / _SCRIPTS_SUFFIX)
|
|
170
|
+
vetted.append(entry / "Data" / _SANDBOX_FALLBACK_SUFFIX)
|
|
108
171
|
|
|
109
172
|
usable: list[Path] = []
|
|
110
173
|
for path in candidates:
|
|
111
174
|
# Non-sandboxed: the install created the tree, so require it.
|
|
112
175
|
if path.is_dir() or (path.parent.is_dir() and os.access(path.parent, os.W_OK)):
|
|
113
176
|
usable.append(path)
|
|
114
|
-
|
|
115
|
-
usable.extend(sandboxed)
|
|
177
|
+
usable.extend(vetted)
|
|
116
178
|
return list(dict.fromkeys(usable))
|
|
117
179
|
|
|
118
180
|
|
|
@@ -197,7 +259,23 @@ def framework_pythons() -> list[str]:
|
|
|
197
259
|
|
|
198
260
|
|
|
199
261
|
def python_preflight() -> dict:
|
|
200
|
-
"""Will Resolve list the Python probe we are about to install?
|
|
262
|
+
"""Will Resolve list the Python probe we are about to install?
|
|
263
|
+
|
|
264
|
+
The framework-Python trap is **macOS-only**: `/Library/Frameworks/...` does
|
|
265
|
+
not exist on Windows or Linux, where Resolve finds Python by other means (the
|
|
266
|
+
registry, or the system interpreter). Running the macOS check there always
|
|
267
|
+
found nothing and emitted the macOS remediation — telling the Windows 11 user
|
|
268
|
+
in issue #106, who had a working python.org 3.12, to go install the Python
|
|
269
|
+
they already had. Report "no known reason it will not list" off macOS rather
|
|
270
|
+
than a false alarm; the Lua canary still ships either way, so a genuine
|
|
271
|
+
enumeration problem remains diagnosable.
|
|
272
|
+
"""
|
|
273
|
+
if sys.platform != "darwin":
|
|
274
|
+
return {
|
|
275
|
+
"framework_pythons": [],
|
|
276
|
+
"resolve_will_list_python_scripts": True,
|
|
277
|
+
"advice": None,
|
|
278
|
+
}
|
|
201
279
|
frameworks = framework_pythons()
|
|
202
280
|
return {
|
|
203
281
|
"framework_pythons": frameworks,
|
|
@@ -256,48 +334,41 @@ def install(*, probe_only: bool, port: int, rotate: bool) -> dict:
|
|
|
256
334
|
"No writable DaVinci Resolve Scripts/Utility folder found. Is Resolve installed?"
|
|
257
335
|
)
|
|
258
336
|
installed: list[str] = []
|
|
337
|
+
# A target can be real, scanned by Resolve, and still unwritable — the
|
|
338
|
+
# Windows all-users tree under %PROGRAMDATA% needs elevation, and Windows
|
|
339
|
+
# `os.access(W_OK)` reports the read-only flag rather than the ACL, so the
|
|
340
|
+
# filter above cannot screen it out. One un-writable target must not abort an
|
|
341
|
+
# install that already succeeded into the per-user tree, so failures are
|
|
342
|
+
# collected and only a clean sweep is fatal.
|
|
343
|
+
skipped: list[tuple[str, str]] = []
|
|
259
344
|
for target in targets:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
installed.append(str(runtime / module))
|
|
275
|
-
# A sandboxed (App Store) build cannot read ~/.config at all — inside
|
|
276
|
-
# the container `~` IS the container, so an absolute real-home path is
|
|
277
|
-
# unreachable by construction (measured: "Operation not permitted").
|
|
278
|
-
# The runtime dir is already inside the container, so the config goes
|
|
279
|
-
# beside the modules. Same token, so the out-of-sandbox client still
|
|
280
|
-
# authenticates against ~/.config.
|
|
281
|
-
runtime_config = runtime / "bridge.json"
|
|
282
|
-
shutil.copy2(CONFIG_PATH, runtime_config)
|
|
283
|
-
os.chmod(runtime_config, 0o600)
|
|
284
|
-
installed.append(str(runtime_config))
|
|
285
|
-
# The launcher IS the menu entry. Without it the modules sit in the
|
|
286
|
-
# runtime dir with nothing able to start them.
|
|
287
|
-
launcher_source = (REPO / "scripts/resolve_bridge_launcher.py").read_text(encoding="utf-8")
|
|
288
|
-
launcher = launcher_source.replace(
|
|
289
|
-
"@@RUNTIME_ROOT_B64@@",
|
|
290
|
-
base64.urlsafe_b64encode(str(runtime).encode("utf-8")).decode("ascii"),
|
|
291
|
-
).replace(
|
|
292
|
-
"@@CONFIG_PATH_B64@@",
|
|
293
|
-
base64.urlsafe_b64encode(str(CONFIG_PATH).encode("utf-8")).decode("ascii"),
|
|
294
|
-
)
|
|
295
|
-
launcher_path = target / "resolve_bridge.py"
|
|
296
|
-
launcher_path.write_text(launcher, encoding="utf-8")
|
|
297
|
-
installed.append(str(launcher_path))
|
|
345
|
+
try:
|
|
346
|
+
target.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
347
|
+
_install_to(target, probe_only=probe_only, installed=installed)
|
|
348
|
+
except OSError as exc:
|
|
349
|
+
skipped.append((str(target), str(exc)))
|
|
350
|
+
if not installed:
|
|
351
|
+
raise SystemExit(
|
|
352
|
+
"Found DaVinci Resolve Scripts/Utility folders but could not write to "
|
|
353
|
+
"any of them:\n"
|
|
354
|
+
+ "\n".join(f" {path}: {reason}" for path, reason in skipped)
|
|
355
|
+
+ "\nRe-run from an account that can write these folders (on Windows, "
|
|
356
|
+
"the %PROGRAMDATA% tree needs an elevated prompt; the per-user "
|
|
357
|
+
"%APPDATA% tree does not)."
|
|
358
|
+
)
|
|
298
359
|
result = {"installed": installed, "probe_only": probe_only, "python": python_preflight()}
|
|
299
360
|
stale = stale_container_warning(targets)
|
|
300
361
|
result["warnings"] = [stale] if stale else []
|
|
362
|
+
if skipped:
|
|
363
|
+
result["warnings"].append(
|
|
364
|
+
"Skipped "
|
|
365
|
+
+ str(len(skipped))
|
|
366
|
+
+ " unwritable Scripts/Utility folder(s): "
|
|
367
|
+
+ "; ".join(f"{path} ({reason})" for path, reason in skipped)
|
|
368
|
+
+ ". This is harmless as long as the bridge appears under "
|
|
369
|
+
"Workspace > Scripts."
|
|
370
|
+
)
|
|
371
|
+
result["skipped"] = [path for path, _ in skipped]
|
|
301
372
|
if not probe_only:
|
|
302
373
|
loaded = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
303
374
|
result["config"] = {"path": str(CONFIG_PATH),
|
|
@@ -305,6 +376,53 @@ def install(*, probe_only: bool, port: int, rotate: bool) -> dict:
|
|
|
305
376
|
return result
|
|
306
377
|
|
|
307
378
|
|
|
379
|
+
def _install_to(target: Path, *, probe_only: bool, installed: list[str]) -> None:
|
|
380
|
+
"""Place the probes, canary and (unless probe-only) the bridge into `target`.
|
|
381
|
+
|
|
382
|
+
Raises OSError if the target turns out to be unwritable; `install()` treats
|
|
383
|
+
that as a skip rather than a failure so one un-writable tree cannot abort an
|
|
384
|
+
install that succeeded elsewhere.
|
|
385
|
+
"""
|
|
386
|
+
for probe in ("resolve_bridge_probe.py", "resolve_capability_probe.py"):
|
|
387
|
+
shutil.copy2(REPO / "scripts" / probe, target / probe)
|
|
388
|
+
installed.append(str(target / probe))
|
|
389
|
+
# Lua always enumerates; Python only with a framework install. The canary
|
|
390
|
+
# makes "Python not detected" distinguishable from "wrong folder".
|
|
391
|
+
canary = target / "resolve_bridge_canary.lua"
|
|
392
|
+
canary.write_text(_LUA_CANARY, encoding="utf-8")
|
|
393
|
+
installed.append(str(canary))
|
|
394
|
+
if probe_only:
|
|
395
|
+
return
|
|
396
|
+
runtime = target.parents[1] / ".davinci_mcp_runtime"
|
|
397
|
+
runtime.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
398
|
+
for module in ("resolve_bridge.py", "resolve_bridge_ops.py"):
|
|
399
|
+
shutil.copy2(REPO / "src/utils" / module, runtime / module)
|
|
400
|
+
installed.append(str(runtime / module))
|
|
401
|
+
# A sandboxed (App Store) build cannot read ~/.config at all — inside
|
|
402
|
+
# the container `~` IS the container, so an absolute real-home path is
|
|
403
|
+
# unreachable by construction (measured: "Operation not permitted").
|
|
404
|
+
# The runtime dir is already inside the container, so the config goes
|
|
405
|
+
# beside the modules. Same token, so the out-of-sandbox client still
|
|
406
|
+
# authenticates against ~/.config.
|
|
407
|
+
runtime_config = runtime / "bridge.json"
|
|
408
|
+
shutil.copy2(CONFIG_PATH, runtime_config)
|
|
409
|
+
os.chmod(runtime_config, 0o600)
|
|
410
|
+
installed.append(str(runtime_config))
|
|
411
|
+
# The launcher IS the menu entry. Without it the modules sit in the
|
|
412
|
+
# runtime dir with nothing able to start them.
|
|
413
|
+
launcher_source = (REPO / "scripts/resolve_bridge_launcher.py").read_text(encoding="utf-8")
|
|
414
|
+
launcher = launcher_source.replace(
|
|
415
|
+
"@@RUNTIME_ROOT_B64@@",
|
|
416
|
+
base64.urlsafe_b64encode(str(runtime).encode("utf-8")).decode("ascii"),
|
|
417
|
+
).replace(
|
|
418
|
+
"@@CONFIG_PATH_B64@@",
|
|
419
|
+
base64.urlsafe_b64encode(str(CONFIG_PATH).encode("utf-8")).decode("ascii"),
|
|
420
|
+
)
|
|
421
|
+
launcher_path = target / "resolve_bridge.py"
|
|
422
|
+
launcher_path.write_text(launcher, encoding="utf-8")
|
|
423
|
+
installed.append(str(launcher_path))
|
|
424
|
+
|
|
425
|
+
|
|
308
426
|
def main() -> int:
|
|
309
427
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
310
428
|
parser.add_argument("--probe-only", action="store_true",
|
|
@@ -11953,9 +11953,33 @@ def _serialize_resolve(func):
|
|
|
11953
11953
|
return wrapper
|
|
11954
11954
|
|
|
11955
11955
|
|
|
11956
|
+
def _bridge_requested() -> bool:
|
|
11957
|
+
"""Has the operator asked for the in-app bridge?
|
|
11958
|
+
|
|
11959
|
+
Read at call time rather than at import, so a panel started before the
|
|
11960
|
+
variable was set still honours it.
|
|
11961
|
+
"""
|
|
11962
|
+
try:
|
|
11963
|
+
from src.utils import resolve_bridge_client
|
|
11964
|
+
|
|
11965
|
+
return resolve_bridge_client.bridge_enabled()
|
|
11966
|
+
except Exception: # pragma: no cover - the client is optional
|
|
11967
|
+
return False
|
|
11968
|
+
|
|
11969
|
+
|
|
11956
11970
|
def _connect_resolve_read_only() -> Tuple[Any, Optional[str]]:
|
|
11957
11971
|
global _RESOLVE_ENV_READY
|
|
11958
11972
|
with _RESOLVE_API_LOCK:
|
|
11973
|
+
# Blackmagic's module ships with the *installer*, not the App Store
|
|
11974
|
+
# build, so a free-edition machine has no Developer/Scripting/Modules
|
|
11975
|
+
# tree and both the environment setup and the import fail. Returning
|
|
11976
|
+
# here made the control panel unreachable on exactly the configuration
|
|
11977
|
+
# the bridge exists for: the MCP server connects fine over the bridge
|
|
11978
|
+
# while the panel — a separate process with its own connector — reports
|
|
11979
|
+
# "Resolve unavailable". `connect_resolve` accepts None in bridge mode,
|
|
11980
|
+
# and this returned before ever calling it (server.py:_try_connect
|
|
11981
|
+
# carries the same guard for the same reason).
|
|
11982
|
+
bridge_on = _bridge_requested()
|
|
11959
11983
|
# Environment + sys.path setup is pure overhead and never goes stale, so
|
|
11960
11984
|
# run it once per process rather than on every connection.
|
|
11961
11985
|
if not _RESOLVE_ENV_READY:
|
|
@@ -11968,20 +11992,41 @@ def _connect_resolve_read_only() -> Tuple[Any, Optional[str]]:
|
|
|
11968
11992
|
sys.path.append(candidate)
|
|
11969
11993
|
_RESOLVE_ENV_READY = True
|
|
11970
11994
|
except Exception as exc:
|
|
11971
|
-
|
|
11995
|
+
if not bridge_on:
|
|
11996
|
+
return None, f"Resolve scripting API unavailable: {exc}"
|
|
11997
|
+
dvr_script = None
|
|
11972
11998
|
try:
|
|
11973
|
-
import DaVinciResolveScript as
|
|
11999
|
+
import DaVinciResolveScript as _dvr_script # type: ignore
|
|
12000
|
+
|
|
12001
|
+
dvr_script = _dvr_script
|
|
11974
12002
|
except Exception as exc:
|
|
11975
|
-
|
|
12003
|
+
if not bridge_on:
|
|
12004
|
+
return None, f"Resolve scripting API unavailable: {exc}"
|
|
11976
12005
|
try:
|
|
11977
12006
|
resolve = connect_resolve(dvr_script)
|
|
11978
12007
|
except Exception as exc:
|
|
11979
12008
|
return None, f"Resolve connection failed: {exc}"
|
|
11980
12009
|
if resolve is None:
|
|
11981
|
-
return None,
|
|
12010
|
+
return None, _not_connected_message(bridge_on)
|
|
11982
12011
|
return resolve, None
|
|
11983
12012
|
|
|
11984
12013
|
|
|
12014
|
+
def _not_connected_message(bridge_on: bool) -> str:
|
|
12015
|
+
"""Name the fix that applies, rather than assuming Studio.
|
|
12016
|
+
|
|
12017
|
+
The old text sent every reader to "open Resolve Studio", which is wrong
|
|
12018
|
+
advice for a free-edition user — the edition external scripting refuses by
|
|
12019
|
+
design, and the one the bridge exists to reach.
|
|
12020
|
+
"""
|
|
12021
|
+
if bridge_on:
|
|
12022
|
+
return ("The in-app bridge is enabled but not answering. In Resolve, run "
|
|
12023
|
+
"Workspace > Scripts > resolve_bridge; launching Resolve cannot start it.")
|
|
12024
|
+
return ("DaVinci Resolve is not connected. On Studio, enable Preferences > General > "
|
|
12025
|
+
"'External scripting using' = Local. On the free edition, install the in-app "
|
|
12026
|
+
"bridge, run Workspace > Scripts > resolve_bridge, and set "
|
|
12027
|
+
"DAVINCI_RESOLVE_BRIDGE=1.")
|
|
12028
|
+
|
|
12029
|
+
|
|
11985
12030
|
@_serialize_resolve
|
|
11986
12031
|
def _current_resolve_project_id() -> Tuple[Optional[str], Optional[str]]:
|
|
11987
12032
|
"""(project_id, error) for the currently-open Resolve project.
|
package/src/granular/common.py
CHANGED
|
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
85
85
|
handlers=[logging.StreamHandler()],
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
VERSION = "2.70.
|
|
88
|
+
VERSION = "2.70.2"
|
|
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()}")
|