davinci-resolve-mcp 4.8.2 → 4.8.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,67 @@
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.3 — nested folder ids resolve for delete and move
6
+
7
+ ### Fixed
8
+
9
+ - **`media_pool.delete_folders` and `media_pool.move_folders` resolve
10
+ `folder_ids` anywhere in the Media Pool tree.** Both actions scanned only the
11
+ root folder's direct children, so an id belonging to any nested folder came
12
+ back `{"error":{"message":"No folders found"}}` — even though the caller was
13
+ holding the id `GetUniqueId()` had just handed them, and `mp.DeleteFolders()`
14
+ on the resolved object worked fine. Both now use the existing recursive
15
+ `_find_folder_by_id` walk through a shared `_folders_from_ids` resolver.
16
+ - **An unresolved id fails the call instead of being dropped** — the same
17
+ all-or-nothing rule 4.8.2 applied to the clip actions. Previously only an
18
+ all-empty result errored, so a mixed batch deleted or moved the subset that
19
+ happened to resolve and answered `{"success": true}`; `move_folders` did not
20
+ even stop at an empty result and answered `success: true` after
21
+ `MoveFolders([], target)`. Unresolved ids now return `FOLDER_NOT_FOUND` /
22
+ `invalid_input` naming both the unresolved and the resolved ids in `state`,
23
+ with nothing deleted or moved.
24
+ - An empty or non-list `folder_ids` returns `INVALID_FOLDER_IDS` instead of
25
+ iterating a bare string character by character; a missing one keeps the
26
+ surface-wide `MISSING_FOLDER_IDS`. The root (Master) folder — newly reachable
27
+ now that the search is recursive — is refused with
28
+ `ROOT_FOLDER_NOT_ELIGIBLE` before Resolve sees it. `move_folders`' "Target
29
+ folder not found" now carries `FOLDER_NOT_FOUND` / `invalid_input` too.
30
+
31
+ ### Validation
32
+
33
+ - New `tests/test_media_pool_folder_ids.py` pins both actions against a
34
+ three-level-deep folder, the partial-batch case, and the boundary inputs.
35
+ Against the 4.8.2 code 10 of its 11 tests fail; all 11 pass after.
36
+ - Offline suite: 3,732 tests run, 84 skipped, 11 errors — the same environment
37
+ gaps as in 4.8.2 (`numpy` and `requests` absent from the venv, `node_modules`
38
+ not installed). None touch `media_pool`. Drift guards, api-parity,
39
+ api-limitations and read/write symmetry clean; tool counts unchanged.
40
+ - Live reproduction on Resolve Studio 21.1.0.14 (reporter): deleting
41
+ `Master/OUTDOORS/1_FOOTAGE/wetransfer_dscf1065-mov_2022-01-31_1207` by its
42
+ `GetUniqueId()` returned `No folders found`, while `mp.DeleteFolders()` with
43
+ the recursively resolved folder object succeeded.
44
+ - **Live-validated on Resolve Studio 21.1.0.14** with the new
45
+ `tests/live_nested_folder_ids_check.py`
46
+ (`venv/bin/python tests/live_nested_folder_ids_check.py`) against a real project
47
+ of 144 folders and 2,600 clips, probing a folder seven levels below Master.
48
+ `delete_folders` on its id reached `confirmation_required` naming exactly that
49
+ folder; `move_folders` resolved it and its current parent as the target;
50
+ partial and all-missing batches came back `FOLDER_NOT_FOUND` for both actions,
51
+ Master `ROOT_FOLDER_NOT_ELIGIBLE`, bare string and empty list
52
+ `INVALID_FOLDER_IDS`, no key `MISSING_FOLDER_IDS`. The check is strictly
53
+ non-destructive, with the same tripwire as the 4.8.2 clip check: the MediaPool
54
+ it hands the actions forwards only `GetRootFolder` / `GetCurrentFolder` and
55
+ intercepts every other method, so `DeleteFolders` / `MoveFolders` never reach
56
+ Resolve whatever the code does; token gating is forced on and no token is
57
+ passed; move probes aim at the folder's current parent; the destructive hook's
58
+ analysis-root writes are disabled and its logs go to a temp directory. Every
59
+ folder's parent and every clip's folder read back identical after the run.
60
+ - The same live check against the 4.8.2 code fails with 14 findings and
61
+ reproduces the reported bug: the depth-7 id came back `No folders found` from
62
+ `delete_folders`, and the tripwire intercepted five `MoveFolders` calls with an
63
+ empty folder list, each answered as a plain result rather than an error. The
64
+ pool was unchanged after that run too.
65
+
5
66
  ## What's New in v4.8.2 — a clip id that resolves to nothing fails the whole clip batch
