davinci-resolve-mcp 2.212.1 → 2.212.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,69 @@
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.212.3 — a cached update prompt is re-judged against the version actually running
6
+
7
+ ### Fixed
8
+
9
+ - **A persisted "update available" outlived the upgrade it recommended.** The
10
+ update checker caches its last verdict (in memory and in
11
+ `logs/update-check.json`) and serves it without network for the throttle
12
+ interval. That verdict was stored as a *status*, not as a comparison, so
13
+ after upgrading from the version it was computed for — 2.135.0 → 2.210.0,
14
+ say — a server now running 2.212.1 kept prompting to update to 2.210.0.
15
+ Every cached path (in-memory, persisted, and the throttled branch of
16
+ `check_for_updates`) now re-compares the cached `latest_version` against
17
+ the running version and reclassifies to `update_available` /
18
+ `up_to_date` / `current_ahead`; `error` and `disabled` results are left
19
+ as they were. Contributed in #194 by @diesdaas.
20
+
21
+ ### Documentation
22
+
23
+ - **The READMEs said there was no beat detection; there has been for some
24
+ time.** Both the English and Simplified Chinese "not supported" tables
25
+ claimed "no beat or downbeat detection yet" — twenty lines below an
26
+ optional-extras table listing `pip install librosa` for exactly that. The
27
+ row now describes what actually exists: optional `librosa` beat detection
28
+ and beat / bar / phrase cut-point plans, downbeats inferred from the first
29
+ beat with `beat_offset` for pickups, and cut *points* rather than a finished
30
+ music edit. Speech-silence tools remain the wrong instrument for music.
31
+
32
+ ### Validation
33
+
34
+ - Four regression tests from the PR cover the persisted, in-memory and
35
+ throttled cache paths plus the preserved error/disabled states. On landing,
36
+ the update-check tests gained a `setUp` that resets the module-wide cache
37
+ around every test, so a seeded verdict cannot leak into another module's
38
+ cached-status read later in the run. Full offline suite, drift guards and
39
+ the advanced Node suite are green. No Resolve behavior changed; live test
40
+ not required.
41
+
42
+ ## What's New in v2.212.2 — failed verification evidence survives the operation envelope
43
+
44
+ ### Fixed
45
+
46
+ - **A payload that carried its own `verification` block won outright, hiding
47
+ contradicting evidence beside it.** `extract_verification` returned any
48
+ pre-shaped `verification` dict untouched, so a top-level `contradiction:
49
+ true`, a `readback.missing` list, or a failed check inside the block itself
50
+ could sit next to `status: "passed"`. Evidence now merges: the explicit
51
+ status, readback misses, post-state readback and property-restore failures
52
+ are all collected, and precedence runs contradiction > failed > partial >
53
+ passed > unverified. Contributed in #195 by @denoise.
54
+ - **Bulk command counts no longer count as verification.** `succeeded` /
55
+ `failed` tallies record what the server sent, not what Resolve honoured; a
56
+ `succeeded: 3, failed: 0` result used to read `verification.status:
57
+ "passed"` with no readback at all. The counts still drive the envelope's
58
+ own `status` (`partial` when both are non-zero), but the verification block
59
+ stays `unverified` until real evidence — a readback — arrives.
60
+
61
+ ### Validation
62
+
63
+ - The PR's regression tests plus two added on landing: bulk counts cannot
64
+ mask a failed readback, and readback evidence is what establishes a pass on
65
+ a bulk result. Full offline suite, the drift guards and the advanced Node
66
+ suite are green. No Resolve behavior changed; live test not required.
67
+
5
68
  ## What's New in v2.212.1 — the networked transport's generated bearer token no longer lands in server.log
6
69
 
