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 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
- [![Version](https://img.shields.io/badge/version-2.70.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.70.2-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)
@@ -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
- Requires a **framework Python** (python.org). Resolve enumerates `.py` scripts
55
- only when it finds one — Homebrew, pyenv and conda interpreters are not
56
- detected, and the script silently never appears in the menu. A Lua canary is
57
- installed alongside so you can tell that apart from a wrong folder.
58
-
59
- Validated on free 21.0.3.7 and Studio 19.1.3.7. This is the documented in-app
60
- path, not a licence circumvention, but Blackmagic could close it treat it as a
61
- supported-until-it-is-not tier. Loopback only, HMAC-signed requests, one-use
62
- nonces.
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 Python**
31
- (python.org). Homebrew/pyenv/conda are not detected and the script simply never
32
- appears, with no error. The installer preflights this and ships a Lua canary,
33
- which always lists, so "Python not detected" is distinguishable from "wrong
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.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.70.0",
3
+ "version": "2.70.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
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", "/Applications/DaVinci Resolve/DaVinci 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 App
7
- Store container used by the free edition — which is why the free edition can be
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
- candidates = [
97
- home / _SCRIPTS_SUFFIX,
98
- Path("/") / _SCRIPTS_SUFFIX,
99
- home / ".local/share/DaVinciResolve/Fusion/Scripts/Utility",
100
- ]
101
- containers = home / "Library/Containers"
102
- sandboxed: list[Path] = []
103
- if containers.is_dir():
104
- for entry in sorted(containers.iterdir()):
105
- if _is_resolve_container(entry) and (entry / "Data").is_dir():
106
- sandboxed.append(entry / "Data" / _SCRIPTS_SUFFIX)
107
- sandboxed.append(entry / "Data" / _SANDBOX_FALLBACK_SUFFIX)
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
- # Sandboxed: the container's existence is the signal; the tree gets created.
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
- target.mkdir(mode=0o700, parents=True, exist_ok=True)
261
- for probe in ("resolve_bridge_probe.py", "resolve_capability_probe.py"):
262
- shutil.copy2(REPO / "scripts" / probe, target / probe)
263
- installed.append(str(target / probe))
264
- # Lua always enumerates; Python only with a framework install. The canary
265
- # makes "Python not detected" distinguishable from "wrong folder".
266
- canary = target / "resolve_bridge_canary.lua"
267
- canary.write_text(_LUA_CANARY, encoding="utf-8")
268
- installed.append(str(canary))
269
- if not probe_only:
270
- runtime = target.parents[1] / ".davinci_mcp_runtime"
271
- runtime.mkdir(mode=0o700, parents=True, exist_ok=True)
272
- for module in ("resolve_bridge.py", "resolve_bridge_ops.py"):
273
- shutil.copy2(REPO / "src/utils" / module, runtime / module)
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
- return None, f"Resolve scripting API unavailable: {exc}"
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 dvr_script # type: ignore
11999
+ import DaVinciResolveScript as _dvr_script # type: ignore
12000
+
12001
+ dvr_script = _dvr_script
11974
12002
  except Exception as exc:
11975
- return None, f"Resolve scripting API unavailable: {exc}"
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, "DaVinci Resolve is not connected. Open Resolve Studio with a project loaded."
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.
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.70.0"
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()}")
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.70.0"
14
+ VERSION = "2.70.2"
15
15
 
16
16
  import base64
17
17
  import os