davinci-resolve-mcp 2.62.1 → 2.62.3

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,56 @@
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.62.3
6
+
7
+ A single grading fix (PR #90, by @Mldphotohraphie), extended and hardened. No
8
+ new tool surface.
9
+
10
+ ### Fixed
11
+
12
+ - **`graph set_lut` now applies LUTs/DCTLs installed by the `dctl` tool** —
13
+ the `dctl` tool installs into Resolve's per-user LUT directory, but
14
+ `Graph.SetLUT()` resolves LUT paths (relative names *and* absolute paths)
15
+ only against the master (system) LUT directory. A freshly installed LUT could
16
+ therefore never be applied, and `set_lut` always returned
17
+ `{"success": false}`. Verified live on Resolve Studio 21: `SetLUT` fails for a
18
+ user-dir LUT even after `RefreshLUTList()` and even via an absolute user-dir
19
+ path, so relocation into the master dir is genuinely required. On a `SetLUT`
20
+ failure the server now locates the LUT, stages it under a namespaced
21
+ `MCP/` subfolder of the master LUT dir (avoiding basename collisions with
22
+ stock/vendor LUTs), calls `RefreshLUTList()`, and retries. No behavior change
23
+ when `SetLUT` already succeeds. Applied to both `graph set_lut` (`src/server.py`)
24
+ and the granular `graph_set_lut` (`src/granular/graph.py`) via a shared
25
+ `src/utils/lut_paths.py` helper, with offline coverage in
26
+ `tests/test_lut_paths.py`.
27
+
28
+ ## What's New in v2.62.2
29
+
30
+ A single cross-platform launcher fix. No new tool surface; default behavior is
31
+ unchanged on macOS/Linux.
32
+
33
+ ### Fixed
34
+
35
+ - **Advanced server launcher no longer crashes on Windows** —
36
+ `bin/davinci-resolve-advanced-mcp.mjs` resolved the server entry to an
37
+ absolute filesystem path and passed it straight to dynamic `import()`. On
38
+ Windows that path (e.g. `C:\...\resolve-advanced\server\index.mjs`) is parsed
39
+ by Node's ESM loader as a URL whose drive letter reads as the protocol, so the
40
+ launcher died on arrival with
41
+ `ERR_UNSUPPORTED_ESM_URL_SCHEME` ("Received protocol 'c:'"). The path is now
42
+ converted with `pathToFileURL()` before importing — the canonical
43
+ cross-platform way to hand an absolute path to `import()`. macOS/Linux behavior
44
+ is unchanged, and paths containing spaces or special characters are now handled
45
+ correctly on every platform. Thanks to Ryan Saunders (@Alpha7449) for the
46
+ report and fix.
47
+
48
+ ### Validation
49
+
50
+ - Static checks and `node --check` on the modified launcher run. This is the only
51
+ dynamic `import()` call site in the repo receiving an absolute filesystem path;
52
+ the fix touches the Node advanced launcher only and changes no Resolve
53
+ scripting behavior, so a live Resolve run is not required.
54
+
5
55
  ## What's New in v2.62.1
6
56
 
7
57
  Two correctness fixes for real-world project/DRP layouts. No new tool surface;
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.62.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.62.3-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)
@@ -10,11 +10,11 @@
10
10
  * the repo root for the published package.)
11
11
  */
12
12
 
13
- import { fileURLToPath } from 'node:url';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
14
  import path from 'node:path';
15
15
 
16
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
17
  const serverEntry = path.resolve(__dirname, '..', 'resolve-advanced', 'server', 'index.mjs');
18
18
 
19
- const { startServer } = await import(serverEntry);
19
+ const { startServer } = await import(pathToFileURL(serverEntry).href);
20
20
  await startServer();
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.62.1"
38
+ VERSION = "2.62.3"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.62.1",
3
+ "version": "2.62.3",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.62.1"
83
+ VERSION = "2.62.3"
84
84
  logger = logging.getLogger("davinci-resolve-mcp")
85
85
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
86
  logger.info(f"Detected platform: {get_platform()}")
@@ -1,6 +1,7 @@
1
1
  """Color page graph, LUT, and color-group tools."""
2
2
 
3
3
  from src.granular.common import * # noqa: F401,F403
