davinci-resolve-mcp 2.98.7 → 2.99.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,136 @@
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 v2.99.0
6
+
7
+ **`timeline.ripple_insert`, and a verified-delete gate on `move_clips`.**
8
+ Contributed in [#156](https://github.com/samuelgursky/davinci-resolve-mcp/pull/156)
9
+ by @handst97, driven by a real data-loss incident: a session used `move_clips`
10
+ with a record offset smaller than the item duration to "open a gap",
11
+ `AppendToTimeline` returned items whose ids could not be read, the code counted
12
+ them as successful duplicates, and the delete phase removed 26 source clips.
13
+
14
+ ### Added
15
+
16
+ - **`timeline.ripple_insert`** — insert media-pool source ranges at a record
17
+ point and shift all later video/audio items right. There is no ripple-insert
18
+ primitive in the scripting API, and duplicate-then-delete corrupts the timeline
19
+ when the shift is smaller than an item. This plans a rebuild instead: capture
20
+ every tail item's pool media and source trim, delete the tail (verified),
21
+ re-append it shifted, then place the inserts into the opened gap — **tail
22
+ first, so the worst mid-failure state is a gap, never lost content**. Dry-run
23
+ by default, with straddler / blocker / locked-track / subtitle detection;
24
+ executing is confirm-token gated and archives the timeline first. Shifted items
25
+ are re-created from pool media with transform/crop/composite/retime re-applied;
26
+ grades, keyframes, transitions and link state are NOT preserved (the archive
27
+ keeps them).
28
+
29
+ ### Fixed
30
+
31
+ - **`move_clips` no longer deletes a source it could not verify.** Each duplicate
32
+ now carries `duplicate_verified` — a live item or a real id recovered from the
33
+ timeline — and sources are deleted only when every duplicate, primary and
34
+ linked, verified. Null-id appends keep the source with an explicit warning.
35
+ - **`safe_set_cdl` / `apply_look_to_items` preflight the node.** `NodeIndex` is
36
+ 1-based and there is no `GetCDL`, so a bare `False` was undiagnosable; the node
37
+ count is now read before `SetCDL` and a failure comes back with a structured
38
+ reason and a clip-type-aware diagnosis.
39
+ - **Magic Mask reports the human step instead of a bare false.** Magic Mask v2
40
+ isolates via operator clicks and the API cannot place them, so `CreateMagicMask`
41
+ on a fresh item can only return `False`. It now returns `needs_hitl` with the
42
+ exact Color-page steps, and the mode aliases (`'Forward'` → `'F'`) are
43
+ normalized — the granular `ti_create_magic_mask` defaulted to a spelling
44
+ Resolve rejects.
45
+
46
+ ### Fixed in review
47
+
48
+ Three defects found reviewing #156, each with a test that fails without the fix:
49
+
50
+ - **A dry-run plan archived a timeline version.** `ripple_insert` is the only
51
+ destructive action whose *default* call mutates nothing, and the pending-confirm
52
+ skip only fires while the confirm-token preference is on — so with it off,
53
+ routine planning calls littered the version chain.
54
+ - **Asymmetric per-track insert durations left an unreported gap.** Every track
55
+ shifts by the longest inserted run, so a shorter insert leaves a hole the
56
+ readback structurally cannot see — it only checks the positions it placed. The
57
+ plan and the result now carry `gap_frames_by_track` and a warning.
58
+ - **Subtitle straddlers passed the feasibility check.** Video and audio
59
+ straddlers already refuse the plan; a subtitle across the insert point stayed
60
+ put while the picture under it moved.
61
+
62
+ ### Live validation
63
+
64
+ Verified on Studio 19.1.3.7 via `tests/live_ripple_insert_validation.py`:
65
+ dry-run plan exact (insert@86448, shift 24, tail 2, no straddlers), confirm-token
66
+ round-trip, post-insert layout `[(86400,48),(86448,24),(86472,48),(86520,48)]`,
67
+ `readback.missing=[]`, and ZoomX/Y=0.5 surviving the shift with
68
+ `property_restore_failures=0`.
69
+
70
+ ### A note on the Fusion measurement in #156
71
+
72
+ The PR proposed recording that whether an API-created Fusion comp renders is
73
+ Resolve-version-dependent, from a 21.0.4 run where a wired comp delivered a render
74
+ bit-identical to the baseline. That reading does not survive: those comps were
75
+ built and set through this server, and the v2.98.5 comp-lock bug produced exactly
76
+ that symptom on any build. Running the PR's own phase-2 harness unchanged against
77
+ the fixed code now reports **RENDERED** (Blur 24.38 dB, Transform 7.95 dB) where
78
+ it previously reported IGNORED. The `AddFusionComp` entry records the 21.0.4
79
+ observation as independent evidence that the lock bug is not specific to
80
+ 19.1.3.7 — the one thing this machine cannot test.
81
+
82
+ ## What's New in v2.98.8
83
+
84
+ **The comp-lock mechanism, settled — and the v2.98.6 scope correction was itself
85
+ wrong.** Chasing why `add_fusion_mask` and `set_text_plus` escaped the bug found
86
+ that they don't. Their test cases were priming the comp and could not have
87
+ failed.
88
+
89
+ ### The mechanism
90
+
91
+ | | |
92
+ | --- | --- |
93
+ | **Precondition** | The comp's graph was built through lock-wrapped `AddTool`/`ConnectInput`. The same locked write against a graph wired by plain attribute assignment renders normally. |
94
+ | **Trigger** | The locked write is the **first value write to that comp** since the build. |
95
+ | **Primes it away** | **Any** unlocked value write anywhere in the comp — even writing a *default* value to an *unrelated* tool. Also `StartUndo`/`EndUndo` around the write. |
96
+ | **Does not** | A structural `ConnectInput` inside the same lock; a `GetInput` readback after `Unlock`. |
97
+
98
+ Priming is why every raw-API attempt to reproduce the bug kept coming back green,
99
+ and why a control that was supposed to fail didn't.
100
+
101
+ ### The corrected scope
102
+
103
+ | path | locked write |
104
+ | --- | --- |
105
+ | `set_input` | **suppressed** |
106
+ | `safe_set_inputs` | **suppressed** |
107
+ | `set_text_plus` | **suppressed** |
108
+ | `add_fusion_mask` | **suppressed** |
109
+ | `bulk_set_inputs` | escapes — wrapped in `StartUndo`/`EndUndo` |
110
+ | `bulk_set_expressions` | escapes — wrapped in `StartUndo`/`EndUndo` |
111
+
112
+ So **four of six** paths were genuinely broken, not two. v2.98.5's original
113
+ "all six were the same bug" was closer to right than v2.98.6's correction of it;
114
+ the two real escapes are explained by their undo wrapper.
115
+
116
+ ### Fixed in the tests
117
+
118
+ `tests/live_fusion_value_write_validation.py` had two cases that could not fail.
119
+ Both set up their graph with plain `SetInput` calls — a text `Size`, a blur
120
+ amount — before the call under test, which primed the comp. Removing that:
121
+
122
+ - the `set_text_plus` case now builds its rooted graph and writes **nothing**
123
+ before the call, leaving the Text+ at its default size;
124
+ - the `add_fusion_mask` case composites a **Background** rather than making a
125
+ Blur visible, because a Background is visible at its defaults and needs no
126
+ setup write.
127
+
128
+ Both now report `PSNR inf -> IGNORED at render` with the lock reintroduced, and
129
+ 13–17 dB APPLIED without it. The header carries a standing rule: a case in that
130
+ file must perform no value write of any kind before the call it is testing.
131
+
132
+ No production code changed. The v2.98.5 fix has been correct throughout; this is
133
+ the third and final correction to the *description* of what it fixed.
134
+
5
135
  ## What's New in v2.98.7
6
136
 
7
137
  **Identifying the mechanism behind the Fusion comp-lock bug — and correcting the
@@ -61,6 +191,11 @@ it. What changed is the explanation, in `api_truth` and the harness docs.
61
191
 
62
192
  ## What's New in v2.98.6
63
193
 
194
+ > **Corrected in v2.98.8.** The table below says four of six paths escape the
195
+ > bug. Two of those four — `set_text_plus` and `add_fusion_mask` — do not; their
196
+ > test cases primed the comp with setup writes and could not fail. Only
197
+ > `bulk_set_inputs` and `bulk_set_expressions` genuinely escape.
198
+
64
199
  **Correcting the scope of the v2.98.5 Fusion fix, and covering all six paths
65
200
  with a render.** v2.98.5 removed a `Comp.Lock()` from six Fusion value writes and
66
201
  described all six as the same bug. Only two of them were proven with a render at
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-2.98.7-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.99.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-35%20(353%20full)-blue.svg)](#server-modes)
@@ -211,7 +211,7 @@ The open-source servers are complete and fully functional on their own.
211
211
  | App and project control | Launch/reconnect, page switching, project CRUD, project folders, databases, cloud project wrappers, settings, presets, archives |
212
212
  | Media pool and ingest | Safe import, image sequences, multicam prep timelines, bin organization, metadata normalization, metadata field inventory, marks, annotations, relink/proxy/full-resolution guards |
213
213
  | Media analysis | Source-safe file/clip/bin/project analysis, 2-pop/slate-clap sync-event detection, default Resolve metadata and Media Pool marker writeback, persisted analysis artifacts, existing-report reuse, host_chat_paths visual analysis (finalized per clip with `commit_vision`, works with any vision-capable MCP client) with opt-out, transcription with opt-out |
214
- | Timeline editing and conform | Track/item probing, title text key scans/writes, copy/move/duplicate helpers, range operations, gaps/overlaps, source ranges, checked interchange exports/imports |
214
+ | Timeline editing and conform | Track/item probing, title text key scans/writes, copy/move/duplicate helpers, ripple insert, range operations, gaps/overlaps, source ranges, checked interchange exports/imports |
215
215
  | Review annotations | Timeline/item/clip markers, custom data, flags, clip color, copy/move/sync cleanup, review reports, marker thumbnail review |
216
216
  | Color and grading | Node graph probing, CDL validation, grade copy, DRX/LUT helpers, versions, Gallery stills, color groups |
217
217
  | Fusion | Timeline-item comps, safe tool creation, input writes, port inspection, validated connections, scoped bulk writes |
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-2.98.7-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.99.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-35%20(353%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
- > 本翻译对应 v2.98.7 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.99.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -142,7 +142,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
142
142
  | 应用与项目控制 | 启动/重连、页面切换、项目增删改查、项目文件夹、数据库、云项目封装、设置、预设、归档 |
143
143
  | 媒体池与摄入 | 安全导入、图像序列、多机位预备时间线、bin 整理、元数据规范化、元数据字段清单、标记、注释、重链/代理/全分辨率护栏 |
144
144
  | 媒体分析 | 源媒体安全的文件/片段/bin/项目分析,2-pop/场记板同步事件检测,默认写回 Resolve 元数据与媒体池标记,持久化分析产物,复用既有报告,host_chat_paths 视觉分析(每个片段以 `commit_vision` 定稿,任何具备视觉能力的 MCP 客户端都可用)可选退出,转写可选退出 |
145
- | 时间线剪辑与套底 | 轨道/条目探测、标题文字键扫描/写入、复制/移动/克隆助手、区间操作、缝隙/重叠、源范围、带检查的交换格式导入导出 |
145
+ | 时间线剪辑与套底 | 轨道/条目探测、标题文字键扫描/写入、复制/移动/克隆助手、波纹插入、区间操作、缝隙/重叠、源范围、带检查的交换格式导入导出 |
146
146
  | 审阅注释 | 时间线/条目/片段标记、自定义数据、旗标、片段颜色、复制/移动/同步清理、审阅报告、标记缩略图审阅 |
147
147
  | 调色 | 节点图探测、CDL 校验、调色复制、DRX/LUT 助手、版本、Gallery 静帧、调色组 |
148
148
  | Fusion | 时间线条目合成、安全工具创建、输入写入、端口检查、带校验的连接、限定范围的批量写入 |
package/docs/SKILL.md CHANGED
@@ -444,7 +444,11 @@ same timecodes, then restore the previous active version or node-enabled state
444
444
  after any temporary bypass capture. Treat untreated frames as diagnostic
445
445
  evidence, not as permission to discard an existing creative grade.
446
446
 
447
- Prefer `safe_set_cdl` for small reversible primary corrections. Use DRX/stills
447
+ Prefer `safe_set_cdl` for small reversible primary corrections. `SetCDL`'s
448
+ `NodeIndex` is 1-BASED (scripting README line 6) and there is no `GetCDL`
449
+ readback — `safe_set_cdl` and `apply_look_to_items` now read the node graph's
450
+ `GetNumNodes` first and return a structured reason/diagnosis on a false
451
+ `SetCDL` instead of a bare boolean. Use DRX/stills
448
452
  or grade copy only when the user accepts whole-grade replacement/transfer
449
453
  semantics. Use DCTL/LUT authoring only for reusable mathematical transforms, not
450
454
  as a substitute for hand-built windows, qualifiers, or tracked secondaries. Do
@@ -1377,7 +1381,20 @@ Key actions:
1377
1381
  unsupported because Resolve's public scripting API does not expose transition
1378
1382
  cloning. `copy_keyframes=True` adds the `keyframes` group.
1379
1383
  - `copy_clips(...)` / `move_clips(...)` — same safe append path; `move_clips`
1380
- deletes successfully duplicated source items afterward
1384
+ deletes only sources whose duplicate was VERIFIED live on the timeline
1385
+ (AppendToTimeline can return null-id items — e.g. into an occupied span — and
1386
+ unverified sources are kept with a warning; see api_truth
1387
+ 'AppendToTimeline null-id'). NEVER use `move_clips` to open a gap for an
1388
+ insert; that is `ripple_insert`'s job.
1389
+ - `ripple_insert(clip_infos, record_frame|record_timecode, record_frame_mode?,
1390
+ dry_run?, confirm_token?)` — insert media-pool source ranges at a record point
1391
+ and shift ALL later video/audio items right. DRY-RUN by default (full plan
1392
+ with straddler/blocker detection); executing is confirm-token gated and
1393
+ archives the timeline first. Shifted items are re-created from pool media
1394
+ with transform/crop/composite/retime re-applied; grades, keyframes,
1395
+ transitions, and link state on shifted items are NOT preserved (the archive
1396
+ keeps them). Refuses mid-item insert points, non-pool items in the tail
1397
+ (titles/generators/Fusion comps), subtitle shifts, and locked tracks.
1381
1398
  - `copy_range` / `duplicate_range` — copy exact video/audio source segments
1382
1399
  from `start_frame`/`end_frame` or mark in/out to `record_frame`
1383
1400
  - `overwrite_range` — delete whole destination overlaps, then copy the exact
@@ -1403,7 +1420,11 @@ Key actions:
1403
1420
  page and only while it is frontmost; the tool switches page automatically and
1404
1421
  restores the previous one. Expect a page flash in the GUI,
1405
1422
  and note that landing on Color can kick off cache/render work for the current
1406
- clip — on a large timeline the switch is not free
1423
+ clip — on a large timeline the switch is not free.
1424
+ NOT WYSIWYG for Fusion: thumbnails do not reflect Fusion composition output
1425
+ (a warp demo read as identical before/after from a contact sheet,
1426
+ 2026-08-19). Prove Fusion/grade claims with `gallery_stills grab_and_export`
1427
+ or an extracted RENDERED frame, never a thumbnail
1407
1428
  - `edit_kernel_capabilities` — report supported, partially supported, and
1408
1429
  unsupported timeline edit kernel behavior
1409
1430
  - `probe_edit_kernel_item(clip_ids? selected? timeline_item?)` — read-only
@@ -1602,7 +1623,12 @@ Key actions:
1602
1623
  clip version
1603
1624
  - `stabilize`, `smart_reframe`
1604
1625
  - `create_magic_mask(mode)` — mode: `"F"` forward, `"B"` backward, `"BI"` bidirectional
1605
- (requires DaVinci Neural Engine and Color page)
1626
+ (requires DaVinci Neural Engine and Color page). Magic Mask v2 isolates via
1627
+ operator CLICKS on the subject (manual ch. 139; strokes are legacy v1) and
1628
+ the API cannot place clicks — with none present this returns
1629
+ `{needs_hitl: true, hitl: {steps...}}` instead of a bare false. Never call it
1630
+ as if it isolates a subject unattended; prove any isolation with a rendered
1631
+ frame (`gallery_stills grab_and_export`).
1606
1632
 
1607
1633
  Color / Grade kernel actions (v2.11.0+) add safer grade inspection and
1608
1634
  boundary helpers: `grade_capabilities`, `probe_grade_item`,
@@ -1691,13 +1717,13 @@ Target a comp either from a timeline item (pass `clip_id`, `timeline_item_id`, o
1691
1717
  page comp (omit timeline scope).
1692
1718
 
1693
1719
  READBACK IS NOT PROOF FOR FUSION PARAMETERS. Up to v2.98.4 every value write
1694
- here ran inside a `Comp.Lock()`. For `set_input` and `safe_set_inputs` that was
1695
- load-bearing: the value is stored in the graph and returned by `get_input` while
1696
- the RENDER ignores it entirely (Studio 19.1.3.7: PSNR inf vs the no-comp
1697
- baselinethe delivered file was bit-identical to no comp at all). The other
1698
- four locked paths did not reproduce it, so the blast radius was narrower than
1699
- first reported; the locks came off all six regardless. Fixed in v2.98.5
1700
- (measurement corrected in v2.98.6), and guarded by
1720
+ here ran inside a `Comp.Lock()`, and a value written under a comp lock is stored
1721
+ in the graph and returned by `get_input` while the RENDER ignores it entirely
1722
+ (Studio 19.1.3.7: PSNR inf vs the no-comp baseline). Four of the six affected
1723
+ paths`set_input`, `safe_set_inputs`, `set_text_plus`, `add_fusion_mask` were
1724
+ confirmed broken by rendering; `bulk_set_inputs` and `bulk_set_expressions`
1725
+ escape because they wrap their write in `StartUndo`/`EndUndo`. Fixed in v2.98.5,
1726
+ mechanism settled in v2.98.8, and guarded by
1701
1727
  `tests/test_fusion_value_write_lock.py` plus the rendered-frame harness
1702
1728
  `tests/live_fusion_value_write_validation.py`. The lesson outlives the bug: a
1703
1729
  Fusion parameter that reads back correctly has proven nothing about the output,
@@ -2118,6 +2144,18 @@ The server provides several mechanisms to inspect a frame as Resolve has process
2118
2144
  it, including color grading, effects, and compositing — not just the raw source
2119
2145
  file.
2120
2146
 
2147
+ WYSIWYG hierarchy (live-verified 2026-08-20): a `grab_and_export` gallery still
2148
+ faithfully reflects edit sizing (Inspector transforms) and grades; media-pool
2149
+ thumbnails and `thumbnail_contact_sheet` output do NOT reflect Fusion
2150
+ composition output. Also note that whether an API-created Fusion comp is
2151
+ honoured at render is Resolve-version-dependent: a wired comp rendered on
2152
+ Studio 19.1.3.7, but on Studio 21.0.4 the same Blur configuration and a
2153
+ Transform variant both rendered bit-identical to the no-comp baseline, and no
2154
+ API selects an item's active composition (api_truth
2155
+ 'AddFusionComp'). The only acceptable proof of a Fusion or grade claim is a
2156
+ rendered frame: `grab_and_export`, an exported gallery still, or a frame
2157
+ extracted from a delivered render.
2158
+
2121
2159
  **Start here: `timeline_frame(action="capture")`** — Returns the frame at the
2122
2160
  playhead (or at any `timecode`/`frame` you name) as MCP image content, so a
2123
2161
  multimodal assistant can simply look at it. It renders that one frame, which is
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 27 missing capabilities, 36 bugs / unreliable behaviors.
15
+ **Totals:** 29 missing capabilities, 37 bugs / unreliable behaviors.
16
16
 
17
17
  The authoritative source is the runtime-queryable `api_truth` ledger
18
18
  (`resolve_control api_truth "<query>"`); this document is generated from
@@ -239,6 +239,22 @@ equivalent, blocking full automation.
239
239
  - **Workaround / current handling:** Check GetRenderCodecs(format) first; when it is empty, treat the format as unreachable through this API rather than guessing a codec value. Render audio-only via ExportVideo=False on a format that does expose codecs, or drive it from a saved render preset.
240
240
  - **Tags:** render, deliver, audio, unsupported
241
241
 
242
+ ### TimelineItem.CreateMagicMask (needs operator clicks)
243
+
244
+ - **Object:** `TimelineItem`
245
+ - **Signature:** `(mode) -> bool`
246
+ - **Behavior:** Returns False when the item carries no Magic Mask clicks. Magic Mask v2 is click-driven (manual ch. 139; strokes are the legacy v1 interface) and the scripting API has no way to place a click, so on a fresh item the call can never isolate anything — it only tracks a mask the operator already seeded. Mode strings are 'F', 'B', 'BI'; long spellings like 'Forward' are rejected.
247
+ - **Workaround / current handling:** Treat CreateMagicMask as track-only: have the operator click the subject (Color page > Magic Mask palette) and then call it, or surface the HITL steps instead of a bare False. Verify isolation with a rendered frame (gallery_stills grab_and_export), never a thumbnail.
248
+ - **Tags:** ai, magic-mask, hitl, silent-failure
249
+
250
+ ### TimelineItem.SetCDL (write-only, no GetCDL)
251
+
252
+ - **Object:** `TimelineItem`
253
+ - **Signature:** `({NodeIndex, Slope, Offset, Power, Saturation}) -> bool`
254
+ - **Behavior:** There is no GetCDL, so applied CDL values cannot be read back; the bool is the only signal and it returns False with no reason (missing node, still/generator item, values silently rejected). NodeIndex is 1-based (README line 6) and must not exceed Graph.GetNumNodes() — note TimelineItem.GetNumNodes is deprecated; the count lives on item.GetNodeGraph().
255
+ - **Workaround / current handling:** Read item.GetNodeGraph().GetNumNodes() before SetCDL and diagnose a False against the node count and clip type. Prove the applied look with a rendered frame (gallery_stills grab_and_export or Project.ExportCurrentFrameAsStill), not the return value.
256
+ - **Tags:** color, cdl, readback, silent-failure
257
+
242
258
  ## Bugs / Unreliable Behavior (please fix)
243
259
 
244
260
  Methods that exist but misbehave — silent failures, unreliable return
@@ -343,16 +359,16 @@ values, or automation-hostile modal prompts.
343
359
 
344
360
  - **Object:** `Composition (Fusion, via TimelineItem comps)`
345
361
  - **Signature:** `Lock() / Unlock()`
346
- - **Behavior:** A numeric tool.SetInput() performed between Comp.Lock() and Comp.Unlock(), when that write is the only thing the call does, is stored in the graph and reads back correctly from GetInput() but is NOT applied when the timeline is rendered. Measured live on Studio 19.1.3.7 (2026-08-21) with MediaIn -> Blur(XBlurSize 20) -> MediaOut on a media-backed clip: written under the lock the delivered H.264 render is bit-identical to the no-comp baseline (ffmpeg PSNR inf); the identical write with the lock removed renders at PSNR 24.38 dB and the file shrinks 2.0 MB -> 727 KB, as a blur should. The variable was isolated against the comp handle (AddFusionComp, GetFusionCompByIndex and GetFusionCompByName all render), the node name, and the write form (attribute assignment and SetInput both render unlocked). STRUCTURAL edits are unaffected: AddTool and ConnectInput inside a lock render normally, so the lock is not broadly unsafe — it suppresses the parameter-change invalidation that a value write depends on. Lock() is widely recommended for batching Fusion edits, which is how this reaches production code. PRECONDITION (2026-08-22): the suppression only reproduces on a graph that was BUILT through lock-wrapped AddTool/ConnectInput calls. The same locked write against a graph wired by plain attribute assignment renders normally, so 'a lock around a value write' is necessary but not sufficient the comp has to have been through prior lock cycles. RESCUERS, each verified by rendering: Comp.EndUndo() after the locked write restores it, and so does any subsequent UNLOCKED value write. NOT rescuers: a structural ConnectInput inside the same lock, and a GetInput readback after the Unlock both still render the stale frame. This explains why undo-wrapped bulk writes escape the bug; two further call shapes (a mask whose inputs are written right after AddTool, and a StyledText string write) escape it for reasons still unidentified. Keep value writes outside the lock everywhere rather than relying on an escape.
362
+ - **Behavior:** A numeric tool.SetInput() performed between Comp.Lock() and Comp.Unlock(), when that write is the only thing the call does, is stored in the graph and reads back correctly from GetInput() but is NOT applied when the timeline is rendered. Measured live on Studio 19.1.3.7 (2026-08-21) with MediaIn -> Blur(XBlurSize 20) -> MediaOut on a media-backed clip: written under the lock the delivered H.264 render is bit-identical to the no-comp baseline (ffmpeg PSNR inf); the identical write with the lock removed renders at PSNR 24.38 dB and the file shrinks 2.0 MB -> 727 KB, as a blur should. The variable was isolated against the comp handle (AddFusionComp, GetFusionCompByIndex and GetFusionCompByName all render), the node name, and the write form (attribute assignment and SetInput both render unlocked). STRUCTURAL edits are unaffected: AddTool and ConnectInput inside a lock render normally, so the lock is not broadly unsafe — it suppresses the parameter-change invalidation that a value write depends on. Lock() is widely recommended for batching Fusion edits, which is how this reaches production code. MECHANISM (settled 2026-08-22). PRECONDITION: it only reproduces on a comp whose graph was BUILT through lock-wrapped AddTool/ConnectInput; the same locked write against a graph wired by plain attribute assignment renders normally. TRIGGER: the locked write is lost when it is the FIRST value write to that comp since the build. PRIMING: any unlocked value write anywhere in the comp clears the condition permanently even writing a DEFAULT value to an unrelated tool and so does StartUndo/EndUndo around the write. A structural ConnectInput inside the same lock does NOT clear it, and neither does a GetInput readback. Priming makes false negatives easy: a test that sets anything up with a plain SetInput before the call under test will pass even with the bug present.
347
363
  - **Workaround / current handling:** Never hold a comp lock across a value write. Lock only structural work (AddTool/ConnectInput) and set inputs outside it. Because every readback the API offers agrees with the value that was written, this failure is invisible without a render — prove Fusion parameter changes with a delivered frame or gallery_stills grab_and_export, never with GetInput.
348
364
  - **Tags:** fusion, silent-failure, render, readback
349
365
 
350
366
  ### TimelineItem.AddFusionComp / LoadFusionCompByName
351
367
 
352
368
  - **Object:** `TimelineItem (media-backed clip)`
353
- - **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally. REPRODUCED 2026-08-21 on Studio 19.1.3.7: a rooted MediaIn -> Blur -> MediaOut comp built entirely through the API renders (PSNR 24.38 dB vs the no-comp baseline), so the 2026-08-02 correction stands. Note that an important share of 'the comp was ignored' readings are NOT this entry at all but the Composition.Lock bug above — a parameter written under a comp lock reads back correctly and never reaches the render, which looks identical from the API side.
354
- - **Workaround / current handling:** Wire the graph so MediaOut descends from MediaIn — that is the difference between a comp that renders and one that is silently bypassed, and it is what made this look like 'Fusion never renders from the API'. Never leave a tool unrooted: a MediaOut fed by a tool with no source failed the render job outright. For text or effects over picture, insert a Fusion title/generator as its own timeline clip and set its Text+ (fusion_comp set_text_plus), rather than attaching a comp to the media clip. Note the destination track cannot be chosen from the API (see the Track Selector entry), so overlaying onto an existing clip's track is not currently reachable end-to-end. Building the comp in the Fusion page UI works; only the API-created comp is ignored.
355
- - **Tags:** fusion, silent-failure, render
369
+ - **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally. REPRODUCED 2026-08-21 on Studio 19.1.3.7: a rooted MediaIn -> Blur -> MediaOut comp built entirely through the API renders (PSNR 24.38 dB vs the no-comp baseline), so the 2026-08-02 correction stands. Note that an important share of 'the comp was ignored' readings are NOT this entry at all but the Composition.Lock bug above — a parameter written under a comp lock reads back correctly and never reaches the render, which looks identical from the API side. That is what an independent 2026-08-20 report on Studio 21.0.4.5 was measuring: a wired MediaIn -> Blur -> MediaOut comp, and a Transform variant, both delivered renders bit-identical to the no-comp baseline (PSNR inf) when built and set through this server. It was read at the time as a version regression (19.1.3.7 honours a wired comp, 21.0.4.5 does not); with the lock bug identified, the simpler reading is that the same defect reproduces on 21.0.4.5 — and since the lock bug has only been isolated on 19.1.3.7, that report is the only evidence it is not build-specific. Treat 'a wired comp renders' as established for 19.1.3.7 and unverified elsewhere.
370
+ - **Workaround / current handling:** Wire the graph so MediaOut descends from MediaIn — that is the difference between a comp that renders and one that is silently bypassed, and it is what made this look like 'Fusion never renders from the API'. Never leave a tool unrooted: a MediaOut fed by a tool with no source failed the render job outright. For text or effects over picture, insert a Fusion title/generator as its own timeline clip and set its Text+ (fusion_comp set_text_plus), rather than attaching a comp to the media clip. Note the destination track cannot be chosen from the API (see the Track Selector entry), so overlaying onto an existing clip's track is not currently reachable end-to-end. Building the comp in the Fusion page UI works; only the API-created comp is ignored. Never claim a Fusion effect from comp readback alone — prove it with a rendered frame (gallery_stills grab_and_export before/after, or a delivered render). On setups where even the wired comp does not render (see the 2026-08-20 measurement), treat per-item Fusion effects as HITL (a human builds or activates the comp in the UI) and bake stills motion with ffmpeg when unattended output is required.
371
+ - **Tags:** fusion, silent-failure, render, readback, hitl
356
372
 
357
373
  ### Composition.Paste
358
374
 
@@ -532,3 +548,11 @@ values, or automation-hostile modal prompts.
532
548
  - **Behavior:** PANEL-dependent, not mode-dependent — and the distinction took three revisions of this entry to pin down, so the evidence is recorded rather than summarised. Across four controlled 92-probe sweeps (2 GUI, 2 headless, 2026-08-01) it returned False and wrote nothing in ALL FOUR, while Timeline.GrabStill() succeeded in all four — so it is not being handed an empty still. In one earlier GUI session it DID work, returning True and writing 2 files. The variable that differed is not the mode: it is whether the Gallery panel was visible on the Color page, which depends on the restored workspace layout and which the harness does not control. A headless session can never satisfy it, so in practice the call never works headless; a GUI session satisfies it only sometimes. Project.ExportCurrentFrameAsStill worked in all four runs in both modes.
533
549
  - **Workaround / current handling:** Do not use ExportStills unattended in either mode — a GUI session is not sufficient, only a GUI session with the Gallery panel open. Use Project.ExportCurrentFrameAsStill for pixels (verified in both modes, four for four) or drp.extract_node_graphs for grades. If ExportStills must be used, have the user open Workspace > Gallery first and verify the written files rather than trusting the return.
534
550
  - **Tags:** gallery, stills, headless, unreliable-return
551
+
552
+ ### MediaPool.AppendToTimeline (null-id timeline items)
553
+
554
+ - **Object:** `MediaPool`
555
+ - **Signature:** `([{clipInfo}, ...]) -> [TimelineItem]`
556
+ - **Behavior:** The returned TimelineItem objects can have an unreadable/empty GetUniqueId, notably when the clipInfo recordFrame lands in a span still occupied by another item (any duplicate-then-delete 'move' whose offset is smaller than the item duration hits this). The item may not actually exist on the timeline. A session that trusted the non-empty return and deleted the sources lost 26 clips (Portugal timeline, 2026-08-19).
557
+ - **Workaround / current handling:** Never treat AppendToTimeline's return as proof of placement. Re-enumerate the track and match on record frame + duration before any dependent delete; never shift items by duplicate-then-delete into occupied spans — use timeline.ripple_insert, which rebuilds the tail into free space and verifies by readback.
558
+ - **Tags:** editorial, silent-failure, unreliable-return, timeline
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.98.7"
40
+ VERSION = "2.99.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": "2.98.7",
3
+ "version": "2.99.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.98.7"
90
+ VERSION = "2.99.0"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
@@ -1926,17 +1926,34 @@ def ti_load_burn_in_preset(preset_name: str, item_index: int = 0, track_type: st
1926
1926
 
1927
1927
 
1928
1928
  @mcp.tool()
1929
- def ti_create_magic_mask(mode: str = "Forward", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1929
+ def ti_create_magic_mask(mode: str = "F", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1930
1930
  """Create a Magic Mask on a timeline item.
1931
1931
 
1932
+ Magic Mask v2 needs operator CLICKS on the subject (Color page > Magic Mask
1933
+ palette > click subject > Track Forward); the API cannot place them, so with
1934
+ no clicks present Resolve returns False and no isolation exists.
1935
+
1932
1936
  Args:
1933
- mode: 'Forward' or 'Backward'. Default: 'Forward'.
1937
+ mode: 'F' (forward), 'B' (backward), or 'BI' (bidirection) per the
1938
+ scripting README ('Forward'/'Backward' are accepted as aliases).
1934
1939
  item_index: 0-based item index. Default: 0.
1935
1940
  """
1941
+ aliases = {"F": "F", "FORWARD": "F", "B": "B", "BACKWARD": "B",
1942
+ "BI": "BI", "BIDIRECTION": "BI", "BIDIRECTIONAL": "BI"}
1943
+ normalized = aliases.get(str(mode).strip().upper())
1944
+ if not normalized:
1945
+ return {"error": "mode must be 'F', 'B', or 'BI'"}
1936
1946
  item, err = _get_timeline_item(track_type, track_index, item_index)
1937
1947
  if err:
1938
1948
  return err
1939
- return {"success": bool(item.CreateMagicMask(mode))}
1949
+ if bool(item.CreateMagicMask(normalized)):
1950
+ return {"success": True, "mode": normalized}
1951
+ return {
1952
+ "success": False,
1953
+ "needs_hitl": True,
1954
+ "hitl": "No Magic Mask clicks on this item — a human must click the subject "
1955
+ "on the Color page Magic Mask palette, then press Track Forward.",
1956
+ }
1940
1957
 
1941
1958
 
1942
1959
  @mcp.tool()