6
67
 
7
68
  ### 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-4.8.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.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-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.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.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-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.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.8.3 版 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
@@ -925,7 +925,10 @@ Key actions: `get_root_folder`, `get_current_folder`, `set_current_folder(path)`
925
925
  `delete_clips`, `move_clips`, `relink` and `unlink` are all-or-nothing: if any id
926
926
  in `clip_ids` matches no clip, the call fails with `CLIP_NOT_FOUND` (the
927
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.
928
+ stale ids and retry; do not read a partial batch as done. `delete_folders(folder_ids)`
929
+ and `move_folders(folder_ids, target_path)` work the same way with
930
+ `FOLDER_NOT_FOUND`; they resolve `folder_ids` at any depth (pass the ids
931
+ `folder get_subfolders` returns) and refuse the Master folder itself.
929
932
 
930
933
  Media Pool / Ingest kernel actions (v2.8.0+) add safer agent-facing workflows:
931
934
  `ingest_capabilities`, `probe_media_pool`, `probe_ingest_item`,
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.2"
40
+ VERSION = "4.8.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": "4.8.2",
3
+ "version": "4.8.3",
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.2"
96
+ VERSION = "4.8.3"
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.2"
14
+ VERSION = "4.8.3"
15
15
 
16
16
  import base64
17
17
  import os
@@ -2967,6 +2967,13 @@ def _find_clip_with_parent(folder, clip_id, _parent=None):
2967
2967
  return found_clip, found_parent
2968
2968
  return None, None
2969
2969
 
2970
+
2971
+ _FOLDER_ID_REMEDIATION = (
2972
+ "List folders with folder get_subfolders (walking down from "
2973
+ "path=\"Master\") and address by exact path or folder_id."
2974
+ )
2975
+
2976
+
2970
2977
  def _find_folder_by_id(folder, folder_id):
2971
2978
  if folder.GetUniqueId() == folder_id:
2972
2979
  return folder
@@ -2977,6 +2984,57 @@ def _find_folder_by_id(folder, folder_id):
2977
2984
  return None
2978
2985
 
2979
2986
 
2987
+ def _folders_from_ids(root, folder_ids, *, verb):
2988
+ """Resolve Media Pool folder unique IDs to folder objects. Returns (folders, error).
2989
+
2990
+ Searches the WHOLE tree. The ids callers hold come from GetUniqueId() /
2991
+ folder.get_subfolders, which hand them out at every depth, so the shallow
2992
+ scan of root's direct children this replaced answered "No folders found" for
2993
+ any nested folder — verified live on Resolve Studio 21.1.0.14, where
2994
+ deleting Master/OUTDOORS/1_FOOTAGE/<clip bin> by its own id failed while
2995
+ mp.DeleteFolders() on the recursively resolved object worked.
2996
+
2997
+ Unresolved ids abort the call instead of being dropped. Skipping them let a
2998
+ partial match delete or move the subset it did find and answer plain
2999
+ {"success": true}, which is indistinguishable from every id having resolved.
3000
+
3001
+ The root (Master) folder is refused rather than passed to Resolve: it cannot
3002
+ be deleted or moved, and the shallow search could never return it.
3003
+ """
3004
+ if not isinstance(folder_ids, list) or not folder_ids:
3005
+ return None, _err(
3006
+ "folder_ids must be a non-empty list of folder unique IDs",
3007
+ code="INVALID_FOLDER_IDS", category="invalid_input",
3008
+ remediation=_FOLDER_ID_REMEDIATION)
3009
+ root_id = root.GetUniqueId()
3010
+ folders, unresolved, named_root = [], [], False
3011
+ for fid in folder_ids:
3012
+ fid = str(fid)
3013
+ if fid == root_id:
3014
+ named_root = True
3015
+ continue
3016
+ found = _find_folder_by_id(root, fid)
3017
+ if found is None:
3018
+ unresolved.append(fid)
3019
+ else:
3020
+ folders.append(found)
3021
+ if named_root:
3022
+ return None, _err(
3023
+ f"The root (Master) folder cannot be {verb}",
3024
+ code="ROOT_FOLDER_NOT_ELIGIBLE", category="invalid_input",
3025
+ state={"root_folder_id": root_id})
3026
+ if unresolved:
3027
+ return None, _err(
3028
+ f"Folder(s) not found: {', '.join(unresolved)}",
3029
+ code="FOLDER_NOT_FOUND", category="invalid_input",
3030
+ reason=(f"{len(unresolved)} of {len(folder_ids)} folder_ids matched no folder "
3031
+ f"anywhere in the Media Pool; nothing was {verb}."),
3032
+ remediation=_FOLDER_ID_REMEDIATION,
3033
+ state={"unresolved_folder_ids": unresolved,
3034
+ "resolved_folder_ids": [f.GetUniqueId() for f in folders]})
3035
+ return folders, None
3036
+
3037
+
2980
3038
  def _folder_from_params(mp, p, *path_keys, no_address="current"):
2981
3039
  """Resolve the folder an action was aimed at. Returns (folder, error).
2982
3040
 
@@ -2997,8 +3055,7 @@ def _folder_from_params(mp, p, *path_keys, no_address="current"):
2997
3055
  `_navigate_folder(mp, "")`, which returns root). This fix must not also
2998
3056
  change what omitting the address means.
2999
3057
  """
3000
- remediation = ("List folders with folder get_subfolders (walking down from "
3001
- "path=\"Master\") and address by exact path or folder_id.")
3058
+ remediation = _FOLDER_ID_REMEDIATION
3002
3059
  path = _first_param(p, *path_keys)
3003
3060
  if path:
3004
3061
  f = _navigate_folder(mp, path)
@@ -21088,7 +21145,10 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21088
21145
  add_subfolder(name, parent_path?) -> {success, name, id}
21089
21146
  delete_folders(folder_ids) -> {success}
21090
21147
  DESTRUCTIVE. Deletes folders + every clip they contain.
21148
+ folder_ids resolve at ANY depth; an id matching nothing fails the whole
21149
+ call (FOLDER_NOT_FOUND) rather than deleting the ids that did resolve.
21091
21150
  move_folders(folder_ids, target_path) -> {success}
21151
+ folder_ids resolve at ANY depth; unresolved ids fail the whole call.
21092
21152
  refresh() -> {success}
21093
21153
  create_timeline(name, if_exists?) -> {success, name, id}
21094
21154
  — if_exists: version (default), reuse, or fail
@@ -21196,14 +21256,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21196
21256
  f = mp.AddSubFolder(parent, p["name"])
21197
21257
  return _ok(name=f.GetName(), id=f.GetUniqueId()) if f else _err("Failed to create subfolder")
21198
21258
  elif action == "delete_folders":
21199
- folders = []
21200
- for fid in p["folder_ids"]:
21201
- # Search for folder by ID (simplified - searches root subfolders)
21202
- for sub in (root.GetSubFolderList() or []):
21203
- if sub.GetUniqueId() == fid:
21204
- folders.append(sub)
21205
- if not folders:
21206
- return _err("No folders found")
21259
+ folders, folders_err = _folders_from_ids(root, p["folder_ids"], verb="deleted")
21260
+ if folders_err:
21261
+ return folders_err
21207
21262
  if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
21208
21263
  return _issue_confirm_token(
21209
21264
  action="media_pool.delete_folders", params=p,
@@ -21219,12 +21274,12 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
21219
21274
  elif action == "move_folders":
21220
21275
  target = _navigate_folder(mp, p["target_path"])
21221
21276
  if not target:
21222
- return _err(f"Target folder not found: {p['target_path']}")
21223
- folders = []
21224
- for fid in p["folder_ids"]:
21225
- for sub in (root.GetSubFolderList() or []):
21226
- if sub.GetUniqueId() == fid:
21227
- folders.append(sub)
21277
+ return _err(f"Target folder not found: {p['target_path']}",
21278
+ code="FOLDER_NOT_FOUND", category="invalid_input",
21279
+ remediation=_FOLDER_ID_REMEDIATION)
21280
+ folders, folders_err = _folders_from_ids(root, p["folder_ids"], verb="moved")
21281
+ if folders_err:
21282
+ return folders_err
21228
21283
  return {"success": bool(mp.MoveFolders(folders, target))}
21229
21284
  elif action == "refresh":
21230
21285
  return {"success": bool(mp.RefreshFolders())}