4
+ from src.utils.lut_paths import ensure_lut_in_master
4
5
 
5
6
  resolve = ResolveProxy()
6
7
 
@@ -39,8 +40,25 @@ def graph_set_lut(node_index: int, lut_path: str, item_index: int = 0, track_typ
39
40
  graph = item.GetNodeGraph()
40
41
  if not graph:
41
42
  return {"error": "No node graph available"}
42
- result = graph.SetLUT(node_index, lut_path)
43
- return {"success": bool(result)}
43
+ ok = bool(graph.SetLUT(node_index, lut_path))
44
+ if not ok:
45
+ # SetLUT resolves LUTs only against the master LUT dir, not the per-user
46
+ # dir dctl installs to. Relocate into the master dir and retry.
47
+ relocated = ensure_lut_in_master(lut_path)
48
+ if relocated:
49
+ try:
50
+ project = resolve.GetProjectManager().GetCurrentProject()
51
+ if project:
52
+ project.RefreshLUTList()
53
+ except Exception:
54
+ pass
55
+ ok = bool(graph.SetLUT(node_index, relocated))
56
+ if ok:
57
+ return {"success": True, "resolved_lut": relocated,
58
+ "note": "LUT staged under the master LUT dir "
59
+ "(MCP/ subfolder) and applied; SetLUT does "
60
+ "not resolve the user LUT dir."}
61
+ return {"success": ok}
44
62
 
45
63
 
46
64
  @mcp.tool()
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.62.1"
14
+ VERSION = "2.62.3"
15
15
 
16
16
  import base64
17
17
  import os
@@ -97,6 +97,7 @@ from src.utils.media_analysis_jobs import (
97
97
  run_batch_job_slice as run_media_analysis_batch_job_slice,
98
98
  )
99
99
  from src.utils.platform import get_resolve_paths, get_resolve_plugin_paths
100
+ from src.utils.lut_paths import master_lut_dir, ensure_lut_in_master
100
101
  from src.utils import fuse_templates, dctl_templates, script_templates
101
102
  from src.utils.timeline_title_text import (
102
103
  candidate_title_property_keys as _candidate_title_property_keys,
@@ -22070,7 +22071,27 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
22070
22071
  elif action == "get_lut":
22071
22072
  return {"lut": g.GetLUT(p["node_index"])}
22072
22073
  elif action == "set_lut":
22073
- return {"success": bool(g.SetLUT(p["node_index"], p["lut_path"]))}
22074
+ node_index = p["node_index"]
22075
+ lut_path = p["lut_path"]
22076
+ ok = bool(g.SetLUT(node_index, lut_path))
22077
+ if not ok:
22078
+ # Resolve resolves LUTs for SetLUT only against the master LUT dir,
22079
+ # not the per-user dir that dctl install writes to. Relocate and retry.
22080
+ relocated = _ensure_lut_in_master(lut_path)
22081
+ if relocated:
22082
+ try:
22083
+ _, proj, _lut_err = _check()
22084
+ if proj:
22085
+ proj.RefreshLUTList()
22086
+ except Exception:
22087
+ pass
22088
+ ok = bool(g.SetLUT(node_index, relocated))
22089
+ if ok:
22090
+ return {"success": True, "resolved_lut": relocated,
22091
+ "note": "LUT staged under the master LUT dir "
22092
+ "(MCP/ subfolder) and applied; SetLUT does "
22093
+ "not resolve the user LUT dir."}
22094
+ return {"success": ok}
22074
22095
  elif action == "get_node_cache":
22075
22096
  return {"cache": g.GetNodeCacheMode(p["node_index"])}
22076
22097
  elif action == "set_node_cache":
@@ -23771,6 +23792,13 @@ def _dctl_dir(category: str = "lut") -> str:
23771
23792
  "Valid: lut, aces_idt, aces_odt")
23772
23793
 
23773
23794
 
23795
+ # LUT relocation for Graph.SetLUT lives in src.utils.lut_paths so server.py and
23796
+ # src/granular/graph.py share one implementation (see the module docstring for
23797
+ # the live-verified behavior). Thin aliases keep the existing call sites stable.
23798
+ _master_lut_dir = master_lut_dir
23799
+ _ensure_lut_in_master = ensure_lut_in_master
23800
+
23801
+
23774
23802
  def _validate_dctl_name(name: str) -> Optional[Dict[str, Any]]:
23775
23803
  if not name or not _DCTL_NAME_RE.match(name):
23776
23804
  return _err(f"Invalid DCTL name '{name}'. "
@@ -0,0 +1,80 @@
1
+ """Master LUT directory resolution for Graph.SetLUT().
2
+
3
+ Resolve's Graph.SetLUT() resolves relative LUT names -- and even absolute
4
+ paths -- ONLY against the master (system) LUT root, NOT the per-user LUT dir
5
+ that the dctl tool installs into. Verified live on Resolve Studio 21: SetLUT
6
+ succeeds for a LUT in the master dir (by relative or subfolder path) and
7
+ returns False for the same file in the user dir, even after RefreshLUTList and
8
+ even via an absolute user-dir path. These helpers relocate a user-dir LUT into
9
+ a namespaced subfolder of the master dir so SetLUT can resolve it.
10
+ """
11
+
12
+ import os
13
+ import platform
14
+ import shutil
15
+ from typing import List, Optional
16
+
17
+ from src.utils.platform import get_resolve_plugin_paths
18
+
19
+ # Subfolder under the master LUT root where relocated LUTs are staged. SetLUT
20
+ # resolves relative paths against the master root, so a subfolder path like
21
+ # "MCP/Foo.cube" works AND avoids clobbering stock/vendor LUTs that share a
22
+ # basename (e.g. InstantC.cube). Verified live on Resolve Studio 21.
23
+ MASTER_LUT_RELOCATE_SUBDIR = "MCP"
24
+
25
+
26
+ def master_lut_dir() -> str:
27
+ """Return Resolve's master (system) LUT directory for this platform."""
28
+ plat = platform.system().lower()
29
+ if plat == "windows":
30
+ programdata = os.environ.get("PROGRAMDATA", r"C:\ProgramData")
31
+ return os.path.join(programdata, "Blackmagic Design",
32
+ "DaVinci Resolve", "Support", "LUT")
33
+ if plat == "linux":
34
+ for cand in ("/opt/resolve/LUT", "/home/resolve/LUT"):
35
+ if os.path.isdir(cand):
36
+ return cand
37
+ return "/opt/resolve/LUT"
38
+ # darwin / default
39
+ return "/Library/Application Support/Blackmagic Design/DaVinci Resolve/LUT"
40
+
41
+
42
+ def _user_lut_dir() -> Optional[str]:
43
+ """Return the per-user LUT dir (where dctl install writes), or None."""
44
+ try:
45
+ return get_resolve_plugin_paths()["dctl_dir"]
46
+ except Exception:
47
+ return None
48
+
49
+
50
+ def ensure_lut_in_master(lut_path: str) -> Optional[str]:
51
+ """Make a LUT resolvable by Graph.SetLUT().
52
+
53
+ Locates the file named by lut_path (absolute, user LUT dir, or already in
54
+ the master dir), copies it into a namespaced subfolder of the master LUT
55
+ dir when needed, and returns the master-relative path (forward slashes, as
56
+ GetLUT reports) to hand back to SetLUT. Returns None if the source cannot
57
+ be found or the master dir is not writable.
58
+ """
59
+ master = master_lut_dir()
60
+ base = os.path.basename(lut_path)
61
+ candidates: List[str] = []
62
+ if os.path.isabs(lut_path):
63
+ candidates.append(lut_path)
64
+ else:
65
+ user_dir = _user_lut_dir()
66
+ if user_dir:
67
+ candidates.append(os.path.join(user_dir, lut_path))
68
+ candidates.append(os.path.join(master, lut_path))
69
+ src = next((c for c in candidates if os.path.isfile(c)), None)
70
+ if not src:
71
+ return None
72
+ dst_dir = os.path.join(master, MASTER_LUT_RELOCATE_SUBDIR)
73
+ dst = os.path.join(dst_dir, base)
74
+ if os.path.abspath(src) != os.path.abspath(dst):
75
+ try:
76
+ os.makedirs(dst_dir, exist_ok=True)
77
+ shutil.copy2(src, dst)
78
+ except Exception:
79
+ return None
80
+ return f"{MASTER_LUT_RELOCATE_SUBDIR}/{base}"