davinci-resolve-mcp 2.80.2 → 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,73 @@
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
+
5
72
  ## What's New in v2.80.2
6
73
 
7
74
  Agent tooling only. Ten Claude Code skills that this repository has shipped and
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.2-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
@@ -61,7 +61,15 @@ Each skill is a directory containing `SKILL.md`. Claude Code does not discover
61
61
  loose `.md` files in `.claude/skills/`; a skill placed at the top level of that
62
62
  directory silently never loads.
63
63
 
64
- Two skills sit outside the domain routing:
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:
65
73
 
66
74
  - `house-style` (`.claude/skills/house-style/SKILL.md`) — accumulated editorial
67
75
  corrections, so the same note is not given twice. Claude-only; append to it
@@ -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.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.80.2",
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>",
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.80.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.80.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
- 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",