davinci-resolve-mcp 2.223.0 → 2.224.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,113 @@
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.224.1 — the bridge installer explains the outcome it was built to detect
6
+
7
+ Reported by @hemna (#219). No behaviour changed; the installer writes exactly
8
+ what it wrote before and says considerably more about it.
9
+
10
+ ### Fixed
11
+
12
+ - **The canary-only outcome had no printed guidance at all.** Seeing
13
+ `resolve_bridge_canary` in Workspace ▸ Scripts while `resolve_bridge_probe`
14
+ is absent is not a failed install — it is the single most informative thing
15
+ the installer can tell you, and the exact signal the Lua canary exists to
16
+ produce. But the printed steps were a fixed four-line list that assumed the
17
+ Python probe had listed, so a user in this case followed step 3 to a menu
18
+ entry that cannot exist. The explanation was written down the whole time —
19
+ inside the canary's own Lua comments, which nobody has any reason to open.
20
+ There is now a real branch for it that says the install worked, says not to
21
+ re-run it, and explains what the missing probe means.
22
+ - **Duplicate canary entries are now expected rather than alarming.** The
23
+ installer writes into every Scripts/Utility folder Resolve scans, giving the
24
+ canary the same filename in each, so Resolve lists it once per folder with no
25
+ way to tell them apart. The reporter saw two and reasonably read it as a
26
+ broken install; a real run on the maintainer's machine produces **four**. The
27
+ guidance now names the number and says running any one of them is the same as
28
+ running any other. The count is derived from what was actually installed, and
29
+ the filename now has a single definition shared by the writer and the
30
+ counter — those two disagreeing would produce guidance promising entries that
31
+ are not there.
32
+ - **The Console is named.** The canary reports through `print()`, which lands in
33
+ Workspace ▸ Console and nowhere else. The installer had never mentioned the
34
+ Console — the string does not appear in it — so running the canary with no
35
+ Console open looks exactly like nothing happening, which is what was
36
+ reported.
37
+
38
+ ### Changed
39
+
40
+ - The canary-only explanation is **split by edition instead of asserting a
41
+ single cause**. The canary's own text predates Resolve 21.1 and blames Python
42
+ discovery — `PYTHON3HOME`, then `/usr/local/bin/python3`, and nowhere else.
43
+ That is still right on Studio and on 21.0.x and earlier, but on **free 21.1
44
+ it is wrong**: Python scripting moved to the Studio edition (#203), so `.py`
45
+ files do not list there whatever Python is installed, and the older advice
46
+ would send a user chasing a setting that cannot fix their problem. Both
47
+ branches are now stated, newer cause first.
48
+
49
+ ### Validation
50
+
51
+ - Full suite green: 3,484 passed, 1 skipped. Six new tests cover the duplicate
52
+ count and its wording, the absence of that wording for a single canary, the
53
+ canary-only branch, the Console pointer, both edition branches with the
54
+ newer one ordered first, and the single-definition guarantee on the canary
55
+ filename.
56
+ - Verified by running the installer for real on this machine, which is where
57
+ the four-entry figure comes from.
58
+ - **Still open in #219**: the canary's own embedded remediation text carries
59
+ the pre-21.1 single-cause diagnosis. Correcting what it says to a specific
60
+ user needs their edition, which has been asked for; the printed guidance
61
+ above no longer depends on that answer.
62
+
63
+ ## What's New in v2.224.0 — native Resolve 21.1 DCTL encryption
64
+
65
+ Contributed by @legionsound (#216), live-validated on Studio 21.1.0.14.
66
+
67
+ ### Added
68
+
69
+ - **`dctl encrypt_native`**, with the granular twin `encrypt_dctl_native`,
70
+ calling native 21.1 DCTL encryption. The caller supplies an existing `.dctl`
71
+ input, a new `.dctle` output path and an optional expiry; the wrapper reports
72
+ the actual final path, size and hash. Tool count 376 → 377.
73
+ - **A destination is never replaced.** Resolve writes into isolated staging and
74
+ only a verified non-empty regular file is published. The publish uses
75
+ `O_EXCL` creation at `0o600`, so a file that appears *during* encryption
76
+ cannot be clobbered, and the existence check uses `lexists` so a **dangling
77
+ symlink** counts as an occupied destination rather than a free one — the case
78
+ a plain existence test silently gets wrong. The cross-volume fallback removes
79
+ its own partial output if the copy fails.
80
+ - Source bytes are preserved, and the action neither installs nor applies the
81
+ shader.
82
+
83
+ ### Changed
84
+
85
+ - Classified **LOW** risk rather than MEDIUM, with a destructive-action hook
86
+ entry and dry-run refusal coverage. LOW is the honest rating here: the action
87
+ only ever creates a new file and is incapable of overwriting one, so grouping
88
+ it with operations that rewrite existing work would make the rating mean
89
+ less. Verified: `dctl.encrypt_native` classifies LOW / destructive /
90
+ recognised, and is in the destructive registry.
91
+ - Two native boundaries handled explicitly rather than papered over: Resolve
92
+ appends `.dctle` itself, so a fixed staging stem prevents a doubled suffix on
93
+ a user-supplied name; and an empty expiry string is normalized to null,
94
+ because an isolated probe measured the native call returning false for `""`
95
+ and true for null or omission. Other expiry strings pass through untouched.
96
+
97
+ ### Validation
98
+
99
+ - Full suite green: 3,478 passed, 1 skipped. Drift guards, api-parity and read/write
100
+ symmetry all clean with the 377 count.
101
+ - Live evidence is @legionsound's on Studio 21.1.0.14, through both interfaces
102
+ with synthetic identity code: each exported a non-empty file, reported
103
+ correct size and hash, preserved the source bytes, produced owner-only
104
+ permissions, and refused a repeat export without altering the destination.
105
+ Not reproduced here; this machine is Studio 19.1.3.7, below the 21.1 floor.
106
+ - **Not claimed**: that an encrypted shader is accepted by a render, and
107
+ nothing at all about cipher strength. Observed file sizes are recorded as
108
+ observations, not format guarantees.
109
+ - This PR was branched from current `main` and merged **without adaptation** —
110
+ the first in the 21.1 series to need none.
111
+
5
112
  ## What's New in v2.223.0 — native Resolve 21.1 DCTL validation
6
113
 
7
114
  Contributed by @legionsound (#215), live-validated on Studio 21.1.0.14.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.223.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.224.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
- [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(376%20full)-blue.svg)](#server-modes)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(377%20full)-blue.svg)](#server-modes)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#server-modes)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
@@ -133,7 +133,7 @@ The command starts a loopback-only server and opens the control panel in your br
133
133
  | Mode | Entry point | Tools | Best for |
134
134
  |------|-------------|-------|----------|
135
135
  | Compound | `src/server.py` | 36 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
136
- | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 376 | Power users who want one MCP tool per Resolve API method. |
136
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 377 | Power users who want one MCP tool per Resolve API method. |
137
137
 
138
138
  The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
139
139
 
@@ -365,7 +365,7 @@ The default server is a local stdio process launched by your MCP client; it does
365
365
 
366
366
  | Metric | Value |
367
367
  |--------|-------|
368
- | MCP Tools | **36** compound / **376** granular (live server) |
368
+ | MCP Tools | **36** compound / **377** granular (live server) |
369
369
  | Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
370
370
  | Kernel Actions | **136** guarded workflow actions across 9 compound tools |
371
371
  | API Methods Covered | **361/361** (100%) |
package/README.zh-CN.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.223.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.224.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
- [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(376%20full)-blue.svg)](#服务器模式)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(377%20full)-blue.svg)](#服务器模式)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#服务器模式)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.223.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.224.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -88,7 +88,7 @@ venv/bin/python -m src.control_panel
88
88
  | 模式 | 入口 | 工具数 | 适合谁 |
89
89
  |------|------|--------|--------|
90
90
  | Compound(复合) | `src/server.py` | 36 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
91
- | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 376 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
91
+ | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 377 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
92
92
 
93
93
  除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
94
94
 
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
226
226
 
227
227
  | 指标 | 数值 |
228
228
  |------|------|
229
- | MCP 工具 | **36** 复合 / **376** 细粒度(实时服务器) |
229
+ | MCP 工具 | **36** 复合 / **377** 细粒度(实时服务器) |
230
230
  | Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
231
231
  | 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
232
232
  | API 方法覆盖 | **361/361**(100%) |
package/docs/SKILL.md CHANGED
@@ -345,7 +345,7 @@ to the user as verified.
345
345
  | Mode | Entry point | Tool count | Use when |
346
346
  |---|---|---|---|
347
347
  | Compound (default) | `src/server.py` | 36 tools | Most workflows — keeps context lean |
348
- | Granular (full) | `src/server.py --full` | 376 tools | Power users needing one tool per API method |
348
+ | Granular (full) | `src/server.py --full` | 377 tools | Power users needing one tool per API method |
349
349
 
350
350
  Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
351
351
  for edition, presets, audio formats/codecs, normalization modes, speed, fades
@@ -2492,3 +2492,5 @@ Native audio normalization: [21.1 controls](reference/resolve211-normalization.m
2492
2492
  Native timecode/waveform alignment: [21.1 controls](reference/resolve211-alignment.md), including linked-item selection semantics and rendered video/audio evidence.
2493
2493
 
2494
2494
  Resolve-native DCTL validation: [21.1 controls](reference/resolve211-dctl-validation.md), separate from static validation and shader rendering.
2495
+
2496
+ Native DCTL encryption: [21.1 controls](reference/resolve211-encryption.md), with explicit destination handling and export-evidence limits.
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
64
64
  ├── install.py # Universal installer (macOS/Windows/Linux)
65
65
  ├── src/
66
66
  │ ├── server.py # Compound MCP server — 36 tools (default)
67
- │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 376 tools
67
+ │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 377 tools
68
68
  │ ├── granular/ # Modular full-server implementation
69
69
  │ └── utils/ # Platform detection, Resolve connection helpers
70
70
  ├── tests/ # offline suite (test_*.py) + live harnesses (live_*.py):
package/docs/install.md CHANGED
@@ -144,7 +144,7 @@ The MCP server comes in two modes:
144
144
  | Mode | File | Tools | Best For |
145
145
  |------|------|-------|----------|
146
146
  | **Compound** (default) | `src/server.py` | 36 | Most users — fast, clean, low context usage |
147
- | **Full** | `src/resolve_mcp_server.py` | 376 | Power users who want one tool per API method |
147
+ | **Full** | `src/resolve_mcp_server.py` | 377 | Power users who want one tool per API method |
148
148
 
149
149
  The compound server's `timeline_item` tool includes dedicated actions for common workflows:
150
150
 
@@ -159,7 +159,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
159
159
 
160
160
  The installer uses the compound server by default. To use the full server:
161
161
  ```bash
162
- python src/server.py --full # Launch full 376-tool server
162
+ python src/server.py --full # Launch full 377-tool server
163
163
  # Or point your MCP config directly at src/resolve_mcp_server.py
164
164
  ```
165
165
 
@@ -25,7 +25,7 @@ Every non-deprecated method in the bundled legacy README is represented. This
25
25
  does not claim complete coverage of the newer Resolve 21.1 typed API. The
26
26
  default compound server exposes **36 tools** that group related operations by
27
27
  action parameter, keeping LLM context windows lean. The full granular server
28
- provides **376 individual tools** for power users. The legacy coverage spans
28
+ provides **377 individual tools** for power users. The legacy coverage spans
29
29
  13 API object classes. MCP-level kernel actions are tracked separately in
30
30
  [Kernel Action Coverage](../kernels/README.md).
31
31
 
@@ -642,3 +642,5 @@ Native audio normalization: [21.1 controls](resolve211-normalization.md), with i
642
642
  Native timecode/waveform alignment: [21.1 controls](resolve211-alignment.md), including linked-item selection semantics and rendered video/audio evidence.
643
643
 
644
644
  Resolve-native DCTL validation: [21.1 controls](resolve211-dctl-validation.md), separate from static validation and shader rendering.
645
+
646
+ Native DCTL encryption: [21.1 controls](resolve211-encryption.md), with explicit destination handling and export-evidence limits.
@@ -0,0 +1,48 @@
1
+ # Native Resolve 21.1 DCTL encryption
2
+
3
+ Compound `dctl encrypt_native` and granular `encrypt_dctl_native` accept
4
+ `input_path`, `output_path` and optional `expiry`. Input must be an existing .dctl;
5
+ output must be a new .dctle. Existing destinations, including dangling symlinks,
6
+ are refused. The source is never modified by the wrapper.
7
+
8
+ The native Name and OutputFolder options are derived from an isolated staging
9
+ file, then the completed file is published to the requested output path. This
10
+ avoids native filename surprises: Resolve appends .dctle even when Name already
11
+ ends with that suffix. Expiry null/omission means no expiry. Empty string is
12
+ normalized to null because native empty-string expiry returned false in the
13
+ contributor fixture while null succeeded. Other strings pass through as native
14
+ ISO 8601 expiry requests; native failure is preserved.
15
+
16
+ Resolve-facing staging uses the existing safe-directory helper. A successful
17
+ native return must be accompanied by a non-empty regular encrypted file. An
18
+ atomic no-replace hard link publishes it; filesystems without that support use
19
+ exclusive file creation and copy. A failed copy removes only the new partial
20
+ output. An output appearing during encryption is not overwritten. New files
21
+ use owner-only permissions where the platform supports them. The result reports
22
+ actual path, byte length and SHA-256, not a guessed native filename.
23
+
24
+ The method has a 21.1 floor. The dctl tool has the destructive-action hook, and
25
+ encrypt_native is registered/rated LOW because it only creates new output and
26
+ never replaces existing content. Explicit compound dry runs refuse before the
27
+ handler. The granular tool declares a write. No DCTL is installed or applied by
28
+ this operation.
29
+
30
+ ## Contributor evidence and limits
31
+
32
+ Contributor-validated on macOS Studio 21.1.0.14 via official MCP probes and both
33
+ actual community wrappers, using a synthetic multiline identity shader. Native
34
+ omitted/null expiry and a future date succeeded; empty string returned false in
35
+ the isolated probe. Both wrappers created 2032-byte files in the tested runs,
36
+ returned matching file hashes/sizes, left source bytes unchanged and refused a
37
+ repeat export without changing the destination. These byte lengths are observed,
38
+ not format requirements. Ciphertext equality across separate encryptions is not
39
+ assumed.
40
+
41
+ Tests exercise output races, missing native output, native false, cross-volume
42
+ fallback, failed-copy cleanup, dangling symlinks, bad arguments and write gates.
43
+ This is encryption/export evidence, not shader application or rendered-image
44
+ acceptance, and not a security assessment of Blackmagic's encryption scheme.
45
+
46
+ `tests/live_resolve211_encryption.py SOURCE_DCTL OUTPUT_DIR` uses a synthetic
47
+ identity source and a fresh output directory. It does not install the results or
48
+ change a project. Never substitute someone else's shader without authorization.
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.223.0"
40
+ VERSION = "2.224.1"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
@@ -1543,7 +1543,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
1543
1543
 
1544
1544
  def print_banner():
1545
1545
  title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
1546
- subtitle = "36 compound · 376 full · 3 platforms"
1546
+ subtitle = "36 compound · 377 full · 3 platforms"
1547
1547
  print()
1548
1548
  print(bold(" ╔══════════════════════════════════════════════════════╗"))
1549
1549
  print(bold(f" ║{title:^54}║"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.223.0",
3
+ "version": "2.224.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -281,6 +281,11 @@ _FRAMEWORK_PYTHON_ROOTS = (
281
281
  #: PYTHON3HOME is unset. This is the one python.org's installer creates.
282
282
  _FALLBACK_PYTHON3 = Path("/usr/local/bin/python3")
283
283
 
284
+ #: Filename of the Lua enumeration canary. Named once: the installer writes it
285
+ #: and the post-install guidance counts it, and those two disagreeing is how a
286
+ #: user ends up told to expect entries that are not there.
287
+ _CANARY_NAME = "resolve_bridge_canary.lua"
288
+
284
289
  _LUA_CANARY = """-- Installed by davinci-resolve-mcp as an enumeration canary.
285
290
  -- If THIS appears under Workspace > Scripts but resolve_bridge_probe does not,
286
291
  -- Resolve is listing Lua and silently skipping Python: it cannot find a Python 3.
@@ -650,7 +655,7 @@ def _install_to(target: Path, *, probe_only: bool, installed: list[str]) -> None
650
655
  installed.append(str(target / probe))
651
656
  # Lua always enumerates; Python only with a framework install. The canary
652
657
  # makes "Python not detected" distinguishable from "wrong folder".
653
- canary = target / "resolve_bridge_canary.lua"
658
+ canary = target / _CANARY_NAME
654
659
  canary.write_text(_LUA_CANARY, encoding="utf-8")
655
660
  installed.append(str(canary))
656
661
  if probe_only:
@@ -686,6 +691,65 @@ def _install_to(target: Path, *, probe_only: bool, installed: list[str]) -> None
686
691
  installed.append(str(launcher_path))
687
692
 
688
693
 
694
+ def canary_count(result: dict) -> int:
695
+ """How many identical `resolve_bridge_canary` entries Resolve will list.
696
+
697
+ One per Scripts/Utility folder installed into, all with the same filename,
698
+ so they are indistinguishable in the menu. Reported in issue #219 by a user
699
+ who reasonably read two identical entries as a broken install.
700
+ """
701
+ return sum(1 for path in result.get("installed", [])
702
+ if str(path).endswith(_CANARY_NAME))
703
+
704
+
705
+ def next_steps(result: dict) -> list:
706
+ """The post-install instructions, as lines.
707
+
708
+ Built rather than printed inline so the canary-only branch is testable: it
709
+ is the single most likely outcome on macOS and, until issue #219, the only
710
+ one the installer had no words for. The user followed step 3 to a menu
711
+ entry that cannot exist, and the explanation was sitting in a Lua comment
712
+ they had no reason to open.
713
+ """
714
+ lines = [
715
+ "Next:",
716
+ " 1. Restart DaVinci Resolve so it re-scans the Scripts folders.",
717
+ " 2. Open a saved project (the Scripts menu is empty in Project Manager).",
718
+ " 3. Workspace > Scripts > resolve_bridge_probe — run it TWICE.",
719
+ # The probe runs INSIDE Resolve, which never sees the shell's
720
+ # DAVINCI_RESOLVE_BRIDGE_CONFIG — it always writes to the fixed default
721
+ # directory, so the guidance must not follow the override.
722
+ " 4. Read ~/.config/davinci-resolve-mcp/host-model-probe.json",
723
+ ]
724
+ count = canary_count(result)
725
+ if count > 1:
726
+ lines += [
727
+ "",
728
+ f"Expect {count} identical 'resolve_bridge_canary' entries — one per Scripts",
729
+ "folder this installed into. That is normal, not a double install; running",
730
+ "any one of them is the same as running any other.",
731
+ ]
732
+ lines += [
733
+ "",
734
+ "If step 3 shows no 'resolve_bridge_probe' and you can only see",
735
+ "'resolve_bridge_canary':",
736
+ " - The install worked. Do not re-run it. That is the canary doing its job:",
737
+ " Resolve is enumerating scripts, listing Lua, and skipping Python.",
738
+ " - Run 'resolve_bridge_canary' and read its output in Workspace > Console.",
739
+ " It reports with print(), not a dialog, so with no Console open it looks",
740
+ " like nothing happened.",
741
+ " - What it means depends on your edition:",
742
+ " * Resolve 21.1+ FREE — Python scripting moved to the Studio edition,",
743
+ " so .py files no longer list there at all and no Python setting will",
744
+ " change that. See issue #203.",
745
+ " * Studio, or 21.0.x and earlier — Resolve cannot find a Python 3. It",
746
+ " looks at PYTHON3HOME and then /usr/local/bin/python3 and nowhere",
747
+ " else, which is why Homebrew, pyenv, uv and conda builds go unseen.",
748
+ " The canary's own output carries the fix.",
749
+ ]
750
+ return lines
751
+
752
+
689
753
  def main() -> int:
690
754
  parser = argparse.ArgumentParser(description=__doc__)
691
755
  parser.add_argument("--probe-only", action="store_true",
@@ -709,14 +773,8 @@ def main() -> int:
709
773
  print("WARNING: " + warning)
710
774
  print("!" * 72)
711
775
  print()
712
- print("Next:")
713
- print(" 1. Restart DaVinci Resolve so it re-scans the Scripts folders.")
714
- print(" 2. Open a saved project (the Scripts menu is empty in Project Manager).")
715
- print(" 3. Workspace > Scripts > resolve_bridge_probe — run it TWICE.")
716
- # The probe runs INSIDE Resolve, which never sees the shell's
717
- # DAVINCI_RESOLVE_BRIDGE_CONFIG — it always writes to the fixed default
718
- # directory, so the guidance must not follow the override.
719
- print(" 4. Read ~/.config/davinci-resolve-mcp/host-model-probe.json")
776
+ for line in next_steps(result):
777
+ print(line)
720
778
  return 0
721
779
 
722
780
 
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.223.0"
90
+ VERSION = "2.224.1"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
@@ -3,11 +3,12 @@ from src.utils.resolve211_dctl import native_dctl_result
3
3
  from src.utils.resolve211_alignment import auto_align
4
4
  from src.utils.resolve211_normalization import normalize_audio
5
5
  from src.utils.resolve211_blanking import validate_blanking
6
+ from src.utils.resolve211_encryption import encrypt_dctl
6
7
  from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
7
8
  from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
8
9
  from src.granular.common import (
9
10
  mcp, READ_ONLY_TOOL, WRITE_TOOL, DESTRUCTIVE_TOOL, get_resolve, get_current_project,
10
- _get_timeline, _get_timeline_item, _find_clip_by_id, _requires_method, has_method,
11
+ _get_timeline, _get_timeline_item, _resolve_safe_dir, _find_clip_by_id, _requires_method, has_method,
11
12
  )
12
13
 
13
14
 
@@ -319,3 +320,14 @@ def validate_dctl_native(source: str) -> dict:
319
320
  if missing:
320
321
  return missing
321
322
  return native_dctl_result(r, source)
323
+
324
+ @mcp.tool(annotations=WRITE_TOOL)
325
+ def encrypt_dctl_native(input_path: str, output_path: str, expiry: str | None = None) -> dict:
326
+ """Encrypt a .dctl to a new .dctle using Resolve 21.1. Never overwrites. Empty/null expiry means no expiry; ISO 8601 strings pass to Resolve. Source is unchanged."""
327
+ r = get_resolve()
328
+ if r is None:
329
+ return {"error": "Not connected to DaVinci Resolve"}
330
+ missing = _requires_method(r, "EncryptDCTL", "21.1")
331
+ if missing:
332
+ return missing
333
+ return encrypt_dctl(r, input_path, output_path, expiry, _resolve_safe_dir)
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
34
34
  if __name__ == "__main__":
35
35
  try:
36
36
  start_background_update_check(VERSION, project_dir, logger)
37
- logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (376 granular tools)")
37
+ logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (377 granular tools)")
38
38
  run_fastmcp_stdio(mcp)
39
39
  except KeyboardInterrupt:
40
40
  logger.info("Server shutdown requested")
package/src/server.py CHANGED
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
8
8
 
9
9
  Usage:
10
10
  python src/server.py # Start the MCP server
11
- python src/server.py --full # Start the 376-tool granular server instead
11
+ python src/server.py --full # Start the 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.223.0"
14
+ VERSION = "2.224.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -42,6 +42,7 @@ for p in [current_dir, project_dir]:
42
42
  if p not in sys.path:
43
43
  sys.path.insert(0, p)
44
44
 
45
+ from src.utils.resolve211_encryption import encrypt_dctl
45
46
  from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
46
47
  from src.utils.resolve211_blanking import validate_blanking
47
48
  from src.utils.resolve211_alignment import auto_align
@@ -31114,6 +31115,7 @@ _DCTL_VALID_CATEGORIES = ("lut", "aces_idt", "aces_odt")
31114
31115
 
31115
31116
  @mcp.tool()
31116
31117
  @_guard_missing_params
31118
+ @_destructive_op("dctl")
31117
31119
  def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
31118
31120
  """Author and install DCTL files (Color page custom shaders + ACES transforms).
31119
31121
 
@@ -31142,6 +31144,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
31142
31144
  — ext: '.dctl' (default) or '.dctle' (encrypted)
31143
31145
  remove(name, category?, subdir?, ext?) -> {success}
31144
31146
  read(name, category?, subdir?, ext?) -> {source, encrypted}
31147
+ encrypt_native(input_path, output_path, expiry?) -> {success, path?, bytes?, sha256?} — native 21.1; never overwrites.
31145
31148
  validate(source) -> {valid, errors, warnings, checker}
31146
31149
  validate_native(source) -> {valid, diagnostic, checker} — Resolve 21.1 validation; source and diagnostic unchanged.
31147
31150
  template(kind, name, options?) -> {source, kind, name, suggested_category}
@@ -31309,6 +31312,15 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
31309
31312
  return missing
31310
31313
  return native_dctl_result(r, source)
31311
31314
 
31315
+ if action == "encrypt_native":
31316
+ r = get_resolve()
31317
+ if r is None:
31318
+ return _not_connected_error()
31319
+ missing = _requires_method(r, "EncryptDCTL", "21.1")
31320
+ if missing:
31321
+ return missing
31322
+ return encrypt_dctl(r, p.get("input_path"), p.get("output_path"), p.get("expiry"), _resolve_safe_dir)
31323
+
31312
31324
  if action == "validate":
31313
31325
  source = p.get("source")
31314
31326
  if not isinstance(source, str):
@@ -31335,7 +31347,7 @@ def dctl(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]
31335
31347
  }
31336
31348
 
31337
31349
  return _unknown(action, ["path", "list", "install", "remove", "read",
31338
- "validate_native", "validate", "template", "list_templates"])
31350
+ "encrypt_native", "validate_native", "validate", "template", "list_templates"])
31339
31351
 
31340
31352
 
31341
31353
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -32751,9 +32763,9 @@ if __name__ == "__main__":
32751
32763
  start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
32752
32764
  _install_threaded_tool_dispatch(mcp)
32753
32765
 
32754
- # Support --full flag to run the 376-tool granular server instead
32766
+ # Support --full flag to run the 377-tool granular server instead
32755
32767
  if "--full" in sys.argv:
32756
- logger.info("Starting full 376-tool granular server...")
32768
+ logger.info("Starting full 377-tool granular server...")
32757
32769
  sys.argv = [arg for arg in sys.argv if arg != "--full"]
32758
32770
  from src.granular import mcp as granular_mcp
32759
32771
 
@@ -63,6 +63,7 @@ SAFE_MODE_BLOCKED_RISK_LEVELS: FrozenSet[str] = frozenset({
63
63
  # replace_clip/link_*). The test_destructive_registry_drift guard asserts every
64
64
  # string here is a real handler so this can't regress.
65
65
  DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
66
+ "dctl": frozenset({"encrypt_native"}),
66
67
  "media_pool": frozenset({
67
68
  "delete_clips",
68
69
  "delete_folders",
@@ -193,6 +193,7 @@ class RiskClassificationHook(LifecycleHook):
193
193
  #: them unrecognised, i.e. it warns that the risk is unestablished for the
194
194
  #: actions whose risk is the best established of any we dispatch.
195
195
  _LOW_RISK_ACTIONS: Set[Tuple[str, str]] = {
196
+ ("dctl", "encrypt_native"), # Creates a new file; never replaces existing content.
196
197
  ("timeline_markers", "add"),
197
198
  ("timeline_markers", "update_custom_data"),
198
199
  ("timeline_item_markers", "add"),
@@ -0,0 +1,59 @@
1
+ """Stage native DCTL encryption, then publish without replacing an existing file."""
2
+ import errno
3
+ import hashlib
4
+ import os
5
+ from pathlib import Path
6
+ import shutil
7
+ import tempfile
8
+
9
+
10
+ def encrypt_dctl(r, input_path, output_path, expiry, safe_dir):
11
+ if not isinstance(input_path,str) or not input_path or not isinstance(output_path,str) or not output_path:
12
+ return {'error':'input_path and output_path must be non-empty strings'}
13
+ if expiry is not None and not isinstance(expiry,str):
14
+ return {'error':'expiry must be an ISO 8601 string or null'}
15
+ source=Path(input_path).expanduser().absolute()
16
+ target=Path(output_path).expanduser().absolute()
17
+ if source.suffix.lower()!='.dctl' or not source.is_file():
18
+ return {'error':'input_path must name an existing .dctl file'}
19
+ if target.suffix.lower()!='.dctle':
20
+ return {'error':'output_path must end in .dctle'}
21
+ if os.path.lexists(target):
22
+ return {'error':'Output already exists; refusing to overwrite','success':False}
23
+ try:
24
+ target.parent.mkdir(parents=True,exist_ok=True)
25
+ staging_root=Path(safe_dir(str(target.parent.resolve())))
26
+ staging_root.mkdir(parents=True,exist_ok=True)
27
+ with tempfile.TemporaryDirectory(prefix='resolve-mcp-dctl-',dir=staging_root) as stage:
28
+ # Native Name is a stem: Resolve appends .dctle itself. Use a fixed
29
+ # staging name, so user filename characters never become native paths.
30
+ options={'Name':'encrypted','OutputFolder':stage,'Expiry':None if expiry=='' else expiry}
31
+ if not r.EncryptDCTL(str(source),options):
32
+ return {'success':False}
33
+ encrypted=Path(stage)/'encrypted.dctle'
34
+ if encrypted.is_symlink() or not encrypted.is_file() or encrypted.stat().st_size==0:
35
+ return {'success':False,'error':'Resolve reported success without a non-empty encrypted file'}
36
+ os.chmod(encrypted,0o600)
37
+ digest=hashlib.sha256(encrypted.read_bytes()).hexdigest()
38
+ size=encrypted.stat().st_size
39
+ try:
40
+ os.link(encrypted,target)
41
+ except OSError as exc:
42
+ if exc.errno not in (errno.EXDEV,errno.EPERM,errno.EOPNOTSUPP,errno.ENOSYS):
43
+ raise
44
+ # Cross-volume/no-hardlink filesystem: exclusive creation still
45
+ # prevents overwriting an existing target, including a symlink.
46
+ fd=os.open(target,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
47
+ try:
48
+ with os.fdopen(fd,'wb') as out, encrypted.open('rb') as src:
49
+ shutil.copyfileobj(src,out)
50
+ out.flush()
51
+ os.fsync(out.fileno())
52
+ except BaseException:
53
+ target.unlink(missing_ok=True)
54
+ raise
55
+ return {'success':True,'path':str(target),'bytes':size,'sha256':digest}
56
+ except FileExistsError:
57
+ return {'success':False,'error':'Output appeared during encryption; refusing to overwrite'}
58
+ except Exception as exc:
59
+ return {'success':False,'error':'Encryption/export failed: '+(str(exc) or type(exc).__name__)}
@@ -227,6 +227,8 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
227
227
  CODE_FLOORS: Dict[str, str] = {
228
228
  "Timeline.AutoAlignClips": "21.1",
229
229
  "Resolve.ValidateDCTL": "21.1",
230
+
231
+ "Resolve.EncryptDCTL": "21.1",
230
232
  "MediaPool.CreateMulticamClip": "21.1",
231
233
  "TimelineItem.FlattenMulticam": "21.1",
232
234
  "Timeline.SetOutputBlanking": "21.1",