davinci-resolve-mcp 4.8.1 → 4.8.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,76 @@
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 v4.8.2 — a clip id that resolves to nothing fails the whole clip batch
6
+
7
+ ### Fixed
8
+
9
+ - **`media_pool` `delete_clips`, `move_clips`, `relink` and `unlink` no longer act on
10
+ the part of a batch that happened to resolve.** All four resolved `clip_ids` with
11
+ `[_find_clip(root, cid) for cid in ...]` and then dropped every miss, so Resolve was
12
+ handed whatever was left. `delete_clips` errored only when *every* id missed
13
+ (`"No clips found"`); a mixed batch deleted the subset that resolved and answered
14
+ `{"success": true}`, and with confirm tokens on, the preview and the token covered
15
+ that subset too. `move_clips`, `relink` and `unlink` did not check for an empty
16
+ result at all: an all-missing batch reached `MoveClips([], target)` /
17
+ `RelinkClips([], path)` / `UnlinkClips([])` and reported whatever Resolve's bool
18
+ said. The caller could not tell a partial change from a whole one.
19
+ - A new `_clips_from_ids` resolver fails the call before anything reaches Resolve
20
+ (and before a confirm token is issued) with `CLIP_NOT_FOUND` /
21
+ `invalid_input`, naming the ids in `error.state.unresolved_clip_ids` and
22
+ `error.state.resolved_clip_ids`. Nothing is deleted, moved, relinked or unlinked.
23
+ - An empty or non-list `clip_ids` returns `INVALID_CLIP_IDS` instead of iterating a
24
+ bare string character by character; a missing one keeps the surface-wide
25
+ `MISSING_CLIP_IDS`.
26
+
27
+ ### Not changed
28
+
29
+ - The same drop-the-misses pattern remains in four other `media_pool` actions, which
30
+ this release does not touch: `create_timeline_from_clips` (simple `clip_ids` mode)
31
+ builds the timeline from the subset, `append_to_timeline` (legacy `clip_ids` mode)
32
+ appends the subset and verifies against the resolved count, `export_metadata`
33
+ exports the subset — and with every id missing calls `ExportMetadata(path, [])`,
34
+ whose behaviour on an empty list is not measured — and `auto_sync_audio` syncs the
35
+ subset. The granular server's `delete_media_pool_clips` and `move_clips_to_folder`
36
+ have the same partial-batch behaviour (they do report a count). The `safe_*` /
37
+ `organize_clips` family reports its misses in `missing` rather than dropping them
38
+ silently.
39
+
40
+ ### Validation
41
+
42
+ - New `tests/test_media_pool_clip_ids.py` pins all four actions against a fake
43
+ Media Pool that records every mutation call: a partial batch, an all-missing
44
+ batch, a partial `delete_clips` under confirm-token gating (no token is issued),
45
+ and the missing / empty / bare-string `clip_ids` shapes, plus the happy path with
46
+ a clip two folders deep. Against the pre-fix code 5 of its 10 tests fail
47
+ (17 subtests — every partial and all-missing case, the token case and both
48
+ shape cases); the 5 that pin preserved behaviour pass on both. All 10 pass after.
49
+ - Offline suite: 3,721 tests run, 84 skipped, 11 errors — all environment gaps on the
50
+ machine that ran it (`numpy` and `requests` absent from the venv, so five test
51
+ modules fail to import and one LUT test cannot do arithmetic; `node_modules` not
52
+ installed, so five `test_offline_fallback` cases cannot load `jszip`). None touch
53
+ `media_pool`. Drift guards, api-parity, api-limitations and read/write symmetry
54
+ clean; `test_static_undefined_names` skipped because pyflakes is not installed.
55
+ - **Live-validated on Resolve Studio 21.1.0.14** with the new
56
+ `tests/live_clip_ids_check.py` (`venv/bin/python tests/live_clip_ids_check.py`)
57
+ against a real project of 144 folders and 2,600 clips, probing a clip seven
58
+ folders deep. Partial and all-missing batches came back `CLIP_NOT_FOUND` for all
59
+ four actions, with no confirm token for the partial delete; bare string and
60
+ empty list → `INVALID_CLIP_IDS`, no key → `MISSING_CLIP_IDS`; a whole batch still
61
+ resolved to the real clip. The check is strictly non-destructive: the MediaPool it
62
+ hands the actions forwards only `GetRootFolder` / `GetCurrentFolder` and
63
+ intercepts every other method, so `DeleteClips` / `MoveClips` / `RelinkClips` /
64
+ `UnlinkClips` never reach Resolve whatever the code does; token gating is forced
65
+ on and no token is passed; the destructive hook's analysis-root writes are
66
+ disabled and its logs go to a temp directory. Every clip's folder, every folder's
67
+ parent and the probe clip's File Path read back identical after the run.
68
+ - The same live check against the pre-fix code fails with 18 findings: the tripwire
69
+ intercepted 10 MediaPool mutations the old code sent — `MoveClips`,
70
+ `RelinkClips` and `UnlinkClips` with just the probe clip for the partial batches,
71
+ the same three with an empty list for the all-missing batches, `UnlinkClips([])`
72
+ for `clip_ids: []` — and the partial `delete_clips` was issued a confirm token.
73
+ The pool was unchanged after that run too.
74
+
5
75
  ## What's New in v4.8.1 — text modifiers (Follower) attach to TextPlus inputs
