davinci-resolve-mcp 2.215.2 → 2.216.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,37 @@
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.216.0 — twelve Resolve 21.1 read-only controls, in both server modes
6
+
7
+ Contributed by @legionsound (#206), live-measured on Studio 21.1.0.14.
8
+
9
+ ### Added
10
+
11
+ - **Twelve readers for data 21.1 exposes natively**, each as a compound
12
+ action and a granular tool: `resolve_control is_studio`,
13
+ `get_keyboard_presets`, `get_current_keyboard_preset`; `project_settings
14
+ get_project_settings_presets`; `render get_audio_formats`,
15
+ `get_audio_codecs(format)`; `timeline get_normalize_audio_modes`,
16
+ `get_output_blanking`; `timeline_item get_speed`, `get_fades`,
17
+ `get_output_blanking`, `get_use_timeline_for_output_blanking`. Native
18
+ values pass through untouched, including `false`, empty inherited blanking
19
+ and fractional fade durations; the four blanking values are pixel
20
+ coordinates, not margins. A build without the method returns an explicit
21
+ "requires DaVinci Resolve 21.1+" error — confirmed here on Studio 19.1.3.7,
22
+ where all twelve refuse cleanly. The granular count moves from 353 to 365
23
+ tools; docs, generated agent rules, the version ledger and the coverage
24
+ reference are updated together. `GetProjectLastModifiedTime` was deferred
25
+ because it returned `None` for existing projects on the test install.
26
+ See `docs/reference/resolve211-read-controls.md`.
27
+
28
+ ### Validation
29
+
30
+ - The PR's offline contracts (absent methods, argument forwarding, preserved
31
+ false/empty payloads, invalid locators), the full offline suite, drift
32
+ guards and the advanced Node suite. Positive payloads are the contributor's
33
+ measurement on 21.1.0.14 through both interfaces; the version-floor refusal
34
+ is measured here on 19.1.3.7.
35
+
5
36
  ## What's New in v2.215.2 — the granular server accepts Resolve 21.1's lowercase item types; the 21.1 typed API ships as reference
6
37
 
