davinci-resolve-mcp 2.80.1 → 2.81.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,138 @@
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.81.0
6
+
7
+ One render bug where every readback agreed and the file disagreed, plus the two
8
+ community skill contributions that were open against it.
9
+
10
+ ### Fixed
11
+
12
+ - **`prepare_render_job` inherited the Deliver page's loaded preset, and could
13
+ queue an mp4 that rendered with no video stream** (issue #123, reported with
14
+ a full measurement by @chenyuxiaojin). `SetRenderSettings` applies the keys a
15
+ caller passes *on top of* whatever render state the Deliver page is holding
16
+ rather than replacing it, and a loaded preset carries more state than those
17
+ keys. Measured 2026-07-08: after an MP3 render through the stock **Audio
18
+ Only** preset, a job queued with an explicit `ExportVideo: true` and an `.mp4`
19
+ target returned `settings_success: true` and a real `job_id`, `list_jobs`
20
+ reported `IsExportVideo: true`, and the rendered file held only an AAC stream.
21
+ The single visible tell was 18 minutes of material "rendering" in ~10 seconds.
22
+
23
+ No caller-side check could have caught it, and the reason is worse than the
24
+ bug itself: the scripting API documents neither `GetRenderSettings` nor
25
+ `GetCurrentRenderPresetName`, so the inherited state cannot be read at all.
26
+ Detection is unreachable; only pinning is.
27
+
28
+ ### Added
29
+
30
+ - **`from_preset` on `prepare_render_job`** (and through
31
+ `prepare_delivery_job`) runs `LoadRenderPreset` before the explicit settings
32
+ go on top, so a caller pins the base state instead of inheriting one.
33
+ `PresetName` flipping to `Custom` once the explicit settings land is expected.
34
+ The name is validated against `GetRenderPresetList` first, because
35
+ `LoadRenderPreset` refuses an unknown name with a bare `False` that is
36
+ indistinguishable from any other refusal — and a `False` of either kind now
37
+ refuses to queue rather than falling through to an inheriting render.
38
+ - **An inherited-state warning** when a job asks for `ExportVideo: true` without
39
+ a pin, naming the risk and saying plainly that the job readback is not a
40
+ witness for the rendered file — verify a `codec_type=video` stream before
41
+ reporting a deliverable. The `before` snapshot now also reports
42
+ `settings_readable: false` and what is unreadable, instead of leaving the gap
43
+ unnamed.
44
+ - **`resolve-tighten-recording` skill** (PR #126, @chenyuxiaojin) — the
45
+ subtractive counterpart to `resolve-rough-cut`: one long single-take recording
46
+ in, a tightened variant timeline out, original untouched. Measured live
47
+ against Studio 21.0.1.11 on a real 28.5-minute recording. Its centerpiece is
48
+ the coordinate-system trap between plan `keep_ranges` (source frames,
49
+ exclusive end) and `structural_diff.added` (record frames) — feed one where
50
+ the other is expected and every clip lands at the wrong moment of the right
51
+ file, with correct cut lengths and no error. Also documents the three classes
52
+ of content silence-driven tightening cannot hear, including the whisper
53
+ swallowed-retake blind spot (issue #125).
54
+
55
+ ### Changed
56
+
57
+ - **`resolve-rough-cut` reconciled with the `api_truth` ledger** (PR #115,
58
+ @bolnet). Two rows contradicted the ledger the skill itself points at. Import
59
+ order was backwards — `ImportMedia` has no destination parameter and always
60
+ lands in the *current* folder, so the bin must be created and made current
61
+ *before* importing. And the traps table still asserted that a comp attached to
62
+ a media clip "never renders", a blanket claim the ledger retracted on
63
+ 2026-08-02: a comp wired `MediaIn → Blur → MediaOut` does render, and an
64
+ unrooted `MediaOut` fails the render job outright rather than being silently
65
+ bypassed. Every row now names the build it was confirmed on, and a note
66
+ records that a **running** MCP keeps executing the version it started with, so
67
+ `git pull` does not refresh the ledger until restart.
68
+ - The skill index in `docs/README.md` now lists the two end-to-end assembly
69
+ recipes (`resolve-rough-cut`, `resolve-tighten-recording`), neither of which
70
+ had ever appeared there, and `resolve-edit` points at the tighten skill.
71
+
72
+ ## What's New in v2.80.2
73
+
74
+ Agent tooling only. Ten Claude Code skills that this repository has shipped and
75
+ advertised were never loading; they load now. No runtime behavior changed and
76
+ nothing under `src/` was touched.
77
+
78
+ ### Fixed
79
+
80
+ - **The ten `.claude/skills/` domain skills were invisible to every agent.**
81
+ Claude Code discovers skills at `.claude/skills/<name>/SKILL.md`. All ten were
82
+ loose `.md` files at the top level of that directory — a layout the loader
83
+ does not scan — so `resolve-color`, `resolve-edit`, `resolve-conform`,
84
+ `resolve-delivery`, `resolve-audio`, `resolve-fusion`, `resolve-media-pool`,
85
+ `resolve-media-analysis`, `resolve-rough-cut`, and `resolve-mcp` never
86
+ appeared in a session, while the generated domain-routing block in `AGENTS.md`
87
+ and the index in `docs/README.md` both listed them as available. The failure
88
+ was silent: no warning, no error, no degraded mode. Each skill now lives in a
89
+ directory named for its frontmatter `name` (recorded as 100% renames, so
90
+ history follows), and both `docs/README.md` and
91
+ `scripts/agent-rules/README.md` — the file consulted when authoring a new
92
+ skill — state the directory requirement so the next one is not written back
93
+ into the bug.
94
+
95
+ ### Added
96
+
97
+ - **Two opt-in `PreToolUse` guard scripts** for rules `AGENTS.md` has only ever
98
+ stated in prose. They ship as scripts and are deliberately *not* wired
99
+ repo-wide; `docs/README.md` carries the block to paste into a personal
100
+ gitignored `.claude/settings.local.json`. `frame_verification_guard.py`
101
+ refuses grade-applying actions on `timeline_item_color` until the session has
102
+ actually looked at a Resolve-rendered frame, and asks before `safe_copy_grade`
103
+ / `bulk_match_to_hero` push a whole-grade artifact across clips; `dry_run`
104
+ passes through. `source_media_guard.py` refuses shell commands that write,
105
+ move, or delete source media outside a scratch root — paths are normalized and
106
+ matched by whole path component, and a derivative-output directory
107
+ (`proxies`/`renders`/`exports`) exempts a *write* but never a delete or a
108
+ move, so a camera card with an `exports` folder is still a camera card. Its
109
+ docstring states what it cannot catch — extension-less directory deletes,
110
+ `find -delete`, `xargs rm`, and scripts that write media themselves — because
111
+ it is a tripwire for the common direct mistake, not a sandbox.
112
+ - **Two review subagents** in `.claude/agents/`, run in their own context so
113
+ frame images stay out of the main session. `cut-reviewer` screens an assembled
114
+ timeline from its frames and is told explicitly that a metadata summary is not
115
+ a review, because assembling through an API succeeds loudly and fails quietly.
116
+ `grade-match-verifier` measures shot match numerically against the project's
117
+ R−B tolerance and must report the pixel count behind every masked
118
+ measurement — a near-empty skin mask returns a delta near zero and reads as a
119
+ perfect match.
120
+ - **Two skills outside the domain routing.** `house-style` accumulates editorial
121
+ corrections so the same note is not given twice, and `/resolve-session`
122
+ reports connection, edition, project, timeline, and media-pool state before
123
+ editing begins.
124
+
125
+ ### Validation
126
+
127
+ - Full offline suite: 2460 passed, 1 skipped — level with the v2.80.1 baseline,
128
+ as expected for a change that touches no runtime code.
129
+ - Both guard scripts exercised against a 26-case matrix covering deny, ask, and
130
+ silent-allow: `ffprobe` reads, `ffmpeg` into scratch, `ffmpeg` overwriting a
131
+ card, chained `ffprobe && rm`, redirection onto a media file, glob `rm`,
132
+ quoted paths, writes into `renders`/`proxies`/`exports`, deletes out of those
133
+ same directories, `..` traversal, and ordinary repo commands (`git status`,
134
+ the test runner, `npm run build`).
135
+ - No live Resolve validation: no behavior changed.
136
+
5
137
  ## What's New in v2.80.1
6
138
 
7
139
  A correction to the retime measurement contract published in v2.80.0, and a fix
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.80.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.81.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#server-modes)
package/docs/README.md CHANGED
@@ -47,19 +47,96 @@ Per-domain skills in `.claude/skills/` route craft ↔ live tools ↔ offline
47
47
  advanced tools automatically when an agent works in that domain. They are thin
48
48
  bridges — the authoritative depth stays in the kernels and guides above.
49
49
 
50
- - `resolve-mcp` (`.claude/skills/resolve.md`) — orientation/index: the map to the domain skills below (self-trigger; not an auto-loader)
51
- - `resolve-color` (`.claude/skills/color-grade.md`) — grading, looks, shot match, LUT/CDL/DRX
52
- - `resolve-edit` (`.claude/skills/timeline-edit.md`) — cutting, ranges, variants, changelist
53
- - `resolve-conform` (`.claude/skills/conform.md`) — conform, relink, finishing QC, grade tracing
54
- - `resolve-delivery` (`.claude/skills/delivery.md`) — render, deliverable QC, media/provenance
55
- - `resolve-fusion` (`.claude/skills/fusion.md`) — Fusion comps (titles, motion graphics, VFX)
56
- - `resolve-audio` (`.claude/skills/audio.md`) — audio/Fairlight tracks, buses, loudness, sync
57
- - `resolve-media-pool` (`.claude/skills/media-pool.md`) — media pool ingest, organize, multicam
58
- - `resolve-media-analysis` (`.claude/skills/media-analysis.md`) — source-safe media intelligence
50
+ - `resolve-mcp` (`.claude/skills/resolve-mcp/SKILL.md`) — orientation/index: the map to the domain skills below (self-trigger; not an auto-loader)
51
+ - `resolve-color` (`.claude/skills/resolve-color/SKILL.md`) — grading, looks, shot match, LUT/CDL/DRX
52
+ - `resolve-edit` (`.claude/skills/resolve-edit/SKILL.md`) — cutting, ranges, variants, changelist
53
+ - `resolve-conform` (`.claude/skills/resolve-conform/SKILL.md`) — conform, relink, finishing QC, grade tracing
54
+ - `resolve-delivery` (`.claude/skills/resolve-delivery/SKILL.md`) — render, deliverable QC, media/provenance
55
+ - `resolve-fusion` (`.claude/skills/resolve-fusion/SKILL.md`) — Fusion comps (titles, motion graphics, VFX)
56
+ - `resolve-audio` (`.claude/skills/resolve-audio/SKILL.md`) — audio/Fairlight tracks, buses, loudness, sync
57
+ - `resolve-media-pool` (`.claude/skills/resolve-media-pool/SKILL.md`) — media pool ingest, organize, multicam
58
+ - `resolve-media-analysis` (`.claude/skills/resolve-media-analysis/SKILL.md`) — source-safe media intelligence
59
+
60
+ Each skill is a directory containing `SKILL.md`. Claude Code does not discover
61
+ loose `.md` files in `.claude/skills/`; a skill placed at the top level of that
62
+ directory silently never loads.
63
+
64
+ Two end-to-end assembly recipes sit alongside the domain skills. Where a domain
65
+ skill routes, these two walk a whole job:
66
+
67
+ - `resolve-rough-cut` (`.claude/skills/resolve-rough-cut/SKILL.md`) — **additive**:
68
+ select shots from a folder of many clips into an assembled timeline.
69
+ - `resolve-tighten-recording` (`.claude/skills/resolve-tighten-recording/SKILL.md`)
70
+ — **subtractive**: remove dead air from one long single-take recording.
71
+
72
+ Two more sit outside the domain routing:
73
+
74
+ - `house-style` (`.claude/skills/house-style/SKILL.md`) — accumulated editorial
75
+ corrections, so the same note is not given twice. Claude-only; append to it
76
+ when an editorial decision is corrected.
77
+ - `resolve-session` (`.claude/skills/resolve-session/SKILL.md`) — `/resolve-session`
78
+ connects, confirms edition and bridge, and reports project/timeline/pool state.
59
79
 
60
80
  The offline half of every one is the advanced server; see
61
81
  [Advanced Server](../resolve-advanced/README.md).
62
82
 
83
+ ## Claude Code Hooks and Subagents
84
+
85
+ Two `PreToolUse` guards enforce rules `AGENTS.md` states in prose. They ship as
86
+ scripts but are **not** wired up by default — the repository does not enable
87
+ hooks on your behalf. Opt in by adding the block below to your own
88
+ `.claude/settings.local.json` (gitignored, so it stays yours):
89
+
90
+ ```json
91
+ {
92
+ "hooks": {
93
+ "PreToolUse": [
94
+ {
95
+ "matcher": "mcp__davinci-resolve__(timeline_item_color|color_group)",
96
+ "hooks": [
97
+ {
98
+ "type": "command",
99
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/frame_verification_guard.py",
100
+ "timeout": 15
101
+ }
102
+ ]
103
+ },
104
+ {
105
+ "matcher": "Bash",
106
+ "hooks": [
107
+ {
108
+ "type": "command",
109
+ "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/source_media_guard.py",
110
+ "timeout": 15
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ }
117
+ ```
118
+
119
+ Hooks are read at session start, so restart Claude Code after adding them. The
120
+ two guards are:
121
+
122
+ - `.claude/hooks/frame_verification_guard.py` — denies grade-applying actions on
123
+ `timeline_item_color` until the session has actually looked at a
124
+ Resolve-rendered frame, and asks before whole-grade artifacts
125
+ (`safe_copy_grade`, `bulk_match_to_hero`) overwrite hand-work. `dry_run`
126
+ passes through untouched.
127
+ - `.claude/hooks/source_media_guard.py` — denies shell commands that write,
128
+ move, or delete source media outside a scratch root. Reads (`ffprobe`, and
129
+ `ffmpeg` writing into scratch) pass.
130
+
131
+ Two review subagents in `.claude/agents/` run in their own context so frame
132
+ images stay out of the main session:
133
+
134
+ - `cut-reviewer` — screens an assembled timeline from its frames and reports on
135
+ pacing, shot order, continuity, and coverage gaps.
136
+ - `grade-match-verifier` — measures shot match numerically from rendered frames
137
+ against the project's R−B tolerance, and reports mask pixel counts so an empty
138
+ skin mask cannot pass as a match.
139
+
63
140
  ## Authoring References
64
141
 
65
142
  - [Fuse + DCTL Authoring](authoring/fuse-dctl-authoring.md)
@@ -88,7 +88,7 @@ patches audio **offline, no Resolve running**:
88
88
 
89
89
  Rule of thumb: plan/measure offline, apply mix/track changes live; use
90
90
  `fairlight` for bus work the scripting API can't reach. See the `resolve-audio`
91
- skill (`.claude/skills/audio.md`) and the `/audio_workflow` prompt.
91
+ skill (`.claude/skills/resolve-audio/SKILL.md`) and the `/audio_workflow` prompt.
92
92
 
93
93
  ## Live Probe
94
94
 
@@ -134,7 +134,7 @@ Cross-server rules an agent must know:
134
134
  - **Deps.** The grading catalog needs `sharp`; call the advanced `capabilities`
135
135
  tool for live status and install hints.
136
136
 
137
- See the `resolve-color` skill (`.claude/skills/color-grade.md`) for the
137
+ See the `resolve-color` skill (`.claude/skills/resolve-color/SKILL.md`) for the
138
138
  craft ↔ live ↔ offline routing and the frame-first rule.
139
139
 
140
140
  ## Live Probe
@@ -119,7 +119,7 @@ running**, via the `fusion` tool:
119
119
  Rule of thumb: author and verify the comp offline, then apply it live with
120
120
  `fusion_comp` `safe_add_tool` → `safe_set_inputs` → `safe_connect_tools` (the
121
121
  `to_api_calls` output maps directly onto those). See the `resolve-fusion` skill
122
- (`.claude/skills/fusion.md`) and the `/fusion_workflow` prompt.
122
+ (`.claude/skills/resolve-fusion/SKILL.md`) and the `/fusion_workflow` prompt.
123
123
 
124
124
  ## Live Probe
125
125
 
@@ -137,7 +137,7 @@ actions):
137
137
  Rule of thumb: verify and inventory the card offline *before* importing, then
138
138
  import/organize live. `media` also serves the delivery side (see the
139
139
  `resolve-delivery` skill). See the `resolve-media-pool` skill
140
- (`.claude/skills/media-pool.md`) and the `/media_pool_workflow` prompt. Never
140
+ (`.claude/skills/resolve-media-pool/SKILL.md`) and the `/media_pool_workflow` prompt. Never
141
141
  rename or derive camera originals without explicit approval.
142
142
 
143
143
  ## Live Evidence
@@ -135,7 +135,7 @@ Rules an agent must know:
135
135
  - **Deps.** `deliverable`/`media` QC needs **ffmpeg + ffprobe on PATH** (GPL, not
136
136
  bundled) — call the advanced `capabilities` tool for status + install hints.
137
137
 
138
- See the `resolve-delivery` skill (`.claude/skills/delivery.md`) for the
138
+ See the `resolve-delivery` skill (`.claude/skills/resolve-delivery/SKILL.md`) for the
139
139
  craft ↔ live ↔ offline routing.
140
140
 
141
141
  ## Live Evidence
@@ -121,7 +121,7 @@ Gotchas the live path shares:
121
121
  - **Deps.** `better-sqlite3` gates lineage/reverse/DB; `sharp`/ffmpeg gate frame
122
122
  compare — call the advanced `capabilities` tool.
123
123
 
124
- See the `resolve-conform` skill (`.claude/skills/conform.md`) for the
124
+ See the `resolve-conform` skill (`.claude/skills/resolve-conform/SKILL.md`) for the
125
125
  craft ↔ live ↔ offline routing.
126
126
 
127
127
  ## Live Probe
@@ -196,7 +196,7 @@ Use these to answer "what changed between v3 and v4" or to hand a conform an
196
196
  accurate change list without opening either timeline. For conforming/relinking
197
197
  that change list, see the Timeline Conform / Interchange kernel and the
198
198
  `resolve-conform` skill; for the edit ↔ offline routing, see the `resolve-edit`
199
- skill (`.claude/skills/timeline-edit.md`).
199
+ skill (`.claude/skills/resolve-edit/SKILL.md`).
200
200
 
201
201
  ## Development Guardrails
202
202
 
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 25 missing capabilities, 28 bugs / unreliable behaviors.
15
+ **Totals:** 25 missing capabilities, 29 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
@@ -436,6 +436,15 @@ values, or automation-hostile modal prompts.
436
436
  - **Reference:** [issue #59](https://github.com/samuelgursky/davinci-resolve-mcp/issues/59)
437
437
  - **Tags:** render, deliver, silent-failure, id-vs-label
438
438
 
439
+ ### Project.SetRenderSettings (inherits the loaded preset)
440
+
441
+ - **Object:** `Project`
442
+ - **Signature:** `({settings}) -> bool`
443
+ - **Behavior:** SetRenderSettings applies the passed keys ON TOP of whatever render state the Deliver page is holding; it does not replace it. A loaded preset carries more state than the keys a caller passes, and that state survives. Measured 2026-07-08: after a render through the stock 'Audio Only' preset, a job queued with an explicit ExportVideo=True and an .mp4 target returned settings_success=True and a real job id, GetRenderJobList reported IsExportVideo=True, and the rendered .mp4 contained only an AAC stream with NO video stream (ffprobe) — 18 minutes of material 'rendered' in ~10 seconds. The job readback is therefore NOT a witness for the rendered file. There is also no way to detect the inherited state: the scripting API documents no GetRenderSettings and no GetCurrentRenderPresetName, so the base state can be pinned but never read.
444
+ - **Workaround / current handling:** Pin the base state instead of inheriting one — prepare_render_job(from_preset='<a video preset>') runs LoadRenderPreset before the explicit settings go on top (PresetName flips to 'Custom' once they do, which is expected). Then verify the OUTPUT, not the job: ffprobe for a codec_type=video stream. A long timeline that completes in seconds is the tell.
445
+ - **Reference:** [issue #123](https://github.com/samuelgursky/davinci-resolve-mcp/issues/123)
446
+ - **Tags:** render, deliver, silent-failure, preset, readback-lies
447
+
439
448
  ### ProjectManager.SaveProject
440
449
 
441
450
  - **Object:** `ProjectManager`
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.80.1"
39
+ VERSION = "2.81.0"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.80.1",
3
+ "version": "2.81.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -24,7 +24,7 @@ cannot drift.
24
24
  | Platform | File(s) | Trigger | Source |
25
25
  |---|---|---|---|
26
26
  | Codex / OpenCode / Zed / others | `AGENTS.md` (`## Domain Routing` block) | always-on | generated block |
27
- | Claude Code | `.claude/skills/*.md` | semantic (description) | hand-authored (rich) |
27
+ | Claude Code | `.claude/skills/<name>/SKILL.md` | semantic (description) | hand-authored (rich) |
28
28
  | Cursor | `.cursor/rules/*.mdc` | `alwaysApply` repo rule + per-domain `description` (agent-requested) | generated |
29
29
  | VS Code / Copilot | `.github/copilot-instructions.md` + `.github/instructions/*.instructions.md` | always-on + `applyTo` | generated |
30
30
  | Windsurf | `.windsurf/rules/*.md` + `.windsurfrules` | rules dir + legacy flat | generated |
@@ -33,7 +33,10 @@ cannot drift.
33
33
  | Continue | `.continue/rules/resolve-mcp.md` | always-on | generated |
34
34
  | Claude Desktop | — (chat client, no repo rules) | — | MCP prompts only |
35
35
 
36
- `.claude/skills/*` stays hand-authored (Claude's rich semantic skills); every
36
+ `.claude/skills/*` stays hand-authored (Claude's rich semantic skills). Each
37
+ one is a **directory** holding a `SKILL.md` named for its frontmatter `name`;
38
+ a loose `.md` at the top level of `.claude/skills/` is never discovered and
39
+ fails silently. Every
37
40
  other file is generated. `AGENTS.md` remains the universal backstop for any
38
41
  client not listed. Always-on rule dirs (Cline/Roo/Continue) get one compact
39
42
  combined file (repo hygiene + domain table), since they load every rule file
@@ -61,8 +64,8 @@ node scripts/agent-rules/generate.mjs --check # exit 1 if anything is stale (C
61
64
  ## Adding a domain or a platform
62
65
 
63
66
  - **New domain:** add an entry to `DOMAINS` in `generate.mjs`, add a matching
64
- `@mcp.prompt` in `src/server.py`, and (optionally) a rich `.claude/skills/*.md`.
65
- Regenerate.
67
+ `@mcp.prompt` in `src/server.py`, and (optionally) a rich
68
+ `.claude/skills/<name>/SKILL.md`. Regenerate.
66
69
  - **New platform:** add an `emit(...)` for its convention in `generate.mjs`,
67
70
  reusing `domainBody(d)` / `repoHygiene`. Regenerate. Do not hand-edit generated
68
71
  files — the drift guard will fail.
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.80.1"
88
+ VERSION = "2.81.0"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  logger.info(f"Detected platform: {get_platform()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.80.1"
14
+ VERSION = "2.81.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -16124,13 +16124,66 @@ def _render_settings_snapshot(proj):
16124
16124
  settings = _ser(proj.GetRenderSettings())
16125
16125
  else:
16126
16126
  settings = {"error": "GetRenderSettings unavailable"}
16127
- return {
16127
+ snapshot = {
16128
16128
  "format_and_codec": _ser(proj.GetCurrentRenderFormatAndCodec()),
16129
16129
  "mode": _ser(proj.GetCurrentRenderMode()),
16130
16130
  "settings": settings,
16131
16131
  "jobs": _ser(proj.GetRenderJobList() or []),
16132
16132
  "is_rendering": bool(proj.IsRenderingInProgress()),
16133
16133
  }
16134
+ # The Deliver page's loaded preset carries render state beyond the keys a
16135
+ # caller passes, and SetRenderSettings applies on TOP of it rather than
16136
+ # replacing it. The scripting API exposes no reader for either half — there
16137
+ # is no GetCurrentRenderPresetName, and GetRenderSettings is not in the
16138
+ # documented surface — so this snapshot cannot report what is being
16139
+ # inherited. Say that in the payload instead of leaving the gap unnamed.
16140
+ if not isinstance(settings, dict) or "error" in settings:
16141
+ snapshot["settings_readable"] = False
16142
+ snapshot["inherited_state"] = (
16143
+ "unreadable: no GetRenderSettings / GetCurrentRenderPresetName in the "
16144
+ "scripting API. Whatever preset the Deliver page holds survives into "
16145
+ "this job for every key not explicitly passed. Pass from_preset to pin "
16146
+ "the base state."
16147
+ )
16148
+ else:
16149
+ snapshot["settings_readable"] = True
16150
+ return snapshot
16151
+
16152
+
16153
+ def _render_preset_pin(proj, preset_name: str):
16154
+ """LoadRenderPreset with the name validated against the live preset list.
16155
+
16156
+ Returns (result_dict, err). LoadRenderPreset returns a bare False for a name
16157
+ that does not exist, which is indistinguishable from any other refusal, so
16158
+ the name is checked first and a miss names the available presets.
16159
+ """
16160
+ try:
16161
+ available = [str(x) for x in (proj.GetRenderPresetList() or [])]
16162
+ except Exception:
16163
+ available = []
16164
+ if available and preset_name not in available:
16165
+ return None, _err(
16166
+ f"Render preset not found: {preset_name}",
16167
+ code="RENDER_PRESET_NOT_FOUND",
16168
+ category="invalid_input",
16169
+ reason="LoadRenderPreset refuses an unknown name with a bare False; no job was queued.",
16170
+ remediation="Use render(action='list_presets') for the names this project carries.",
16171
+ state={"requested_preset": preset_name, "available_presets": available},
16172
+ )
16173
+ loaded = bool(proj.LoadRenderPreset(preset_name))
16174
+ if not loaded:
16175
+ return None, _err(
16176
+ f"Could not load render preset: {preset_name}",
16177
+ code="RENDER_PRESET_LOAD_FAILED",
16178
+ category="engine_refused",
16179
+ reason=(
16180
+ "LoadRenderPreset returned False. Refusing to queue: the job would "
16181
+ "otherwise inherit whatever unrelated state the Deliver page holds."
16182
+ ),
16183
+ remediation="Check render(action='list_presets'), then retry.",
16184
+ state={"requested_preset": preset_name, "available_presets": available},
16185
+ )
16186
+ return {"preset": preset_name, "loaded": True}, None
16134
16187
 
16135
16188
 
16136
16189
  def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_target: bool = False):
@@ -16230,6 +16283,16 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
16230
16283
  if p.get("dry_run"):
16231
16284
  return _ok(validation=validation, format=p.get("format"), codec=p.get("codec"))
16232
16285
  before = _render_settings_snapshot(proj)
16286
+ # Pin the base render state before layering explicit settings on top. Without
16287
+ # this the job inherits the Deliver page's loaded preset for every key the
16288
+ # caller does not pass — an Audio Only preset plus ExportVideo:True has been
16289
+ # measured to queue a job that reads back IsExportVideo:True and renders an
16290
+ # mp4 with no video stream (issue #123).
16291
+ preset_pin = None
16292
+ if p.get("from_preset"):
16293
+ preset_pin, err = _render_preset_pin(proj, str(p["from_preset"]))
16294
+ if err:
16295
+ return err
16233
16296
  format_success = None
16234
16297
  if p.get("format") and p.get("codec"):
16235
16298
  formats = _render_formats(proj)
@@ -16261,7 +16324,7 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
16261
16324
  )
16262
16325
  settings_success = bool(proj.SetRenderSettings(settings))
16263
16326
  job_id = proj.AddRenderJob() if settings_success else None
16264
- return {
16327
+ result = {
16265
16328
  "success": bool(job_id),
16266
16329
  "job_id": job_id,
16267
16330
  "format_success": format_success,
@@ -16269,6 +16332,30 @@ def _prepare_render_job(proj, p: Dict[str, Any]):
16269
16332
  "before": before,
16270
16333
  "settings": settings,
16271
16334
  }
16335
+ if preset_pin:
16336
+ result["preset_pinned"] = preset_pin
16337
+ elif settings.get("ExportVideo") is True:
16338
+ # No pin, and the caller is asking for video. This is the exact shape
16339
+ # that produced a video-less mp4 in issue #123: the queued job's
16340
+ # IsExportVideo readback agreed with the request and the file did not.
16341
+ # The job readback is not a witness for the rendered file, so say so
16342
+ # here rather than let success=True imply a verified deliverable.
16343
+ result["warnings"] = result.get("warnings", []) + [{
16344
+ "code": "RENDER_PRESET_STATE_INHERITED",
16345
+ "message": (
16346
+ "This job inherits the Deliver page's current render state for every "
16347
+ "key not passed in settings, and that state is not readable from the "
16348
+ "scripting API. A previously loaded audio-only preset has been measured "
16349
+ "to survive an explicit ExportVideo:true and render an mp4 with no video "
16350
+ "stream, while the job readback reported IsExportVideo:true."
16351
+ ),
16352
+ "remediation": (
16353
+ "Pass from_preset='<a video preset>' to pin the base state, and verify "
16354
+ "the rendered file has a codec_type=video stream before reporting it "
16355
+ "delivered — a long timeline that 'renders' in seconds is the tell."
16356
+ ),
16357
+ }]
16358
+ return result
16272
16359
 
16273
16360
 
16274
16361
  # ── Delivery targets ────────────────────────────────────────────────────────
@@ -16585,7 +16672,11 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
16585
16672
  probe_render_settings() -> {format_and_codec, mode, settings, jobs, is_rendering}
16586
16673
  validate_render_settings(settings, require_temp_target?) -> {valid, errors, unknown_keys}
16587
16674
  safe_set_render_settings(settings, dry_run?, restore?, require_temp_target?) -> {success, diff}
16588
- prepare_render_job(target_dir, settings?, format?, codec?, custom_name?, dry_run?) -> {success, job_id}
16675
+ prepare_render_job(target_dir, settings?, format?, codec?, custom_name?, from_preset?, dry_run?) -> {success, job_id}
16676
+ from_preset pins the base render state (LoadRenderPreset) before the
16677
+ explicit settings go on top. Without it the job inherits the Deliver
16678
+ page's loaded preset for every key not passed, which the API gives no
16679
+ way to read back — see the SetRenderSettings api_truth entry.
16589
16680
  render_job_lifecycle_probe(target_dir, settings?, format?, codec?, custom_name?) -> {success, job_id, status_before_delete}
16590
16681
  quick_export_capabilities() -> {presets, safe_params, guards}
16591
16682
  safe_quick_export(preset, target_dir?|params?, custom_name?, dry_run?, allow_render?) -> {success, status}
@@ -1373,6 +1373,36 @@ API_TRUTH: List[Dict[str, Any]] = [
1373
1373
  "issue": 59,
1374
1374
  "mitigation": ["_render_format_id", "_render_codec_id"],
1375
1375
  },
1376
+ {
1377
+ "symbol": "Project.SetRenderSettings (inherits the loaded preset)",
1378
+ "object": "Project",
1379
+ "signature": "({settings}) -> bool",
1380
+ "reality": "SetRenderSettings applies the passed keys ON TOP of whatever "
1381
+ "render state the Deliver page is holding; it does not replace "
1382
+ "it. A loaded preset carries more state than the keys a caller "
1383
+ "passes, and that state survives. Measured 2026-07-08: after a "
1384
+ "render through the stock 'Audio Only' preset, a job queued with "
1385
+ "an explicit ExportVideo=True and an .mp4 target returned "
1386
+ "settings_success=True and a real job id, GetRenderJobList "
1387
+ "reported IsExportVideo=True, and the rendered .mp4 contained "
1388
+ "only an AAC stream with NO video stream (ffprobe) — 18 minutes "
1389
+ "of material 'rendered' in ~10 seconds. The job readback is "
1390
+ "therefore NOT a witness for the rendered file. There is also no "
1391
+ "way to detect the inherited state: the scripting API documents "
1392
+ "no GetRenderSettings and no GetCurrentRenderPresetName, so the "
1393
+ "base state can be pinned but never read.",
1394
+ "recommended": "Pin the base state instead of inheriting one — "
1395
+ "prepare_render_job(from_preset='<a video preset>') runs "
1396
+ "LoadRenderPreset before the explicit settings go on top "
1397
+ "(PresetName flips to 'Custom' once they do, which is "
1398
+ "expected). Then verify the OUTPUT, not the job: ffprobe for "
1399
+ "a codec_type=video stream. A long timeline that completes "
1400
+ "in seconds is the tell.",
1401
+ "tags": ["render", "deliver", "silent-failure", "preset", "readback-lies"],
1402
+ "submit": "bug",
1403
+ "issue": 123,
1404
+ "mitigation": ["_render_preset_pin", "_prepare_render_job"],
1405
+ },
1376
1406
  {
1377
1407
  "symbol": "Project.SetCurrentRenderFormatAndCodec",
1378
1408
  "object": "Project",