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 +61 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +4 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +72 -17
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v4.8.
|
|
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.
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
93
93
|
handlers=[logging.StreamHandler()],
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
VERSION = "4.8.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
21201
|
-
|
|
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
|
-
|
|
21224
|
-
|
|
21225
|
-
|
|
21226
|
-
|
|
21227
|
-
|
|
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())}
|