7
38
  Both contributed by @legionsound (#204, #205), measured 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.215.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.216.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(353%20full)-blue.svg)](#server-modes)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(365%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` | 353 | Power users who want one MCP tool per Resolve API method. |
136
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 365 | 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 / **353** granular (live server) |
368
+ | MCP Tools | **36** compound / **365** 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.215.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.216.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(353%20full)-blue.svg)](#服务器模式)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(365%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.215.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.216.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` | 353 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
91
+ | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 365 | 想要"一个 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** 复合 / **353** 细粒度(实时服务器) |
229
+ | MCP 工具 | **36** 复合 / **365** 细粒度(实时服务器) |
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,11 @@ 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` | 353 tools | Power users needing one tool per API method |
348
+ | Granular (full) | `src/server.py --full` | 365 tools | Power users needing one tool per API method |
349
+
350
+ Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
351
+ for edition, presets, audio formats/codecs, normalization modes, speed, fades
352
+ and blanking in both server interfaces. These readers do not invoke setters.
349
353
 
350
354
  This skill document covers the **compound server** (the default). Each compound
351
355
  tool accepts an `action` string and an optional `params` object.
@@ -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 — 353 tools
67
+ │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 365 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` | 353 | Power users who want one tool per API method |
147
+ | **Full** | `src/resolve_mcp_server.py` | 365 | 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 353-tool server
162
+ python src/server.py --full # Launch full 365-tool server
163
163
  # Or point your MCP config directly at src/resolve_mcp_server.py
164
164
  ```
165
165
 
@@ -1,12 +1,16 @@
1
1
  # API Coverage and Test Results
2
2
 
3
- Complete Resolve scripting API coverage, live-test status, and method-by-method reference.
3
+ Legacy-reference coverage, incremental Resolve 21.1 coverage, live-test status,
4
+ and method-by-method reference.
4
5
 
5
6
  ## Key Stats
6
7
 
8
+ The API totals below refer to the bundled legacy README baseline. Resolve 21.1
9
+ additions are tracked separately in the section below.
10
+
7
11
  | Metric | Value |
8
12
  |--------|-------|
9
- | MCP Tools | **36** compound (default) / **353** granular |
13
+ | MCP Tools | **36** compound (default) / **365** granular |
10
14
  | Kernel Actions | **136** guarded MCP workflow actions across 9 compound tools |
11
15
  | API Methods Covered | **361/361** (100%) |
12
16
  | Methods Live Tested | **338/361** (93.6%) |
@@ -17,7 +21,18 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
17
21
 
18
22
  ## API Coverage
19
23
 
20
- Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **36 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **353 individual tools** for power users. Both modes cover all 13 API object classes. MCP-level kernel actions are tracked separately in [Kernel Action Coverage](../kernels/README.md).
24
+ Every non-deprecated method in the bundled legacy README is represented. This
25
+ does not claim complete coverage of the newer Resolve 21.1 typed API. The
26
+ default compound server exposes **36 tools** that group related operations by
27
+ action parameter, keeping LLM context windows lean. The full granular server
28
+ provides **365 individual tools** for power users. The legacy coverage spans
29
+ 13 API object classes. MCP-level kernel actions are tracked separately in
30
+ [Kernel Action Coverage](../kernels/README.md).
31
+
32
+ Twelve additional [Resolve 21.1 read-only controls](resolve211-read-controls.md)
33
+ are implemented in both interfaces and were live-measured by a contributor on
34
+ Studio 21.1.0.14. Their evidence and limitations are recorded separately from
35
+ the historical aggregate above.
21
36
 
22
37
  The 34th compound tool is `timeline_versioning` (C6) — an MCP-level workflow
23
38
  tool, not a wrapper around a Resolve API method. It surfaces the
@@ -28,7 +43,8 @@ for usage.
28
43
  Workflow helpers can go beyond one-to-one API method coverage while still using
29
44
  only public Resolve calls. For example, `media_pool.setup_multicam_timeline`
30
45
  prepares a stacked timeline for Resolve's multicam UI, but native multicam clip
31
- creation itself is not exposed by the scripting API. Similarly,
46
+ creation is available in Resolve 21.1 through CreateMulticamClip; that native
47
+ route is not implemented by this helper. Similarly,
32
48
  `media_analysis.detect_sync_events` is a source-safe FFmpeg/FFprobe helper for
33
49
  advisory 2-pop and slate-clap sync points; it is not a Resolve API method.
34
50
  `media_analysis.add_sync_event_markers` is an explicit marker-write helper for
@@ -0,0 +1,60 @@
1
+ # Resolve 21.1 read-only discovery
2
+
3
+ Twelve readers expose native 21.1 data in both compound and granular modes.
4
+ They do not load presets, alter timeline settings, create render jobs, change
5
+ clips or perform normalization. Calls require a callable native method; older
6
+ builds without it return an explicit method/version error.
7
+
8
+ | Compound tool/action | Granular tool | Result |
9
+ |---|---|---|
10
+ | resolve_control is_studio | is_resolve_studio | is_studio boolean |
11
+ | resolve_control get_keyboard_presets | get_keyboard_presets | presets list |
12
+ | resolve_control get_current_keyboard_preset | get_current_keyboard_preset | active preset name |
13
+ | project_settings get_project_settings_presets | get_project_settings_presets | preset records with Name, Width, Height |
14
+ | render get_audio_formats | get_audio_render_formats | format descriptions mapped to extensions |
15
+ | render get_audio_codecs | get_audio_render_codecs | codec descriptions mapped to native codec identifiers |
16
+ | timeline get_normalize_audio_modes | get_normalize_audio_modes | native normalization-mode names |
17
+ | timeline get_output_blanking | get_timeline_output_blanking | blanking dictionary |
18
+ | timeline_item get_speed | get_timeline_item_speed | native speed options |
19
+ | timeline_item get_fades | get_timeline_item_fades | FadeIn/FadeOut frame durations |
20
+ | timeline_item get_output_blanking | get_timeline_item_output_blanking | blanking dictionary; empty when inherited |
21
+ | timeline_item get_use_timeline_for_output_blanking | get_timeline_item_use_timeline_for_output_blanking | use_timeline boolean |
22
+
23
+ For audio codecs, `format` is a file extension from the audio formats reader,
24
+ for example `wav`, not the display label `Wave`. Empty and non-string inputs
25
+ are refused before making the native call.
26
+
27
+ Clip readers accept `track_type` (`video` or `audio`), a 1-based `track_index`
28
+ and a 0-based `item_index`; defaults are video, 1, 0. Granular readers refuse
29
+ negative item indexes and invalid track types/indexes before lookup. These are
30
+ the existing compound item-location conventions.
31
+
32
+ The payload is passed through without coercing false, empty lists/dictionaries
33
+ or fractional frame durations. Empty clip blanking is valid when inheritance
34
+ is enabled. The four blanking values are native pixel coordinates; do not
35
+ reinterpret them as four independent margin widths. The speed reader returns
36
+ the native options rather than trying to derive speed from source/timeline
37
+ frame spans.
38
+
39
+ ## Validation and limits
40
+
41
+ Contributed live measurement on macOS **DaVinci Resolve Studio 21.1.0.14**,
42
+ September 9, 2026: all twelve readers returned matching domain payloads through
43
+ both interfaces. Compound `_operation` instrumentation is separate from that
44
+ domain payload. No project/preset/clip mutation was invoked.
45
+
46
+ ```sh
47
+ python -m unittest tests.test_resolve211_read_controls
48
+ python tests/live_resolve211_read_controls.py
49
+ ```
50
+
51
+ The live script requires an existing project and timeline with a normal video
52
+ clip on video track 1. Its receipt omits project, clip and preset names.
53
+ Offline contracts exercise all readers, absent methods, preserved false/empty
54
+ values, required argument forwarding, and invalid granular item locators.
55
+
56
+ This is not a claim of full 21.1 API coverage, or a promise that corresponding
57
+ setters/multicam/transitions/normalization are implemented. The generic
58
+ GetProjectLastModifiedTime reader was intentionally deferred after the native
59
+ API returned None for several existing projects on the test installation.
60
+ That observation needs investigation rather than an advertised timestamp.
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.215.2"
40
+ VERSION = "2.216.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 · 353 full · 3 platforms"
1546
+ subtitle = "36 compound · 365 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.215.2",
3
+ "version": "2.216.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -10,6 +10,7 @@ from src.granular import (
10
10
  media_storage,
11
11
  project,
12
12
  resolve_control,
13
+ resolve_211,
13
14
  timeline,
14
15
  timeline_item,
15
16
  )
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.215.2"
90
+ VERSION = "2.216.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()}")
@@ -0,0 +1,148 @@
1
+ """Read-only discovery added to the typed Resolve 21.1 API surface."""
2
+ from src.granular.common import (
3
+ mcp, READ_ONLY_TOOL, get_resolve, get_current_project,
4
+ _get_timeline, _get_timeline_item, _requires_method, has_method,
5
+ )
6
+
7
+
8
+ @mcp.tool(annotations=READ_ONLY_TOOL)
9
+ def is_resolve_studio() -> dict:
10
+ """Read IsStudio (documented on Resolve 21.1+)."""
11
+ r = get_resolve()
12
+ if r is None:
13
+ return {"error": "Not connected to DaVinci Resolve"}
14
+ if not has_method(r, "IsStudio"):
15
+ return {"error": "IsStudio is unavailable on this Resolve build"}
16
+ return {"is_studio": r.IsStudio()}
17
+
18
+ @mcp.tool(annotations=READ_ONLY_TOOL)
19
+ def get_keyboard_presets() -> dict:
20
+ """Read GetKeyboardPresetList (documented on Resolve 21.1+)."""
21
+ r = get_resolve()
22
+ if r is None:
23
+ return {"error": "Not connected to DaVinci Resolve"}
24
+ missing = _requires_method(r, "GetKeyboardPresetList", "21.1")
25
+ if missing:
26
+ return missing
27
+ return {"presets": r.GetKeyboardPresetList()}
28
+
29
+ @mcp.tool(annotations=READ_ONLY_TOOL)
30
+ def get_current_keyboard_preset() -> dict:
31
+ """Read GetCurrentKeyboardPreset (documented on Resolve 21.1+)."""
32
+ r = get_resolve()
33
+ if r is None:
34
+ return {"error": "Not connected to DaVinci Resolve"}
35
+ missing = _requires_method(r, "GetCurrentKeyboardPreset", "21.1")
36
+ if missing:
37
+ return missing
38
+ return {"name": r.GetCurrentKeyboardPreset()}
39
+
40
+ @mcp.tool(annotations=READ_ONLY_TOOL)
41
+ def get_project_settings_presets() -> dict:
42
+ """Read GetProjectSettingsPresetList (documented on Resolve 21.1+)."""
43
+ _, proj = get_current_project()
44
+ if proj is None:
45
+ return {"error": "No project currently open"}
46
+ missing = _requires_method(proj, "GetProjectSettingsPresetList", "21.1")
47
+ if missing:
48
+ return missing
49
+ return {"presets": proj.GetProjectSettingsPresetList()}
50
+
51
+
52
+ @mcp.tool(annotations=READ_ONLY_TOOL)
53
+ def get_audio_render_formats() -> dict:
54
+ """Read GetAudioRenderFormats (documented on Resolve 21.1+)."""
55
+ _, proj = get_current_project()
56
+ if proj is None:
57
+ return {"error": "No project currently open"}
58
+ missing = _requires_method(proj, "GetAudioRenderFormats", "21.1")
59
+ if missing:
60
+ return missing
61
+ return {"formats": proj.GetAudioRenderFormats()}
62
+
63
+ @mcp.tool(annotations=READ_ONLY_TOOL)
64
+ def get_audio_render_codecs(format: str) -> dict:
65
+ """Read GetAudioRenderCodecs (documented on Resolve 21.1+). Pass an audio file extension such as wav."""
66
+ if not isinstance(format, str) or not format.strip():
67
+ return {"error": "get_audio_codecs requires a non-empty format string"}
68
+ _, proj = get_current_project()
69
+ if proj is None:
70
+ return {"error": "No project currently open"}
71
+ missing = _requires_method(proj, "GetAudioRenderCodecs", "21.1")
72
+ if missing:
73
+ return missing
74
+ return {"codecs": proj.GetAudioRenderCodecs(format)}
75
+
76
+ @mcp.tool(annotations=READ_ONLY_TOOL)
77
+ def get_normalize_audio_modes() -> dict:
78
+ """Read GetNormalizeAudioModes (documented on Resolve 21.1+)."""
79
+ _, tl, error = _get_timeline()
80
+ if error:
81
+ return error
82
+ missing = _requires_method(tl, "GetNormalizeAudioModes", "21.1")
83
+ if missing:
84
+ return missing
85
+ return {"modes": tl.GetNormalizeAudioModes()}
86
+
87
+ @mcp.tool(annotations=READ_ONLY_TOOL)
88
+ def get_timeline_output_blanking() -> dict:
89
+ """Read GetOutputBlanking (documented on Resolve 21.1+). Pixel coordinates; clips inheriting timeline blanking return an empty dict."""
90
+ _, tl, error = _get_timeline()
91
+ if error:
92
+ return error
93
+ missing = _requires_method(tl, "GetOutputBlanking", "21.1")
94
+ if missing:
95
+ return missing
96
+ return {"blanking": tl.GetOutputBlanking()}
97
+
98
+ @mcp.tool(annotations=READ_ONLY_TOOL)
99
+ def get_timeline_item_speed(track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
100
+ """Read GetSpeed (documented on Resolve 21.1+)."""
101
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
102
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
103
+ item, error = _get_timeline_item(track_type, track_index, item_index)
104
+ if error:
105
+ return error
106
+ missing = _requires_method(item, "GetSpeed", "21.1")
107
+ if missing:
108
+ return missing
109
+ return {"speed": item.GetSpeed()}
110
+
111
+ @mcp.tool(annotations=READ_ONLY_TOOL)
112
+ def get_timeline_item_fades(track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
113
+ """Read GetFades (documented on Resolve 21.1+). Durations are in frames."""
114
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
115
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
116
+ item, error = _get_timeline_item(track_type, track_index, item_index)
117
+ if error:
118
+ return error
119
+ missing = _requires_method(item, "GetFades", "21.1")
120
+ if missing:
121
+ return missing
122
+ return {"fades": item.GetFades()}
123
+
124
+ @mcp.tool(annotations=READ_ONLY_TOOL)
125
+ def get_timeline_item_output_blanking(track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
126
+ """Read GetOutputBlanking (documented on Resolve 21.1+). Pixel coordinates; clips inheriting timeline blanking return an empty dict."""
127
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
128
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
129
+ item, error = _get_timeline_item(track_type, track_index, item_index)
130
+ if error:
131
+ return error
132
+ missing = _requires_method(item, "GetOutputBlanking", "21.1")
133
+ if missing:
134
+ return missing
135
+ return {"blanking": item.GetOutputBlanking()}
136
+
137
+ @mcp.tool(annotations=READ_ONLY_TOOL)
138
+ def get_timeline_item_use_timeline_for_output_blanking(track_type: str = "video", track_index: int = 1, item_index: int = 0) -> dict:
139
+ """Read GetUseTimelineForOutputBlanking (documented on Resolve 21.1+)."""
140
+ if track_type not in ("video", "audio") or track_index < 1 or item_index < 0:
141
+ return {"error": "Use video/audio, a 1-based track index and a non-negative item index"}
142
+ item, error = _get_timeline_item(track_type, track_index, item_index)
143
+ if error:
144
+ return error
145
+ missing = _requires_method(item, "GetUseTimelineForOutputBlanking", "21.1")
146
+ if missing:
147
+ return missing
148
+ return {"use_timeline": item.GetUseTimelineForOutputBlanking()}
@@ -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} (353 granular tools)")
37
+ logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (365 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 353-tool granular server instead
11
+ python src/server.py --full # Start the 365-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.215.2"
14
+ VERSION = "2.216.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -16571,6 +16571,9 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
16571
16571
  """App-level DaVinci Resolve operations.
16572
16572
 
16573
16573
  Actions:
16574
+ is_studio() -> {is_studio} — documented on Resolve 21.1+.
16575
+ get_keyboard_presets() -> {presets} — documented on Resolve 21.1+.
16576
+ get_current_keyboard_preset() -> {name} — documented on Resolve 21.1+.
16574
16577
  launch(headless?) -> {success, message, running, headless, guidance}
16575
16578
  — Launch DaVinci Resolve if not running. Call this FIRST if any tool returns
16576
16579
  a 'Not connected' error. headless=true starts it with no UI (-nogui):
@@ -16928,6 +16931,21 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
16928
16931
  if r is None:
16929
16932
  return _not_connected_error()
16930
16933
 
16934
+ if action == "is_studio":
16935
+ if not _has_method(r, "IsStudio"):
16936
+ return _err("IsStudio is unavailable on this Resolve build")
16937
+ return {"is_studio": _ser(r.IsStudio())}
16938
+ if action == "get_keyboard_presets":
16939
+ missing = _requires_method(r, "GetKeyboardPresetList", "21.1")
16940
+ if missing:
16941
+ return missing
16942
+ return {"presets": _ser(r.GetKeyboardPresetList())}
16943
+ if action == "get_current_keyboard_preset":
16944
+ missing = _requires_method(r, "GetCurrentKeyboardPreset", "21.1")
16945
+ if missing:
16946
+ return missing
16947
+ return {"name": _ser(r.GetCurrentKeyboardPreset())}
16948
+
16931
16949
  if action == "get_version":
16932
16950
  update_env = _setup_update_env()
16933
16951
  mcp_update = get_cached_update_status(project_dir, VERSION, env=update_env)
@@ -17048,7 +17066,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17048
17066
  if err:
17049
17067
  return _err(err)
17050
17068
  return {"success": bool(r.ExportUserPreferencesPreset(clean["name"], clean["path"]))}
17051
- return _unknown(action, ["launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","job_status","list_jobs","get_execution_trace","get_execution","list_recent_executions","begin_execution","end_execution","export_execution_report","clear_executions","inspect_operation","list_lifecycle_hooks","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","list_user_preferences_presets","save_user_preferences_preset","load_user_preferences_preset","delete_user_preferences_preset","import_user_preferences_preset","export_user_preferences_preset","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
17069
+ return _unknown(action, ["is_studio","get_keyboard_presets","get_current_keyboard_preset","launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","job_status","list_jobs","get_execution_trace","get_execution","list_recent_executions","begin_execution","end_execution","export_execution_report","clear_executions","inspect_operation","list_lifecycle_hooks","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","list_user_preferences_presets","save_user_preferences_preset","load_user_preferences_preset","delete_user_preferences_preset","import_user_preferences_preset","export_user_preferences_preset","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
17052
17070
 
17053
17071
 
17054
17072
  # ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
@@ -19342,6 +19360,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
19342
19360
  """Project metadata, settings, and color groups.
19343
19361
 
19344
19362
  Actions:
19363
+ get_project_settings_presets() -> {presets} — documented on Resolve 21.1+.
19345
19364
  get_name() -> {name}
19346
19365
  set_name(name) -> {success}
19347
19366
  get_setting(name?) -> {settings} — omit name for all settings
@@ -19370,6 +19389,12 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
19370
19389
  if err:
19371
19390
  return err
19372
19391
 
19392
+ if action == "get_project_settings_presets":
19393
+ missing = _requires_method(proj, "GetProjectSettingsPresetList", "21.1")
19394
+ if missing:
19395
+ return missing
19396
+ return {"presets": _ser(proj.GetProjectSettingsPresetList())}
19397
+
19373
19398
  if action == "get_name":
19374
19399
  return {"name": proj.GetName()}
19375
19400
  elif action == "set_name":
@@ -19497,7 +19522,7 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
19497
19522
  result = _ai_result_payload(proj.ResetIntellisearchAnalysis())
19498
19523
  _rec.success = result["success"]
19499
19524
  return result
19500
- return _unknown(action, ["get_name","set_name","get_setting","set_setting","get_unique_id","get_presets","set_preset","refresh_luts","get_gallery","export_frame_as_still","project_summary","load_burnin_preset","insert_audio","get_color_groups","add_color_group","delete_color_group","apply_fairlight_preset","generate_speech","reset_intellisearch_analysis"])
19525
+ return _unknown(action, ["get_project_settings_presets","get_name","set_name","get_setting","set_setting","get_unique_id","get_presets","set_preset","refresh_luts","get_gallery","export_frame_as_still","project_summary","load_burnin_preset","insert_audio","get_color_groups","add_color_group","delete_color_group","apply_fairlight_preset","generate_speech","reset_intellisearch_analysis"])
19501
19526
 
19502
19527
 
19503
19528
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -20311,6 +20336,8 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
20311
20336
  """Render pipeline: jobs, presets, formats, codecs, and rendering.
20312
20337
 
20313
20338
  Actions:
20339
+ get_audio_formats() -> {formats} — documented on Resolve 21.1+.
20340
+ get_audio_codecs(format) -> {codecs} — documented on Resolve 21.1+. Audio file extension, e.g. wav.
20314
20341
  add_job() -> {job_id}
20315
20342
  delete_job(job_id) -> {success}
20316
20343
  delete_all_jobs() -> {success}
@@ -20582,6 +20609,19 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
20582
20609
  return _ok()
20583
20610
  elif action == "is_rendering":
20584
20611
  return {"rendering": bool(proj.IsRenderingInProgress())}
20612
+ elif action == "get_audio_formats":
20613
+ missing = _requires_method(proj, "GetAudioRenderFormats", "21.1")
20614
+ if missing:
20615
+ return missing
20616
+ return {"formats": _ser(proj.GetAudioRenderFormats())}
20617
+ elif action == "get_audio_codecs":
20618
+ if not isinstance(p.get("format"), str) or not p["format"].strip():
20619
+ return _err("get_audio_codecs requires a non-empty format string")
20620
+ missing = _requires_method(proj, "GetAudioRenderCodecs", "21.1")
20621
+ if missing:
20622
+ return missing
20623
+ return {"codecs": _ser(proj.GetAudioRenderCodecs(p["format"]))}
20624
+
20585
20625
  elif action == "get_formats":
20586
20626
  return {"formats": _ser(proj.GetRenderFormats())}
20587
20627
  elif action == "get_codecs":
@@ -20699,7 +20739,7 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
20699
20739
  return _safe_quick_export(proj, p)
20700
20740
  elif action == "export_render_boundary_report":
20701
20741
  return _export_render_boundary_report(proj, p)
20702
- return _unknown(action, ["add_job","delete_job","delete_all_jobs","list_jobs","get_job_status","verify_output","start","stop","is_rendering","get_formats","get_codecs","get_format_and_codec","set_format_and_codec","get_mode","set_mode","get_resolutions","get_settings","set_settings","list_presets","load_preset","save_preset","delete_preset","quick_export_presets","quick_export",*_RENDER_KERNEL_ACTIONS])
20742
+ return _unknown(action, ["get_audio_formats","get_audio_codecs","add_job","delete_job","delete_all_jobs","list_jobs","get_job_status","verify_output","start","stop","is_rendering","get_formats","get_codecs","get_format_and_codec","set_format_and_codec","get_mode","set_mode","get_resolutions","get_settings","set_settings","list_presets","load_preset","save_preset","delete_preset","quick_export_presets","quick_export",*_RENDER_KERNEL_ACTIONS])
20703
20743
 
20704
20744
 
20705
20745
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -25003,6 +25043,7 @@ def edit_engine(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
25003
25043
 
25004
25044
 
25005
25045
  _TIMELINE_ACTIONS = [
25046
+ "get_normalize_audio_modes", "get_output_blanking",
25006
25047
  # Offline authoring — served without a Resolve connection, above the _check() gate.
25007
25048
  "author_offline", "offline_fallback_capabilities",
25008
25049
  "list", "get_current", "set_current", "get_name", "set_name", "get_start_frame",
@@ -25051,6 +25092,8 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
25051
25092
  (resolve_control api_truth "GetSourceStartFrame").
25052
25093
 
25053
25094
  Actions:
25095
+ get_normalize_audio_modes() -> {modes} — documented on Resolve 21.1+.
25096
+ get_output_blanking() -> {blanking} — documented on Resolve 21.1+. Pixel coordinates; empty on a clip inheriting timeline blanking.
25054
25097
  list() -> {timelines}
25055
25098
  get_current() -> {name, id, start_frame, end_frame, start_timecode}
25056
25099
  set_current(index|id|name) -> {success} — id/name are stable across archives; index is 1-based
@@ -25367,6 +25410,17 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
25367
25410
  if not tl:
25368
25411
  return _err("No current timeline")
25369
25412
 
25413
+ if action == "get_normalize_audio_modes":
25414
+ missing = _requires_method(tl, "GetNormalizeAudioModes", "21.1")
25415
+ if missing:
25416
+ return missing
25417
+ return {"modes": _ser(tl.GetNormalizeAudioModes())}
25418
+ if action == "get_output_blanking":
25419
+ missing = _requires_method(tl, "GetOutputBlanking", "21.1")
25420
+ if missing:
25421
+ return missing
25422
+ return {"blanking": _ser(tl.GetOutputBlanking())}
25423
+
25370
25424
  if action == "clip_where":
25371
25425
  return _timeline_clip_where(tl, p)
25372
25426
  if action == "get_current":
@@ -26218,6 +26272,10 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26218
26272
  Identify by track_type, track_index, item_index (item_index is 0-BASED: 0 = first clip; track_index is 1-based).
26219
26273
 
26220
26274
  Actions:
26275
+ get_speed(...) -> {speed} — documented on Resolve 21.1+.
26276
+ get_fades(...) -> {fades} — documented on Resolve 21.1+. Native frame durations.
26277
+ get_output_blanking(...) -> {blanking} — documented on Resolve 21.1+. Pixel coordinates; empty on a clip inheriting timeline blanking.
26278
+ get_use_timeline_for_output_blanking(...) -> {use_timeline} — documented on Resolve 21.1+.
26221
26279
  get_name(track_type?, track_index?, item_index?) -> {name}
26222
26280
  get_property(key?, ...) -> {properties}
26223
26281
  set_property(key, value, ...) -> {success}
@@ -26268,6 +26326,27 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26268
26326
  if err:
26269
26327
  return err
26270
26328
 
26329
+ if action == "get_speed":
26330
+ missing = _requires_method(item, "GetSpeed", "21.1")
26331
+ if missing:
26332
+ return missing
26333
+ return {"speed": _ser(item.GetSpeed())}
26334
+ if action == "get_fades":
26335
+ missing = _requires_method(item, "GetFades", "21.1")
26336
+ if missing:
26337
+ return missing
26338
+ return {"fades": _ser(item.GetFades())}
26339
+ if action == "get_output_blanking":
26340
+ missing = _requires_method(item, "GetOutputBlanking", "21.1")
26341
+ if missing:
26342
+ return missing
26343
+ return {"blanking": _ser(item.GetOutputBlanking())}
26344
+ if action == "get_use_timeline_for_output_blanking":
26345
+ missing = _requires_method(item, "GetUseTimelineForOutputBlanking", "21.1")
26346
+ if missing:
26347
+ return missing
26348
+ return {"use_timeline": _ser(item.GetUseTimelineForOutputBlanking())}
26349
+
26271
26350
  if action == "get_name":
26272
26351
  return {"name": item.GetName()}
26273
26352
  elif action == "get_property":
@@ -26443,7 +26522,7 @@ def timeline_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[
26443
26522
  return _err(f"Invalid interpolation. Must be one of: {', '.join(valid)}")
26444
26523
  return {"success": bool(item.SetKeyframeInterpolation(p["property"], p["frame"], p["interpolation"]))}
26445
26524
 
26446
- return _unknown(action, ["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"])
26525
+ return _unknown(action, ["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"])
26447
26526
 
26448
26527
 
26449
26528
  # ═══════════════════════════════════════════════════════════════════════════════
@@ -32565,9 +32644,9 @@ if __name__ == "__main__":
32565
32644
  start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
32566
32645
  _install_threaded_tool_dispatch(mcp)
32567
32646
 
32568
- # Support --full flag to run the 353-tool granular server instead
32647
+ # Support --full flag to run the 365-tool granular server instead
32569
32648
  if "--full" in sys.argv:
32570
- logger.info("Starting full 353-tool granular server...")
32649
+ logger.info("Starting full 365-tool granular server...")
32571
32650
  sys.argv = [arg for arg in sys.argv if arg != "--full"]
32572
32651
  from src.granular import mcp as granular_mcp
32573
32652
 
@@ -225,6 +225,20 @@ _EVIDENCE_GATES: List[Dict[str, Any]] = [
225
225
  # disagree, which is what keeps the two from drifting apart again.
226
226
 
227
227
  CODE_FLOORS: Dict[str, str] = {
228
+ # Documented in the shipped 21.1 scripting CHANGELOG; read-only contributor
229
+ # validation on Studio 21.1.0.14. No claim of a live older-build bisect.
230
+ "Resolve.GetKeyboardPresetList": "21.1",
231
+ "Resolve.GetCurrentKeyboardPreset": "21.1",
232
+ "Project.GetProjectSettingsPresetList": "21.1",
233
+ "Project.GetAudioRenderFormats": "21.1",
234
+ "Project.GetAudioRenderCodecs": "21.1",
235
+ "Timeline.GetNormalizeAudioModes": "21.1",
236
+ "Timeline.GetOutputBlanking": "21.1",
237
+ "TimelineItem.GetSpeed": "21.1",
238
+ "TimelineItem.GetFades": "21.1",
239
+ "TimelineItem.GetOutputBlanking": "21.1",
240
+ "TimelineItem.GetUseTimelineForOutputBlanking": "21.1",
241
+
228
242
  "MediaPoolItem.LinkProxyMedia": "17.0",
229
243
  "MediaPoolItem.LinkFullResolutionMedia": "20.0",
230
244
  "MediaPoolItem.MonitorGrowingFile": "20.0",