davinci-resolve-mcp 2.63.2 → 2.65.0

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,87 @@
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.65.0
6
+
7
+ Bundles two community contributions from @double2tea: an optional HTTP
8
+ transcription backend and a localized, build-aware control panel. Both are
9
+ opt-in and change no default behavior — with neither configured, existing
10
+ transcription backends and the English panel work exactly as before.
11
+
12
+ ### Added
13
+
14
+ - **MLX Audio Router transcription backend** (PR #95, @double2tea) — an optional
15
+ HTTP transcription backend that runs outside the Resolve MCP Python
16
+ environment. Enabled only when `DAVINCI_RESOLVE_MCP_MLX_AUDIO_URL` is set and a
17
+ bounded `GET /health` probe succeeds; an optional
18
+ `DAVINCI_RESOLVE_MCP_MLX_AUDIO_MODEL` overrides the router's default model.
19
+ When configured and healthy it is preferred in transcription capability
20
+ ordering, and its response is normalized into the existing JSON/SRT/VTT
21
+ transcript artifacts. Standard-library only (`urllib`); the MCP server never
22
+ installs, starts, or downloads anything on the router's behalf, and existing
23
+ Whisper backends are untouched when no URL is set.
24
+ - **Control panel localization (English / Simplified Chinese)** (PR #96,
25
+ @double2tea) — a persistent language switch on the local control panel.
26
+ English remains the authored, canonical UI; the browser stores only the
27
+ selected locale in `localStorage` and the initial locale follows browser
28
+ preferences. Translation runs client-side over the DOM (text nodes plus
29
+ `aria-label`/`title`/`placeholder`) and is fully reversible; no server API or
30
+ persisted project data changes.
31
+ - **Build-aware AI console** (PR #96, @double2tea) — the Resolve AI console now
32
+ uses the runtime capability payload to disable actions the connected Resolve
33
+ build cannot execute, distinguishing "Requires Resolve 21+" from "Unavailable
34
+ on this build" and surfacing the Resolve 20 transcription methods
35
+ (`TranscribeAudio` / `ClearTranscription`). Narrow-screen navigation and the
36
+ mobile footer were also improved.
37
+
38
+ ### Validation
39
+
40
+ - Static checks and drift guards pass; `tests/test_media_analysis.py` (110),
41
+ `tests/test_control_panel_i18n.py` + `tests/test_control_panel_ai_capabilities.py`
42
+ + `tests/test_open_control_panel.py` (14) pass on the merged tree.
43
+ - No DaVinci Resolve scripting behavior changed: PR #95 is a self-contained
44
+ HTTP/stdlib backend gated behind an env var, and PR #96 changes only the
45
+ control panel's client-side HTML/JS. Live Resolve validation not required.
46
+
47
+ ## What's New in v2.64.0
48
+
49
+ Adds opt-in support for DaVinci Resolve's **Network** external-scripting mode
50
+ (PR #91… correction: PR #94, by @double2tea), so the MCP can drive a Resolve
51
+ instance addressed by IP — including a Resolve running on the same machine with
52
+ `External scripting using = Network`. Contributed in PR #94 by @double2tea,
53
+ with two additional connection sites hardened during adoption. Local mode is
54
+ unchanged and remains the default.
55
+
56
+ ### Added
57
+
58
+ - **Resolve Network scripting mode** — set `RESOLVE_SCRIPT_HOST` to the Resolve
59
+ host IP (`127.0.0.1` on the same machine) to route connections through
60
+ Resolve's explicit IP-targeted `scriptapp("Resolve", host, timeout)` overload.
61
+ `RESOLVE_SCRIPT_TIMEOUT` (positive finite seconds, default 5) bounds the
62
+ connection wait. When `RESOLVE_SCRIPT_HOST` is absent, the server uses the
63
+ one-argument Local discovery exactly as before. A shared `connect_resolve`
64
+ helper centralizes this behavior; the compound server, granular server,
65
+ analysis dashboard, `scripts/doctor.py`, and the installer's post-install
66
+ connection probe all route through it. The installer copies
67
+ `RESOLVE_SCRIPT_HOST`/`RESOLVE_SCRIPT_TIMEOUT` into generated client configs
68
+ when present in its environment.
69
+ - **`doctor.py` Network flags** — `python3 scripts/doctor.py --resolve-host
70
+ <ip> [--resolve-timeout <seconds>]` runs the read-only connection check
71
+ against a Network-mode host.
72
+
73
+ ### Security
74
+
75
+ - Network scripting permits remote control of Resolve. Documentation (SKILL.md,
76
+ install.md) advises using Local mode when remote access is unnecessary, and
77
+ otherwise restricting access with host firewall and network controls.
78
+
79
+ ### Validation
80
+
81
+ - Live-validated on DaVinci Resolve Studio 19.1.3.7: Local mode (one-argument
82
+ discovery) and Network mode (`--resolve-host 127.0.0.1 --resolve-timeout 8`)
83
+ both connect through the shared `connect_resolve` helper. PR author validated
84
+ against Studio 20.3.2.9 in Network mode. Full offline suite: 1514 tests.
85
+
5
86
  ## What's New in v2.63.2
6
87
 
7
88
  Installer fix for Windows MSIX builds of Claude Desktop (issue #93, reported by
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.63.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.65.0-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)
package/docs/SKILL.md CHANGED
@@ -14,8 +14,14 @@ session — projects, timelines, clips, color grading, Fusion compositions, audi
14
14
  render queues, and more — through natural language.
15
15
 
16
16
  DaVinci Resolve must be running with **Preferences > General > "External scripting
17
- using"** set to **Local**. The server auto-launches Resolve if it is not running,
18
- but that first connection can take up to 60 seconds.
17
+ using"** set to **Local**, or set to **Network** with `RESOLVE_SCRIPT_HOST`
18
+ configured to the Resolve host IP (use `127.0.0.1` on the same machine). The
19
+ server auto-launches Resolve if it is not running, but that first connection can
20
+ take up to 60 seconds.
21
+
22
+ Network scripting permits remote control of Resolve. Use Local mode when remote
23
+ access is unnecessary; otherwise restrict access with host firewall and network
24
+ controls.
19
25
 
20
26
  **Session-start update note.** The first `resolve_control(action="get_version")`
21
27
  of a session returns an `mcp` block with the cached update check
@@ -79,6 +79,23 @@ which ffmpeg && ffmpeg -version 2>&1 | head -1
79
79
  which whisper 2>/dev/null || which whisper-cpp 2>/dev/null || python3 -c "import whisper" 2>/dev/null
80
80
  ```
81
81
 
82
+ An HTTP MLX Audio Router can be used without installing a transcription module
83
+ into the Resolve MCP Python environment:
84
+
85
+ ```bash
86
+ export DAVINCI_RESOLVE_MCP_MLX_AUDIO_URL=http://127.0.0.1:8000
87
+ # Optional: omit this to use the router's own default model.
88
+ export DAVINCI_RESOLVE_MCP_MLX_AUDIO_MODEL=mlx-community/Qwen3-ASR-1.7B-8bit
89
+ ```
90
+
91
+ The configured service must expose `GET /health`, returning JSON with
92
+ `{"status":"ok"}`, and `POST /stt`. The transcription request uses the local
93
+ source path and requests JSON output; the response must contain a `transcript`
94
+ string whose value is a JSON object with `text` and `segments`. The
95
+ [Audiobox MLX Audio Router](https://github.com/double2tea/Audiobox) implements
96
+ this contract. Model download behavior follows the existing
97
+ `allow_model_download` transcription option.
98
+
82
99
  FFprobe is required. If missing:
83
100
  - macOS: `brew install ffmpeg`
84
101
  - Linux: `sudo apt install ffmpeg` or `sudo dnf install ffmpeg`
package/docs/install.md CHANGED
@@ -7,7 +7,9 @@ This guide covers Resolve requirements, the universal installer, supported MCP c
7
7
  - **DaVinci Resolve Studio** 18.5+ (macOS, Windows, or Linux) — the free edition does not support external scripting
8
8
  - **Python 3.10+** (the MCP SDK requires 3.10). **3.10–3.12 is the lowest-risk
9
9
  choice**; 3.13/3.14 also work on recent Resolve builds — see below
10
- - DaVinci Resolve running with **Preferences > General > "External scripting using"** set to **Local**
10
+ - DaVinci Resolve running with **Preferences > General > "External scripting using"**
11
+ set to **Local**, or **Network** with `RESOLVE_SCRIPT_HOST` set to the Resolve
12
+ host IP (`127.0.0.1` when Resolve and the MCP run on the same machine)
11
13
 
12
14
  > **Python 3.13 / 3.14:** these are **allowed** — setup will use them and warn.
13
15
  > Python 3.14 is verified working against DaVinci Resolve Studio 20.3.2. On
@@ -170,6 +172,26 @@ If you prefer to set things up yourself, add to your MCP client config:
170
172
 
171
173
  On Windows, installer-generated configs also include `PYTHONHOME`. That scopes Resolve's Python binding to the selected interpreter and avoids the Resolve 20.3 multi-Python crash reported in [Issue #26](https://github.com/samuelgursky/davinci-resolve-mcp/issues/26).
172
174
 
175
+ For Resolve's **Network** scripting mode, add
176
+ `"RESOLVE_SCRIPT_HOST": "127.0.0.1"` to the `env` object above, replacing the
177
+ address when Resolve runs on another host. When this variable is present, the
178
+ server uses Resolve's explicit IP-targeted `scriptapp` call with a bounded
179
+ connection timeout. Set `RESOLVE_SCRIPT_TIMEOUT` to override the default
180
+ five-second timeout with a positive finite number of seconds. The installer
181
+ includes both values in generated client configs when they are present in its
182
+ environment.
183
+
184
+ Network scripting permits remote control of Resolve. Prefer Local mode when
185
+ remote access is unnecessary; otherwise restrict access with host firewall and
186
+ network controls.
187
+
188
+ Run the read-only doctor against Network mode explicitly:
189
+
190
+ ```bash
191
+ python3 scripts/doctor.py --resolve-host 127.0.0.1
192
+ python3 scripts/doctor.py --resolve-host resolve.example.test --resolve-timeout 12.5
193
+ ```
194
+
173
195
  When the compound server is running, `resolve_control(action="get_version")`
174
196
  includes the local MCP version, the last update-check status, and the current
175
197
  update decision under the `mcp` key. `resolve_control(action="mcp_update_status",
package/install.py CHANGED
@@ -15,6 +15,7 @@ Usage:
15
15
 
16
16
  import argparse
17
17
  import json
18
+ import math
18
19
  import os
19
20
  import platform
20
21
  import shutil
@@ -35,7 +36,7 @@ from src.utils.update_check import (
35
36
 
36
37
  # ─── Version ──────────────────────────────────────────────────────────────────
37
38
 
38
- VERSION = "2.63.2"
39
+ VERSION = "2.65.0"
39
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
42
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
@@ -474,6 +475,13 @@ def build_server_env(python_path, api_path, lib_path, system=SYSTEM, python_home
474
475
  if system == "Windows":
475
476
  env["PYTHONHOME"] = str(python_home or get_python_base_install(python_path))
476
477
 
478
+ host = os.environ.get("RESOLVE_SCRIPT_HOST")
479
+ if host:
480
+ env["RESOLVE_SCRIPT_HOST"] = host
481
+ timeout = os.environ.get("RESOLVE_SCRIPT_TIMEOUT")
482
+ if timeout:
483
+ env["RESOLVE_SCRIPT_TIMEOUT"] = timeout
484
+
477
485
  return env
478
486
 
479
487
 
@@ -810,12 +818,21 @@ def verify_resolve_connection(python_path, api_path, lib_path):
810
818
 
811
819
  env = {**os.environ, **build_server_env(python_path, api_path, lib_path)}
812
820
  modules_path = env["PYTHONPATH"]
821
+ repo_root = str(Path(__file__).resolve().parent)
822
+ # Route through connect_resolve so Network mode (RESOLVE_SCRIPT_HOST, propagated
823
+ # into env by build_server_env) uses the explicit IP-targeted overload. Fall
824
+ # back to Local-mode discovery if the helper cannot be imported.
813
825
  test_script = textwrap.dedent(f"""\
814
826
  import sys
815
827
  sys.path.insert(0, {modules_path!r})
828
+ sys.path.insert(0, {repo_root!r})
816
829
  try:
817
830
  import DaVinciResolveScript as dvr
818
- resolve = dvr.scriptapp('Resolve')
831
+ try:
832
+ from src.utils.resolve_connection import connect_resolve
833
+ except Exception:
834
+ connect_resolve = lambda mod: mod.scriptapp('Resolve')
835
+ resolve = connect_resolve(dvr)
819
836
  if resolve:
820
837
  name = resolve.GetProductName()
821
838
  ver = resolve.GetVersionString()
@@ -828,12 +845,22 @@ def verify_resolve_connection(python_path, api_path, lib_path):
828
845
  print(f"ERROR: {{e}}")
829
846
  """)
830
847
 
848
+ process_timeout = 10.0
849
+ configured_timeout = env.get("RESOLVE_SCRIPT_TIMEOUT")
850
+ if configured_timeout:
851
+ try:
852
+ network_timeout = float(configured_timeout)
853
+ if math.isfinite(network_timeout) and network_timeout > 0:
854
+ process_timeout = max(process_timeout, network_timeout + 2)
855
+ except ValueError:
856
+ pass
857
+
831
858
  try:
832
859
  result = subprocess.run(
833
860
  [str(python_path), "-c", test_script],
834
861
  capture_output=True,
835
862
  text=True,
836
- timeout=10,
863
+ timeout=process_timeout,
837
864
  env=env,
838
865
  )
839
866
  output = result.stdout.strip() or result.stderr.strip()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.63.2",
3
+ "version": "2.65.0",
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
@@ -7,16 +7,19 @@ mutating anything. Prints OK/WARN/FAIL lines (or JSON with --json) and exits
7
7
  nonzero only when a FAIL is present.
8
8
 
9
9
  Run: `python3 scripts/doctor.py`
10
+ Network mode: `python3 scripts/doctor.py --resolve-host 127.0.0.1`
10
11
 
11
12
  Overridable via environment: DAVINCI_MCP_REPO, DAVINCI_MCP_PYTHON,
12
13
  CODEX_HOME, CLAUDE_DESKTOP_CONFIG, RESOLVE_APP, RESOLVE_SCRIPT_API,
13
- RESOLVE_SCRIPT_MODULES, RESOLVE_SCRIPT_LIB.
14
+ RESOLVE_SCRIPT_MODULES, RESOLVE_SCRIPT_LIB, RESOLVE_SCRIPT_HOST,
15
+ RESOLVE_SCRIPT_TIMEOUT.
14
16
  """
15
17
 
16
18
  from __future__ import annotations
17
19
 
18
20
  import argparse
19
21
  import json
22
+ import math
20
23
  import os
21
24
  import re
22
25
  import subprocess
@@ -128,26 +131,41 @@ def git_head() -> str:
128
131
  return head["stdout"] or head["stderr"] or "git describe produced no output"
129
132
 
130
133
 
131
- def resolve_probe() -> dict[str, Any]:
134
+ def resolve_probe(
135
+ resolve_host: str | None = None,
136
+ resolve_timeout: float | None = None,
137
+ ) -> dict[str, Any]:
132
138
  if not PYTHON.exists():
133
139
  return {"import_ok": False, "error": f"{PYTHON} is missing"}
134
140
 
135
141
  code = f"""
136
142
  import json
137
143
  import sys
144
+ sys.path.insert(0, {str(REPO)!r})
138
145
  sys.path.insert(0, {str(RESOLVE_MODULES)!r})
139
146
  try:
140
147
  import DaVinciResolveScript as dvr
141
- resolve = dvr.scriptapp("Resolve")
142
- payload = {{
143
- "import_ok": True,
144
- "module": getattr(dvr, "__file__", None),
145
- "resolve_connected": bool(resolve),
146
- "product": resolve.GetProductName() if resolve else None,
147
- "version": resolve.GetVersionString() if resolve else None,
148
- }}
148
+ from src.utils.resolve_connection import connect_resolve
149
149
  except Exception as exc:
150
150
  payload = {{"import_ok": False, "error": repr(exc)}}
151
+ else:
152
+ try:
153
+ resolve = connect_resolve(dvr)
154
+ except Exception as exc:
155
+ payload = {{
156
+ "import_ok": True,
157
+ "module": getattr(dvr, "__file__", None),
158
+ "resolve_connected": False,
159
+ "connection_error": repr(exc),
160
+ }}
161
+ else:
162
+ payload = {{
163
+ "import_ok": True,
164
+ "module": getattr(dvr, "__file__", None),
165
+ "resolve_connected": bool(resolve),
166
+ "product": resolve.GetProductName() if resolve else None,
167
+ "version": resolve.GetVersionString() if resolve else None,
168
+ }}
151
169
  print(json.dumps(payload))
152
170
  """
153
171
  env = {
@@ -156,7 +174,26 @@ print(json.dumps(payload))
156
174
  "RESOLVE_SCRIPT_LIB": str(RESOLVE_LIB),
157
175
  "PYTHONPATH": str(RESOLVE_MODULES),
158
176
  }
159
- proc = subprocess.run([str(PYTHON), "-c", code], capture_output=True, text=True, timeout=12, env=env)
177
+ if resolve_host is not None:
178
+ env["RESOLVE_SCRIPT_HOST"] = resolve_host
179
+ if resolve_timeout is not None:
180
+ env["RESOLVE_SCRIPT_TIMEOUT"] = str(resolve_timeout)
181
+ process_timeout = 12.0
182
+ configured_timeout = env.get("RESOLVE_SCRIPT_TIMEOUT")
183
+ if configured_timeout:
184
+ try:
185
+ network_timeout = float(configured_timeout)
186
+ if math.isfinite(network_timeout) and network_timeout > 0:
187
+ process_timeout = max(process_timeout, network_timeout + 2)
188
+ except ValueError:
189
+ pass
190
+ proc = subprocess.run(
191
+ [str(PYTHON), "-c", code],
192
+ capture_output=True,
193
+ text=True,
194
+ timeout=process_timeout,
195
+ env=env,
196
+ )
160
197
  output = (proc.stdout or proc.stderr).strip()
161
198
  try:
162
199
  payload = json.loads(output)
@@ -166,7 +203,10 @@ print(json.dumps(payload))
166
203
  return payload
167
204
 
168
205
 
169
- def collect() -> list[dict[str, str]]:
206
+ def collect(
207
+ resolve_host: str | None = None,
208
+ resolve_timeout: float | None = None,
209
+ ) -> list[dict[str, str]]:
170
210
  results: list[dict[str, str]] = []
171
211
 
172
212
  check(results, "OK" if REPO.exists() else "FAIL", "MCP checkout", str(REPO))
@@ -186,18 +226,27 @@ def collect() -> list[dict[str, str]]:
186
226
  pyver = run([str(PYTHON), "--version"]) if PYTHON.exists() else {"ok": False, "stdout": "", "stderr": "missing"}
187
227
  check(results, "OK" if pyver["ok"] else "FAIL", "Python version", pyver["stdout"] or pyver["stderr"])
188
228
 
189
- probe = resolve_probe()
229
+ probe = resolve_probe(resolve_host, resolve_timeout)
190
230
  if probe.get("import_ok"):
191
231
  check(results, "OK", "DaVinciResolveScript import", str(probe.get("module")))
192
232
  if probe.get("resolve_connected"):
193
233
  detail = f"{probe.get('product')} {probe.get('version')}"
194
234
  check(results, "OK", "Resolve scripting connection", detail)
235
+ elif probe.get("connection_error"):
236
+ check(
237
+ results,
238
+ "FAIL",
239
+ "Resolve scripting connection",
240
+ str(probe["connection_error"]),
241
+ )
195
242
  else:
196
243
  check(
197
244
  results,
198
245
  "WARN",
199
246
  "Resolve scripting connection",
200
- 'Module import worked, but scriptapp("Resolve") returned no object. Open DaVinci Resolve Studio, set Preferences > General > External scripting using = Local, and restart Resolve.',
247
+ "Module import worked, but scriptapp returned no object. Use "
248
+ "External scripting = Local, or Network with --resolve-host "
249
+ "set to the Resolve host IP, then restart Resolve.",
201
250
  )
202
251
  else:
203
252
  check(results, "FAIL", "DaVinciResolveScript import", str(probe.get("error")))
@@ -211,9 +260,18 @@ def collect() -> list[dict[str, str]]:
211
260
  def main() -> int:
212
261
  parser = argparse.ArgumentParser(description=__doc__)
213
262
  parser.add_argument("--json", action="store_true", help="Print machine-readable JSON")
263
+ parser.add_argument(
264
+ "--resolve-host",
265
+ help="Resolve host IP for Network scripting mode",
266
+ )
267
+ parser.add_argument(
268
+ "--resolve-timeout",
269
+ type=float,
270
+ help="Network connection timeout in seconds (default: 5)",
271
+ )
214
272
  args = parser.parse_args()
215
273
 
216
- results = collect()
274
+ results = collect(args.resolve_host, args.resolve_timeout)
217
275
  if args.json:
218
276
  print(json.dumps({"checks": results}, indent=2))
219
277
  else: