davinci-resolve-mcp 2.219.0 → 2.221.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,102 @@
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.221.0 — native Resolve 21.1 audio level normalization
6
+
7
+ Contributed by @legionsound (#214), live-validated on Studio 21.1.0.14.
8
+
9
+ ### Added
10
+
11
+ - **`timeline normalize_audio_level`**, with the granular twin
12
+ `normalize_timeline_audio_level`, calling native 21.1 `NormalizeAudioLevel`
13
+ on explicit audio timeline item IDs. All four `NormalizeAudioOptions` fields
14
+ are supported — `normalizationMode`, `targetLevel` in dBFS, `targetLoudness`
15
+ in LKFS and `setLevelMode` — with either documented constant names or
16
+ integral native values. Every ID resolves before anything is written;
17
+ duplicate or missing IDs and malformed options are refused with nothing
18
+ changed. A native `false` stays `false`, and native defaults stay native
19
+ defaults rather than being pre-filled by the wrapper.
20
+ - Registered in **both** write tables with a 21.1 callable-method floor,
21
+ destructive granular annotations and compound dry-run refusal tests. Tool
22
+ count 373 → 374.
23
+ - **Source audio files are untouched** — this writes clip levels on the
24
+ timeline, not the media on disk.
25
+
26
+ ### Validation
27
+
28
+ - Full suite green: 3,461 passed, 1 skipped. `timeline.normalize_audio_level`
29
+ probed directly: MEDIUM / destructive / recognised in the classifier, and
30
+ present in the destructive registry.
31
+ - Live evidence is @legionsound's on Studio 21.1.0.14, and it is measured from
32
+ the **exported audio** rather than from a readback — independent FFmpeg
33
+ analysis of WAVs the wrappers actually produced: relative peak normalization
34
+ to −6 dBFS preserved the source 12 dB difference (−6.0 / −18.0 dBFS),
35
+ independent peak normalization measured −6.0 / −6.0 dBFS, and an EBU R128
36
+ target of −23 LKFS measured −22.9 LUFS, within 0.1 LU. Complete decoded PCM
37
+ was identical between both interfaces in every case. Not reproduced here;
38
+ this machine is Studio 19.1.3.7, below the 21.1 floor.
39
+ - **Not claimed**: other normalization modes, difficult true-peak limiting,
40
+ multichannel routing, long programs and other source formats.
41
+
42
+ ### Changed
43
+
44
+ - Adapted on merge, the same way #213 was. The branch was rebased onto
45
+ v2.219.0, so its 370 → 371 count bump and every generated agent-rule file
46
+ collided with the 373 that output blanking had landed. Counts resolved to
47
+ **374**, confirmed independently by the agent-rule generator, and the
48
+ generated files regenerated rather than hand-merged. No behaviour changed in
49
+ the adaptation.
50
+
51
+ ## What's New in v2.220.0 — native Resolve 21.1 output blanking, timeline and clip
52
+
53
+ Contributed by @legionsound (#213), live-validated on Studio 21.1.0.14.
54
+
55
+ ### Added
56
+
57
+ - **`timeline set_output_blanking`** and **`timeline_item set_output_blanking`**,
58
+ with granular twins `set_timeline_output_blanking` and
59
+ `set_timeline_item_output_blanking`, calling native 21.1 `SetOutputBlanking`.
60
+ `Top`/`Bottom`/`Left`/`Right` are **native pixel coordinates, not independent
61
+ margin widths** — the wrapper says so rather than letting the names imply
62
+ otherwise. Whole-valued native floats round-trip without conversion,
63
+ malformed values and unknown keys are refused, and no undocumented geometry
64
+ limits are invented.
65
+ - **`timeline_item set_use_timeline_for_output_blanking`**, with the granular
66
+ twin `set_timeline_item_use_timeline_for_output_blanking`, as an explicit
67
+ inheritance switch.
68
+ - **A clip override does not silently disable timeline inheritance.** Measured:
69
+ a clip write while inheritance was on returned false without changing
70
+ inheritance, and succeeded only after an explicit disable. The wrapper
71
+ preserves that native false instead of quietly flipping the switch on the
72
+ caller's behalf — a clip that stops inheriting is a change nobody asked for.
73
+ - All three actions carry method floors, entries in **both** write-risk tables,
74
+ destructive granular annotations and dry-run refusal tests. Tool count 370 →
75
+ 373.
76
+
77
+ ### Validation
78
+
79
+ - Full suite green: 3,457 passed, 1 skipped. All six mutating actions added
80
+ across #209, #211 and #213 probed directly against the classifier and the
81
+ destructive registry — every one MEDIUM / destructive / recognised in both.
82
+ - Live evidence is @legionsound's on Studio 21.1.0.14: both interfaces exported
83
+ synthetic red-clip PNGs with exact lit-pixel bounds — full `0/0/640/360`,
84
+ timeline `64/36/576/324`, clip `128/72/512/288`, restored inheritance
85
+ `64/36/576/324` — and a native partial write of `Top: 80.0` updated one
86
+ coordinate while preserving the others on readback. Recorded as sampled-frame
87
+ and readback results, **not** whole-movie or out-of-range behaviour claims.
88
+ Not reproduced here; this machine is Studio 19.1.3.7, below the 21.1 floor.
89
+
90
+ ### Changed
91
+
92
+ - Adapted on merge. The branch was cut before #211, so its tool-count bump
93
+ (368 → 371) and every generated agent-rule file conflicted with the 370 that
94
+ multicam had landed. Counts were resolved to **373** — confirmed
95
+ independently by the agent-rule generator rather than by arithmetic alone —
96
+ and the generated files were regenerated instead of hand-merged, which is
97
+ the only resolution that cannot silently disagree with its source. Both
98
+ documentation pointers were kept. No behaviour was changed in the
99
+ adaptation.
100
+
5
101
  ## What's New in v2.219.0 — native Resolve 21.1 multicam creation and flattening
6
102
 
7
103
  Contributed by @legionsound (#211), live-validated on Studio 21.1.0.14.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.219.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.221.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
- [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(370%20full)-blue.svg)](#server-modes)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(374%20full)-blue.svg)](#server-modes)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#server-modes)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
@@ -133,7 +133,7 @@ The command starts a loopback-only server and opens the control panel in your br
133
133
  | Mode | Entry point | Tools | Best for |
134
134
  |------|-------------|-------|----------|
135
135
  | Compound | `src/server.py` | 36 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
136
- | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 370 | Power users who want one MCP tool per Resolve API method. |
136
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 374 | Power users who want one MCP tool per Resolve API method. |
137
137
 
138
138
  The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
139
139
 
@@ -365,7 +365,7 @@ The default server is a local stdio process launched by your MCP client; it does
365
365
 
366
366
  | Metric | Value |
367
367
  |--------|-------|
368
- | MCP Tools | **36** compound / **370** granular (live server) |
368
+ | MCP Tools | **36** compound / **374** granular (live server) |
369
369
  | Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
370
370
  | Kernel Actions | **136** guarded workflow actions across 9 compound tools |
371
371
  | API Methods Covered | **361/361** (100%) |
package/README.zh-CN.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.219.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.221.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
- [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(370%20full)-blue.svg)](#服务器模式)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(374%20full)-blue.svg)](#服务器模式)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#服务器模式)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
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.219.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.221.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -88,7 +88,7 @@ venv/bin/python -m src.control_panel
88
88
  | 模式 | 入口 | 工具数 | 适合谁 |
89
89
  |------|------|--------|--------|
90
90
  | Compound(复合) | `src/server.py` | 36 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
91
- | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 370 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
91
+ | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 374 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
92
92
 
93
93
  除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
94
94
 
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
226
226
 
227
227
  | 指标 | 数值 |
228
228
  |------|------|
229
- | MCP 工具 | **36** 复合 / **370** 细粒度(实时服务器) |
229
+ | MCP 工具 | **36** 复合 / **374** 细粒度(实时服务器) |
230
230
  | Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
231
231
  | 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
232
232
  | API 方法覆盖 | **361/361**(100%) |
package/docs/SKILL.md CHANGED
@@ -345,7 +345,7 @@ to the user as verified.
345
345
  | Mode | Entry point | Tool count | Use when |
346
346
  |---|---|---|---|
347
347
  | Compound (default) | `src/server.py` | 36 tools | Most workflows — keeps context lean |
348
- | Granular (full) | `src/server.py --full` | 370 tools | Power users needing one tool per API method |
348
+ | Granular (full) | `src/server.py --full` | 374 tools | Power users needing one tool per API method |
349
349
 
350
350
  Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
351
351
  for edition, presets, audio formats/codecs, normalization modes, speed, fades
@@ -2485,3 +2485,6 @@ Native Resolve 21.1 speed and fade setters: see [speed/fades](reference/resolve2
2485
2485
  Native 21.1 transition creation: see [transition controls](reference/resolve211-native-transitions.md) for options, item-index changes and contributor-rendered evidence.
2486
2486
 
2487
2487
  Native multicam creation and flattening: [21.1 controls](reference/resolve211-multicam.md), with contributor render evidence and remaining family coverage.
2488
+ Native timeline/clip output blanking: [21.1 controls](reference/resolve211-blanking.md), with explicit inheritance and pixel-bound evidence.
2489
+
2490
+ Native audio normalization: [21.1 controls](reference/resolve211-normalization.md), with independently measured exported-audio evidence.
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
64
64
  ├── install.py # Universal installer (macOS/Windows/Linux)
65
65
  ├── src/
66
66
  │ ├── server.py # Compound MCP server — 36 tools (default)
67
- │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 370 tools
67
+ │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 374 tools
68
68
  │ ├── granular/ # Modular full-server implementation
69
69
  │ └── utils/ # Platform detection, Resolve connection helpers
70
70
  ├── tests/ # offline suite (test_*.py) + live harnesses (live_*.py):
package/docs/install.md CHANGED
@@ -144,7 +144,7 @@ The MCP server comes in two modes:
144
144
  | Mode | File | Tools | Best For |
145
145
  |------|------|-------|----------|
146
146
  | **Compound** (default) | `src/server.py` | 36 | Most users — fast, clean, low context usage |
147
- | **Full** | `src/resolve_mcp_server.py` | 370 | Power users who want one tool per API method |
147
+ | **Full** | `src/resolve_mcp_server.py` | 374 | Power users who want one tool per API method |
148
148
 
149
149
  The compound server's `timeline_item` tool includes dedicated actions for common workflows:
150
150
 
@@ -159,7 +159,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
159
159
 
160
160
  The installer uses the compound server by default. To use the full server:
161
161
  ```bash
162
- python src/server.py --full # Launch full 370-tool server
162
+ python src/server.py --full # Launch full 374-tool server
163
163
  # Or point your MCP config directly at src/resolve_mcp_server.py
164
164
  ```
165
165
 
@@ -25,7 +25,7 @@ Every non-deprecated method in the bundled legacy README is represented. This
25
25
  does not claim complete coverage of the newer Resolve 21.1 typed API. The
26
26
  default compound server exposes **36 tools** that group related operations by
27
27
  action parameter, keeping LLM context windows lean. The full granular server
28
- provides **370 individual tools** for power users. The legacy coverage spans
28
+ provides **374 individual tools** for power users. The legacy coverage spans
29
29
  13 API object classes. MCP-level kernel actions are tracked separately in
30
30
  [Kernel Action Coverage](../kernels/README.md).
31
31
 
@@ -635,3 +635,6 @@ Native speed and fade setters in both interfaces are documented in [Resolve 21.1
635
635
  Native 21.1 transition creation: see [transition controls](resolve211-native-transitions.md) for options, item-index changes and contributor-rendered evidence.
636
636
 
637
637
  Native multicam creation and flattening: [21.1 controls](resolve211-multicam.md), with contributor render evidence and remaining family coverage.
638
+ Native timeline/clip output blanking: [21.1 controls](resolve211-blanking.md), with explicit inheritance and pixel-bound evidence.
639
+
640
+ Native audio normalization: [21.1 controls](resolve211-normalization.md), with independently measured exported-audio evidence.
@@ -2,8 +2,8 @@
2
2
 
3
3
  # Read/Write Symmetry Audit
4
4
 
5
- - write-style action occurrences scanned: **121**
6
- - write-style action occurrences with a matching read: **73**
5
+ - write-style action occurrences scanned: **124**
6
+ - write-style action occurrences with a matching read: **76**
7
7
  - distinct high-signal `set_` actions without a direct/known readback: **4**
8
8
 
9
9
  ## High-signal gaps — `set_` with no direct/known readback
@@ -0,0 +1,55 @@
1
+ # Resolve 21.1 output blanking
2
+
3
+ The native setters are exposed in both interfaces:
4
+
5
+ | Compound tool/action | Granular tool |
6
+ |---|---|
7
+ | timeline set_output_blanking | set_timeline_output_blanking |
8
+ | timeline_item set_output_blanking | set_timeline_item_output_blanking |
9
+ | timeline_item set_use_timeline_for_output_blanking | set_timeline_item_use_timeline_for_output_blanking |
10
+
11
+ Blanking setters take `options` containing one or more Top/Bottom/Left/Right
12
+ pixel coordinates. These are bounds, not four independent margin widths. For a
13
+ 640x360 frame, full picture is Top0/Bottom360/Left0/Right640. Whole-valued floats
14
+ from native getter results are accepted; fractional/non-finite values, booleans,
15
+ unknown keys and empty dictionaries are refused. No undocumented geometry bounds
16
+ are invented, and partial dictionaries are forwarded unchanged to Resolve.
17
+
18
+ The inheritance setter requires a strict `use_timeline` boolean. To set a clip
19
+ override, disable inheritance explicitly first. On the measured build, setting
20
+ clip blanking while inheritance was enabled returned false and changed nothing.
21
+ The wrapper preserves that failure and never silently changes inheritance.
22
+ Re-enabling inheritance returns the clip to the timeline's blanking. Native
23
+ getter payloads may be empty when the clip inherits timeline blanking.
24
+
25
+ All three actions have 21.1 method floors, registered destructive MEDIUM-risk
26
+ writes in both classifier tables, and destructive granular annotations. Explicit
27
+ compound dry-run requests are refused before writes. Native failure is preserved
28
+ as success:false. Clip locators use the existing 1-based tracks/0-based item
29
+ indexes.
30
+
31
+ ## Contributor validation
32
+
33
+ Contributor-validated on macOS Studio 21.1.0.14 using a generated solid red clip
34
+ in a disposable 640x360 project, through both actual community interfaces.
35
+ Resolve-exported PNGs showed exact lit-pixel bounds:
36
+
37
+ - Full picture: left0/top0/right640/bottom360.
38
+ - Timeline blanking: left64/top36/right576/bottom324.
39
+ - Clip override: left128/top72/right512/bottom288.
40
+ - Inheritance restored: timeline bounds64/36/576/324 again.
41
+
42
+ The override frame was visually inspected; both interfaces returned the same
43
+ bounds. Inherited clip-set refusal was also verified live. This is sampled-frame
44
+ and readback evidence, not whole-movie verification, or proof of
45
+ negative-coordinate, out-of-frame or audio-item behavior. A native-only partial
46
+ update with Top80.0 also succeeded and preserved Bottom/Left/Right in readback;
47
+ that extra case was not image-verified. Those cases are not
48
+ claimed tested.
49
+
50
+ `tests/live_resolve211_blanking.py OUTPUT_DIR` requires the disposable project
51
+ Codex Blanking Validation 20260909 with one synthetic red.mov video clip, at
52
+ 640x360/24 fps with timeline start00:00:00:00. Generate red.mov using the color
53
+ fixture in resolve211-native-transitions.md. The script invokes both wrappers,
54
+ exports comparison PNGs and restores full timeline blanking/inheritance before
55
+ saving the scratch project. Never substitute production media.
@@ -0,0 +1,58 @@
1
+ # Native Resolve 21.1 audio normalization
2
+
3
+ Compound `timeline normalize_audio_level` and granular
4
+ `normalize_timeline_audio_level` take explicit audio timeline `item_ids` and
5
+ optional `options`. IDs are resolved on the current timeline's audio tracks,
6
+ with no implicit linked-item expansion. Missing/duplicate IDs and malformed
7
+ options are refused before the native write. Native false stays success:false.
8
+
9
+ All NormalizeAudioOptions fields are supported:
10
+
11
+ - normalizationMode: native name from get_normalize_audio_modes.
12
+ - targetLevel: finite dBFS number, for example -6.
13
+ - targetLoudness: finite LKFS number, for example -23.
14
+ - setLevelMode: NORMALIZE_AUDIO_SET_LEVEL_RELATIVE or
15
+ NORMALIZE_AUDIO_SET_LEVEL_INDEPENDENT, or an integral native constant value.
16
+
17
+ Names resolve against the live Resolve object. Unknown option keys, invalid
18
+ named constants, booleans in numeric fields and non-finite values are rejected.
19
+ Omitted options stay omitted; Resolve supplies its native defaults. Mode names
20
+ are passed through rather than hard-coded into a stale list. Use the existing
21
+ mode reader for the current build. The wrapper does not invent range clamps or
22
+ turn a refused native normalization into success.
23
+
24
+ The action has a 21.1 callable-method floor, entries in both destructive/risk
25
+ tables (MEDIUM), destructive granular annotations, and explicit compound dry-run
26
+ refusal. Normalization changes project clip gain, not source audio files.
27
+
28
+ ## Contributor audio evidence
29
+
30
+ Contributor-validated on macOS Studio 21.1.0.14 in a disposable project using
31
+ 997 Hz synthetic stereo tones, with one source 12 dB quieter than the other.
32
+ Both actual community interfaces produced byte-identical decoded PCM per case.
33
+ Independent FFmpeg measurements of exported 24-bit/48 kHz WAVs:
34
+
35
+ | Requested case | Measured output |
36
+ |---|---|
37
+ | Sample Peak Program, relative, target -6 dBFS | Two clip segments peak -6.0/-18.0 dBFS; 12 dB difference preserved |
38
+ | Sample Peak Program, independent, target -6 dBFS | Both segments peak -6.0 dBFS |
39
+ | EBU R128, target -23 LKFS, peak setting-1 dBFS | Integrated -22.9 LUFS, within 0.1 LU of requested target |
40
+
41
+ These are exported-audio measurements, not just gain readback. They do not prove
42
+ all normalization modes, true-peak limiting on difficult signals, long-program
43
+ loudness, multichannel bus behavior, or different source formats.
44
+
45
+ `tests/live_resolve211_normalization.py OUTPUT_DIR` requires disposable project
46
+ Codex Normalization Validation 20260909 at 24 fps, with tone.wav and quiet.wav.
47
+ The script creates timelines/renders and saves that project. Generate fixtures
48
+ without production media:
49
+
50
+ ```sh
51
+ ffmpeg -f lavfi -i 'sine=frequency=997:sample_rate=48000:duration=4' \
52
+ -af 'pan=stereo|c0=c0|c1=c0' -c:a pcm_s24le tone.wav
53
+ ffmpeg -i tone.wav -af volume=-12dB -c:a pcm_s24le quiet.wav
54
+ ```
55
+
56
+ Measure each four-second segment with volumedetect and the loudness case with
57
+ ebur128. Compare full decoded PCM between interfaces. Pure tones are deliberate
58
+ meter fixtures; this does not substitute for representative-program QA.
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.219.0"
40
+ VERSION = "2.221.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
@@ -1543,7 +1543,7 @@ def verify_resolve_connection(python_path, api_path, lib_path):
1543
1543
 
1544
1544
  def print_banner():
1545
1545
  title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
1546
- subtitle = "36 compound · 370 full · 3 platforms"
1546
+ subtitle = "36 compound · 374 full · 3 platforms"
1547
1547
  print()
1548
1548
  print(bold(" ╔══════════════════════════════════════════════════════╗"))
1549
1549
  print(bold(f" ║{title:^54}║"))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.219.0",
3
+ "version": "2.221.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.219.0"
90
+ VERSION = "2.221.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()}")
@@ -1,4 +1,6 @@
1
1
  """Native Resolve 21.1 discovery and editing controls."""
2
+ from src.utils.resolve211_normalization import normalize_audio
3
+ from src.utils.resolve211_blanking import validate_blanking
2
4
  from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
3
5
  from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
4
6
  from src.granular.common import (
@@ -229,3 +231,63 @@ def flatten_timeline_item_multicam(grade_option: str = "FLATTEN_MULTICAM_COPY_GR
229
231
  if error:
230
232
  return {"error": error}
231
233
  return {"success": bool(item.FlattenMulticam(grade))}
234
+
235
+
236
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
237
+ def set_timeline_output_blanking(options: dict) -> dict:
238
+ """Set native 21.1 timeline Top/Bottom/Left/Right pixel coordinates."""
239
+ error = validate_blanking(options)
240
+ if error:
241
+ return {"error": error}
242
+ _, tl, error = _get_timeline()
243
+ if error:
244
+ return error
245
+ missing = _requires_method(tl, "SetOutputBlanking", "21.1")
246
+ if missing:
247
+ return missing
248
+ return {"success": bool(tl.SetOutputBlanking(dict(options)))}
249
+
250
+
251
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
252
+ def set_timeline_item_output_blanking(options: dict, track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
253
+ """Set native 21.1 clip pixel coordinates. Disable timeline blanking inheritance first; this call does not change inheritance."""
254
+ error = validate_blanking(options)
255
+ if error:
256
+ return {"error": error}
257
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
258
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
259
+ item, error = _get_timeline_item(track_type, track_index, item_index)
260
+ if error:
261
+ return error
262
+ missing = _requires_method(item, "SetOutputBlanking", "21.1")
263
+ if missing:
264
+ return missing
265
+ return {"success": bool(item.SetOutputBlanking(dict(options)))}
266
+
267
+
268
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
269
+ def set_timeline_item_use_timeline_for_output_blanking(use_timeline: bool, track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
270
+ """Explicitly enable/disable native 21.1 timeline blanking inheritance for a clip."""
271
+ if type(use_timeline) is not bool:
272
+ return {"error": "use_timeline must be a boolean"}
273
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
274
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
275
+ item, error = _get_timeline_item(track_type, track_index, item_index)
276
+ if error:
277
+ return error
278
+ missing = _requires_method(item, "SetUseTimelineForOutputBlanking", "21.1")
279
+ if missing:
280
+ return missing
281
+ return {"success": bool(item.SetUseTimelineForOutputBlanking(use_timeline))}
282
+
283
+
284
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
285
+ def normalize_timeline_audio_level(item_ids: list[str], options: dict | None = None) -> dict:
286
+ """Native 21.1 normalization of explicit audio timeline item IDs. Options normalizationMode, targetLevel (dBFS), targetLoudness (LKFS), setLevelMode; use get_normalize_audio_modes for names."""
287
+ _, timeline, error = _get_timeline()
288
+ if error:
289
+ return error
290
+ missing = _requires_method(timeline, "NormalizeAudioLevel", "21.1")
291
+ if missing:
292
+ return missing
293
+ return normalize_audio(get_resolve(), timeline, item_ids, {} if options is None else options)
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
34
34
  if __name__ == "__main__":
35
35
  try:
36
36
  start_background_update_check(VERSION, project_dir, logger)
37
- logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (370 granular tools)")
37
+ logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (374 granular tools)")
38
38
  run_fastmcp_stdio(mcp)
39
39
  except KeyboardInterrupt:
40
40
  logger.info("Server shutdown requested")
package/src/server.py CHANGED
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
8
8
 
9
9
  Usage:
10
10
  python src/server.py # Start the MCP server
11
- python src/server.py --full # Start the 370-tool granular server instead
11
+ python src/server.py --full # Start the 374-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.219.0"
14
+ VERSION = "2.221.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -43,6 +43,8 @@ for p in [current_dir, project_dir]:
43
43
  sys.path.insert(0, p)
44
44
 
45
45
  from src.utils.resolve211_multicam import create_multicam, resolve_constant, GRADES
46
+ from src.utils.resolve211_blanking import validate_blanking
47
+ from src.utils.resolve211_normalization import normalize_audio
46
48
  from src.utils.resolve211_edits import validate_edit_options, validate_transition_options, transition_result
47
49
 
48
50
  # Platform-specific Resolve paths
@@ -25052,7 +25054,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
25052
25054
 
25053
25055
 
25054
25056
  _TIMELINE_ACTIONS = [
25055
- "get_normalize_audio_modes", "get_output_blanking",
25057
+ "set_output_blanking", "normalize_audio_level", "get_normalize_audio_modes", "get_output_blanking",
25056
25058
  # Offline authoring — served without a Resolve connection, above the _check() gate.
25057
25059
  "author_offline", "offline_fallback_capabilities",
25058
25060
  "list", "get_current", "set_current", "get_name", "set_name", "get_start_frame",
@@ -25101,6 +25103,8 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
25101
25103
  (resolve_control api_truth "GetSourceStartFrame").
25102
25104
 
25103
25105
  Actions:
25106
+ set_output_blanking(options) -> {success} — native 21.1 pixel coordinates.
25107
+ normalize_audio_level(item_ids, options?) -> {success} — native 21.1 normalization; audio timeline item IDs.
25104
25108
  get_normalize_audio_modes() -> {modes} — documented on Resolve 21.1+.
25105
25109
  get_output_blanking() -> {blanking} — documented on Resolve 21.1+. Pixel coordinates; empty on a clip inheriting timeline blanking.
25106
25110
  list() -> {timelines}
@@ -25419,6 +25423,21 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
25419
25423
  if not tl:
25420
25424
  return _err("No current timeline")
25421
25425
 
25426
+ if action == "normalize_audio_level":
25427
+ missing = _requires_method(tl, "NormalizeAudioLevel", "21.1")
25428
+ if missing:
25429
+ return missing
25430
+ return normalize_audio(get_resolve(), tl, p.get("item_ids"), {} if p.get("options") is None else p["options"])
25431
+
25432
+ if action == "set_output_blanking":
25433
+ error = validate_blanking(p.get("options"))
25434
+ if error:
25435
+ return _err(error)
25436
+ missing = _requires_method(tl, "SetOutputBlanking", "21.1")
25437
+ if missing:
25438
+ return missing
25439
+ return {"success": bool(tl.SetOutputBlanking(dict(p["options"])))}
25440
+
25422
25441
  if action == "get_normalize_audio_modes":
25423
25442
  missing = _requires_method(tl, "GetNormalizeAudioModes", "21.1")
25424
25443
  if missing:
@@ -26282,6 +26301,8 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26282
26301
 
26283
26302
  Actions:
26284
26303
  flatten_multicam(grade_option?, ...) -> {success} — native 21.1, replaces multicam with its current angle.
26304
+ set_output_blanking(options, ...) -> {success} — native 21.1; disable timeline inheritance first.
26305
+ set_use_timeline_for_output_blanking(use_timeline, ...) -> {success} — explicit inheritance switch.
26285
26306
  add_transition(options, ...) -> {success, transition?} — native 21.1 transition; reports actual span.
26286
26307
  set_speed(options, ...) -> {success} — native 21.1 speed options; RippleTimeline defaults false.
26287
26308
  set_fades(options, ...) -> {success} — native 21.1 FadeIn/FadeOut integer frames.
@@ -26348,6 +26369,23 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26348
26369
  return _err(error)
26349
26370
  return {"success": bool(item.FlattenMulticam(grade))}
26350
26371
 
26372
+ if action == "set_output_blanking":
26373
+ error = validate_blanking(p.get("options"))
26374
+ if error:
26375
+ return _err(error)
26376
+ missing = _requires_method(item, "SetOutputBlanking", "21.1")
26377
+ if missing:
26378
+ return missing
26379
+ return {"success": bool(item.SetOutputBlanking(dict(p["options"])))}
26380
+
26381
+ if action == "set_use_timeline_for_output_blanking":
26382
+ if type(p.get("use_timeline")) is not bool:
26383
+ return _err("use_timeline must be a boolean")
26384
+ missing = _requires_method(item, "SetUseTimelineForOutputBlanking", "21.1")
26385
+ if missing:
26386
+ return missing
26387
+ return {"success": bool(item.SetUseTimelineForOutputBlanking(p["use_timeline"]))}
26388
+
26351
26389
  if action == "add_transition":
26352
26390
  options = p.get("options")
26353
26391
  error = validate_transition_options(options)
@@ -26569,7 +26607,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26569
26607
  return _err(f"Invalid interpolation. Must be one of: {', '.join(valid)}")
26570
26608
  return {"success": bool(item.SetKeyframeInterpolation(p["property"], p["frame"], p["interpolation"]))}
26571
26609
 
26572
- return _unknown(action, ["flatten_multicam","add_transition","set_speed","set_fades","get_speed","get_fades","get_output_blanking","get_use_timeline_for_output_blanking","get_name","get_property","set_property","get_duration","get_start","get_end","get_source_start_frame","get_source_end_frame","get_source_start_time","get_source_end_time","get_left_offset","get_right_offset","set_clip_enabled","get_clip_enabled","update_sidecar","get_unique_id","get_media_pool_item","get_stereo_convergence","get_stereo_left_window","get_stereo_right_window","get_linked_items","get_track_type_and_index","get_source_audio_mapping","load_burnin_preset","set_name","get_voice_isolation_state","set_voice_isolation_state","get_retime","set_retime","get_transform","set_transform","get_crop","set_crop","get_composite","set_composite","get_audio","set_audio","get_keyframes","add_keyframe","modify_keyframe","delete_keyframe","set_keyframe_interpolation"])
26610
+ return _unknown(action, ["set_output_blanking","set_use_timeline_for_output_blanking","flatten_multicam","add_transition","set_speed","set_fades","get_speed","get_fades","get_output_blanking","get_use_timeline_for_output_blanking","get_name","get_property","set_property","get_duration","get_start","get_end","get_source_start_frame","get_source_end_frame","get_source_start_time","get_source_end_time","get_left_offset","get_right_offset","set_clip_enabled","get_clip_enabled","update_sidecar","get_unique_id","get_media_pool_item","get_stereo_convergence","get_stereo_left_window","get_stereo_right_window","get_linked_items","get_track_type_and_index","get_source_audio_mapping","load_burnin_preset","set_name","get_voice_isolation_state","set_voice_isolation_state","get_retime","set_retime","get_transform","set_transform","get_crop","set_crop","get_composite","set_composite","get_audio","set_audio","get_keyframes","add_keyframe","modify_keyframe","delete_keyframe","set_keyframe_interpolation"])
26573
26611
 
26574
26612
 
26575
26613
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -32691,9 +32729,9 @@ if __name__ == "__main__":
32691
32729
  start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
32692
32730
  _install_threaded_tool_dispatch(mcp)
32693
32731
 
32694
- # Support --full flag to run the 370-tool granular server instead
32732
+ # Support --full flag to run the 374-tool granular server instead
32695
32733
  if "--full" in sys.argv:
32696
- logger.info("Starting full 370-tool granular server...")
32734
+ logger.info("Starting full 374-tool granular server...")
32697
32735
  sys.argv = [arg for arg in sys.argv if arg != "--full"]
32698
32736
  from src.granular import mcp as granular_mcp
32699
32737
 
@@ -87,6 +87,8 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
87
87
  "execute_swap",
88
88
  }),
89
89
  "timeline": frozenset({
90
+ "set_output_blanking",
91
+ "normalize_audio_level",
90
92
  "delete_clips",
91
93
  "move_clips",
92
94
  "duplicate_clips",
@@ -138,6 +140,8 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
138
140
  "create_subtitles",
139
141
  }),
140
142
  "timeline_item": frozenset({
143
+ "set_output_blanking",
144
+ "set_use_timeline_for_output_blanking",
141
145
  "set_clip_enabled",
142
146
  "set_property",
143
147
  "set_name",
@@ -276,6 +276,11 @@ class RiskClassificationHook(LifecycleHook):
276
276
  # how the clip's edges render. Existing content altered, not deleted.
277
277
  ("media_pool", "create_multicam_clip"),
278
278
  ("timeline_item", "flatten_multicam"),
279
+ ("timeline", "set_output_blanking"),
280
+ ("timeline_item", "set_output_blanking"),
281
+ ("timeline_item", "set_use_timeline_for_output_blanking"),
282
+
283
+ ("timeline", "normalize_audio_level"),
279
284
  ("timeline_item", "add_transition"),
280
285
  ("timeline_item", "set_speed"),
281
286
  ("timeline_item", "set_fades"),
@@ -0,0 +1,17 @@
1
+ """Preserve native pixel coordinates without inventing undocumented geometry limits."""
2
+ import math
3
+
4
+
5
+ def validate_blanking(options):
6
+ if not isinstance(options,dict) or not options:
7
+ return 'options must be a non-empty dictionary'
8
+ if set(options)-{'Top','Bottom','Left','Right'}:
9
+ return 'Only Top, Bottom, Left and Right are accepted'
10
+ for value in options.values():
11
+ try:
12
+ valid = type(value) in (int,float) and math.isfinite(value) and int(value)==value
13
+ except OverflowError:
14
+ valid = False
15
+ if not valid:
16
+ return 'Blanking values must be finite whole-pixel coordinates'
17
+ return None
@@ -0,0 +1,42 @@
1
+ """Native normalization: validate explicit audio-item selection before writes."""
2
+ import math
3
+
4
+
5
+ def finite_number(value):
6
+ try:
7
+ return type(value) in (int,float) and math.isfinite(value)
8
+ except OverflowError:
9
+ return False
10
+
11
+
12
+ def normalize_audio(r, timeline, item_ids, options):
13
+ if not isinstance(item_ids,list) or not item_ids or any(not isinstance(i,str) or not i for i in item_ids):
14
+ return {'error':'item_ids must be a non-empty list of audio timeline item unique IDs'}
15
+ if len(set(item_ids))!=len(item_ids):
16
+ return {'error':'item_ids must not contain duplicates'}
17
+ if not isinstance(options,dict) or set(options)-{'normalizationMode','targetLevel','targetLoudness','setLevelMode'}:
18
+ return {'error':'Unknown normalization options or non-dictionary options'}
19
+ normalized=dict(options)
20
+ for key,value in options.items():
21
+ if key=='normalizationMode':
22
+ if not isinstance(value,str) or not value.strip():
23
+ return {'error':'normalizationMode must be a non-empty native mode name'}
24
+ elif key=='setLevelMode':
25
+ if isinstance(value,str):
26
+ if value not in ('NORMALIZE_AUDIO_SET_LEVEL_RELATIVE','NORMALIZE_AUDIO_SET_LEVEL_INDEPENDENT'):
27
+ return {'error':'Unknown setLevelMode constant'}
28
+ value=getattr(r,value,None)
29
+ if not finite_number(value) or int(value)!=value:
30
+ return {'error':'setLevelMode must be a documented constant name or integral native value'}
31
+ normalized[key]=value
32
+ elif not finite_number(value):
33
+ return {'error':key+' must be a finite number'}
34
+ wanted=set(item_ids);found={}
35
+ for index in range(1,timeline.GetTrackCount('audio')+1):
36
+ for item in timeline.GetItemListInTrack('audio',index) or []:
37
+ uid=item.GetUniqueId()
38
+ if uid in wanted:
39
+ found[uid]=item
40
+ if any(uid not in found for uid in item_ids):
41
+ return {'error':'One or more IDs were not found on audio tracks; no normalization performed'}
42
+ return {'success':bool(timeline.NormalizeAudioLevel([found[uid] for uid in item_ids],normalized))}
@@ -227,6 +227,11 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
227
227
  CODE_FLOORS: Dict[str, str] = {
228
228
  "MediaPool.CreateMulticamClip": "21.1",
229
229
  "TimelineItem.FlattenMulticam": "21.1",
230
+ "Timeline.SetOutputBlanking": "21.1",
231
+ "TimelineItem.SetOutputBlanking": "21.1",
232
+ "TimelineItem.SetUseTimelineForOutputBlanking": "21.1",
233
+
234
+ "Timeline.NormalizeAudioLevel": "21.1",
230
235
  "TimelineItem.AddTransition": "21.1",
231
236
  "TimelineItem.SetSpeed": "21.1",
232
237
  "TimelineItem.SetFades": "21.1",