davinci-resolve-mcp 2.70.0 → 2.70.1

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,92 @@
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.1
6
+
7
+ Windows support for the free-edition in-app bridge, and the end of doctor.py's
8
+ macOS-only path assumptions. Reported in issue #106 by @kacemmosbah8-afk.
9
+
10
+ ### The bug
11
+
12
+ `script_targets()` in `scripts/install_resolve_bridge.py` enumerated macOS and
13
+ Linux candidates only. There was no Windows branch at all, so the candidate list
14
+ came back empty on every Windows machine and the installer exited with:
15
+
16
+ ```
17
+ No writable DaVinci Resolve Scripts/Utility folder found. Is Resolve installed?
18
+ ```
19
+
20
+ on installs where Resolve was demonstrably present. Since the in-app bridge is
21
+ the *only* route to the free edition — external scripting is Studio-gated by
22
+ Blackmagic — Windows free-edition users had no working path to this server at
23
+ all. The bridge shipped in v2.68.0; it has been macOS/Linux-only that whole time.
24
+
25
+ Windows now targets the two Scripts/Utility trees Blackmagic documents, per-user
26
+ first because `%PROGRAMDATA%` typically needs an elevated prompt:
27
+
28
+ - `%APPDATA%\Blackmagic Design\DaVinci Resolve\Support\Fusion\Scripts\Utility`
29
+ - `%PROGRAMDATA%\Blackmagic Design\DaVinci Resolve\Fusion\Scripts\Utility`
30
+
31
+ Three traps came out of implementing that, each now pinned by a test:
32
+
33
+ - **The per-user tree carries a `Support` segment the all-users tree does not.**
34
+ They are not one layout under two roots; deriving either from the other lands
35
+ in a folder Resolve never scans.
36
+ - **Blackmagic's own README writes the per-user root as `%APPDATA%\Roaming\...`,
37
+ which is wrong** — `%APPDATA%` already *is* `...\AppData\Roaming`. Transcribing
38
+ it verbatim yields `AppData\Roaming\Roaming\...`: a real, creatable folder that
39
+ is silently never read.
40
+ - **Resolve does not create its `Fusion\Scripts` tree until a script is
41
+ installed**, so gating on that tree existing — or on its parent being writable
42
+ — skips a fresh free-edition install, the exact build the bridge exists for.
43
+ The candidates are gated on Resolve's product folder instead, mirroring how the
44
+ macOS sandbox container is handled.
45
+
46
+ `install()` no longer aborts when one target is unwritable. The `%PROGRAMDATA%`
47
+ tree needs elevation, and Windows `os.access(W_OK)` reports the read-only flag
48
+ rather than the ACL, so it cannot be screened out in advance — without this, the
49
+ newly added candidate would have crashed the installer on every non-elevated
50
+ Windows account *after* it had already succeeded into the per-user tree. Skips
51
+ are reported in `warnings`; only a clean sweep is fatal, and that error now names
52
+ the folders and the reason instead of asking whether Resolve is installed.
53
+
54
+ ### The macOS framework-Python alarm was firing off macOS
55
+
56
+ `python_preflight()` looked for a framework Python under `/Library/Frameworks`,
57
+ a path that cannot exist on Windows or Linux — so it always found nothing and
58
+ emitted the macOS remediation, telling the reporter (running a working python.org
59
+ 3.12.9) to install the Python they already had. Linux had the same false alarm.
60
+ The check is now macOS-only. The Lua canary still ships everywhere, so a genuine
61
+ enumeration failure stays diagnosable.
62
+
63
+ ### doctor.py was macOS-only too
64
+
65
+ `scripts/doctor.py` hardcoded macOS defaults for `RESOLVE_APP`,
66
+ `RESOLVE_SCRIPT_API` and `RESOLVE_SCRIPT_LIB`. Run standalone — that is, without
67
+ the environment variables the npm/`install.py` flow injects — it reported four
68
+ `[FAIL]` lines naming a `.app` bundle and `fusionscript.so` on a Windows 11
69
+ machine that had neither, while `install.py` detected that same install
70
+ correctly. Two tables describing one thing, one of them platform-blind. Linux was
71
+ equally affected.
72
+
73
+ doctor now selects per-platform candidates, and picks the first that exists so
74
+ the reported path matches reality. macOS values are byte-identical to before.
75
+ A new `tests/test_doctor_paths.py` drift guard asserts every path doctor names
76
+ also appears in `install.py`'s `RESOLVE_PATHS`, so the two cannot diverge again.
77
+ Claude Desktop's config path is now MSIX-aware on Windows as well, matching the
78
+ issue #93 fix that `install.py` already had.
79
+
80
+ ### Scope — what is and is not verified
81
+
82
+ The path construction is unit-tested, and a simulated Windows run exercises the
83
+ installer end to end. **No Windows hardware has confirmed that Resolve actually
84
+ lists the bridge from these folders.** The reporter verified the folders exist
85
+ and are writable; issue #104 is precedent that "the folder exists" and "Resolve
86
+ reads it" are different claims. The README and `docs/SKILL.md` say so plainly
87
+ rather than implying Windows is a supported, tested tier.
88
+
89
+ Suite: 2312 → 2336.
90
+
5
91
  ## What's New in v2.70.0
6
92
 
7
93
  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.1-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. **Windows paths were
61
+ added in v2.70.1 (issue #106) and have not yet been confirmed on Windows
62
+ hardware** — the installer targets the `%APPDATA%` and `%PROGRAMDATA%` script
63
+ folders Blackmagic documents, but whether Resolve lists the bridge from them is
64
+ unverified. 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,17 @@ 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 is unconfirmed.** The `%APPDATA%`/`%PROGRAMDATA%` script folders are
37
+ targeted as of v2.70.1, but no Windows machine has verified that Resolve lists
38
+ the bridge from them. If a user reports the menu entry missing on Windows, ask
39
+ whether the Lua canary lists — that separates "wrong folder" from "Python not
40
+ detected" there too.
35
41
  - The in-Resolve runtime is a **copy taken at install time**. After changing the
36
42
  repository, re-run the installer and then ask the running bridge to reload —
37
43
  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.1"
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.1",
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",
@@ -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.1"
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.1"
15
15
 
16
16
  import base64
17
17
  import os