6
76
 
7
77
  ### Added
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-4.8.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.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(389%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-4.8.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.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(389%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
- > 本翻译对应 v4.8.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.8.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/docs/SKILL.md CHANGED
@@ -922,6 +922,11 @@ Key actions: `get_root_folder`, `get_current_folder`, `set_current_folder(path)`
922
922
  `setup_multicam_timeline(name, clip_ids|angles, sync_mode?, include_audio?, dry_run?)`,
923
923
  `get_selected`, `set_selected(clip_id)`, `export_metadata(path, clip_ids?)`
924
924
 
925
+ `delete_clips`, `move_clips`, `relink` and `unlink` are all-or-nothing: if any id
926
+ in `clip_ids` matches no clip, the call fails with `CLIP_NOT_FOUND` (the
927
+ unresolved and resolved ids are in `error.state`) and no clip is changed. Drop the
928
+ stale ids and retry; do not read a partial batch as done.
929
+
925
930
  Media Pool / Ingest kernel actions (v2.8.0+) add safer agent-facing workflows:
926
931
  `ingest_capabilities`, `probe_media_pool`, `probe_ingest_item`,
927
932
  `safe_import_media`, `safe_import_sequence`, `safe_import_folder`,
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.8.1"
40
+ VERSION = "4.8.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": "4.8.1",
3
+ "version": "4.8.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
93
93
  handlers=[logging.StreamHandler()],
94
94
  )
95
95
 
96
- VERSION = "4.8.1"
96
+ VERSION = "4.8.2"
97
97
  logger = logging.getLogger("davinci-resolve-mcp")
98
98
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
99
99
  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 = "4.8.1"
14
+ VERSION = "4.8.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -2912,6 +2912,45 @@ def _find_clip(folder, clip_id):
2912
2912
  return None
2913
2913
 
2914
2914
 
2915
+ def _clips_from_ids(root, clip_ids, *, verb):
2916
+ """Resolve Media Pool clip unique IDs to clip objects. Returns (clips, error).
2917
+
2918
+ Unresolved ids abort the call instead of being dropped. The raw clip actions
2919
+ used to filter misses out and hand Resolve whatever was left, so a mixed batch
2920
+ deleted, moved, relinked or unlinked the subset that happened to resolve and
2921
+ answered plain {"success": true} — indistinguishable from every id having
2922
+ resolved. move_clips/relink/unlink did not even stop at an empty result:
2923
+ MoveClips([], target) moved nothing and still reported success.
2924
+
2925
+ A non-list is refused rather than iterated: a bare string used to become one
2926
+ id per character, each matching nothing and each silently dropped.
2927
+ """
2928
+ if not isinstance(clip_ids, list) or not clip_ids:
2929
+ return None, _err(
2930
+ "clip_ids must be a non-empty list of Media Pool clip unique IDs",
2931
+ code="INVALID_CLIP_IDS", category="invalid_input",
2932
+ remediation="Pass clip_ids as a list, e.g. params={\"clip_ids\": [\"<id>\"]}.")
2933
+ clips, resolved, unresolved = [], [], []
2934
+ for cid in clip_ids:
2935
+ cid = str(cid)
2936
+ found = _find_clip(root, cid)
2937
+ if found is None:
2938
+ unresolved.append(cid)
2939
+ else:
2940
+ clips.append(found)
2941
+ resolved.append(cid)
2942
+ if unresolved:
2943
+ return None, _err(
2944
+ f"Clip(s) not found: {', '.join(unresolved)}",
2945
+ code="CLIP_NOT_FOUND", category="invalid_input",
2946
+ reason=(f"{len(unresolved)} of {len(clip_ids)} clip_ids matched no clip "
2947
+ f"anywhere in the Media Pool; nothing was {verb}."),
2948
+ remediation=("List clips with folder get_clips (or media_pool get_selected) "
2949
+ "and pass only ids that still exist."),
2950
+ state={"unresolved_clip_ids": unresolved, "resolved_clip_ids": resolved})
2951
+ return clips, None
2952
+
2953
+
2915
2954
  def _find_clip_with_parent(folder, clip_id, _parent=None):
2916
2955
  """Return (clip, parent_folder) for clip_id, searching recursively.
2917
2956
 
@@ -21086,6 +21125,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21086
21125
  Example: [{"FilePath": "frame_%03d.dpx", "StartIndex": 1, "EndIndex": 100}]
21087
21126
  delete_clips(clip_ids) -> {success}
21088
21127
  DESTRUCTIVE. Removes clips from the Media Pool (does not touch source files).
21128
+ An id matching no clip fails the whole call (CLIP_NOT_FOUND) rather than
21129
+ deleting the ids that did resolve; the same holds for move_clips, relink
21130
+ and unlink.
21089
21131
  move_clips(clip_ids, target_path) -> {success}
21090
21132
  relink(clip_ids, folder_path) -> {success}
21091
21133
  UNSAFE. No dry_run. Prefer safe_relink.
@@ -21393,10 +21435,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21393
21435
  result = mp.ImportMedia(paths)
21394
21436
  return {"imported": len(result) if result else 0}
21395
21437
  elif action == "delete_clips":
21396
- clips = [_find_clip(root, cid) for cid in p["clip_ids"]]
21397
- clips = [c for c in clips if c]
21398
- if not clips:
21399
- return _err("No clips found")
21438
+ clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="deleted")
21439
+ if clips_err:
21440
+ return clips_err
21400
21441
  if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
21401
21442
  return _issue_confirm_token(
21402
21443
  action="media_pool.delete_clips", params=p,
@@ -21413,16 +21454,19 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21413
21454
  target = _navigate_folder(mp, p["target_path"])
21414
21455
  if not target:
21415
21456
  return _err(f"Target folder not found: {p['target_path']}")
21416
- clips = [_find_clip(root, cid) for cid in p["clip_ids"]]
21417
- clips = [c for c in clips if c]
21457
+ clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="moved")
21458
+ if clips_err:
21459
+ return clips_err
21418
21460
  return {"success": bool(mp.MoveClips(clips, target))}
21419
21461
  elif action == "relink":
21420
- clips = [_find_clip(root, cid) for cid in p["clip_ids"]]
21421
- clips = [c for c in clips if c]
21462
+ clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="relinked")
21463
+ if clips_err:
21464
+ return clips_err
21422
21465
  return {"success": bool(mp.RelinkClips(clips, p["folder_path"]))}
21423
21466
  elif action == "unlink":
21424
- clips = [_find_clip(root, cid) for cid in p["clip_ids"]]
21425
- clips = [c for c in clips if c]
21467
+ clips, clips_err = _clips_from_ids(root, p["clip_ids"], verb="unlinked")
21468
+ if clips_err:
21469
+ return clips_err
21426
21470
  return {"success": bool(mp.UnlinkClips(clips))}
21427
21471
  elif action == "export_metadata":
21428
21472
  clip_ids = p.get("clip_ids")