davinci-resolve-mcp 4.12.0 → 4.13.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,60 @@
2
2
 
3
3
  Release history for the DaVinci Resolve MCP Server. The latest release is summarized in the root README; older entries live here to keep the README focused.
4
4
 
5
+ ## What's New in v4.13.0 — Resolve 21.1 render-setting parity, `SetHighPriority(bool)`, and a project-destroying preset order
6
+
7
+ Lands the three items queued for @legionsound in #207. The 21.1 behaviour
8
+ below is **their measurement on Studio 21.1.0.14 / macOS**, recorded as
9
+ *reported*; this machine runs 19.1.3.7 and did not reproduce it.
10
+
11
+ ### New
12
+
13
+ - **`resolve_control set_high_priority` takes `high`** (alias
14
+ `high_priority`, default `true`), and there is a new granular
15
+ **`set_high_priority(high)`** tool. On 21.1+ it calls
16
+ `SetHighPriority(bool)`, which 21.1 types as script execution priority and
17
+ which is reversible there: `high=false` restores normal. The no-argument call
18
+ this server used to make returns `False` on 21.1 and does nothing. Builds
19
+ before 21.1 keep the no-argument call, and `high=false` is refused on them,
20
+ because they cannot lower priority without a restart. `high` is coerced
21
+ (`"false"` is false); anything that doesn't name a boolean is refused before
22
+ any call. No build has a getter, so results carry `readback: null` and claim
23
+ no readback. Registered as a write in both the destructive registry and the
24
+ lifecycle risk sets (MEDIUM).
25
+ - **Compound render settings accept `AudioFormat` and `UseUniqueFilenames`.**
26
+ The compound whitelist held 31 of the 33 typed `RenderSettings` fields and
27
+ reported these two as ignored; granular already forwarded them. A test now
28
+ checks the whitelist against every field of the bundled 21.1 stub's
29
+ `RenderSettings`. `UseUniqueFilenames` is coerced on every path (compound
30
+ `set_settings`, `safe_set_render_settings`, `prepare_render_job`, granular
31
+ `set_render_settings`), and an unrecognised value is refused with
32
+ `INVALID_RENDER_SETTING`. `AudioFormat` with `ExportVideo: true` warns,
33
+ since the stub documents it for audio-only renders.
34
+
35
+ ### API truth ledger
36
+
37
+ - **`Project.StartRendering` is state-dependent** (reported, 21.1.0.14):
38
+ freshly configured jobs sometimes return `False` and stay Ready, while an
39
+ identical later job completes. Direct official calls reproduced both
40
+ outcomes. Mapped to `render start`, so the fact rides on its results. Verify
41
+ job status and output; nothing retries automatically.
42
+ - **Project-settings presets mutated before a project's first `SaveProject`
43
+ destroy the project** (reported, 21.1.0.14, reproduced twice): saving a
44
+ preset, changing resolution, then updating, applying or deleting the preset,
45
+ then saving and closing. Every call returned `True` and SQLite
46
+ `integrity_check` said `ok`, but `LoadProject` then failed with "DaVinci
47
+ Resolve failed to open project". Save the project once before any preset
48
+ write, and prove it survived only by reloading it. No preset-write wrappers
49
+ ship yet; this entry lands first on purpose.
50
+ - The `Resolve.SetHighPriority` entry is now version-scoped: irreversible with
51
+ no argument before 21.1 (measured on 19.1.3.7), and a reversible boolean on
52
+ 21.1 (reported).
53
+
54
+ ### Counts
55
+
56
+ - Granular tools 389 → 390; compound stays at 37. Docs and generated agent
57
+ rules updated.
58
+
5
59
  ## What's New in v4.12.0 — Live subtitle presets and title text across Fusion title tools
6
60
 
