davinci-resolve-mcp 4.4.1 → 4.5.0

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,140 @@
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.0 — safe mode and the audit log reach the granular server
6
+
7
+ v4.4.1 froze 131 destructive-hinted granular tools in a backlog and said plainly
8
+ that nothing enforced anything about them: no safe-mode refusal, no audit row. A
9
+ user running with `destructive.safe_mode` on was protected on the compound server
10
+ and not on the `--full` one, with nothing saying so. This release works that
11
+ backlog to zero.
12
+
13
+ ### Added
14
+
15
+ - **`@granular_destructive_op()` on every destructive-hinted granular tool** — the
16
+ 131 in the backlog plus `ti_copy_grades`, 132 in all. The hook does two things
17
+ and only two: while `destructive.safe_mode` is on, a HIGH-risk call is refused
18
+ unless that call passes `allow_risky_operation: true`; and every call, refused
19
+ or run, writes a row to the security audit log. A dict result is annotated with
20
+ `operation_id` and `security` exactly as compound results are; a list, string or
21
+ boolean result comes back untouched, because several granular tools return
22
+ those.
23
+ - **`allow_risky_operation` is now a parameter on each hooked tool.** Granular
24
+ tools have no `params` object for the compound override to live in, so the hook
25
+ adds the parameter to the tool's own MCP schema (via `__signature__`, which
26
+ FastMCP honours). Every other property of every schema is unchanged — a test
27
+ diffs each hooked tool's advertised properties against its original signature.
28
+ - **Risk is rated from the verb, with one ledger override.** `delete`, `remove`,
29
+ `clear`, `reset`, `replace`, `unlink`, `overwrite`, `quit` and `restart` are
30
+ HIGH; `set`, `load`, `switch`, `close`, `stop` and `lift` are MEDIUM; anything
31
+ else is MEDIUM, never HIGH, so an unassessed verb cannot make safe mode
32
+ over-block. A tool whose body reaches a symbol the `api_truth` ledger marks
33
+ `destroys_prior_work` takes HIGH from the ledger instead: `ti_copy_grades` rates
34
+ MEDIUM by verb and HIGH in fact, mechanically, because `CopyGrades` is in the
35
+ ledger. Flagging a new ledger entry re-rates every tool that reaches it.
36
+ - **The ratchet now counts the hook, and keeps the tiers apart.**
37
+ `UNGATED_GRANULAR_DESTRUCTIVE` is empty; a new destructive-hinted tool without
38
+ the hook fails the suite, and so does a hook placed *outside* `@mcp.tool()`
39
+ (that order registers the bare function — decorated, and enforcing nothing).
40
+ The `destroys_prior_work` test still demands the full `acknowledge_trap` +
41
+ confirm-token gate; the enforcement hook does not satisfy it and must not.
42
+ Enforcement and confirmation are separate tiers — a two-step confirmation on
43
+ `ti_set_clip_color` would make the granular server unusable.
44
+ - **`tests/test_granular_destructive_op.py`** — refusal, override, audit rows for
45
+ allowed and blocked calls, an unwritable audit path that cannot break the call,
46
+ list/scalar passthrough, positional arguments audited by name, and the override
47
+ travelling end-to-end through FastMCP's `call_tool`.
48
+
49
+ ### Not added, on purpose
50
+
51
+ - **No archive.** The compound hook duplicates the timeline into an Archive bin
52
+ before mutating it. Doing that around 132 granular calls would bury a project in
53
+ versions for operations as small as a clip-colour change. A granular write
54
+ therefore has **no recovery version**: it is refused, or it is recorded — never
55
+ recovered. `docs/SKILL.md` and the README now say so rather than implying parity.
56
+
57
+ ### Fixed
58
+
59
+ - **The first draft of the hook was cosmetic.** It rated verbs `"HIGH"` while the
60
+ safe-mode gate holds `RiskLevel.HIGH.value == "high"`; nothing matched, and a
61
+ HIGH tool ran with safe mode on. Reproduced with a probe before the fix, pinned
62
+ by a vocabulary test that asserts every rating is a `RiskLevel` value, and by a
63
+ refusal test that asserts the body never ran.
64
+ - **The refusal names the right argument.** On the granular server the override
65
+ is `allow_risky_operation=true` on the call, not `params.allow_risky_operation`;
66
+ the message and remediation say which.
67
+
68
+ ### Validation
69
+
70
+ - Static checks: API parity audit, api-limitations, read/write symmetry,
71
+ agent-rules, release-surface drift, `git diff --check`.
72
+ - Full offline suite: 3679 passed, 1 skipped, 0 failed, 1412 subtests (v4.4.2 baseline plus the new guards; no count drop).
73
+ - Every new guard was made to fail before it was trusted — nine regressions were
74
+ re-introduced one at a time and restored from a byte copy: hook removed (fails),
75
+ hook outside `@mcp.tool` (fails), bare `@granular_destructive_op` without
76
+ parentheses (the granular package no longer imports — pydantic cannot build a
77
+ schema for the decorator factory), `"HIGH"` casing (3 tests fail), a list result
78
+ mutated (fails), the ledger override removed (2 fail), the confirm-token
79
+ redemption dropped from `ti_copy_grades` with the hook still present (the
80
+ confirmation-tier test fails on its own), `__signature__` not set (133 fail — the
81
+ override never reaches the hook), and an audit write error re-raised (fails).
82
+ - Live, DaVinci Resolve Studio 19.1.3.7, on a disposable project: with safe mode
83
+ on, `set_project_setting` (MEDIUM) ran, returned its plain string untouched and
84
+ was audited `allowed`; `delete_project` (HIGH) was refused with
85
+ `SAFE_MODE_BLOCKED` and audited `blocked`; the same call with
86
+ `allow_risky_operation=true` reached the body and was audited `allowed`. Resolve's
87
+ own `DeleteProject` returned False for the just-created project, which was also
88
+ absent from the folder listing before and after — an artifact of an unsaved new
89
+ project on a PostgreSQL database, not hook behaviour. No project was left behind.
90
+
91
+ ## What's New in v4.4.2 — a refused option now says which one, and why
92
+
93
+ Reported as [#232](https://github.com/samuelgursky/davinci-resolve-mcp/issues/232):
94
+ `timeline.normalize_audio_level` "rejects every documented option schema". It does
95
+ not, and a test now pins all seven documented `NormalizeAudioOptions` shapes reaching
96
+ the native call. The defect was the refusal itself.
97
+
98
+ ### Fixed
99
+
100
+ - **One error message covered two unrelated failures.** `Unknown normalization
101
+ options or non-dictionary options` named neither the offending key nor the type
102
+ actually received, and listed nothing that *would* have been accepted — so a typo
103
+ and a malformed payload were indistinguishable, to the caller and to the bug
104
+ report. The only way to produce that message while passing documented keys is an
105
+ `options` that arrived as a **JSON string**, which some MCP clients produce when
106
+ they serialise a nested object. That caller is looking at a payload that appears
107
+ correct, so the refusal now says so in as many words:
108
+
109
+ ```
110
+ normalization options must be an object with any of normalizationMode,
111
+ targetLevel, targetLoudness, setLevelMode; received a string. It looks like a
112
+ JSON string — send options as a nested object, not as encoded text.
113
+ ```
114
+
115
+ An unknown key reads differently, because the cause and the fix are different:
116
+
117
+ ```
118
+ Unknown normalization option 'normalisationMode'; accepted keys are
119
+ normalizationMode, targetLevel, targetLoudness, setLevelMode.
120
+ ```
121
+
122
+ - **`auto_align_clips` carried the identical conflation** and now shares the same
123
+ builder, `src/utils/option_errors.py`.
124
+
125
+ ### Validation
126
+
127
+ - Full offline suite: **3,655 passed, 1 skipped, 0 failed**, 1,276 subtests.
128
+ - **No behaviour change to accepted input.** The same options are accepted and reach
129
+ the same native call; a test asserts each of the seven documented shapes arrives at
130
+ `NormalizeAudioLevel`, and that a JSON-string payload is refused *without* reaching
131
+ it. No Resolve live run: nothing about the native call changed.
132
+
133
+ ### Still unconfirmed
134
+
135
+ The reporter has not replied, so the JSON-string diagnosis remains the most likely
136
+ cause rather than a measured one. If their payload was something else, the new
137
+ message will now say what — which is the actual fix here.
138
+
5
139
  ## What's New in v4.4.1 — the safety ratchet stops scanning only half the project
6
140
 
7
141
  The write-enforcement ratchet read `src/server.py` and nothing else. The granular
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.4.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.5.0-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(387%20full)-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
- [![Version](https://img.shields.io/badge/version-4.4.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.5.0-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(387%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.4.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.5.0 版 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 客户端的安全提示(只读、破坏性、幂等、外部资源操作)。操作边界、确认指引和漏洞报告见 [安全策略](SECURITY.md)。
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.4.1"
40
+ VERSION = "4.5.0"
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.4.1",
3
+ "version": "4.5.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -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.4.1"
96
+ VERSION = "4.5.0"
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()}")
@@ -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
 
@@ -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
 
@@ -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