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 +50 -0
- package/README.md +1 -1
- package/bin/davinci-resolve-advanced-mcp.mjs +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/graph.py +20 -2
- package/src/server.py +30 -2
- package/src/utils/lut_paths.py +80 -0
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
|
@@ -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.
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.62.
|
|
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()}")
|
package/src/granular/graph.py
CHANGED
|
@@ -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
|
-
|
|
43
|
-
|
|
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.
|
|
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
|
-
|
|
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}"
|