7
61
  Contributed by @sidevconcept (#281). The live behaviour below rests on their
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-4.12.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.13.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-37%20(389%20full)-blue.svg)](#server-modes)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(390%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` | 37 | 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` | 389 | Power users who want one MCP tool per Resolve API method. |
136
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 390 | 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
 
@@ -410,7 +410,7 @@ The default server is a local stdio process launched by your MCP client; it does
410
410
 
411
411
  | Metric | Value |
412
412
  |--------|-------|
413
- | MCP Tools | **37** compound / **389** granular (live server) |
413
+ | MCP Tools | **37** compound / **390** granular (live server) |
414
414
  | Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
415
415
  | Kernel Actions | **136** guarded workflow actions across 9 compound tools |
416
416
  | 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-4.12.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.13.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-37%20(389%20full)-blue.svg)](#服务器模式)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(390%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
- > 本翻译对应 v4.12.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.13.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` | 37 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
91
- | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 389 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
91
+ | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 390 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
92
92
 
93
93
  除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
94
94
 
@@ -226,7 +226,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
226
226
 
227
227
  | 指标 | 数值 |
228
228
  |------|------|
229
- | MCP 工具 | **37** 复合 / **389** 细粒度(实时服务器) |
229
+ | MCP 工具 | **37** 复合 / **390** 细粒度(实时服务器) |
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
@@ -353,7 +353,7 @@ to the user as verified.
353
353
  | Mode | Entry point | Tool count | Use when |
354
354
  |---|---|---|---|
355
355
  | Compound (default) | `src/server.py` | 37 tools | Most workflows — keeps context lean |
356
- | Granular (full) | `src/server.py --full` | 389 tools | Power users needing one tool per API method |
356
+ | Granular (full) | `src/server.py --full` | 390 tools | Power users needing one tool per API method |
357
357
 
358
358
  Resolve 21.1 adds [twelve read-only discovery controls](reference/resolve211-read-controls.md)
359
359
  for edition, presets, audio formats/codecs, normalization modes, speed, fades
@@ -796,6 +796,12 @@ Key actions:
796
796
  is fine. No connection needed, and it never launches Resolve
797
797
  - `get_page` / `open_page(page)` — read or switch the active page
798
798
  - `get_keyframe_mode` / `set_keyframe_mode(mode)`
799
+ - `set_high_priority(high?)` — script execution priority (granular:
800
+ `set_high_priority`). `high` defaults to true; on Resolve 21.1+ it calls
801
+ `SetHighPriority(bool)` and `high=false` restores normal. Older builds take no
802
+ argument and cannot lower it again this session, so `high=false` is refused
803
+ there. No build has a getter — report the native return, never a confirmed
804
+ state. Only call when the user asked for it
799
805
  - `get_fairlight_presets` — Resolve 20.2.2+; returns available Fairlight
800
806
  preset names
801
807
  - `list/save/load/delete/import/export_user_preferences_preset` — Resolve
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` | 37 | Most users — fast, clean, low context usage |
147
- | **Full** | `src/resolve_mcp_server.py` | 389 | Power users who want one tool per API method |
147
+ | **Full** | `src/resolve_mcp_server.py` | 390 | 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 389-tool server
162
+ python src/server.py --full # Launch full 390-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 **37 tools** that group related operations by
27
27
  action parameter, keeping LLM context windows lean. The full granular server
28
- provides **389 individual tools** for power users. The legacy coverage spans
28
+ provides **390 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
 
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 42 missing capabilities, 56 bugs / unreliable behaviors.
15
+ **Totals:** 42 missing capabilities, 58 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
@@ -288,13 +288,14 @@ equivalent, blocking full automation.
288
288
  - **Workaround / current handling:** Record interpolation choices in the caller; readback is not available at any version.
289
289
  - **Tags:** timeline, missing-method, readback, keyframes
290
290
 
291
- ### Resolve.SetHighPriority (write-only, irreversible per session)
291
+ ### Resolve.SetHighPriority (write-only; irreversible before 21.1)
292
292
 
293
293
  - **Object:** `Resolve`
294
- - **Signature:** `() -> Bool`
295
- - **Behavior:** Raises the Resolve process priority; there is no getter and no way to lower it again through the API (confirmed absent from dir() on Studio 19.1.3.7).
296
- - **Workaround / current handling:** Call it only when the user asked for a long render on a dedicated machine; state cannot be read back or undone without restarting Resolve.
297
- - **Tags:** app-control, missing-method, readback
294
+ - **Signature:** `() -> Bool (19.x); (highPriority: bool) -> bool (21.1 typed stub)`
295
+ - **Behavior:** VERSION-SCOPED. On Studio 19.1.3.7 (measured here) the method takes no argument and raises priority; there is no getter and no way to lower it again through the API (no getter in dir()), so it is irreversible for the session. Resolve 21.1 retyped it SetHighPriority(highPriority: bool) -> bool, documented as 'script execution priority to high or normal'. Reported by @legionsound (issue #207) from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 install). One live session, in order: (False) -> True, (True) -> True, () -> False, (1) -> False, ('high') -> False, (False) -> True. So on 21.1 the no-argument call is a NO-OP that reports its own failure, only a real bool is accepted (1 and 'high' are rejected like the missing argument), and the setting is REVERSIBLE: SetHighPriority(False) restores normal. There is still no getter on any build — dir() on the live 21.1 Resolve object shows SetHighPriority as the only priority-related name — so the current state cannot be read back on either generation.
296
+ - **Workaround / current handling:** On 21.1+ pass a real bool; never forward 1 or a string (coerce first — resolve_control set_high_priority and granular set_high_priority do, and refuse values that name no boolean). On older builds call it only when the user asked for a long scripted job on a dedicated machine: it cannot be lowered without restarting Resolve, which is why both tools refuse high=false there. No build offers readback, so report the native return, never a confirmed state.
297
+ - **Reference:** [issue #207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)
298
+ - **Tags:** app-control, missing-method, readback, version-gated, reported
298
299
 
299
300
  ### TimelineItem.CreateMagicMask (needs operator clicks)
300
301
 
@@ -796,6 +797,24 @@ values, or automation-hostile modal prompts.
796
797
  - **Workaround / current handling:** Any wrapper around ValidateDCTL must pass the native diagnostic through verbatim and must not reflow or rewrite the user's source to dodge it; ship the multi-line identity fixture as the known-good control. A 'no return value' error on a one-line function is this quirk, not a missing return — re-run the validation with the function laid out across lines before believing it. This server's own `dctl validate` is a static, offline check (entry point, brace balance, float suffixes) and does not call ValidateDCTL at all. The separate `dctl validate_native` and granular `validate_dctl_native` now expose the native validator without changing source or diagnostics. Both interfaces were contributor-validated against the three fixtures on Studio 21.1.0.14; see resolve211-dctl-validation.md.
797
798
  - **Tags:** dctl, validation, unreliable-return, version-gated, reported
798
799
 
800
+ ### Project.StartRendering
801
+
802
+ - **Object:** `Project`
803
+ - **Signature:** `(jobIds: list[str], isInteractiveMode=False) -> bool (21.1 typed stub)`
804
+ - **Behavior:** STATE-DEPENDENT RETURN. Reported by @legionsound (issue #207) from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 install). In one session, some freshly configured render jobs made StartRendering return False and remain Ready, while an identical job configured later in the same session started and completed. Official direct calls reproduced BOTH outcomes, so a False from one interface and a True from another in that sequence is not an interface difference. The trigger was not isolated; no root cause is established. The same session confirmed the two 21.1 RenderSettings keys the compound whitelist used to drop: UseUniqueFilenames=True gave queued filenames Resolve's '_V1-0001' suffix, and AudioFormat='wav' with LPCM produced complete 14-second stereo 48 kHz 24-bit WAVs whose decoded PCM matched byte for byte across both community interfaces.
805
+ - **Workaround / current handling:** Treat the boolean as a hint in both directions. After start, read render(action='get_job_status') and render(action='verify_output') for the actual output file before reporting a render as running or done; a False with the job still Ready means nothing rendered. Do not auto-retry in a wrapper: the measured later success was a NEW identical job, not a retry of the same one, so a loop would hide the state it depends on. Surface the False and let the caller decide whether to re-queue.
806
+ - **Reference:** [issue #207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)
807
+ - **Tags:** render, unreliable-return, state-dependent, version-gated, reported
808
+
809
+ ### Project settings presets mutated before the project's first SaveProject
810
+
811
+ - **Object:** `Project`
812
+ - **Signature:** `SaveCurrentProjectSettingsAsNewPreset / UpdateProjectSettingsPreset / SetProjectSettingsPreset / DeleteProjectSettingsPreset (presetName) -> bool (21.1+)`
813
+ - **Behavior:** DESTROYS THE PROJECT ITSELF, NOT A GRADE, AND REPORTS SUCCESS THROUGHOUT. Reported by @legionsound (issue #207) from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 install). On a NEW project that had never been saved: save a project-settings preset (SaveCurrentProjectSettingsAsNewPreset), change the timeline resolution, then update (UpdateProjectSettingsPreset), apply (SetProjectSettingsPreset) and delete (DeleteProjectSettingsPreset) the preset, then SaveProject and close. Every native call returned True. SQLite PRAGMA integrity_check on the project database still reported ok, but its Sm2MultiSysData table had ZERO rows, and LoadProject then failed with 'DaVinci Resolve failed to open project.' Reproduced twice, through community wrappers and through direct native calls. The control — the same sequence with one explicit SaveProject BEFORE any preset mutation — kept its Sm2MultiSysData row and reopened. Which single call in the sequence is responsible was not isolated, and preset Export/Import were not part of it, so do not assume any preset write is safe on an unsaved project. ProjectManager.GetProjectLastModifiedTime cannot serve as a 'has this project been saved' guard: it returned None even for saved projects on the same build.
814
+ - **Workaround / current handling:** Before ANY project-settings-preset write on 21.1, call ProjectManager.SaveProject() on the current project and require it to return True; never mutate presets on a project created in this session that has not been saved. Do not read the True returns, a clean integrity_check, or the absence of an error as evidence the project survived — the only proof is closing it and reloading it. A wrapper must not save silently on the caller's behalf (the first save is the caller's decision), so it should require an explicit assertion that the project has been saved, and refuse without one. If the project already holds work, back it up (export a .drp) before the first preset write.
815
+ - **Reference:** [issue #207](https://github.com/samuelgursky/davinci-resolve-mcp/issues/207)
816
+ - **Tags:** destructive, unrecoverable, project, order-dependent, silent-failure, unreliable-return, version-gated, reported
817
+
799
818
  ### TimelineItem.ExportLUT
800
819
 
801
820
  - **Object:** `TimelineItem`
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.12.0"
40
+ VERSION = "4.13.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 = "37 compound · 389 full · 3 platforms"
1546
+ subtitle = "37 compound · 390 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": "4.12.0",
3
+ "version": "4.13.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
91
91
  handlers=[logging.StreamHandler()],
92
92
  )
93
93
 
94
- VERSION = "4.12.0"
94
+ VERSION = "4.13.0"
95
95
  logger = logging.getLogger("davinci-resolve-mcp")
96
96
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
97
97
  logger.info(f"Detected platform: {get_platform()}")
@@ -1,6 +1,7 @@
1
1
  """Project, render, cache, cloud, and project-property tools."""
2
2
 
3
3
  from src.granular.common import * # noqa: F401,F403
4
+ from src.utils.bool_params import recognised_bool
4
5
  from src.utils.page_lock import restoring_page
5
6
 
6
7
  resolve = ResolveProxy()
@@ -1109,6 +1110,11 @@ def get_render_job_list() -> Dict[str, Any]:
1109
1110
  def start_rendering_jobs(job_ids: Optional[List[str]] = None, is_interactive_mode: bool = False) -> Dict[str, Any]:
1110
1111
  """Start rendering jobs. If no job IDs specified, renders all queued jobs.
1111
1112
 
1113
+ StartRendering is state-dependent on Resolve 21.1 (api_truth
1114
+ 'Project.StartRendering'): a freshly configured job can return False and
1115
+ stay Ready while an identical later job completes. Check job status and the
1116
+ output file afterwards; this tool does not retry.
1117
+
1112
1118
  Args:
1113
1119
  job_ids: Optional list of job IDs to render. If None, renders all.
1114
1120
  is_interactive_mode: If True, enables interactive rendering mode.
@@ -1224,8 +1230,19 @@ def set_render_settings(settings: Dict[str, Any]) -> Dict[str, Any]:
1224
1230
  ExportAlpha (bool), EncodingProfile (str), MultiPassEncode (bool),
1225
1231
  AlphaMode (int), NetworkOptimization (bool), ClipStartFrame (int),
1226
1232
  TimelineStartTimecode (str), ReplaceExistingFilesInPlace (bool),
1227
- ExportSubtitle (bool), SubtitleFormat ("BurnIn", "EmbeddedCaptions", "SeparateFile").
1233
+ ExportSubtitle (bool), SubtitleFormat ("BurnIn", "EmbeddedCaptions", "SeparateFile"),
1234
+ and the Resolve 21.1 typed keys UseUniqueFilenames (bool; "true"/"false"
1235
+ strings are coerced, anything else is refused before the call) and
1236
+ AudioFormat (audio file extension, e.g. "wav"; documented for
1237
+ ExportVideo false). A True return means the settings were taken, not
1238
+ that a render will succeed: verify job status and the output file
1239
+ after starting (see api_truth 'Project.StartRendering').
1228
1240
  """
1241
+ if isinstance(settings, dict) and "UseUniqueFilenames" in settings:
1242
+ coerced = recognised_bool(settings["UseUniqueFilenames"])
1243
+ if coerced is None:
1244
+ return {"error": "UseUniqueFilenames must be a boolean (true/false)"}
1245
+ settings = dict(settings, UseUniqueFilenames=coerced)
1229
1246
  resolve = get_resolve()
1230
1247
  if resolve is None:
1231
1248
  return {"error": "Not connected to DaVinci Resolve"}
@@ -3,6 +3,7 @@
3
3
  from src.granular.common import * # noqa: F401,F403
4
4
  import os
5
5
  from src.utils import typed_api_search
6
+ from src.utils.app_control import set_high_priority as set_resolve_high_priority
6
7
 
7
8
  resolve = ResolveProxy()
8
9
 
@@ -316,6 +317,28 @@ def disable_background_tasks_for_current_session() -> Dict[str, Any]:
316
317
  return {"success": True}
317
318
 
318
319
 
320
+ @mcp.tool()
321
+ @granular_destructive_op()
322
+ def set_high_priority(high: bool = True) -> Dict[str, Any]:
323
+ """Set Resolve's script execution priority to high or normal.
324
+
325
+ Granular counterpart of resolve_control set_high_priority. On Resolve
326
+ 21.1+ this calls SetHighPriority(bool) and is reversible (high=False
327
+ restores normal; reported on Studio 21.1.0.14, where the old no-argument
328
+ call is a no-op that returns False). Older builds take no argument and can
329
+ only raise priority for the session, so high=False is refused there. No
330
+ build has a getter, so nothing is read back.
331
+
332
+ Args:
333
+ high: True for high priority (default), False for normal. "true"/"false"
334
+ strings are coerced; anything else is refused before the call.
335
+ """
336
+ resolve = get_resolve()
337
+ if resolve is None:
338
+ return {"error": "Not connected to DaVinci Resolve"}
339
+ return set_resolve_high_priority(resolve, high)
340
+
341
+
319
342
  @mcp.resource("resolve://app/state")
320
343
  def get_app_state_endpoint() -> Dict[str, Any]:
321
344
  """Get DaVinci Resolve application state information."""
@@ -35,7 +35,7 @@ if __name__ == "__main__":
35
35
  try:
36
36
  connect_at_startup()
37
37
  start_background_update_check(VERSION, project_dir, logger)
38
- logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (389 granular tools)")
38
+ logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (390 granular tools)")
39
39
  run_fastmcp_stdio(mcp)
40
40
  except KeyboardInterrupt:
41
41
  logger.info("Server shutdown requested")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "4.12.0"
14
+ VERSION = "4.13.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -79,7 +79,11 @@ from src.utils.proc import safe_run
79
79
  from src.utils.readback import verify_by_readback, verification_stats as _verification_stats
80
80
  from src.utils import operation_result as _operation_result
81
81
  from src.utils import operation_log as _operation_log
82
- from src.utils.bool_params import coerce_bool as _coerce_bool, explicit_bool_param as _explicit_bool_param
82
+ from src.utils.bool_params import (
83
+ coerce_bool as _coerce_bool,
84
+ explicit_bool_param as _explicit_bool_param,
85
+ recognised_bool as _recognised_bool,
86
+ )
83
87
  from src.utils.confirm_tokens import (
84
88
  ConfirmTokenStore as _ConfirmTokenStore,
85
89
  gate_required_from as _gate_required_from,
@@ -17092,7 +17096,13 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17092
17096
  — one 'Class.Method' or one TypedDict name. No connection needed.
17093
17097
  api_surface() -> {methods, option_types, option_fields, objects}
17094
17098
  — what the shipped stub contains. No connection needed.
17095
- set_high_priority() -> {success}
17099
+ set_high_priority(high?) -> {success, high_priority, signature, reversible, readback: null}
17100
+ — script execution priority. high: true (default, the old meaning)
17101
+ or false for normal. Resolve 21.1+ calls SetHighPriority(bool) and
17102
+ is reversible (reported on 21.1.0.14: the old no-argument call is a
17103
+ no-op there). Older builds take no argument and can only RAISE it
17104
+ for the session, so high=false is refused there. No getter exists
17105
+ on any build: nothing is read back.
17096
17106
  disable_background_tasks_for_current_session() -> {success} — Resolve 21+
17097
17107
  list_user_preferences_presets() -> {presets} — Resolve 21.0.4+
17098
17108
  save_user_preferences_preset(name) -> {success} — Resolve 21.0.4+
@@ -17537,7 +17547,23 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17537
17547
  return missing
17538
17548
  return {"presets": _ser(r.GetFairlightPresets())}
17539
17549
  elif action == "set_high_priority":
17540
- return {"success": bool(r.SetHighPriority())}
17550
+ from src.utils.app_control import set_high_priority as _set_high_priority
17551
+ # `high` defaults to True so a bare call keeps its old meaning; the
17552
+ # helper picks SetHighPriority(bool) on 21.1+ and the no-arg form below.
17553
+ # The raw value goes to the helper, which coerces it with
17554
+ # recognised_bool and REFUSES a value naming no boolean, rather than
17555
+ # _coerce_bool's fall-back-to-default (that would turn "hgih" into True).
17556
+ _high_key = next((k for k in ("high", "high_priority") if k in p), None)
17557
+ _hp = _set_high_priority(r, p[_high_key] if _high_key else True)
17558
+ if _hp.get("refused"):
17559
+ return _err(
17560
+ _hp["error"],
17561
+ code="HIGH_PRIORITY_REFUSED",
17562
+ category="invalid_input",
17563
+ reason="Nothing was sent to Resolve.SetHighPriority.",
17564
+ state={k: v for k, v in _hp.items() if k not in ("error", "refused", "success")},
17565
+ )
17566
+ return _hp
17541
17567
  elif action == "disable_background_tasks_for_current_session":
17542
17568
  missing = _requires_method(r, "DisableBackgroundTasksForCurrentResolveSession", "21.0")
17543
17569
  if missing:
@@ -20300,6 +20326,7 @@ _RENDER_SETTING_KEYS = [
20300
20326
  "MarkOut",
20301
20327
  "TargetDir",
20302
20328
  "CustomName",
20329
+ "UseUniqueFilenames",
20303
20330
  "UniqueFilenameStyle",
20304
20331
  "ExportVideo",
20305
20332
  "ExportAudio",
@@ -20308,6 +20335,7 @@ _RENDER_SETTING_KEYS = [
20308
20335
  "FrameRate",
20309
20336
  "PixelAspectRatio",
20310
20337
  "VideoQuality",
20338
+ "AudioFormat",
20311
20339
  "AudioCodec",
20312
20340
  "AudioBitDepth",
20313
20341
  "AudioSampleRate",
@@ -20560,6 +20588,12 @@ def _render_settings_warnings(settings: Dict[str, Any], version_major: Optional[
20560
20588
  "AddFrameHandles is ignored while UseFullExtents is true: Resolve renders the "
20561
20589
  "clip's full extents and the handle count silently does nothing. Drop one of the two."
20562
20590
  )
20591
+ if "AudioFormat" in settings and settings.get("ExportVideo") is True:
20592
+ warnings.append(
20593
+ "AudioFormat is documented for audio-only renders (ExportVideo false) in the "
20594
+ "Resolve 21.1 typed stub; with ExportVideo true the container comes from the "
20595
+ "render format instead. Verify the output's container before relying on it."
20596
+ )
20563
20597
  if ("ExportSubtitle" in settings or "SubtitleFormat" in settings) and (
20564
20598
  version_major is not None and version_major < 21
20565
20599
  ):
@@ -20574,11 +20608,40 @@ def _render_settings_warnings(settings: Dict[str, Any], version_major: Optional[
20574
20608
  return warnings
20575
20609
 
20576
20610
 
20611
+ #: RenderSettings keys typed `bool` in the 21.1 stub that are COERCED rather than
20612
+ #: required to be a JSON boolean: an MCP client that sends "false" means false,
20613
+ #: and forwarding the string would hand Resolve a truthy value. Only keys added
20614
+ #: alongside this helper are listed; the older bool keys keep their strict check.
20615
+ _RENDER_COERCED_BOOL_KEYS = ("UseUniqueFilenames",)
20616
+
20617
+
20618
+ def _coerce_render_bool_settings(settings: Any):
20619
+ """Return ``(settings_copy, errors)`` with the coerced bool keys normalised.
20620
+
20621
+ A value that does not unambiguously name a boolean is an error, never a
20622
+ default: ``UseUniqueFilenames: "maybe"`` must not quietly become False.
20623
+ """
20624
+ if not isinstance(settings, dict):
20625
+ return settings, []
20626
+ out = dict(settings)
20627
+ errors = []
20628
+ for key in _RENDER_COERCED_BOOL_KEYS:
20629
+ if key not in out:
20630
+ continue
20631
+ coerced = _recognised_bool(out[key])
20632
+ if coerced is None:
20633
+ errors.append(f"{key} must be a boolean (true/false)")
20634
+ else:
20635
+ out[key] = coerced
20636
+ return out, errors
20637
+
20638
+
20577
20639
  def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_target: bool = False):
20578
20640
  if not isinstance(settings, dict) or not settings:
20579
20641
  return None, _err("settings must be a non-empty object")
20642
+ settings, coerce_errors = _coerce_render_bool_settings(settings)
20580
20643
  unknown = sorted(key for key in settings if key not in _RENDER_SETTING_KEYS)
20581
- errors = []
20644
+ errors = list(coerce_errors)
20582
20645
  target_dir = settings.get("TargetDir")
20583
20646
  if target_dir is not None:
20584
20647
  if not isinstance(target_dir, str) or not target_dir:
@@ -20597,6 +20660,8 @@ def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_
20597
20660
  errors.append(f"{key} must be a boolean")
20598
20661
  if "AddFrameHandles" in settings and isinstance(settings["AddFrameHandles"], int) and settings["AddFrameHandles"] < 0:
20599
20662
  errors.append("AddFrameHandles must be >= 0")
20663
+ if "AudioFormat" in settings and (not isinstance(settings["AudioFormat"], str) or not settings["AudioFormat"].strip()):
20664
+ errors.append("AudioFormat must be a non-empty string (an audio file extension, e.g. 'wav')")
20600
20665
  if "DataBurnIn" in settings and not isinstance(settings["DataBurnIn"], str):
20601
20666
  errors.append("DataBurnIn must be a string (a burn-in preset name, 'Same as project', or 'None')")
20602
20667
  if "MarkIn" in settings and "MarkOut" in settings and settings["MarkOut"] < settings["MarkIn"]:
@@ -20641,6 +20706,7 @@ def _safe_set_render_settings(proj, p: Dict[str, Any]):
20641
20706
  return {"success": False, "validation": validation}
20642
20707
  if _coerce_bool(p.get("dry_run")):
20643
20708
  return _ok(validation=validation)
20709
+ settings = validation["settings"] # coerced bool keys, see _coerce_render_bool_settings
20644
20710
  before = _render_settings_snapshot(proj)
20645
20711
  success = bool(proj.SetRenderSettings(settings))
20646
20712
  after_settings = _ser(proj.GetRenderSettings()) if _has_method(proj, "GetRenderSettings") else {}
@@ -20675,6 +20741,7 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
20675
20741
  return {"success": False, "validation": validation}
20676
20742
  if _coerce_bool(p.get("dry_run")):
20677
20743
  return _ok(validation=validation, format=p.get("format"), codec=p.get("codec"))
20744
+ settings = validation["settings"] # coerced bool keys, see _coerce_render_bool_settings
20678
20745
  before = _render_settings_snapshot(proj)
20679
20746
  # Pin the base render state before layering explicit settings on top. Without
20680
20747
  # this the job inherits the Deliver page's loaded preset for every key the
@@ -21096,6 +21163,10 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
21096
21163
  collapse warning unless the stated expectation matches the mark range.
21097
21164
  Verify BEFORE deleting the job; deleted jobs carry no TargetDir.
21098
21165
  start(job_ids?, interactive?) -> {success}
21166
+ StartRendering is state-dependent on 21.1 (api_truth
21167
+ 'Project.StartRendering'): a freshly configured job can return False
21168
+ and stay Ready while an identical later job completes. Read the job
21169
+ status and the output; this action does not retry.
21099
21170
  stop() -> {success}
21100
21171
  is_rendering() -> {rendering}
21101
21172
  get_formats() -> {formats}
@@ -21118,6 +21189,14 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
21118
21189
  burn-in, sidecar, or caption track — so setting them on a pre-21
21119
21190
  host comes back in warnings; verify the output before claiming
21120
21191
  caption delivery.
21192
+ Resolve 21.1 typed keys: UseUniqueFilenames (bool; "true"/"false"
21193
+ strings are coerced, anything else is refused before the call) and
21194
+ AudioFormat (audio file extension, e.g. "wav" — the stub documents it
21195
+ for ExportVideo false, so pairing it with ExportVideo true warns).
21196
+ Reported on Studio 21.1.0.14: UseUniqueFilenames=true gave queued
21197
+ files a "_V1-0001" suffix, and AudioFormat "wav" + LPCM rendered
21198
+ 48 kHz/24-bit WAVs. A True here only means the settings were taken;
21199
+ verify the job (get_job_status / verify_output) after start.
21121
21200
  list_presets() -> {presets}
21122
21201
  load_preset(name) -> {success}
21123
21202
  save_preset(name) -> {success}
@@ -21427,6 +21506,14 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
21427
21506
  # Whitelist to documented keys; SetRenderSettings silently ignores unknown
21428
21507
  # keys, so a typo'd setting would be dropped with no signal (P1 #6).
21429
21508
  settings, ignored_settings = _filter_to_keys(p["settings"], _RENDER_SETTING_KEYS)
21509
+ settings, bool_errors = _coerce_render_bool_settings(settings)
21510
+ if bool_errors:
21511
+ return _err(
21512
+ "; ".join(bool_errors),
21513
+ code="INVALID_RENDER_SETTING",
21514
+ category="invalid_input",
21515
+ reason="Nothing was sent to SetRenderSettings.",
21516
+ )
21430
21517
  result = {"success": bool(proj.SetRenderSettings(settings))}
21431
21518
  if ignored_settings:
21432
21519
  result["ignored_settings"] = ignored_settings
@@ -2955,17 +2955,40 @@ API_TRUTH: List[Dict[str, Any]] = [
2955
2955
  "submit": "missing",
2956
2956
  },
2957
2957
  {
2958
- "symbol": "Resolve.SetHighPriority (write-only, irreversible per session)",
2958
+ "symbol": "Resolve.SetHighPriority (write-only; irreversible before 21.1)",
2959
2959
  "object": "Resolve",
2960
- "signature": "() -> Bool",
2961
- "reality": "Raises the Resolve process priority; there is no getter "
2962
- "and no way to lower it again through the API (confirmed "
2963
- "absent from dir() on Studio 19.1.3.7).",
2964
- "recommended": "Call it only when the user asked for a long render on "
2965
- "a dedicated machine; state cannot be read back or "
2966
- "undone without restarting Resolve.",
2967
- "tags": ["app-control", "missing-method", "readback"],
2960
+ "signature": "() -> Bool (19.x); (highPriority: bool) -> bool (21.1 typed stub)",
2961
+ "reality": "VERSION-SCOPED. On Studio 19.1.3.7 (measured here) the "
2962
+ "method takes no argument and raises priority; there is no "
2963
+ "getter and no way to lower it again through the API "
2964
+ "(no getter in dir()), so it is irreversible for the session. "
2965
+ "Resolve 21.1 retyped it SetHighPriority(highPriority: bool) "
2966
+ "-> bool, documented as 'script execution priority to high or "
2967
+ "normal'. Reported by @legionsound (issue #207) from Studio "
2968
+ "21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 "
2969
+ "install). One live session, in order: (False) -> True, "
2970
+ "(True) -> True, () -> False, (1) -> False, ('high') -> False, "
2971
+ "(False) -> True. So on 21.1 the no-argument call is a NO-OP "
2972
+ "that reports its own failure, only a real bool is accepted "
2973
+ "(1 and 'high' are rejected like the missing argument), and "
2974
+ "the setting is REVERSIBLE: SetHighPriority(False) restores "
2975
+ "normal. There is still no getter on any build — dir() on the "
2976
+ "live 21.1 Resolve object shows SetHighPriority as the only "
2977
+ "priority-related name — so the current state cannot be read "
2978
+ "back on either generation.",
2979
+ "recommended": "On 21.1+ pass a real bool; never forward 1 or a string "
2980
+ "(coerce first — resolve_control set_high_priority and "
2981
+ "granular set_high_priority do, and refuse values that "
2982
+ "name no boolean). On older builds call it only when the "
2983
+ "user asked for a long scripted job on a dedicated "
2984
+ "machine: it cannot be lowered without restarting "
2985
+ "Resolve, which is why both tools refuse high=false "
2986
+ "there. No build offers readback, so report the native "
2987
+ "return, never a confirmed state.",
2988
+ "tags": ["app-control", "missing-method", "readback", "version-gated", "reported"],
2968
2989
  "submit": "missing",
2990
+ "issue": 207,
2991
+ "mitigation": ["resolve_control.set_high_priority", "set_high_priority"],
2969
2992
  },
2970
2993
  {
2971
2994
  "symbol": "Project.IsRenderingInProgress (stuck True after deleting the rendering project)",
@@ -3471,6 +3494,86 @@ API_TRUTH: List[Dict[str, Any]] = [
3471
3494
  "tags": ["dctl", "validation", "unreliable-return", "version-gated", "reported"],
3472
3495
  "submit": "bug",
3473
3496
  },
3497
+ {
3498
+ "symbol": "Project.StartRendering",
3499
+ "object": "Project",
3500
+ "signature": "(jobIds: list[str], isInteractiveMode=False) -> bool (21.1 typed stub)",
3501
+ "reality": "STATE-DEPENDENT RETURN. Reported by @legionsound (issue #207) "
3502
+ "from Studio 21.1.0.14 on macOS, 2026-09-09; NOT reproduced "
3503
+ "here (no 21.1 install). In one session, some freshly "
3504
+ "configured render jobs made StartRendering return False and "
3505
+ "remain Ready, while an identical job configured later in the "
3506
+ "same session started and completed. Official direct calls "
3507
+ "reproduced BOTH outcomes, so a False from one interface and a "
3508
+ "True from another in that sequence is not an interface "
3509
+ "difference. The trigger was not isolated; no root cause is "
3510
+ "established. The same session confirmed the two 21.1 "
3511
+ "RenderSettings keys the compound whitelist used to drop: "
3512
+ "UseUniqueFilenames=True gave queued filenames Resolve's "
3513
+ "'_V1-0001' suffix, and AudioFormat='wav' with LPCM produced "
3514
+ "complete 14-second stereo 48 kHz 24-bit WAVs whose decoded PCM "
3515
+ "matched byte for byte across both community interfaces.",
3516
+ "recommended": "Treat the boolean as a hint in both directions. After "
3517
+ "start, read render(action='get_job_status') and "
3518
+ "render(action='verify_output') for the actual output file "
3519
+ "before reporting a render as running or done; a False "
3520
+ "with the job still Ready means nothing rendered. Do not "
3521
+ "auto-retry in a wrapper: the measured later success was a "
3522
+ "NEW identical job, not a retry of the same one, so a loop "
3523
+ "would hide the state it depends on. Surface the False and "
3524
+ "let the caller decide whether to re-queue.",
3525
+ "tags": ["render", "unreliable-return", "state-dependent", "version-gated", "reported"],
3526
+ "submit": "bug",
3527
+ "issue": 207,
3528
+ "verified_on": "DaVinci Resolve Studio 21.1.0.14",
3529
+ },
3530
+ {
3531
+ "symbol": "Project settings presets mutated before the project's first SaveProject",
3532
+ "object": "Project",
3533
+ "signature": "SaveCurrentProjectSettingsAsNewPreset / UpdateProjectSettingsPreset / "
3534
+ "SetProjectSettingsPreset / DeleteProjectSettingsPreset (presetName) "
3535
+ "-> bool (21.1+)",
3536
+ "reality": "DESTROYS THE PROJECT ITSELF, NOT A GRADE, AND REPORTS SUCCESS "
3537
+ "THROUGHOUT. Reported by @legionsound (issue #207) from Studio "
3538
+ "21.1.0.14 on macOS, 2026-09-09; NOT reproduced here (no 21.1 "
3539
+ "install). On a NEW project that had never been saved: save a "
3540
+ "project-settings preset (SaveCurrentProjectSettingsAsNewPreset), "
3541
+ "change the timeline resolution, then update "
3542
+ "(UpdateProjectSettingsPreset), apply (SetProjectSettingsPreset) "
3543
+ "and delete (DeleteProjectSettingsPreset) the preset, then "
3544
+ "SaveProject and close. Every native call returned True. "
3545
+ "SQLite PRAGMA integrity_check on the project database still "
3546
+ "reported ok, but its Sm2MultiSysData table had ZERO rows, and "
3547
+ "LoadProject then failed with 'DaVinci Resolve failed to open "
3548
+ "project.' Reproduced twice, through community wrappers and "
3549
+ "through direct native calls. The control — the same sequence "
3550
+ "with one explicit SaveProject BEFORE any preset mutation — kept "
3551
+ "its Sm2MultiSysData row and reopened. Which single call in the "
3552
+ "sequence is responsible was not isolated, and preset "
3553
+ "Export/Import were not part of it, so do not assume any preset "
3554
+ "write is safe on an unsaved project. "
3555
+ "ProjectManager.GetProjectLastModifiedTime cannot serve as a "
3556
+ "'has this project been saved' guard: it returned None even for "
3557
+ "saved projects on the same build.",
3558
+ "recommended": "Before ANY project-settings-preset write on 21.1, call "
3559
+ "ProjectManager.SaveProject() on the current project and "
3560
+ "require it to return True; never mutate presets on a "
3561
+ "project created in this session that has not been saved. "
3562
+ "Do not read the True returns, a clean integrity_check, or "
3563
+ "the absence of an error as evidence the project survived "
3564
+ "— the only proof is closing it and reloading it. A "
3565
+ "wrapper must not save silently on the caller's behalf "
3566
+ "(the first save is the caller's decision), so it should "
3567
+ "require an explicit assertion that the project has been "
3568
+ "saved, and refuse without one. If the project already "
3569
+ "holds work, back it up (export a .drp) before the first "
3570
+ "preset write.",
3571
+ "tags": ["destructive", "unrecoverable", "project", "order-dependent",
3572
+ "silent-failure", "unreliable-return", "version-gated", "reported"],
3573
+ "submit": "bug",
3574
+ "issue": 207,
3575
+ "verified_on": "DaVinci Resolve Studio 21.1.0.14",
3576
+ },
3474
3577
  # ── Measured on Studio 21.1.0.14, 2026-09-09 (trap-aware execution work) ──
3475
3578
  {
3476
3579
  "symbol": "TimelineItem.CopyGrades",
@@ -3714,6 +3817,10 @@ ACTION_SYMBOLS: Dict[Tuple[str, str], List[str]] = {
3714
3817
  ("fusion_comp", "add_modifier"): ["Tool.AddModifier", "Tool.AddModifier (NestControl inputs)"],
3715
3818
  ("fusion_comp", "add_keyframe"): ["Tool.AddModifier (NestControl inputs)"],
3716
3819
  ("project_manager", "safe_project_archive"): ["ProjectManager.ArchiveProject"],
3820
+ ("render", "start"): ["Project.StartRendering"],
3821
+ ("resolve_control", "set_high_priority"): [
3822
+ "Resolve.SetHighPriority (write-only; irreversible before 21.1)",
3823
+ ],
3717
3824
  }
3718
3825
 
3719
3826
 
@@ -335,3 +335,95 @@ def open_project_settings(resolve_obj) -> Dict[str, Any]:
335
335
  def open_preferences(resolve_obj) -> Dict[str, Any]:
336
336
  """Open the Preferences dialog. See `_open_dialog` for the result shape."""
337
337
  return _open_dialog(resolve_obj, "OpenPreferences", "Preferences")
338
+
339
+
340
+ # ── Script execution priority ───────────────────────────────────────────────
341
+ #
342
+ # Resolve 21.1 retyped `Resolve.SetHighPriority(highPriority: bool) -> bool`
343
+ # ("Sets the script execution priority to high or normal"). Reported by
344
+ # @legionsound on Studio 21.1.0.14 (issue #207): (False) -> True, (True) ->
345
+ # True, and the no-argument call this server used to make -> False, a no-op;
346
+ # (1) and ("high") also -> False. So on 21.1 the argument must be a real bool
347
+ # and the setting is reversible. On 19.1.3.7 the method takes no argument and
348
+ # there is no way to lower it again within the session. No build has a getter.
349
+
350
+ #: First build whose stub types the boolean argument.
351
+ HIGH_PRIORITY_TYPED_FLOOR = "21.1"
352
+
353
+ _NO_READBACK_NOTE = (
354
+ "Resolve exposes no getter for script execution priority on any build, so "
355
+ "the state cannot be read back; success reflects the native return only."
356
+ )
357
+
358
+
359
+ def set_high_priority(resolve_obj, high: Any = True) -> Dict[str, Any]:
360
+ """Set script execution priority, choosing the call form by build.
361
+
362
+ `high` is coerced with `recognised_bool` (so "false" means False) and a
363
+ value that names no boolean is refused before any call: Resolve 21.1
364
+ returns a silent False for 1 or "high", so forwarding a guess would turn
365
+ the caller's mistake into a no-op. The native call always receives a real
366
+ bool. Returns a plain dict; a refusal carries `error` and `refused: True`
367
+ and means nothing was called.
368
+ """
369
+ from src.utils.bool_params import recognised_bool
370
+ from src.utils.resolve_versions import at_least
371
+
372
+ requested = recognised_bool(high)
373
+ if requested is None:
374
+ return {
375
+ "success": False,
376
+ "refused": True,
377
+ "error": "high must be a boolean (true for high priority, false for normal)",
378
+ }
379
+ try:
380
+ version = resolve_obj.GetVersion()
381
+ except Exception: # noqa: BLE001 — an unreadable version routes as unknown
382
+ version = None
383
+ typed = at_least(version, HIGH_PRIORITY_TYPED_FLOOR)
384
+ base: Dict[str, Any] = {
385
+ "high_priority": requested,
386
+ "resolve_version": version,
387
+ "readback": None,
388
+ "note": _NO_READBACK_NOTE,
389
+ }
390
+
391
+ def _typed_call() -> Dict[str, Any]:
392
+ ok = bool(resolve_obj.SetHighPriority(requested))
393
+ out = dict(base, success=ok, signature="SetHighPriority(bool)", reversible=True)
394
+ if not ok:
395
+ out["warning"] = (
396
+ "SetHighPriority returned False. On 21.1 that is the answer to a "
397
+ "rejected call; the priority is most likely unchanged, but it "
398
+ "cannot be read back to confirm."
399
+ )
400
+ return out
401
+
402
+ def _legacy_call() -> Dict[str, Any]:
403
+ if not requested:
404
+ return dict(
405
+ base,
406
+ success=False,
407
+ refused=True,
408
+ signature="SetHighPriority()",
409
+ reversible=False,
410
+ error=(
411
+ "This Resolve build takes SetHighPriority() with no argument and "
412
+ "can only RAISE priority; it cannot be lowered back to normal "
413
+ "without restarting Resolve (Resolve 21.1+ accepts a boolean). "
414
+ "Nothing was called."
415
+ ),
416
+ )
417
+ ok = bool(resolve_obj.SetHighPriority())
418
+ return dict(base, success=ok, signature="SetHighPriority()", reversible=False)
419
+
420
+ if typed is True:
421
+ return _typed_call()
422
+ if typed is False:
423
+ return _legacy_call()
424
+ # Version unreadable: try the typed form and fall back only on the
425
+ # signature mismatch an older build raises, never on a False return.
426
+ try:
427
+ return _typed_call()
428
+ except TypeError:
429
+ return _legacy_call()
@@ -31,3 +31,26 @@ def explicit_bool_param(params: Optional[Dict[str, Any]], *keys: str) -> Optiona
31
31
  if key in params:
32
32
  return coerce_bool(params[key])
33
33
  return None
34
+
35
+
36
+ def recognised_bool(value: Any) -> Optional[bool]:
37
+ """Coerce only values that unambiguously name a boolean, else None.
38
+
39
+ ``coerce_bool`` falls back to a default for anything it does not recognise,
40
+ which is right for an optional flag and wrong for a value that is forwarded
41
+ to Resolve as the setting itself: ``"maybe"`` must be refused, not silently
42
+ read as ``False``. Accepts real bools, the true/false strings
43
+ ``coerce_bool`` knows, and the integers 0 and 1. Everything else is None,
44
+ so the caller can refuse it with a reason.
45
+ """
46
+ if isinstance(value, bool):
47
+ return value
48
+ if isinstance(value, int) and value in (0, 1):
49
+ return bool(value)
50
+ if isinstance(value, str):
51
+ lowered = value.strip().lower()
52
+ if lowered in _TRUE_STRINGS:
53
+ return True
54
+ if lowered in _FALSE_STRINGS:
55
+ return False
56
+ return None
@@ -109,7 +109,10 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
109
109
  "project_settings": frozenset({"delete_color_group"}),
110
110
  "render": frozenset({"delete_all_jobs", "delete_job", "delete_preset"}),
111
111
  "render_presets": frozenset({"delete_burnin"}),
112
- "resolve_control": frozenset({"delete_user_preferences_preset"}),
112
+ # set_high_priority: an app-wide setting with no getter on any build, and
113
+ # irreversible for the session before 21.1 (issue #207). A write every gate
114
+ # must see, even though it touches no project content.
115
+ "resolve_control": frozenset({"delete_user_preferences_preset", "set_high_priority"}),
113
116
  "media_pool": frozenset({
114
117
  "delete_clips",
115
118
  "delete_folders",
@@ -282,6 +282,12 @@ class RiskClassificationHook(LifecycleHook):
282
282
  # wrappers refuse those unless acknowledge_trap. See archive_guard.py.
283
283
  ("project_manager", "archive"),
284
284
  ("project_manager", "safe_project_archive"),
285
+ # Script execution priority. App-wide, no getter on any build, and
286
+ # before 21.1 it cannot be lowered again within the session; on 21.1
287
+ # SetHighPriority(False) restores normal (reported, issue #207).
288
+ # MEDIUM: a real write to state nothing can read back, but no content
289
+ # is touched, so safe mode should not block it.
290
+ ("resolve_control", "set_high_priority"),
285
291
  # 21.0 AI deblur renders NEW media and never touches the source (it is
286
292
  # confirm-token gated for that reason). The `remove_` prefix rule rated
287
293
  # it HIGH on its name alone, which would make safe mode block a create.