davinci-resolve-mcp 3.2.0 → 3.2.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,39 @@
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 v3.2.1 — three correctness fixes to the LUT tool
6
+
7
+ Contributed by @Dev-next-gen (#225, #226, #227), each found by reading the v3.2.0
8
+ `lut` tool rather than by hitting it in use.
9
+
10
+ ### Fixed
11
+
12
+ - **`install` reported `overwritten: true` on a fresh write.** The flag was
13
+ `bool(overwrite and payload is not None)`, and `payload` can never be `None`
14
+ where that line runs, so the field handed the caller's own `overwrite`
15
+ argument back instead of an observation. An install passing `overwrite=true`
16
+ for idempotence, landing on an empty `MCP/`, was reported as having replaced
17
+ existing work. `execution_lifecycle` rates `lut install` MEDIUM precisely
18
+ because it "can replace with overwrite=true", so this flag is what a caller
19
+ and the execution trace read to learn whether an install destroyed anything.
20
+ It is now the `os.path.exists` observation already taken one line above — the
21
+ measured pre-state, not the permission. (#225)
22
+ - **`install(source_path=...)` could not copy a binary LUT.** The copy went
23
+ through a UTF-8 text round-trip, so the two binary extensions this server
24
+ advertises — `.dat` and `.olut` — raised `UnicodeDecodeError` before anything
25
+ was written, surfacing as `LUT_ERROR`. So did an ordinary `.cube` whose vendor
26
+ wrote its `TITLE` line in latin-1. The file-copy branch is now byte-exact; the
27
+ text branch (`source=`) is unchanged. (#226)
28
+ - **`list` marked siblings of the writable subdir as writable.** The `writable`
29
+ flag used `realpath(current).startswith(writable_root)`, a string prefix with
30
+ no separator, so `MCP_old/` left by a hand backup or a vendor pack unpacking
31
+ as `MCPresets/` cleared it. The listing then contradicted the only tool that
32
+ consumes the flag: `remove` resolves names inside `MCP/` and refused the very
33
+ `set_lut_path` the listing had just handed out. It now compares by path
34
+ segment with `commonpath`, which is what `_is_relative_to` and `resolve_writable`
35
+ already use elsewhere in this repo. Nothing that worked before stops working —
36
+ the flag only flips for paths `remove` was already refusing. (#227)
37
+
5
38
  ## What's New in v3.2.0 — LUT files: find them, install them, remove them, gated
6
39
 
7
40
  Contributed by @legionsound (#223), live-validated on Studio 21.1.0.14.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-3.2.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-3.2.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
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(384%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -12,7 +12,7 @@
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
- > 本翻译对应 v3.2.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v3.2.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "3.2.0"
40
+ VERSION = "3.2.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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "3.2.0"
90
+ VERSION = "3.2.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()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "3.2.0"
14
+ VERSION = "3.2.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -50,6 +50,20 @@ def writable_dir() -> str:
50
50
  return os.path.join(master_lut_dir(), WRITABLE_SUBDIR)
51
51
 
52
52
 
53
+ def _is_within(path: str, parent: str) -> bool:
54
+ """Whether ``path`` is ``parent`` itself or sits under it, compared by segment.
55
+
56
+ ``commonpath`` is the comparison the rest of the repo uses for confinement
57
+ checks (`media_analysis._is_relative_to`), and it is the only one that is
58
+ correct here: a string prefix test also accepts a sibling whose name merely
59
+ starts with the same characters.
60
+ """
61
+ try:
62
+ return os.path.commonpath([path, parent]) == parent
63
+ except (ValueError, OSError):
64
+ return False
65
+
66
+
53
67
  def normalize_relative(name: str, *, default_ext: Optional[str] = None) -> str:
54
68
  """Validate a caller-supplied LUT name and return it as a POSIX relative path.
55
69
 
@@ -123,7 +137,13 @@ def list_luts(subdir: Optional[str] = None) -> Dict[str, Any]:
123
137
  except OSError:
124
138
  size = None
125
139
  try:
126
- is_writable = os.path.realpath(current).startswith(writable_root)
140
+ # Segment-wise, not a string prefix: a sibling of the writable
141
+ # subdir whose name starts with it -- MCP_old/ after a manual
142
+ # backup, MCPresets/ from a vendor pack -- cleared
143
+ # `startswith(writable_root)` and was listed as writable, while
144
+ # `remove` resolves names under MCP/ and refuses the very path
145
+ # the listing handed back.
146
+ is_writable = _is_within(os.path.realpath(current), writable_root)
127
147
  except OSError:
128
148
  is_writable = False
129
149
  found.append({
@@ -154,28 +174,45 @@ def install_lut(name: str, *, source: Optional[str] = None,
154
174
  if (source is None) == (source_path is None):
155
175
  raise LutPathError("provide exactly one of source (text) or source_path (a file to copy)")
156
176
  absolute, set_lut_path = resolve_writable(name)
157
- if os.path.exists(absolute) and not overwrite:
177
+ existed = os.path.exists(absolute)
178
+ if existed and not overwrite:
158
179
  raise LutPathError(
159
180
  f"{set_lut_path} already exists. Pass overwrite=true to replace it."
160
181
  )
161
182
  if source_path is not None:
162
183
  if not os.path.isfile(source_path):
163
184
  raise LutPathError(f"source_path not found: {source_path}")
164
- with open(source_path, "r", encoding="utf-8", errors="strict") as handle:
185
+ # A copy has to be byte-exact. Two of the extensions this server
186
+ # advertises -- .dat and .olut -- are binary, and a .cube carries its
187
+ # TITLE in whatever encoding the vendor wrote it in, so decoding the
188
+ # source as UTF-8 turned "install the LUT I already have on disk" into
189
+ # a UnicodeDecodeError surfacing as LUT_ERROR.
190
+ with open(source_path, "rb") as handle:
165
191
  payload = handle.read()
166
192
  else:
167
193
  payload = source
168
194
  if not payload.strip():
169
195
  raise LutPathError("refusing to install an empty LUT")
170
196
  os.makedirs(os.path.dirname(absolute), exist_ok=True)
171
- with open(absolute, "w", encoding="utf-8") as handle:
172
- handle.write(payload)
197
+ if isinstance(payload, bytes):
198
+ with open(absolute, "wb") as handle:
199
+ handle.write(payload)
200
+ else:
201
+ with open(absolute, "w", encoding="utf-8") as handle:
202
+ handle.write(payload)
173
203
  return {
174
204
  "success": True,
175
205
  "path": absolute,
176
206
  "set_lut_path": set_lut_path,
177
207
  "bytes": os.path.getsize(absolute),
178
- "overwritten": bool(overwrite and payload is not None),
208
+ # Whether a file was actually replaced, not whether the caller allowed
209
+ # it: `payload is not None` is always true here, so this reported a
210
+ # replacement for every overwrite=true install, including the ones that
211
+ # landed on an empty MCP/. This flag is the record of what an install
212
+ # destroyed -- execution_lifecycle rates `lut install` on the fact that
213
+ # it "can replace with overwrite=true" -- so it has to be the observed
214
+ # pre-state, not the permission.
215
+ "overwritten": existed,
179
216
  "note": ("Call project_settings(action='refresh_luts') so Resolve picks up "
180
217
  "the new file before applying it."),
181
218
  }