7
70
  ### Fixed
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-2.212.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.212.3-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-36%20(353%20full)-blue.svg)](#server-modes)
@@ -339,7 +339,7 @@ cheaper to read it here than to discover it mid-project.
339
339
  | Not supported | Why, and what you get instead |
340
340
  |---|---|
341
341
  | **Choosing the best take** | Performance is most of what makes a take right, and none of it is measurable from a waveform or a transcript. `rank_takes` ranks *fluency* — fillers, restarts, script coverage — and says so in every response. The take that plays is regularly the least fluent one, because the hesitation is often the acting. Use it to find the clean safety take, not to choose the read. |
342
- | **Cutting to music** | No beat or downbeat detection yet. Speech-driven tools will read a music bed as one long region and are the wrong instrument for it. |
342
+ | **Automatic music editing** | Optional `librosa` support provides beat detection and beat/bar/phrase cut-point plans, not a finished assembly. Downbeats are inferred from the first beat; use `beat_offset` for pickups. Speech-silence tools are unsuitable for finding musical edit points. |
343
343
  | **Judging a cut** | Nothing here has an opinion about whether an edit is good. Every destructive action is plan → review → confirm for that reason. |
344
344
  | **Replacing an editor** | The output is a first-pass assembly, in the assistant-editor sense: ingest, sync, organize, string out, flag problems. It is a starting point you cut, not a finished cut. Defaults are deliberately **generous** — a first assembly is supposed to run long, because trimming is fast and visible while recovering discarded material is slow and invisible. |
345
345
  | **Modifying your source media** | By design and without exception — see below. |
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.212.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.212.3-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-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -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
- > 本翻译对应 v2.212.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.212.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -207,7 +207,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
207
207
  | 不支持 | 原因,以及你能得到什么 |
208
208
  |---|---|
209
209
  | **挑选最佳条次** | 表演是决定一条好坏的大头,而这些从波形或转写里都量不出来。`rank_takes` 排的是*流畅度*——填充词、重来、稿子覆盖率——并在每次响应里说明这一点。最终用的那条常常是最不流畅的一条,因为迟疑往往正是表演。用它找干净的保底条,别用它挑读法。 |
210
- | **跟音乐剪辑** | 还没有节拍/强拍检测。语音驱动的工具会把音乐床读成一整个长区域,用错了工具。 |
210
+ | **自动音乐剪辑** | 可选的 `librosa` 支持节拍检测和按节拍、小节、乐句规划剪切点,但不会生成完整剪辑。强拍从第一个节拍推断;遇到弱起时用 `beat_offset` 调整。语音静音检测工具不适合寻找音乐剪切点。 |
211
211
  | **评判剪辑好坏** | 这里没有任何东西对"这一刀剪得好不好"持有观点。正因如此,一切破坏性操作都走 计划 → 审阅 → 确认。 |
212
212
  | **取代剪辑师** | 输出是助理剪辑意义上的初剪:摄入、同步、整理、串片、标记问题。它是给你继续剪的起点,不是成片。默认参数刻意**宽松**——初剪本来就该偏长,因为往下修快且看得见,而找回已丢弃的素材慢且看不见。 |
213
213
  | **修改你的源媒体** | 设计如此,无例外——见下文。 |
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.212.1"
40
+ VERSION = "2.212.3"
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": "2.212.1",
3
+ "version": "2.212.3",
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 = "2.212.1"
90
+ VERSION = "2.212.3"
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 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.212.1"
14
+ VERSION = "2.212.3"
15
15
 
16
16
  import base64
17
17
  import os
@@ -166,75 +166,57 @@ def extract_verification(raw: Any) -> Dict[str, Any]:
166
166
  if not isinstance(raw, dict):
167
167
  return unverified
168
168
 
169
- # An impl that already speaks this shape wins outright.
170
169
  existing = raw.get("verification")
171
- if isinstance(existing, dict) and existing:
172
- merged = dict(existing)
173
- merged.setdefault(
174
- "status", "passed" if existing.get("verified") else "unverified")
175
- merged.setdefault("checks", [])
176
- merged.setdefault("contradiction", False)
177
- return merged
178
-
179
- checks: List[Dict[str, Any]] = []
180
- status = "unverified"
181
- contradiction = False
170
+ existing = existing if isinstance(existing, dict) else {}
171
+ checks = list(existing.get("checks") or [])
172
+ statuses = []
173
+ existing_status = existing.get("status")
174
+ if existing_status in {"passed", "failed", "partial", "contradiction"}:
175
+ statuses.append(existing_status)
176
+ elif existing.get("verified") is True:
177
+ statuses.append("passed")
178
+ elif existing.get("verified") is False:
179
+ statuses.append("failed")
180
+ contradiction = existing.get("contradiction") is True or raw.get("contradiction") is True
182
181
 
183
182
  readback = raw.get("readback")
184
- if isinstance(readback, dict):
185
- missing = readback.get("missing")
186
- if isinstance(missing, list):
187
- checks.append({
188
- "check": "readback_verification",
189
- "passed": not missing,
190
- "missing_items": len(missing),
191
- })
192
- status = "passed" if not missing else "failed"
193
-
194
- # verify_by_readback's own shape: a mutation that reported success while the
195
- # post-state disagrees is a contradiction, this repo's single most valuable
196
- # reliability signal — it must not be flattened into a plain failure.
197
- if "verified" in raw:
198
- verified = bool(raw["verified"])
199
- contradiction = bool(raw.get("contradiction"))
200
- checks.append({
201
- "check": "readback_post_state",
202
- "passed": verified,
203
- "contradiction": contradiction,
204
- "observed": raw.get("observed"),
205
- })
206
- status = "contradiction" if contradiction else ("passed" if verified else "failed")
183
+ if isinstance(readback, dict) and isinstance(readback.get("missing"), list):
184
+ missing = readback["missing"]
185
+ checks.append({"check": "readback_verification", "passed": not missing,
186
+ "missing_items": len(missing)})
187
+ statuses.append("failed" if missing else "passed")
188
+
189
+ if type(raw.get("verified")) is bool:
190
+ checks.append({"check": "readback_post_state", "passed": raw["verified"],
191
+ "contradiction": contradiction, "observed": raw.get("observed")})
192
+ statuses.append("passed" if raw["verified"] else "failed")
207
193
 
208
194
  if "property_restore_failures" in raw:
209
195
  failures = _as_int(raw.get("property_restore_failures"))
210
- checks.append({
211
- "check": "property_restore",
212
- "passed": failures == 0,
213
- "restored_items": _as_int(raw.get("properties_restored_items")),
214
- "failures": failures,
215
- })
216
- if failures and status in ("passed", "unverified"):
217
- status = "partial"
218
-
219
- succeeded, failed = raw.get("succeeded"), raw.get("failed")
220
- if isinstance(succeeded, int) and isinstance(failed, int):
221
- checks.append({
222
- "check": "bulk_operations",
223
- "passed": failed == 0,
224
- "succeeded": succeeded,
225
- "failed": failed,
226
- })
227
- if status == "unverified":
228
- if failed == 0 and succeeded > 0:
229
- status = "passed"
230
- elif succeeded > 0:
231
- status = "partial"
232
- elif failed > 0:
233
- status = "failed"
234
-
235
- if not checks:
236
- return unverified
237
- return {"status": status, "checks": checks, "contradiction": contradiction}
196
+ checks.append({"check": "property_restore", "passed": failures == 0,
197
+ "restored_items": _as_int(raw.get("properties_restored_items")),
198
+ "failures": failures})
199
+ if failures:
200
+ statuses.append("partial")
201
+
202
+ for check in checks:
203
+ if isinstance(check, dict):
204
+ contradiction = contradiction or check.get("contradiction") is True
205
+ if check.get("passed") is False:
206
+ statuses.append("partial" if check.get("check") == "property_restore" else "failed")
207
+ # Command counts are not evidence that Resolve honored those commands.
208
+ if contradiction or "contradiction" in statuses:
209
+ status = "contradiction"
210
+ contradiction = True
211
+ elif "failed" in statuses:
212
+ status = "failed"
213
+ elif "partial" in statuses:
214
+ status = "partial"
215
+ elif "passed" in statuses:
216
+ status = "passed"
217
+ else:
218
+ status = "unverified"
219
+ return {**existing, "status": status, "checks": checks, "contradiction": contradiction}
238
220
 
239
221
 
240
222
  # ─── Changes ─────────────────────────────────────────────────────────────────
@@ -135,14 +135,13 @@ def get_cached_update_status(
135
135
  with _cached_lock:
136
136
  cached = dict(_cached_status)
137
137
  if cached.get("status") != "unknown":
138
- if current_version and "current_version" not in cached:
139
- cached["current_version"] = current_version
138
+ cached = _reclassify_cached_status(cached, current_version)
139
+ _set_cached_status(cached)
140
140
  return cached
141
141
 
142
142
  state = _read_state(update_state_path(project_dir, env))
143
143
  if state:
144
- if current_version and "current_version" not in state:
145
- state["current_version"] = current_version
144
+ state = _reclassify_cached_status(state, current_version)
146
145
  if "update_mode" not in state:
147
146
  state["update_mode"] = get_update_mode(project_dir, env)
148
147
  return state
@@ -193,11 +192,12 @@ def check_for_updates(
193
192
  and previous
194
193
  and checked_at - float(previous.get("checked_at", 0)) < interval_seconds
195
194
  ):
196
- cached = dict(previous)
195
+ cached = _reclassify_cached_status(previous, current_version)
197
196
  cached["cached"] = True
198
197
  cached["update_mode"] = update_mode
199
198
  cached["next_check_at"] = float(previous.get("checked_at", 0)) + interval_seconds
200
199
  cached["next_check_at_iso"] = _format_timestamp(cached["next_check_at"])
200
+ _write_state(state_path, cached)
201
201
  _set_cached_status(cached)
202
202
  return cached
203
203
 
@@ -621,6 +621,27 @@ def _version_text(value: Any) -> str:
621
621
  return match.group(0) if match else str(value or "").strip()
622
622
 
623
623
 
624
+ def _reclassify_cached_status(
625
+ result: Mapping[str, Any], current_version: Optional[str]
626
+ ) -> Dict[str, Any]:
627
+ """Refresh cached status against the version of the running MCP."""
628
+ refreshed = dict(result)
629
+ if refreshed.get("status") not in _SUCCESS_STATUSES:
630
+ return refreshed
631
+ if current_version:
632
+ refreshed["current_version"] = current_version
633
+ comparison = compare_versions(current_version, refreshed.get("latest_version"))
634
+ if comparison is None:
635
+ return refreshed
636
+ if comparison < 0:
637
+ refreshed["status"] = "update_available"
638
+ elif comparison > 0:
639
+ refreshed["status"] = "current_ahead"
640
+ else:
641
+ refreshed["status"] = "up_to_date"
642
+ return refreshed
643
+
644
+
624
645
  def _read_state(path: Path) -> Dict[str, Any]:
625
646
  try:
626
647
  with path.open("r", encoding="utf-8") as handle: