davinci-resolve-mcp 2.99.0 → 2.99.2

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,80 @@
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.99.2
6
+
7
+ **The Fusion comp-lock question is closed on Resolve 21.** The v2.98.5–v2.98.8
8
+ work isolated the bug on Studio 19.1.3.7 only, and the open caveat was whether
9
+ the "renders on 19.1.3.7, ignored on 21.0.4.5" split reported in
10
+ [#156](https://github.com/samuelgursky/davinci-resolve-mcp/pull/156) had a
11
+ version component on top of it. It does not.
12
+
13
+ Measured on **DaVinci Resolve 21.0.3.7**, driven through the in-app bridge: the
14
+ same wired `MediaIn -> Blur(XBlurSize 20) -> MediaOut` comp, with the value
15
+ written through the fixed `fusion_comp set_input`, **renders** — PSNR 24.38 dB
16
+ against the no-comp baseline, file shrinking 2,017,973 → 727,261 bytes. That is
17
+ the *same* PSNR figure measured on 19.1.3.7 from the same source and blur.
18
+
19
+ So the split was the `Composition.Lock` bug on both sides, and "a wired comp
20
+ renders" now holds across both Resolve generations tested. The `AddFusionComp`
21
+ entry in `api_truth` records it.
22
+
23
+ **Caveat, stated because it matters:** the 21 confirmation is a **free-edition
24
+ 21.0.3.7**, not the Studio 21.0.4.5 the original report used — no Studio 21 was
25
+ available. The graph was also wired with raw proxy calls rather than
26
+ `fusion_comp add_tool`, for the reason below; the value write, which is the step
27
+ that carried the bug, did go through the real server path.
28
+
29
+ ### Documented
30
+
31
+ - **`fusion_comp add_tool` cannot run on the free edition.** The in-app bridge's
32
+ proxy exposes `SetInput`/`GetInput`/`ConnectInput`/`FindMainInput` on a Fusion
33
+ tool but not `GetAttrs`/`SetAttrs`, and `add_tool` calls `GetAttrs()`
34
+ unconditionally to build its return value. Comp-level calls all work, so a
35
+ graph can still be wired with raw `comp.AddTool`/`ConnectInput` and driven
36
+ with `fusion_comp` for value writes. New `api_truth` entry; making `add_tool`
37
+ tolerate a missing `GetAttrs` would restore the action there.
38
+
39
+ ## What's New in v2.99.1
40
+
41
+ **`bulk_set_item_properties` could not set a clip colour on its own.** Reported
42
+ and fixed in [#157](https://github.com/samuelgursky/davinci-resolve-mcp/pull/157)
43
+ by @matoberuc-afk.
44
+
45
+ The action documents `clip_color` and `enabled` as per-op keys and has code to
46
+ apply both — but that code was unreachable for the op shape that needs it most.
47
+ The payload is built by `_merge_property_groups`, which merges only
48
+ `properties`/`transform`/`crop`/`composite`/`audio` and the duplicate-keyframe
49
+ keys. `clip_color` and `enabled` are not `SetProperty` keys, so they never landed
50
+ in that dict, and the `if not properties: continue` guard above returned early —
51
+ leaving the `clip_color` branch twenty-five lines below dead on exactly the ops
52
+ that carry no transform. Colour triage in one round trip is the main reason to
53
+ call a *bulk* setter, and it was the one shape that could not work.
54
+
55
+ ### Fixed
56
+
57
+ - **A colour-only or enabled-only op is now accepted** and applied.
58
+ - **A colour-only op could not fail.** Per-op success was
59
+ `all(row.get("success") for row in ...properties.values())`, and `all([])` is
60
+ `True` — with no property rows the op passed regardless of what `SetClipColor`
61
+ returned. Every branch that runs now votes.
62
+ - **The bulk path trusted the bare bool.** It called `item.SetClipColor` directly,
63
+ bypassing `_set_clip_color_checked` — the helper added for
64
+ [#124](https://github.com/samuelgursky/davinci-resolve-mcp/issues/124) that the
65
+ single-item path already used, because that bool lies twice: a name outside the
66
+ 16-name Edit-page palette is refused with a bare `False`, and a generator or
67
+ title takes the call, returns `True`, and drops the colour. A failure now
68
+ carries `clip_color_detail`.
69
+ - `dry_run` reports `would_set_clip_color` / `would_set_enabled`, and the
70
+ `action_help` example shows the triage shape instead of a `properties`
71
+ dict with a `ClipColor` key that was never a valid `SetProperty` target.
72
+
73
+ ### Live validation
74
+
75
+ Studio 19.1.3.7: three colour-only ops in one call, live readback
76
+ `['Apricot', 'Chocolate', 'Purple']` on the timeline items; a refused colour
77
+ returns `success: false` instead of passing on the empty-list vote.
78
+
5
79
  ## What's New in v2.99.0
6
80
 
7
81
  **`timeline.ripple_insert`, and a verified-delete gate on `move_clips`.**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.99.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.99.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-35%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.99.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.99.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-35%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.99.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.99.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/docs/SKILL.md CHANGED
@@ -1410,7 +1410,11 @@ Key actions:
1410
1410
  `track_index?` (default 1), so multicam angles can be rebuilt onto V2/V3
1411
1411
  rather than collapsing onto V1; missing tracks are added
1412
1412
  - `bulk_set_item_properties(ops, dry_run?, readback?)` — apply transforms,
1413
- crop/composite/audio/property groups to many timeline items in one call
1413
+ crop/composite/audio/property groups to many timeline items in one call. An op
1414
+ may carry `clip_color` and/or `enabled` with nothing else, which is the triage
1415
+ shape: paint a whole selection in one round trip. A colour is verified by
1416
+ readback, so a name outside the Edit-page palette and the generator/title case
1417
+ that returns True and drops the colour both fail the op instead of passing
1414
1418
  - `apply_look_to_items(target_ids, cdl?|copy_from_item_id?, dry_run?)` — apply a
1415
1419
  normalized CDL and/or copy a source grade to multiple video items
1416
1420
  - `thumbnail_contact_sheet` / `marker_thumbnail_review` — sample Resolve
@@ -12,7 +12,7 @@ that none exists).
12
12
 
13
13
  **Verified on:** DaVinci Resolve Studio 21.0.2
14
14
 
15
- **Totals:** 29 missing capabilities, 37 bugs / unreliable behaviors.
15
+ **Totals:** 29 missing capabilities, 38 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
@@ -355,6 +355,14 @@ values, or automation-hostile modal prompts.
355
355
  - **Workaround / current handling:** Author OTIO for Resolve by mirroring what Resolve itself exports, and give every event its media timecode origin. editorial.convert_to_interchange (target 'otio') does this and reports any event whose origin had to be assumed in `mediaOriginAssumed` — a non-empty list means the file will only import if that media really starts at 00:00:00:00. To debug a refusal, export any timeline with EXPORT_OTIO and diff your document against it; do NOT chase missing media or reach for sanitize_media, which cannot even parse a .otio (it is JSON, not XML).
356
356
  - **Tags:** timeline, import, interchange, otio, silent-failure, conform
357
357
 
358
+ ### Fusion tool GetAttrs / SetAttrs (unreachable over the in-app bridge)
359
+
360
+ - **Object:** `Fusion Tool (via the free-edition bridge)`
361
+ - **Signature:** `GetAttrs() -> dict / SetAttrs(dict) -> None`
362
+ - **Behavior:** The in-app bridge's transparent proxy exposes SetInput, GetInput, ConnectInput and FindMainInput on a Fusion tool, but NOT GetAttrs or SetAttrs - the client raises AttributeError('... has no attribute SetAttrs in this Resolve build'). Measured 2026-08-22 on free 21.0.3.7. Because fusion_comp's add_tool calls tool.GetAttrs() unconditionally to build its return value, and SetAttrs whenever a name is passed, `fusion_comp add_tool` cannot run at all on the free edition, which takes connect/set_input with it for any graph the caller wanted to build through the server. Comp-level calls (AddFusionComp, GetFusionCompByIndex, FindTool, AddTool) work, so the graph can still be wired with raw proxy calls.
363
+ - **Workaround / current handling:** On the free edition, wire Fusion graphs with raw comp.AddTool/ConnectInput and use fusion_comp only for value writes. Making add_tool tolerate a missing GetAttrs (fall back to the requested tool_type and a FindTool-based name) would restore the action there.
364
+ - **Tags:** fusion, bridge, free-edition
365
+
358
366
  ### Composition.Lock (suppresses render invalidation for value writes)
359
367
 
360
368
  - **Object:** `Composition (Fusion, via TimelineItem comps)`
@@ -366,7 +374,7 @@ values, or automation-hostile modal prompts.
366
374
  ### TimelineItem.AddFusionComp / LoadFusionCompByName
367
375
 
368
376
  - **Object:** `TimelineItem (media-backed clip)`
369
- - **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally. REPRODUCED 2026-08-21 on Studio 19.1.3.7: a rooted MediaIn -> Blur -> MediaOut comp built entirely through the API renders (PSNR 24.38 dB vs the no-comp baseline), so the 2026-08-02 correction stands. Note that an important share of 'the comp was ignored' readings are NOT this entry at all but the Composition.Lock bug above — a parameter written under a comp lock reads back correctly and never reaches the render, which looks identical from the API side. That is what an independent 2026-08-20 report on Studio 21.0.4.5 was measuring: a wired MediaIn -> Blur -> MediaOut comp, and a Transform variant, both delivered renders bit-identical to the no-comp baseline (PSNR inf) when built and set through this server. It was read at the time as a version regression (19.1.3.7 honours a wired comp, 21.0.4.5 does not); with the lock bug identified, the simpler reading is that the same defect reproduces on 21.0.4.5 — and since the lock bug has only been isolated on 19.1.3.7, that report is the only evidence it is not build-specific. Treat 'a wired comp renders' as established for 19.1.3.7 and unverified elsewhere.
377
+ - **Behavior:** A Fusion composition created on a media clip through the API is not applied at render WHEN MEDIAOUT HAS NO PATH FROM MEDIAIN. The original blanket form of this entry — 'never applied at render' — was too broad and was corrected on 2026-08-02: a comp wired MediaIn -> Blur -> MediaOut, created entirely through the API on an ordinary media clip, DOES render. PSNR between the plain and Fusion renders of the same timeline was 22.7 dB (identical would be infinite), the file shrank 22.5 MB -> 14.8 MB as a blur should, and the output was frame-for-frame identical in GUI and headless. A first attempt that wired ONLY MediaOut -> Blur, leaving the Blur with no source, made the render job come back 'Failed' with an 887-byte file — so an unrooted graph does not merely get bypassed, it can take the render down. What still stands is the original observation for the configuration it actually tested, which is retained below and has NOT been re-measured: AddFusionComp() returns the comp, AddTool/Connect/SetInput all succeed, and the whole graph reads back correctly (GetCompCount 1, MediaOut1.Input wired to the new tool, StyledText returning the value just set) — but the rendered output is byte-for-byte the untouched source media. Verified live on Studio 19.1.3.7 with the strongest form of the test: MediaOut1 fed ONLY by a Text+, with no path from MediaIn at all, still rendered the unmodified clip. LoadFusionCompByName on the sole comp does not activate it either. Contrast InsertFusionTitleIntoTimeline, whose comp DOES render — text set via SetInput('StyledText') appears in the output — so this is specific to comps attached to media-backed clips, not to Fusion through the API generally. REPRODUCED 2026-08-21 on Studio 19.1.3.7: a rooted MediaIn -> Blur -> MediaOut comp built entirely through the API renders (PSNR 24.38 dB vs the no-comp baseline), so the 2026-08-02 correction stands. Note that an important share of 'the comp was ignored' readings are NOT this entry at all but the Composition.Lock bug above — a parameter written under a comp lock reads back correctly and never reaches the render, which looks identical from the API side. That is what an independent 2026-08-20 report on Studio 21.0.4.5 was measuring: a wired MediaIn -> Blur -> MediaOut comp, and a Transform variant, both delivered renders bit-identical to the no-comp baseline (PSNR inf) when built and set through this server. It was read at the time as a version regression (19.1.3.7 honours a wired comp, 21.0.4.5 does not); with the lock bug identified, the simpler reading is that the same defect reproduces on 21.0.4.5 — and since the lock bug has only been isolated on 19.1.3.7, that report is the only evidence it is not build-specific. SETTLED 2026-08-22 on DaVinci Resolve 21.0.3.7 (free edition, driven through the in-app bridge): the same wired MediaIn -> Blur(XBlurSize 20) -> MediaOut comp, with the value written through this server's fixed set_input, RENDERS - PSNR 24.38 dB vs the no-comp baseline and the file shrinking 2,017,973 -> 727,261 bytes, the same figure measured on 19.1.3.7 from the same source. So the 'renders on 19.1.3.7, ignored on 21' split was the Composition.Lock bug on both sides, not a version regression, and 'a wired comp renders' now holds across both Resolve generations tested. The 21 confirmation is a free-edition 21.0.3.7 rather than the Studio 21.0.4.5 the original report used; no Studio 21 was available to test.
370
378
  - **Workaround / current handling:** Wire the graph so MediaOut descends from MediaIn — that is the difference between a comp that renders and one that is silently bypassed, and it is what made this look like 'Fusion never renders from the API'. Never leave a tool unrooted: a MediaOut fed by a tool with no source failed the render job outright. For text or effects over picture, insert a Fusion title/generator as its own timeline clip and set its Text+ (fusion_comp set_text_plus), rather than attaching a comp to the media clip. Note the destination track cannot be chosen from the API (see the Track Selector entry), so overlaying onto an existing clip's track is not currently reachable end-to-end. Building the comp in the Fusion page UI works; only the API-created comp is ignored. Never claim a Fusion effect from comp readback alone — prove it with a rendered frame (gallery_stills grab_and_export before/after, or a delivered render). On setups where even the wired comp does not render (see the 2026-08-20 measurement), treat per-item Fusion effects as HITL (a human builds or activates the comp in the UI) and bake stills motion with ffmpeg when unattended output is required.
371
379
  - **Tags:** fusion, silent-failure, render, readback, hitl
372
380
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.99.0"
40
+ VERSION = "2.99.2"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.99.0",
3
+ "version": "2.99.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.99.0"
90
+ VERSION = "2.99.2"
91
91
  logger = logging.getLogger("davinci-resolve-mcp")
92
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
93
93
  logger.info(f"Detected platform: {get_platform()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.99.0"
14
+ VERSION = "2.99.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -6039,8 +6039,15 @@ def _timeline_bulk_set_item_properties(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6039
6039
  results.append({"index": index, "success": False, "error": f"timeline item not found: {item_id}"})
6040
6040
  continue
6041
6041
  properties = _merge_property_groups(op)
6042
- if not properties:
6043
- results.append({"index": index, "success": False, "error": "op requires properties, transform, crop, composite, audio, or direct property keys"})
6042
+ # clip_color and enabled are not SetProperty keys, so they never reach
6043
+ # `properties`. Requiring a non-empty `properties` made a colour-only op
6044
+ # — the shape triage actually sends, `{"timeline_item_id": id,
6045
+ # "clip_color": "Apricot"}` — bail here, which left the clip_color and
6046
+ # enabled branches below unreachable on exactly the ops that need them.
6047
+ wants_color = "clip_color" in op
6048
+ wants_enabled = "enabled" in op
6049
+ if not properties and not wants_color and not wants_enabled:
6050
+ results.append({"index": index, "success": False, "error": "op requires properties, transform, crop, composite, audio, clip_color, enabled, or direct property keys"})
6044
6051
  continue
6045
6052
  item_result = {
6046
6053
  "index": index,
@@ -6050,6 +6057,10 @@ def _timeline_bulk_set_item_properties(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6050
6057
  }
6051
6058
  if dry_run:
6052
6059
  item_result.update({"success": True, "would_set": properties})
6060
+ if wants_color:
6061
+ item_result["would_set_clip_color"] = op["clip_color"]
6062
+ if wants_enabled:
6063
+ item_result["would_set_enabled"] = bool(op["enabled"])
6053
6064
  results.append(item_result)
6054
6065
  continue
6055
6066
  for key, value in properties.items():
@@ -6065,17 +6076,33 @@ def _timeline_bulk_set_item_properties(tl, p: Dict[str, Any]) -> Dict[str, Any]:
6065
6076
  except Exception as exc:
6066
6077
  row["readback_error"] = str(exc)
6067
6078
  item_result["properties"][key] = row
6068
- if "clip_color" in op:
6079
+ outcomes = [bool(row.get("success")) for row in item_result["properties"].values()]
6080
+ if wants_color:
6081
+ # Routed through the checked helper rather than the bare bool: a name
6082
+ # outside the 16-name Edit-page palette is refused with False and no
6083
+ # reason, and a generator or title takes the call, returns True and
6084
+ # drops the colour (issue #124). The detail only appears on failure,
6085
+ # so the success shape stays the plain bool callers already read.
6069
6086
  try:
6070
- item_result["clip_color"] = bool(item.SetClipColor(op["clip_color"]))
6087
+ outcome = _set_clip_color_checked(item, op["clip_color"], kind="timeline item")
6071
6088
  except Exception as exc:
6072
- item_result["clip_color"] = {"success": False, "error": str(exc)}
6073
- if "enabled" in op:
6089
+ outcome = {"success": False, "error": str(exc)}
6090
+ applied = bool(outcome.get("success"))
6091
+ item_result["clip_color"] = applied
6092
+ if not applied:
6093
+ item_result["clip_color_detail"] = outcome
6094
+ outcomes.append(applied)
6095
+ if wants_enabled:
6074
6096
  try:
6075
- item_result["enabled"] = bool(item.SetClipEnabled(bool(op["enabled"])))
6097
+ enabled_ok = bool(item.SetClipEnabled(bool(op["enabled"])))
6098
+ item_result["enabled"] = enabled_ok
6076
6099
  except Exception as exc:
6100
+ enabled_ok = False
6077
6101
  item_result["enabled"] = {"success": False, "error": str(exc)}
6078
- item_result["success"] = all(row.get("success") for row in item_result["properties"].values())
6102
+ outcomes.append(enabled_ok)
6103
+ # all([]) is True, so a colour-only op used to be able to report success
6104
+ # no matter what SetClipColor did. Every branch that ran now votes.
6105
+ item_result["success"] = all(outcomes)
6079
6106
  results.append(item_result)
6080
6107
  return {"success": all(row.get("success") for row in results), "results": results, "op_count": len(ops)}
6081
6108
 
@@ -22853,6 +22880,8 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
22853
22880
  track_type, default 1); missing tracks are added, so V2/V3 multicam angles survive.
22854
22881
  # example: action_help(name='<action_name>')
22855
22882
  bulk_set_item_properties(ops, dry_run?, readback?) -> {results, op_count}
22883
+ an op may carry clip_color and/or enabled with no other payload -- that is
22884
+ the triage shape (paint N clips in one call).
22856
22885
  # example: action_help(name='<action_name>')
22857
22886
  apply_look_to_items(target_ids, cdl?|copy_from_item_id?, dry_run?) -> {success}
22858
22887
  # example: action_help(name='<action_name>')
@@ -24966,16 +24995,18 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
24966
24995
  ),
24967
24996
  },
24968
24997
  "bulk_set_item_properties": {
24969
- "summary": "Batch SetProperty/clip_color/enabled across many items.",
24970
- "params": "ops: [{timeline_item_id|clip_id, properties|transform|crop|composite|audio, clip_color?, enabled?}], dry_run?, readback?",
24971
- "returns": "{success, results, op_count}",
24998
+ "summary": "Batch SetProperty/clip_color/enabled across many items. clip_color or enabled may be the only key in an op.",
24999
+ "params": "ops: [{timeline_item_id|clip_id, properties?|transform?|crop?|composite?|audio?, clip_color?, enabled?}], dry_run?, readback?",
25000
+ "returns": "{success, results, op_count}; a failed clip_color adds results[].clip_color_detail",
24972
25001
  "example": (
24973
25002
  'timeline(action="bulk_set_item_properties", params={\n'
24974
25003
  ' "ops": [\n'
24975
- ' {"timeline_item_id": "TimelineItem-abc",\n'
24976
- ' "properties": {"ClipColor": "Teal", "ZoomX": 1.05}},\n'
24977
- ' {"timeline_item_id": "TimelineItem-def",\n'
24978
- ' "properties": {"ClipColor": "Teal"}}\n'
25004
+ ' # triage: colour only, no other payload needed\n'
25005
+ ' {"timeline_item_id": "TimelineItem-abc", "clip_color": "Apricot"},\n'
25006
+ ' {"timeline_item_id": "TimelineItem-def", "clip_color": "Chocolate"},\n'
25007
+ ' # or colour plus a transform in the same op\n'
25008
+ ' {"timeline_item_id": "TimelineItem-ghi",\n'
25009
+ ' "clip_color": "Purple", "transform": {"ZoomX": 1.05}}\n'
24979
25010
  ' ],\n'
24980
25011
  ' "dry_run": True, "readback": True\n'
24981
25012
  '})'
@@ -403,6 +403,31 @@ API_TRUTH: List[Dict[str, Any]] = [
403
403
  "tags": ["timeline", "import", "interchange", "otio", "silent-failure", "conform"],
404
404
  "submit": "bug",
405
405
  },
406
+ {
407
+ "symbol": "Fusion tool GetAttrs / SetAttrs (unreachable over the in-app bridge)",
408
+ "object": "Fusion Tool (via the free-edition bridge)",
409
+ "signature": "GetAttrs() -> dict / SetAttrs(dict) -> None",
410
+ "reality": "The in-app bridge's transparent proxy exposes SetInput, "
411
+ "GetInput, ConnectInput and FindMainInput on a Fusion tool, "
412
+ "but NOT GetAttrs or SetAttrs - the client raises "
413
+ "AttributeError('... has no attribute SetAttrs in this "
414
+ "Resolve build'). Measured 2026-08-22 on free 21.0.3.7. "
415
+ "Because fusion_comp's add_tool calls tool.GetAttrs() "
416
+ "unconditionally to build its return value, and SetAttrs "
417
+ "whenever a name is passed, `fusion_comp add_tool` cannot "
418
+ "run at all on the free edition, which takes "
419
+ "connect/set_input with it for any graph the caller wanted "
420
+ "to build through the server. Comp-level calls "
421
+ "(AddFusionComp, GetFusionCompByIndex, FindTool, AddTool) "
422
+ "work, so the graph can still be wired with raw proxy calls.",
423
+ "recommended": "On the free edition, wire Fusion graphs with raw "
424
+ "comp.AddTool/ConnectInput and use fusion_comp only for "
425
+ "value writes. Making add_tool tolerate a missing "
426
+ "GetAttrs (fall back to the requested tool_type and a "
427
+ "FindTool-based name) would restore the action there.",
428
+ "tags": ["fusion", "bridge", "free-edition"],
429
+ "submit": "bug",
430
+ },
406
431
  {
407
432
  "symbol": "Composition.Lock (suppresses render invalidation for value writes)",
408
433
  "object": "Composition (Fusion, via TimelineItem comps)",
@@ -506,8 +531,19 @@ API_TRUTH: List[Dict[str, Any]] = [
506
531
  "that the same defect reproduces on 21.0.4.5 — and since "
507
532
  "the lock bug has only been isolated on 19.1.3.7, that "
508
533
  "report is the only evidence it is not build-specific. "
509
- "Treat 'a wired comp renders' as established for 19.1.3.7 "
510
- "and unverified elsewhere.",
534
+ "SETTLED 2026-08-22 on DaVinci Resolve 21.0.3.7 (free "
535
+ "edition, driven through the in-app bridge): the same wired "
536
+ "MediaIn -> Blur(XBlurSize 20) -> MediaOut comp, with the "
537
+ "value written through this server's fixed set_input, "
538
+ "RENDERS - PSNR 24.38 dB vs the no-comp baseline and the "
539
+ "file shrinking 2,017,973 -> 727,261 bytes, the same figure "
540
+ "measured on 19.1.3.7 from the same source. So the "
541
+ "'renders on 19.1.3.7, ignored on 21' split was the "
542
+ "Composition.Lock bug on both sides, not a version "
543
+ "regression, and 'a wired comp renders' now holds across "
544
+ "both Resolve generations tested. The 21 confirmation is a "
545
+ "free-edition 21.0.3.7 rather than the Studio 21.0.4.5 the "
546
+ "original report used; no Studio 21 was available to test.",
511
547
  "recommended": "Wire the graph so MediaOut descends from MediaIn — that "
512
548
  "is the difference between a comp that renders and one "
513
549
  "that is silently bypassed, and it is what made this look "