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 +70 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +5 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +55 -11
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
|
-
[](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.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.
|
|
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
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.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.
|
|
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 =
|
|
21397
|
-
|
|
21398
|
-
|
|
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 =
|
|
21417
|
-
|
|
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 =
|
|
21421
|
-
|
|
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 =
|
|
21425
|
-
|
|
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")
|