davinci-resolve-mcp 4.4.2 → 4.5.1
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 +129 -0
- package/README.md +2 -2
- package/README.zh-CN.md +3 -3
- package/docs/SKILL.md +15 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +2 -1
- package/src/granular/folder.py +4 -0
- package/src/granular/gallery.py +4 -0
- package/src/granular/graph.py +4 -0
- package/src/granular/media_pool.py +6 -0
- package/src/granular/media_pool_item.py +21 -0
- package/src/granular/project.py +28 -0
- package/src/granular/resolve_211.py +9 -0
- package/src/granular/resolve_control.py +9 -0
- package/src/granular/timeline.py +18 -0
- package/src/granular/timeline_item.py +30 -0
- package/src/server.py +1 -1
- package/src/utils/destructive_hook.py +249 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,135 @@
|
|
|
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.5.1 — the safe-mode refusal reaches the caller on 27 more tools
|
|
6
|
+
|
|
7
|
+
v4.5.0 gave the granular server a working safe-mode gate. On 27 tools it then threw
|
|
8
|
+
the answer away.
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A blocked call raised `ToolError` instead of returning the refusal.** FastMCP
|
|
13
|
+
builds an output schema from a tool's return annotation and validates against it,
|
|
14
|
+
so handing the block dict to a tool annotated `-> str` failed validation:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
ToolError: ...Output / result / Input should be a valid string
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The caller received a generic execution error carrying none of the
|
|
21
|
+
`SAFE_MODE_BLOCKED` code, reason or remediation — the gate fired correctly and its
|
|
22
|
+
answer was destroyed on the way out. Measured on the real `--full` entry path
|
|
23
|
+
against shipped v4.5.0: `clear_folder_transcription` raised rather than refusing.
|
|
24
|
+
|
|
25
|
+
It lands hardest exactly where it matters. The HIGH-rated string-returning tools
|
|
26
|
+
are the calls safe mode exists to stop: `clear_folder_transcription`,
|
|
27
|
+
`unlink_proxy_media`, `replace_clip`, `delete_keyframe`, `quit_app`, `restart_app`.
|
|
28
|
+
|
|
29
|
+
A `-> str` tool is now refused with the message and its remediation as text,
|
|
30
|
+
prefixed with the code. That loses the machine-readable field, which is a real
|
|
31
|
+
cost and worth stating plainly — but a refusal the client can read beats a
|
|
32
|
+
`ToolError` that discards it, and it is the only shape that tool's own schema will
|
|
33
|
+
accept. The 105 tools annotated `-> Dict[str, Any]` or `-> dict` keep the
|
|
34
|
+
structured envelope unchanged.
|
|
35
|
+
|
|
36
|
+
### Validation
|
|
37
|
+
|
|
38
|
+
- Full offline suite: **3,684 passed, 1 skipped, 0 failed**, 1,412 subtests.
|
|
39
|
+
- Both new assertions were confirmed to **fail with the fix reverted**, then pass on
|
|
40
|
+
restore — the guard is not vacuous.
|
|
41
|
+
- Verified end-to-end through the real registry with `destructive.safe_mode` on: a
|
|
42
|
+
`-> str` tool returns readable refusal text, a `-> dict` tool returns the full
|
|
43
|
+
envelope, and `allow_risky_operation=true` still lets a permitted call through on
|
|
44
|
+
both paths.
|
|
45
|
+
- A static check now walks every destructive-decorated tool annotated `-> str` and
|
|
46
|
+
asserts the hook would hand it a string, so tool number 28 cannot reintroduce this.
|
|
47
|
+
|
|
48
|
+
## What's New in v4.5.0 — safe mode and the audit log reach the granular server
|
|
49
|
+
|
|
50
|
+
v4.4.1 froze 131 destructive-hinted granular tools in a backlog and said plainly
|
|
51
|
+
that nothing enforced anything about them: no safe-mode refusal, no audit row. A
|
|
52
|
+
user running with `destructive.safe_mode` on was protected on the compound server
|
|
53
|
+
and not on the `--full` one, with nothing saying so. This release works that
|
|
54
|
+
backlog to zero.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **`@granular_destructive_op()` on every destructive-hinted granular tool** — the
|
|
59
|
+
131 in the backlog plus `ti_copy_grades`, 132 in all. The hook does two things
|
|
60
|
+
and only two: while `destructive.safe_mode` is on, a HIGH-risk call is refused
|
|
61
|
+
unless that call passes `allow_risky_operation: true`; and every call, refused
|
|
62
|
+
or run, writes a row to the security audit log. A dict result is annotated with
|
|
63
|
+
`operation_id` and `security` exactly as compound results are; a list, string or
|
|
64
|
+
boolean result comes back untouched, because several granular tools return
|
|
65
|
+
those.
|
|
66
|
+
- **`allow_risky_operation` is now a parameter on each hooked tool.** Granular
|
|
67
|
+
tools have no `params` object for the compound override to live in, so the hook
|
|
68
|
+
adds the parameter to the tool's own MCP schema (via `__signature__`, which
|
|
69
|
+
FastMCP honours). Every other property of every schema is unchanged — a test
|
|
70
|
+
diffs each hooked tool's advertised properties against its original signature.
|
|
71
|
+
- **Risk is rated from the verb, with one ledger override.** `delete`, `remove`,
|
|
72
|
+
`clear`, `reset`, `replace`, `unlink`, `overwrite`, `quit` and `restart` are
|
|
73
|
+
HIGH; `set`, `load`, `switch`, `close`, `stop` and `lift` are MEDIUM; anything
|
|
74
|
+
else is MEDIUM, never HIGH, so an unassessed verb cannot make safe mode
|
|
75
|
+
over-block. A tool whose body reaches a symbol the `api_truth` ledger marks
|
|
76
|
+
`destroys_prior_work` takes HIGH from the ledger instead: `ti_copy_grades` rates
|
|
77
|
+
MEDIUM by verb and HIGH in fact, mechanically, because `CopyGrades` is in the
|
|
78
|
+
ledger. Flagging a new ledger entry re-rates every tool that reaches it.
|
|
79
|
+
- **The ratchet now counts the hook, and keeps the tiers apart.**
|
|
80
|
+
`UNGATED_GRANULAR_DESTRUCTIVE` is empty; a new destructive-hinted tool without
|
|
81
|
+
the hook fails the suite, and so does a hook placed *outside* `@mcp.tool()`
|
|
82
|
+
(that order registers the bare function — decorated, and enforcing nothing).
|
|
83
|
+
The `destroys_prior_work` test still demands the full `acknowledge_trap` +
|
|
84
|
+
confirm-token gate; the enforcement hook does not satisfy it and must not.
|
|
85
|
+
Enforcement and confirmation are separate tiers — a two-step confirmation on
|
|
86
|
+
`ti_set_clip_color` would make the granular server unusable.
|
|
87
|
+
- **`tests/test_granular_destructive_op.py`** — refusal, override, audit rows for
|
|
88
|
+
allowed and blocked calls, an unwritable audit path that cannot break the call,
|
|
89
|
+
list/scalar passthrough, positional arguments audited by name, and the override
|
|
90
|
+
travelling end-to-end through FastMCP's `call_tool`.
|
|
91
|
+
|
|
92
|
+
### Not added, on purpose
|
|
93
|
+
|
|
94
|
+
- **No archive.** The compound hook duplicates the timeline into an Archive bin
|
|
95
|
+
before mutating it. Doing that around 132 granular calls would bury a project in
|
|
96
|
+
versions for operations as small as a clip-colour change. A granular write
|
|
97
|
+
therefore has **no recovery version**: it is refused, or it is recorded — never
|
|
98
|
+
recovered. `docs/SKILL.md` and the README now say so rather than implying parity.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- **The first draft of the hook was cosmetic.** It rated verbs `"HIGH"` while the
|
|
103
|
+
safe-mode gate holds `RiskLevel.HIGH.value == "high"`; nothing matched, and a
|
|
104
|
+
HIGH tool ran with safe mode on. Reproduced with a probe before the fix, pinned
|
|
105
|
+
by a vocabulary test that asserts every rating is a `RiskLevel` value, and by a
|
|
106
|
+
refusal test that asserts the body never ran.
|
|
107
|
+
- **The refusal names the right argument.** On the granular server the override
|
|
108
|
+
is `allow_risky_operation=true` on the call, not `params.allow_risky_operation`;
|
|
109
|
+
the message and remediation say which.
|
|
110
|
+
|
|
111
|
+
### Validation
|
|
112
|
+
|
|
113
|
+
- Static checks: API parity audit, api-limitations, read/write symmetry,
|
|
114
|
+
agent-rules, release-surface drift, `git diff --check`.
|
|
115
|
+
- Full offline suite: 3679 passed, 1 skipped, 0 failed, 1412 subtests (v4.4.2 baseline plus the new guards; no count drop).
|
|
116
|
+
- Every new guard was made to fail before it was trusted — nine regressions were
|
|
117
|
+
re-introduced one at a time and restored from a byte copy: hook removed (fails),
|
|
118
|
+
hook outside `@mcp.tool` (fails), bare `@granular_destructive_op` without
|
|
119
|
+
parentheses (the granular package no longer imports — pydantic cannot build a
|
|
120
|
+
schema for the decorator factory), `"HIGH"` casing (3 tests fail), a list result
|
|
121
|
+
mutated (fails), the ledger override removed (2 fail), the confirm-token
|
|
122
|
+
redemption dropped from `ti_copy_grades` with the hook still present (the
|
|
123
|
+
confirmation-tier test fails on its own), `__signature__` not set (133 fail — the
|
|
124
|
+
override never reaches the hook), and an audit write error re-raised (fails).
|
|
125
|
+
- Live, DaVinci Resolve Studio 19.1.3.7, on a disposable project: with safe mode
|
|
126
|
+
on, `set_project_setting` (MEDIUM) ran, returned its plain string untouched and
|
|
127
|
+
was audited `allowed`; `delete_project` (HIGH) was refused with
|
|
128
|
+
`SAFE_MODE_BLOCKED` and audited `blocked`; the same call with
|
|
129
|
+
`allow_risky_operation=true` reached the body and was audited `allowed`. Resolve's
|
|
130
|
+
own `DeleteProject` returned False for the just-created project, which was also
|
|
131
|
+
absent from the folder listing before and after — an artifact of an unsaved new
|
|
132
|
+
project on a PostgreSQL database, not hook behaviour. No project was left behind.
|
|
133
|
+
|
|
5
134
|
## What's New in v4.4.2 — a refused option now says which one, and why
|
|
6
135
|
|
|
7
136
|
Reported as [#232](https://github.com/samuelgursky/davinci-resolve-mcp/issues/232):
|
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)
|
|
@@ -404,7 +404,7 @@ This project treats camera originals and source media as immutable. Analysis too
|
|
|
404
404
|
|
|
405
405
|
## Security Posture
|
|
406
406
|
|
|
407
|
-
The default server is a local stdio process launched by your MCP client; it does not expose a network listener or built-in multi-user auth surface. The two opt-in local HTTP surfaces — the control panel and the networked MCP transport — bind loopback only and require a per-launch bearer token on every request, with Host/Origin checks against DNS rebinding and CSRF. Tool metadata includes MCP client-safety hints for read-only, destructive, idempotent, and external-resource operations. See [Security Policy](SECURITY.md) for operational boundaries, confirmation guidance, and vulnerability reporting.
|
|
407
|
+
The default server is a local stdio process launched by your MCP client; it does not expose a network listener or built-in multi-user auth surface. The two opt-in local HTTP surfaces — the control panel and the networked MCP transport — bind loopback only and require a per-launch bearer token on every request, with Host/Origin checks against DNS rebinding and CSRF. Tool metadata includes MCP client-safety hints for read-only, destructive, idempotent, and external-resource operations. Destructive writes on both servers honour `destructive.safe_mode` and the security audit log; only the compound server archives a timeline before mutating it — granular writes are refused or recorded, never recovered. See [Security Policy](SECURITY.md) for operational boundaries, confirmation guidance, and vulnerability reporting.
|
|
408
408
|
|
|
409
409
|
## Key Stats
|
|
410
410
|
|
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.
|
|
15
|
+
> 本翻译对应 v4.5.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -220,7 +220,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
|
|
|
220
220
|
|
|
221
221
|
## 安全态势
|
|
222
222
|
|
|
223
|
-
默认服务器是由你的 MCP 客户端启动的本地 stdio 进程;它不暴露网络监听器,也没有内置多用户认证面。两个可选的本地 HTTP 面——控制面板与联网 MCP 传输——仅绑定回环地址,每个请求都需要每次启动生成的 bearer 令牌,并校验 Host/Origin 以防 DNS 重绑定和 CSRF。工具元数据包含面向 MCP
|
|
223
|
+
默认服务器是由你的 MCP 客户端启动的本地 stdio 进程;它不暴露网络监听器,也没有内置多用户认证面。两个可选的本地 HTTP 面——控制面板与联网 MCP 传输——仅绑定回环地址,每个请求都需要每次启动生成的 bearer 令牌,并校验 Host/Origin 以防 DNS 重绑定和 CSRF。工具元数据包含面向 MCP 客户端的安全提示(只读、破坏性、幂等、外部资源操作)。两个服务器上的破坏性写入都遵守 `destructive.safe_mode` 并写入安全审计日志;只有复合服务器会在修改前归档时间线——细粒度写入只会被拒绝或被记录,绝不会被恢复。操作边界、确认指引和漏洞报告见 [安全策略](SECURITY.md)。
|
|
224
224
|
|
|
225
225
|
## 关键数据
|
|
226
226
|
|
package/docs/SKILL.md
CHANGED
|
@@ -362,6 +362,21 @@ and blanking in both server interfaces. These readers do not invoke setters.
|
|
|
362
362
|
This skill document covers the **compound server** (the default). Each compound
|
|
363
363
|
tool accepts an `action` string and an optional `params` object.
|
|
364
364
|
|
|
365
|
+
**Granular writes are enforced, not archived.** Every destructive-hinted granular
|
|
366
|
+
tool (deletes, clears, resets, replaces, sets, loads — 132 of the 387) runs
|
|
367
|
+
through `granular_destructive_op`: while `destructive.safe_mode` is on, a
|
|
368
|
+
HIGH-risk call is refused unless that call passes `allow_risky_operation: true`
|
|
369
|
+
(a parameter the hook adds to each hooked tool's schema), and every call writes
|
|
370
|
+
a row to the security audit log. Risk is read from the verb — `delete`/`remove`/
|
|
371
|
+
`clear`/`reset`/`replace`/`unlink`/`quit`/`restart` are HIGH, `set`/`load`/
|
|
372
|
+
`switch`/`close`/`stop` are MEDIUM — except that a tool reaching a symbol the
|
|
373
|
+
`api_truth` ledger marks `destroys_prior_work` is HIGH from the ledger
|
|
374
|
+
(`ti_copy_grades`), and those tools also keep their `acknowledge_trap` +
|
|
375
|
+
confirm-token gate. What the granular hook does **not** do is duplicate the
|
|
376
|
+
timeline into an Archive bin first, as the compound hook does: a granular write
|
|
377
|
+
has no recovery version, and a refused-or-audited call is the whole of its
|
|
378
|
+
safety. Use the compound server when you want the archive.
|
|
379
|
+
|
|
365
380
|
### The advanced server (`davinci-resolve-advanced-mcp`)
|
|
366
381
|
|
|
367
382
|
The same package ships an optional third surface: an offline Node server (18
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.
|
|
40
|
+
VERSION = "4.5.1"
|
|
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
|
@@ -28,6 +28,7 @@ from src.utils.app_control import (
|
|
|
28
28
|
restart_resolve_app,
|
|
29
29
|
)
|
|
30
30
|
from src.utils.cdl import normalize_cdl_payload
|
|
31
|
+
from src.utils.destructive_hook import granular_destructive_op
|
|
31
32
|
from src.utils.confirm_tokens import (
|
|
32
33
|
ConfirmTokenStore,
|
|
33
34
|
gate_required_from,
|
|
@@ -92,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
92
93
|
handlers=[logging.StreamHandler()],
|
|
93
94
|
)
|
|
94
95
|
|
|
95
|
-
VERSION = "4.
|
|
96
|
+
VERSION = "4.5.1"
|
|
96
97
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
97
98
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
98
99
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/granular/folder.py
CHANGED
|
@@ -111,6 +111,7 @@ def transcribe_folder_audio(folder_name: str, use_speaker_detection: Optional[bo
|
|
|
111
111
|
|
|
112
112
|
|
|
113
113
|
@mcp.tool()
|
|
114
|
+
@granular_destructive_op()
|
|
114
115
|
def clear_folder_transcription(folder_name: str) -> str:
|
|
115
116
|
"""Clear audio transcription for all clips in a folder.
|
|
116
117
|
|
|
@@ -277,6 +278,7 @@ def folder_transcribe_audio(folder_path: str = "") -> Dict[str, Any]:
|
|
|
277
278
|
|
|
278
279
|
|
|
279
280
|
@mcp.tool()
|
|
281
|
+
@granular_destructive_op()
|
|
280
282
|
def folder_clear_transcription(folder_path: str = "") -> Dict[str, Any]:
|
|
281
283
|
"""Clear transcription for all clips in a Media Pool folder.
|
|
282
284
|
|
|
@@ -332,6 +334,7 @@ def folder_perform_audio_classification(folder_path: str = "") -> Dict[str, Any]
|
|
|
332
334
|
|
|
333
335
|
|
|
334
336
|
@mcp.tool()
|
|
337
|
+
@granular_destructive_op()
|
|
335
338
|
def folder_clear_audio_classification(folder_path: str = "") -> Dict[str, Any]:
|
|
336
339
|
"""Clear audio classification for all clips in a Media Pool folder (Resolve 21+).
|
|
337
340
|
|
|
@@ -395,6 +398,7 @@ def folder_analyze_for_slate(folder_path: str = "", marker_color: str = "Blue")
|
|
|
395
398
|
|
|
396
399
|
|
|
397
400
|
@mcp.tool()
|
|
401
|
+
@granular_destructive_op()
|
|
398
402
|
def folder_remove_motion_blur(folder_path: str = "", deblur_option: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
399
403
|
"""Render motion-deblurred copies of all clips in a folder (Resolve 21+).
|
|
400
404
|
|
package/src/granular/gallery.py
CHANGED
|
@@ -21,6 +21,7 @@ def get_gallery_album_name() -> Dict[str, Any]:
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@mcp.tool()
|
|
24
|
+
@granular_destructive_op()
|
|
24
25
|
def set_gallery_album_name(name: str) -> Dict[str, Any]:
|
|
25
26
|
"""Set the name of the current gallery album.
|
|
26
27
|
|
|
@@ -89,6 +90,7 @@ def get_current_still_album() -> Dict[str, Any]:
|
|
|
89
90
|
|
|
90
91
|
|
|
91
92
|
@mcp.tool()
|
|
93
|
+
@granular_destructive_op()
|
|
92
94
|
def set_current_still_album(album_index: int) -> Dict[str, Any]:
|
|
93
95
|
"""Set the current still album by index.
|
|
94
96
|
|
|
@@ -206,6 +208,7 @@ def get_still_label(album_index: int, still_index: int) -> Dict[str, Any]:
|
|
|
206
208
|
|
|
207
209
|
|
|
208
210
|
@mcp.tool()
|
|
211
|
+
@granular_destructive_op()
|
|
209
212
|
def set_still_label(album_index: int, still_index: int, label: str) -> Dict[str, Any]:
|
|
210
213
|
"""Set the label of a still in a gallery album.
|
|
211
214
|
|
|
@@ -281,6 +284,7 @@ def export_stills_from_album(album_index: int, folder_path: str, file_prefix: st
|
|
|
281
284
|
|
|
282
285
|
|
|
283
286
|
@mcp.tool()
|
|
287
|
+
@granular_destructive_op()
|
|
284
288
|
def delete_stills_from_album(album_index: int, still_indices: List[int]) -> Dict[str, Any]:
|
|
285
289
|
"""Delete stills from a gallery album.
|
|
286
290
|
|
package/src/granular/graph.py
CHANGED
|
@@ -25,6 +25,7 @@ def graph_get_num_nodes(item_index: int = 0, track_type: str = "video", track_in
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
@mcp.tool()
|
|
28
|
+
@granular_destructive_op()
|
|
28
29
|
def graph_set_lut(node_index: int, lut_path: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
29
30
|
"""Set LUT on a node in the color graph.
|
|
30
31
|
|
|
@@ -83,6 +84,7 @@ def graph_get_lut(node_index: int, item_index: int = 0, track_type: str = "video
|
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
@mcp.tool()
|
|
87
|
+
@granular_destructive_op()
|
|
86
88
|
def graph_set_node_cache_mode(node_index: int, cache_value: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
87
89
|
"""Set the cache mode on a node.
|
|
88
90
|
|
|
@@ -165,6 +167,7 @@ def graph_get_tools_in_node(node_index: int, item_index: int = 0, track_type: st
|
|
|
165
167
|
|
|
166
168
|
|
|
167
169
|
@mcp.tool()
|
|
170
|
+
@granular_destructive_op()
|
|
168
171
|
def graph_set_node_enabled(node_index: int, is_enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
169
172
|
"""Enable or disable a node.
|
|
170
173
|
|
|
@@ -226,6 +229,7 @@ def graph_apply_arri_cdl_lut(item_index: int = 0, track_type: str = "video", tra
|
|
|
226
229
|
|
|
227
230
|
|
|
228
231
|
@mcp.tool()
|
|
232
|
+
@granular_destructive_op()
|
|
229
233
|
def graph_reset_all_grades(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
230
234
|
"""Reset all grades on a timeline item's graph.
|
|
231
235
|
|
|
@@ -420,6 +420,7 @@ def import_timeline_from_file(file_path: str, import_options: Optional[Dict[str,
|
|
|
420
420
|
|
|
421
421
|
|
|
422
422
|
@mcp.tool()
|
|
423
|
+
@granular_destructive_op()
|
|
423
424
|
def delete_timelines_by_id(timeline_ids: List[str]) -> Dict[str, Any]:
|
|
424
425
|
"""Delete timelines by their unique IDs.
|
|
425
426
|
|
|
@@ -441,6 +442,7 @@ def delete_timelines_by_id(timeline_ids: List[str]) -> Dict[str, Any]:
|
|
|
441
442
|
|
|
442
443
|
|
|
443
444
|
@mcp.tool()
|
|
445
|
+
@granular_destructive_op()
|
|
444
446
|
def set_current_media_pool_folder(folder_path: str) -> Dict[str, Any]:
|
|
445
447
|
"""Navigate to a specific folder in the Media Pool.
|
|
446
448
|
|
|
@@ -458,6 +460,7 @@ def set_current_media_pool_folder(folder_path: str) -> Dict[str, Any]:
|
|
|
458
460
|
|
|
459
461
|
|
|
460
462
|
@mcp.tool()
|
|
463
|
+
@granular_destructive_op()
|
|
461
464
|
def delete_media_pool_clips(clip_ids: List[str]) -> Dict[str, Any]:
|
|
462
465
|
"""Delete clips from the Media Pool by their unique IDs.
|
|
463
466
|
|
|
@@ -489,6 +492,7 @@ def import_folder_from_file(file_path: str) -> Dict[str, Any]:
|
|
|
489
492
|
|
|
490
493
|
|
|
491
494
|
@mcp.tool()
|
|
495
|
+
@granular_destructive_op()
|
|
492
496
|
def delete_media_pool_folders(folder_names: List[str]) -> Dict[str, Any]:
|
|
493
497
|
"""Delete folders from the current Media Pool location.
|
|
494
498
|
|
|
@@ -589,6 +593,7 @@ def get_timeline_matte_list(item_index: int = 0, track_type: str = "video", trac
|
|
|
589
593
|
|
|
590
594
|
|
|
591
595
|
@mcp.tool()
|
|
596
|
+
@granular_destructive_op()
|
|
592
597
|
def delete_clip_mattes(clip_id: str, matte_paths: List[str]) -> Dict[str, Any]:
|
|
593
598
|
"""Delete clip mattes from a MediaPoolItem.
|
|
594
599
|
|
|
@@ -671,6 +676,7 @@ def get_selected_clips() -> Dict[str, Any]:
|
|
|
671
676
|
|
|
672
677
|
|
|
673
678
|
@mcp.tool()
|
|
679
|
+
@granular_destructive_op()
|
|
674
680
|
def set_selected_clip(clip_id: str) -> Dict[str, Any]:
|
|
675
681
|
"""Set a clip as selected in the Media Pool.
|
|
676
682
|
|
|
@@ -80,6 +80,7 @@ def link_proxy_media(clip_name: str, proxy_file_path: str) -> str:
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
@mcp.tool()
|
|
83
|
+
@granular_destructive_op()
|
|
83
84
|
def unlink_proxy_media(clip_name: str) -> str:
|
|
84
85
|
"""Unlink proxy media from a clip.
|
|
85
86
|
|
|
@@ -117,6 +118,7 @@ def unlink_proxy_media(clip_name: str) -> str:
|
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
@mcp.tool()
|
|
121
|
+
@granular_destructive_op()
|
|
120
122
|
def replace_clip(clip_name: str, replacement_path: str) -> str:
|
|
121
123
|
"""Replace a clip with another media file.
|
|
122
124
|
|
|
@@ -209,6 +211,7 @@ def transcribe_audio(clip_name: str, use_speaker_detection: Optional[bool] = Non
|
|
|
209
211
|
|
|
210
212
|
|
|
211
213
|
@mcp.tool()
|
|
214
|
+
@granular_destructive_op()
|
|
212
215
|
def clear_transcription(clip_name: str) -> str:
|
|
213
216
|
"""Clear audio transcription for a clip.
|
|
214
217
|
|
|
@@ -267,6 +270,7 @@ def get_clip_metadata(clip_id: str, metadata_type: str = "") -> Dict[str, Any]:
|
|
|
267
270
|
|
|
268
271
|
|
|
269
272
|
@mcp.tool()
|
|
273
|
+
@granular_destructive_op()
|
|
270
274
|
def set_clip_metadata(clip_id: str, metadata: Dict[str, str]) -> Dict[str, Any]:
|
|
271
275
|
"""Set metadata on a Media Pool clip.
|
|
272
276
|
|
|
@@ -306,6 +310,7 @@ def get_clip_third_party_metadata(clip_id: str, metadata_key: str = "") -> Dict[
|
|
|
306
310
|
|
|
307
311
|
|
|
308
312
|
@mcp.tool()
|
|
313
|
+
@granular_destructive_op()
|
|
309
314
|
def set_clip_third_party_metadata(clip_id: str, metadata: Dict[str, str]) -> Dict[str, Any]:
|
|
310
315
|
"""Set third-party metadata on a clip.
|
|
311
316
|
|
|
@@ -436,6 +441,7 @@ def get_clip_marker_custom_data(clip_id: str, frame_id: int) -> Dict[str, Any]:
|
|
|
436
441
|
|
|
437
442
|
|
|
438
443
|
@mcp.tool()
|
|
444
|
+
@granular_destructive_op()
|
|
439
445
|
def delete_clip_markers_by_color(clip_id: str, color: str) -> Dict[str, Any]:
|
|
440
446
|
"""Delete all markers of a specific color on a clip.
|
|
441
447
|
|
|
@@ -454,6 +460,7 @@ def delete_clip_markers_by_color(clip_id: str, color: str) -> Dict[str, Any]:
|
|
|
454
460
|
|
|
455
461
|
|
|
456
462
|
@mcp.tool()
|
|
463
|
+
@granular_destructive_op()
|
|
457
464
|
def delete_clip_marker_at_frame(clip_id: str, frame_id: int) -> Dict[str, Any]:
|
|
458
465
|
"""Delete a marker at a specific frame on a clip.
|
|
459
466
|
|
|
@@ -472,6 +479,7 @@ def delete_clip_marker_at_frame(clip_id: str, frame_id: int) -> Dict[str, Any]:
|
|
|
472
479
|
|
|
473
480
|
|
|
474
481
|
@mcp.tool()
|
|
482
|
+
@granular_destructive_op()
|
|
475
483
|
def delete_clip_marker_by_custom_data(clip_id: str, custom_data: str) -> Dict[str, Any]:
|
|
476
484
|
"""Delete a marker by its custom data string.
|
|
477
485
|
|
|
@@ -525,6 +533,7 @@ def get_clip_flag_list(clip_id: str) -> Dict[str, Any]:
|
|
|
525
533
|
|
|
526
534
|
|
|
527
535
|
@mcp.tool()
|
|
536
|
+
@granular_destructive_op()
|
|
528
537
|
def clear_clip_flags(clip_id: str, color: str = "") -> Dict[str, Any]:
|
|
529
538
|
"""Clear flags on a clip.
|
|
530
539
|
|
|
@@ -560,6 +569,7 @@ def get_clip_color(clip_id: str) -> Dict[str, Any]:
|
|
|
560
569
|
|
|
561
570
|
|
|
562
571
|
@mcp.tool()
|
|
572
|
+
@granular_destructive_op()
|
|
563
573
|
def set_clip_color(clip_id: str, color: str) -> Dict[str, Any]:
|
|
564
574
|
"""Set the clip color of a Media Pool item.
|
|
565
575
|
|
|
@@ -578,6 +588,7 @@ def set_clip_color(clip_id: str, color: str) -> Dict[str, Any]:
|
|
|
578
588
|
|
|
579
589
|
|
|
580
590
|
@mcp.tool()
|
|
591
|
+
@granular_destructive_op()
|
|
581
592
|
def clear_clip_color(clip_id: str) -> Dict[str, Any]:
|
|
582
593
|
"""Clear the clip color of a Media Pool item.
|
|
583
594
|
|
|
@@ -595,6 +606,7 @@ def clear_clip_color(clip_id: str) -> Dict[str, Any]:
|
|
|
595
606
|
|
|
596
607
|
|
|
597
608
|
@mcp.tool()
|
|
609
|
+
@granular_destructive_op()
|
|
598
610
|
def set_clip_property(clip_id: str, property_name: str, property_value: str) -> Dict[str, Any]:
|
|
599
611
|
"""Set a property on a Media Pool clip.
|
|
600
612
|
|
|
@@ -635,6 +647,7 @@ def get_clip_property(clip_id: str, property_name: str = "") -> Dict[str, Any]:
|
|
|
635
647
|
|
|
636
648
|
|
|
637
649
|
@mcp.tool()
|
|
650
|
+
@granular_destructive_op()
|
|
638
651
|
def set_media_pool_clip_name(clip_id: str, new_name: str) -> Dict[str, Any]:
|
|
639
652
|
"""Rename a Media Pool clip.
|
|
640
653
|
|
|
@@ -695,6 +708,7 @@ def link_clip_full_resolution_media(clip_id: str, full_res_media_path: str) -> D
|
|
|
695
708
|
|
|
696
709
|
|
|
697
710
|
@mcp.tool()
|
|
711
|
+
@granular_destructive_op()
|
|
698
712
|
def unlink_clip_proxy_media(clip_id: str) -> Dict[str, Any]:
|
|
699
713
|
"""Unlink proxy media from a clip.
|
|
700
714
|
|
|
@@ -712,6 +726,7 @@ def unlink_clip_proxy_media(clip_id: str) -> Dict[str, Any]:
|
|
|
712
726
|
|
|
713
727
|
|
|
714
728
|
@mcp.tool()
|
|
729
|
+
@granular_destructive_op()
|
|
715
730
|
def replace_media_pool_clip(clip_id: str, new_file_path: str) -> Dict[str, Any]:
|
|
716
731
|
"""Replace a clip with a new media file.
|
|
717
732
|
|
|
@@ -737,6 +752,7 @@ def replace_media_pool_clip(clip_id: str, new_file_path: str) -> Dict[str, Any]:
|
|
|
737
752
|
|
|
738
753
|
|
|
739
754
|
@mcp.tool()
|
|
755
|
+
@granular_destructive_op()
|
|
740
756
|
def replace_media_pool_clip_preserve_sub_clip(clip_id: str, file_path: str) -> Dict[str, Any]:
|
|
741
757
|
"""Replace a clip's underlying media while preserving subclip extents.
|
|
742
758
|
|
|
@@ -825,6 +841,7 @@ def transcribe_clip_audio(clip_id: str) -> Dict[str, Any]:
|
|
|
825
841
|
|
|
826
842
|
|
|
827
843
|
@mcp.tool()
|
|
844
|
+
@granular_destructive_op()
|
|
828
845
|
def clear_clip_transcription(clip_id: str) -> Dict[str, Any]:
|
|
829
846
|
"""Clear transcription for a specific clip.
|
|
830
847
|
|
|
@@ -876,6 +893,7 @@ def get_clip_mark_in_out(clip_id: str) -> Dict[str, Any]:
|
|
|
876
893
|
|
|
877
894
|
|
|
878
895
|
@mcp.tool()
|
|
896
|
+
@granular_destructive_op()
|
|
879
897
|
def set_clip_mark_in_out(clip_id: str, mark_in: int, mark_out: int) -> Dict[str, Any]:
|
|
880
898
|
"""Set mark in/out points for a clip.
|
|
881
899
|
|
|
@@ -895,6 +913,7 @@ def set_clip_mark_in_out(clip_id: str, mark_in: int, mark_out: int) -> Dict[str,
|
|
|
895
913
|
|
|
896
914
|
|
|
897
915
|
@mcp.tool()
|
|
916
|
+
@granular_destructive_op()
|
|
898
917
|
def clear_clip_mark_in_out(clip_id: str) -> Dict[str, Any]:
|
|
899
918
|
"""Clear mark in/out points for a clip.
|
|
900
919
|
|
|
@@ -937,6 +956,7 @@ def perform_clip_audio_classification(clip_id: str) -> Dict[str, Any]:
|
|
|
937
956
|
|
|
938
957
|
|
|
939
958
|
@mcp.tool()
|
|
959
|
+
@granular_destructive_op()
|
|
940
960
|
def clear_clip_audio_classification(clip_id: str) -> Dict[str, Any]:
|
|
941
961
|
"""Clear a clip's audio classification (Resolve 21+).
|
|
942
962
|
|
|
@@ -1000,6 +1020,7 @@ def analyze_clip_for_slate(clip_id: str, marker_color: str = "Blue") -> Dict[str
|
|
|
1000
1020
|
|
|
1001
1021
|
|
|
1002
1022
|
@mcp.tool()
|
|
1023
|
+
@granular_destructive_op()
|
|
1003
1024
|
def remove_clip_motion_blur(clip_id: str, deblur_option: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
1004
1025
|
"""Render a motion-deblurred copy of a clip (Resolve 21+).
|
|
1005
1026
|
|