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 +132 -0
- package/README.md +1 -1
- package/docs/README.md +86 -9
- package/docs/kernels/audio-fairlight-kernel.md +1 -1
- package/docs/kernels/color-grade-kernel.md +1 -1
- package/docs/kernels/fusion-composition-kernel.md +1 -1
- package/docs/kernels/media-pool-ingest-kernel.md +1 -1
- package/docs/kernels/render-deliver-kernel.md +1 -1
- package/docs/kernels/timeline-conform-interchange-kernel.md +1 -1
- package/docs/kernels/timeline-edit-kernel.md +1 -1
- package/docs/reference/api-limitations.md +10 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/agent-rules/README.md +7 -4
- package/src/granular/common.py +1 -1
- package/src/server.py +95 -4
- package/src/utils/api_truth.py +30 -0
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-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
|
|
52
|
-
- `resolve-edit` (`.claude/skills/
|
|
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
|
|
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/
|
|
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,
|
|
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.
|
|
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
|
@@ -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
|
|
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)
|
|
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
|
|
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.
|
package/src/granular/common.py
CHANGED
|
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
85
85
|
handlers=[logging.StreamHandler()],
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
VERSION = "2.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
package/src/utils/api_truth.py
CHANGED
|
@@ -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",
|