davinci-resolve-mcp 3.2.1 → 3.2.2

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,53 @@
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.2 — an analysis root that is deleted is actually let go of
6
+
7
+ Contributed by @Dev-next-gen (#228), generalised to the second site and to the
8
+ connection cache underneath both.
9
+
10
+ ### Fixed
11
+
12
+ - **`cleanup_artifacts(frames_only=false)` reported success whether or not it
13
+ removed anything.** The analysis root contains
14
+ `_soul/timeline_brain.sqlite`, which `timeline_brain_db` keeps open in a
15
+ process-wide cache for the life of the server. Nothing let go of it before
16
+ the `shutil.rmtree`, and the rmtree runs with `ignore_errors=True`, so both
17
+ failure modes were swallowed. On Windows the open handle makes the DB
18
+ undeletable: the root survives with `_soul/` and the brain-edit history still
19
+ in it while the tool returns `{"success": true}`. On POSIX the root is
20
+ removed but the stale connection stays cached, so the next write for that
21
+ project goes to a file with no directory entry and is lost — the dashboard,
22
+ which opens the path fresh, sees nothing. `timeline_brain_db.close()` now
23
+ releases one project's connection, and the cleanup returns `success: false`
24
+ if the root is still on disk afterwards. (#228)
25
+ - **The same bug at a second site.** A `session_only` run without
26
+ `keep_artifacts` ingests every report into the brain DB under its output root
27
+ and then deletes that root, with the connection still cached. Because each
28
+ such run gets a fresh temp root, the cache accumulated one dead connection
29
+ per run. Both sites now go through one helper, and
30
+ `artifacts_cleaned_up` reports whether the removal happened rather than that
31
+ it was attempted.
32
+ - **`close()` released nothing when the root was spelled differently.** The
33
+ connection cache keyed on the caller's spelling of the path, and callers
34
+ disagree by construction: `media_analysis` realpaths a root before using it,
35
+ while its own callers pass what the user typed. On macOS that alone was
36
+ enough — every temp root under `/var/folders` is a symlink to
37
+ `/private/var/folders` — so `close()` computed a key that was never in the
38
+ cache, popped nothing, and the fix above silently did not apply. Two
39
+ spellings of one root also opened two connections to one SQLite file. The
40
+ cache now keys on the resolved DB path.
41
+
42
+ ### Release process
43
+
44
+ - **`tests.test_release_surface_drift` is now in the documented gate list.** It
45
+ asserts the README badge, the `README.zh-CN.md` badge and translation line,
46
+ and a `CHANGELOG.md` entry all match `package.json` — and it was the one
47
+ version-surface check missing from `docs/process/release-process.md`. v3.2.1
48
+ shipped with a zh-CN badge still reading v3.2.0 because every documented gate
49
+ passed while none of them looks at a version surface. That badge is corrected
50
+ here.
51
+
5
52
  ## What's New in v3.2.1 — three correctness fixes to the LUT tool
6
53
 
7
54
  Contributed by @Dev-next-gen (#225, #226, #227), each found by reading the v3.2.0
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.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-3.2.2-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
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.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.2-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)](#服务器模式)
@@ -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.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v3.2.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -75,7 +75,7 @@ venv/bin/python scripts/audit_api_parity.py
75
75
  venv/bin/python scripts/gen_api_limitations.py --check
76
76
  venv/bin/python scripts/audit_readwrite_symmetry.py --check
77
77
  node scripts/agent-rules/generate.mjs --check
78
- venv/bin/python -m unittest tests.test_static_undefined_names tests.test_duplicate_definitions tests.test_action_list_drift tests.test_panel_docs_drift tests.test_doc_tool_counts tests.test_agent_rules_drift
78
+ venv/bin/python -m unittest tests.test_static_undefined_names tests.test_duplicate_definitions tests.test_action_list_drift tests.test_panel_docs_drift tests.test_doc_tool_counts tests.test_agent_rules_drift tests.test_release_surface_drift
79
79
  node bin/davinci-resolve-mcp.mjs --help
80
80
  node bin/davinci-resolve-mcp.mjs --version
81
81
  npm pack --dry-run
@@ -89,6 +89,13 @@ regeneration is in the working tree when the test reads it — that ordering is
89
89
  the check is a regeneration followed by a test, not a `git diff --exit-code`,
90
90
  which would fire on the release bump's own legitimate change.
91
91
 
92
+ `test_release_surface_drift` is the gate on the version bump itself: it asserts
93
+ the README badge, the `README.zh-CN.md` badge and its `本翻译对应 vX.Y.Z` line, and
94
+ a `CHANGELOG.md` entry all match `package.json`. It was not in this list until
95
+ v3.2.2, and v3.2.1 shipped with a zh-CN badge still reading v3.2.0 as a direct
96
+ result — every other gate passed, because none of them looks at a version
97
+ surface. Run it before tagging, not after.
98
+
92
99
  `test_duplicate_definitions` asserts no module-level name is defined twice under
93
100
  `src/`. A second `def foo` silently replaces the first, and in a module the size
94
101
  of `src/server.py` the two can be thousands of lines apart with different
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.1"
40
+ VERSION = "3.2.2"
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.1",
3
+ "version": "3.2.2",
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.1"
90
+ VERSION = "3.2.2"
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.1"
14
+ VERSION = "3.2.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -2590,6 +2590,48 @@ def _read_json(path: str) -> Dict[str, Any]:
2590
2590
  return json.load(f)
2591
2591
 
2592
2592
 
2593
+ def _drop_brain_db_and_rmtree(project_root: str, cleanup_root: str) -> bool:
2594
+ """Delete `cleanup_root`, releasing `project_root`'s brain DB first.
2595
+
2596
+ `<project_root>/_soul/timeline_brain.sqlite` is held open in a
2597
+ process-wide cache for the life of the server, and nothing else lets go of
2598
+ it. Deleting the root underneath that handle fails differently on each
2599
+ platform and silently on both, because rmtree runs with
2600
+ ``ignore_errors=True``:
2601
+
2602
+ - Windows refuses to delete the open file, so the root survives with
2603
+ `_soul/` and the brain-edit history still in it while the caller is told
2604
+ it is gone.
2605
+ - POSIX unlinks it, but the cache then hands the next writer a connection
2606
+ to a file with no directory entry, so the write lands nowhere.
2607
+
2608
+ Returns whether `cleanup_root` is actually gone afterwards, so callers can
2609
+ report a removal that happened rather than one they attempted.
2610
+ """
2611
+ from src.utils import timeline_brain_db as _brain_db
2612
+
2613
+ _brain_db.close(project_root)
2614
+ shutil.rmtree(cleanup_root, ignore_errors=True)
2615
+ return not os.path.isdir(cleanup_root)
2616
+
2617
+
2618
+ def _release_session_root(manifest: Dict[str, Any], project_root: str,
2619
+ cleanup_root: str) -> bool:
2620
+ """Session-only artifact cleanup: same rule as `cleanup_artifacts`.
2621
+
2622
+ A session-only run ingests every report into the brain DB under
2623
+ `project_root` and then throws the root away, and each run gets a fresh
2624
+ temp root -- so without the release the cache accumulates one dead
2625
+ connection per run.
2626
+ """
2627
+ removed = _drop_brain_db_and_rmtree(project_root, cleanup_root)
2628
+ if not removed:
2629
+ manifest.setdefault("memory_layer_warnings", []).append(
2630
+ f"Could not fully remove the session analysis root: {cleanup_root}"
2631
+ )
2632
+ return removed
2633
+
2634
+
2593
2635
  def _ingest_report_into_db(project_root: str, report: Dict[str, Any], clip_dir: Optional[str]) -> Dict[str, Any]:
2594
2636
  """C1 — write a report into the DB-canonical store (rows in a transaction).
2595
2637
 
@@ -5971,8 +6013,9 @@ async def execute_plan_async(
5971
6013
  and _is_relative_to(output_root, candidate)
5972
6014
  ):
5973
6015
  cleanup_root = candidate
5974
- shutil.rmtree(cleanup_root, ignore_errors=True)
5975
- manifest["artifacts_cleaned_up"] = True
6016
+ manifest["artifacts_cleaned_up"] = _release_session_root(
6017
+ manifest, output_root, cleanup_root
6018
+ )
5976
6019
  manifest["artifact_cleanup_root"] = cleanup_root
5977
6020
 
5978
6021
  return manifest
@@ -7057,7 +7100,14 @@ def cleanup_artifacts(project_root: str, *, frames_only: bool = True) -> Dict[st
7057
7100
  shutil.rmtree(full, ignore_errors=True)
7058
7101
  removed.append(full)
7059
7102
  else:
7060
- shutil.rmtree(root, ignore_errors=True)
7103
+ # The whole root goes, including `_soul/timeline_brain.sqlite`.
7104
+ if not _drop_brain_db_and_rmtree(root, root):
7105
+ return {
7106
+ "success": False,
7107
+ "error": f"Could not fully remove the project analysis root: {root}",
7108
+ "removed": removed,
7109
+ "frames_only": frames_only,
7110
+ }
7061
7111
  removed.append(root)
7062
7112
  return {"success": True, "removed": removed, "frames_only": frames_only}
7063
7113
 
@@ -50,6 +50,26 @@ def db_path_for_project(project_root: str) -> str:
50
50
  return os.path.join(project_root, SOUL_DIRNAME, DB_FILENAME)
51
51
 
52
52
 
53
+ def _cache_key(project_root: str) -> str:
54
+ """The connection-cache key for a project root: its DB path, realpath'd.
55
+
56
+ Two spellings of one root must not become two cached connections to one
57
+ file. They do without this, because callers disagree about spelling by
58
+ construction -- `media_analysis` normalizes a root through `realpath`
59
+ before using it, while its own callers pass whatever the user typed. On
60
+ macOS that alone is enough: every temp root under `/var/folders/...` is a
61
+ symlink to `/private/var/folders/...`.
62
+
63
+ The damage is not just a duplicate. `close()` pops by key, so a mismatch
64
+ makes it silently no-op and leave the connection it was called to release
65
+ -- which is the whole point of calling it before deleting the root.
66
+
67
+ `realpath` on a path that does not exist yet resolves the ancestors that do
68
+ and leaves the rest literal, which is what a not-yet-created DB needs.
69
+ """
70
+ return os.path.realpath(db_path_for_project(project_root))
71
+
72
+
53
73
  def _ensure_parent_dir(path: str) -> None:
54
74
  os.makedirs(os.path.dirname(path), exist_ok=True)
55
75
 
@@ -224,7 +244,7 @@ def connect(project_root: str) -> sqlite3.Connection:
224
244
  """
225
245
  if not project_root:
226
246
  raise ValueError("project_root is required")
227
- path = db_path_for_project(project_root)
247
+ path = _cache_key(project_root)
228
248
  with _CONNECTION_LOCK:
229
249
  existing = _CONNECTIONS.get(path)
230
250
  if existing is not None:
@@ -250,6 +270,25 @@ def close_all() -> None:
250
270
  _CONNECTIONS.clear()
251
271
 
252
272
 
273
+ def close(project_root: str) -> None:
274
+ """Drop and close the cached connection for `project_root`, if any.
275
+
276
+ Callers that are about to delete or move a project's analysis root need
277
+ this. On Windows the open handle makes `_soul/timeline_brain.sqlite`
278
+ undeletable; on POSIX the cache would otherwise hand the next writer a
279
+ connection to a file that no longer has a directory entry, so the write
280
+ lands nowhere.
281
+ """
282
+ path = _cache_key(project_root)
283
+ with _CONNECTION_LOCK:
284
+ conn = _CONNECTIONS.pop(path, None)
285
+ if conn is not None:
286
+ try:
287
+ conn.close()
288
+ except sqlite3.Error:
289
+ pass
290
+
291
+
253
292
  @contextmanager
254
293
  def transaction(project_root: str) -> Iterator[sqlite3.Connection]:
255
294
  """Context manager wrapping a write transaction.
@@ -291,14 +330,8 @@ def transaction(project_root: str) -> Iterator[sqlite3.Connection]:
291
330
 
292
331
  def reset_for_test(project_root: str) -> None:
293
332
  """Drop + recreate every table. Tests only."""
294
- path = db_path_for_project(project_root)
295
- with _CONNECTION_LOCK:
296
- conn = _CONNECTIONS.pop(path, None)
297
- if conn is not None:
298
- try:
299
- conn.close()
300
- except sqlite3.Error:
301
- pass
333
+ path = _cache_key(project_root)
334
+ close(project_root)
302
335
  for suffix in ("", "-wal", "-shm"):
303
336
  try:
304
337
  os.remove(path + suffix)