davinci-resolve-mcp 2.56.1 → 2.57.1

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,46 @@
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.57.1
6
+
7
+ Investigation outcome for community feature request #74 — no behavior change.
8
+
9
+ - **Note** (issue #74) the DaVinci Resolve scripting API does not expose the
10
+ Source/Auto Track Selector, so there is no way to choose the destination track
11
+ when inserting Text+/titles/generators. Verified live on Resolve Studio 21.0.0:
12
+ no get/set for the selector exists; the `Insert*IntoTimeline` family takes no
13
+ `trackIndex` and always lands on V1; locking lower tracks makes the insert fail
14
+ rather than redirect; and titles/generators can't be relocated afterward (no
15
+ `MediaPoolItem`). Closed as won't-fix (API limitation).
16
+ - **Documentation** recorded the limitation in the verified `api_truth` ledger
17
+ (query at runtime with `resolve_control api_truth "track"`) and in
18
+ `docs/reference/api-coverage.md`, with the supported alternative for
19
+ media-backed clips (`MediaPool.AppendToTimeline` clipInfo `trackIndex`, exposed
20
+ as `media_pool.append_to_timeline` `clip_infos`). Added a regression test.
21
+
22
+ ## What's New in v2.57.0
23
+
24
+ Community feature requests #72 and #73.
25
+
26
+ - **Added** OpenCode to the installer's supported MCP clients (issue #72). The
27
+ installer now writes/merges `~/.config/opencode/opencode.json` using OpenCode's
28
+ own schema (`type`/`enabled`, a combined `command` array, and an `environment`
29
+ block), and `--manual` prints a ready-to-paste OpenCode snippet.
30
+ - **Added** `fusion_comp(action="add_fusion_mask", ...)` (issue #73): a one-call
31
+ Rectangle/Ellipse mask — adds the mask tool, sets its params (`corner_radius`,
32
+ `width`, `height`, `center`/`center_x`/`center_y`, etc., all 0..1), and
33
+ optionally wires it into a tool's mask input (`EffectMask` by default). Each
34
+ input is applied independently so one unsupported parameter never aborts the rest.
35
+ - **Added** `fusion_comp(action="set_text_plus" / "get_text_plus", ...)` (issue
36
+ #73): read/write the text of a Fusion `Text+` tool or Fusion title template
37
+ (e.g. a "Deep" title), auto-finding the `Text+` tool when `tool_name` is
38
+ omitted. Complements `timeline(action="set_title_text")` for generator titles.
39
+ - **Note** (issue #73) per-clip audio Stereo↔Mono conversion is not in
40
+ Blackmagic's scripting API and was not added. The supported surface is already
41
+ exposed: `timeline get_track_sub_type` (query channel format), `add_track` with
42
+ `audioType` (create mono/stereo tracks), `convert_to_stereo` (timeline-wide),
43
+ and `timeline_item get_source_audio_channel_mapping`.
44
+
5
45
  ## What's New in v2.56.1
6
46
 
7
47
  Final reliability batch from the exhaustive audit (Wave B + P2/P3 selections).
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.56.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.57.1-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)
@@ -21,7 +21,7 @@ A local browser control panel ships with the server for inspecting Resolve state
21
21
  npx davinci-resolve-mcp setup
22
22
  ```
23
23
 
24
- Before connecting, open DaVinci Resolve Studio and set **Preferences > General > External scripting using** to **Local**. The npm launcher installs a managed copy under your user application-data directory, then runs the universal Python installer. The installer creates a virtual environment, detects Resolve paths, and can configure Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, Zed, Continue, Cline, Roo Code, and JetBrains IDEs.
24
+ Before connecting, open DaVinci Resolve Studio and set **Preferences > General > External scripting using** to **Local**. The npm launcher installs a managed copy under your user application-data directory, then runs the universal Python installer. The installer creates a virtual environment, detects Resolve paths, and can configure Claude Desktop, Claude Code, Cursor, VS Code, Windsurf, Zed, Continue, Cline, Roo Code, OpenCode, and JetBrains IDEs.
25
25
 
26
26
  For source installs:
27
27
 
@@ -168,3 +168,6 @@ Samuel Gursky (samgursky@gmail.com)
168
168
 
169
169
  - [@rgxdev](https://github.com/rgxdev) — configurable Media Pool inventory walk
170
170
  (exclude bins + inventory limit), [#69](https://github.com/samuelgursky/davinci-resolve-mcp/pull/69) (v2.53.0)
171
+ - [@swayll](https://github.com/swayll) — Source Track Selector investigation
172
+ ([#74](https://github.com/samuelgursky/davinci-resolve-mcp/issues/74)); confirmed
173
+ + documented API limitation (v2.57.1)
package/docs/SKILL.md CHANGED
@@ -1219,6 +1219,14 @@ mutation wrappers around the raw Fusion API:
1219
1219
  - `safe_set_inputs(tool_name, inputs, readback?)`
1220
1220
  - `safe_connect_tools(target_tool, input_name, source_tool, dry_run?)`
1221
1221
  - `fusion_boundary_report(include_io?)`
1222
+ - `add_fusion_mask(mask_type?, width?, height?, corner_radius?, center?|center_x?/center_y?,
1223
+ angle?, soft_edge?, border_width?, invert?, inputs?, connect_to?, connect_input?, readback?)`
1224
+ — one-call Rectangle/Ellipse mask (e.g. rounded corners): adds the mask tool, sets its
1225
+ params (0..1), and optionally wires it into `connect_to`'s mask input (`EffectMask` default).
1226
+ - `set_text_plus(text, tool_name?, input_name?, readback?)` / `get_text_plus(tool_name?, input_name?)`
1227
+ — read/write the text of a Fusion `Text+` tool or Fusion title template (e.g. a "Deep"
1228
+ title). Auto-finds the `Text+` tool when `tool_name` is omitted; `input_name` defaults to
1229
+ `StyledText`. For non-Fusion generator titles, use `timeline(action="set_title_text")` instead.
1222
1230
 
1223
1231
  ---
1224
1232
 
package/docs/install.md CHANGED
@@ -59,6 +59,7 @@ The installer can automatically configure any of these clients:
59
59
  | Roo Code | VS Code global storage |
60
60
  | Zed | `~/.config/zed/settings.json` |
61
61
  | Continue | `~/.continue/config.json` |
62
+ | OpenCode | `~/.config/opencode/opencode.json` (or project-root `opencode.json`) |
62
63
  | JetBrains IDEs | Manual (Settings > Tools > AI Assistant > MCP) |
63
64
 
64
65
  You can configure multiple clients at once, or use `--clients manual` to get copy-paste config snippets.
@@ -86,6 +86,17 @@ env RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci R
86
86
  venv/bin/python tests/live_v233_validation.py
87
87
  ```
88
88
 
89
+ For Fusion comp changes (masks, Text+/title text), run the issue #73 harness,
90
+ which creates a disposable Fusion-title project, exercises `add_fusion_mask` and
91
+ `set_text_plus` / `get_text_plus`, and deletes the project:
92
+
93
+ ```bash
94
+ env RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting" \
95
+ RESOLVE_SCRIPT_LIB="/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so" \
96
+ PYTHONPATH="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules" \
97
+ venv/bin/python tests/live_fusion_mask_title_validation.py
98
+ ```
99
+
89
100
  One-off live harnesses are acceptable during review, but reusable coverage
90
101
  should live under `tests/`. Do not commit generated media or disposable project
91
102
  artifacts.
@@ -44,6 +44,19 @@ maps `shot_descriptions[shot_index]` onto Media Pool shot markers, and triggers
44
44
  metadata writeback for that clip. Works with any MCP client whose chat model
45
45
  is vision-capable; no `sampling/createMessage` support required.
46
46
 
47
+ Some Edit-page behavior is simply not reachable through public scripting. The
48
+ **Source/Auto Track Selector** (the patch panel that chooses which video track a
49
+ clip lands on) has no get/set in the API, and the `Insert*IntoTimeline` family
50
+ (titles, generators, OFX, Fusion comps) takes no `trackIndex` — they always drop
51
+ onto the selector's current target (V1 in practice). Locking lower tracks does
52
+ not redirect the insert; it just makes the insert fail. Titles and generators
53
+ can't be relocated afterward either, since they have no `MediaPoolItem` for
54
+ `AppendToTimeline`/`MoveClips` to act on. For media-backed clips you *can* target
55
+ a track via `MediaPool.AppendToTimeline`'s clipInfo `trackIndex` (exposed as
56
+ `media_pool.append_to_timeline` with `clip_infos`). This limitation is recorded
57
+ in the verified `api_truth` ledger (query `resolve_control api_truth "track"`);
58
+ see issue #74.
59
+
47
60
  | Class | Methods | Tools | Description |
48
61
  |-------|---------|-------|-------------|
49
62
  | Resolve | 22 | 22 | App control, pages, layout presets, render/burn-in presets, keyframe mode |
package/install.py CHANGED
@@ -4,7 +4,7 @@ DaVinci Resolve MCP Server — Universal Installer
4
4
 
5
5
  Supports: macOS, Windows, Linux
6
6
  Configures: Claude Desktop, Claude Code, Cursor, VS Code (Copilot),
7
- Windsurf, Cline, Roo Code, Zed, Continue, and manual setup.
7
+ Windsurf, Cline, Roo Code, Zed, Continue, OpenCode, and manual setup.
8
8
 
9
9
  Usage:
10
10
  python install.py # Interactive mode
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.56.1"
38
+ VERSION = "2.57.1"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
@@ -392,6 +392,16 @@ MCP_CLIENTS = [
392
392
  "config_key": "mcpServers",
393
393
  "notes": "Open-source AI code assistant",
394
394
  },
395
+ {
396
+ "id": "opencode",
397
+ "name": "OpenCode",
398
+ # OpenCode uses ~/.config/opencode/opencode.json on every platform
399
+ # (it also reads a project-root opencode.json, but the global file is
400
+ # the safe default for an installer). See https://opencode.ai/docs/config/
401
+ "get_path": lambda: home() / ".config" / "opencode" / "opencode.json",
402
+ "config_key": "mcp",
403
+ "notes": "AI coding agent (uses its own type/enabled/command-array format)",
404
+ },
395
405
  ]
396
406
 
397
407
  CLIENT_IDS = [c["id"] for c in MCP_CLIENTS]
@@ -456,6 +466,33 @@ def build_zed_entry(python_path, server_path, api_path, lib_path, system=SYSTEM,
456
466
  "settings": {},
457
467
  }
458
468
 
469
+
470
+ def build_opencode_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
471
+ """Build OpenCode-specific server entry (issue #72).
472
+
473
+ OpenCode's schema differs from the standard format in three ways: it wraps
474
+ the entry in a ``"mcp"`` key, the interpreter and script are a single
475
+ ``"command"`` array (no separate ``args``), and the environment block is
476
+ ``"environment"`` rather than ``"env"``. It also expects ``type``/``enabled``
477
+ discriminators. See https://opencode.ai/docs/mcp-servers/
478
+ """
479
+ return {
480
+ "type": "local",
481
+ "enabled": True,
482
+ "command": [str(python_path), str(server_path)],
483
+ "environment": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
484
+ }
485
+
486
+
487
+ def build_entry_for_client(client, python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
488
+ """Return the server entry shaped for a specific client's config schema."""
489
+ builders = {
490
+ "zed": build_zed_entry,
491
+ "opencode": build_opencode_entry,
492
+ }
493
+ builder = builders.get(client["id"], build_server_entry)
494
+ return builder(python_path, server_path, api_path, lib_path, system=system, python_home=python_home)
495
+
459
496
  # ─── Config File Operations ──────────────────────────────────────────────────
460
497
 
461
498
  class ConfigParseError(Exception):
@@ -599,13 +636,9 @@ def write_client_config(client, python_path, server_path, api_path, lib_path, dr
599
636
  return False, f"{client['name']} is not available on {platform_name()}"
600
637
 
601
638
  config_key = client["config_key"]
602
- is_zed = client["id"] == "zed"
603
639
 
604
- # Build the server entry
605
- if is_zed:
606
- server_entry = build_zed_entry(python_path, server_path, api_path, lib_path)
607
- else:
608
- server_entry = build_server_entry(python_path, server_path, api_path, lib_path)
640
+ # Build the server entry (some clients use a non-standard schema)
641
+ server_entry = build_entry_for_client(client, python_path, server_path, api_path, lib_path)
609
642
 
610
643
  if dry_run:
611
644
  preview = {config_key: {"davinci-resolve": server_entry}}
@@ -637,12 +670,14 @@ def generate_manual_config(python_path, server_path, api_path, lib_path):
637
670
  """Generate config snippets for manual setup."""
638
671
  entry = build_server_entry(python_path, server_path, api_path, lib_path)
639
672
  zed_entry = build_zed_entry(python_path, server_path, api_path, lib_path)
673
+ opencode_entry = build_opencode_entry(python_path, server_path, api_path, lib_path)
640
674
 
641
675
  standard = json.dumps({"mcpServers": {"davinci-resolve": entry}}, indent=2)
642
676
  vscode_fmt = json.dumps({"servers": {"davinci-resolve": entry}}, indent=2)
643
677
  zed_fmt = json.dumps({"context_servers": {"davinci-resolve": zed_entry}}, indent=2)
678
+ opencode_fmt = json.dumps({"mcp": {"davinci-resolve": opencode_entry}}, indent=2)
644
679
 
645
- return standard, vscode_fmt, zed_fmt
680
+ return standard, vscode_fmt, zed_fmt, opencode_fmt
646
681
 
647
682
  # ─── Virtual Environment ─────────────────────────────────────────────────────
648
683
 
@@ -1539,7 +1574,7 @@ def main():
1539
1574
 
1540
1575
  # Show manual config
1541
1576
  if show_manual:
1542
- standard, vscode_fmt, zed_fmt = generate_manual_config(
1577
+ standard, vscode_fmt, zed_fmt, opencode_fmt = generate_manual_config(
1543
1578
  python_path, server_path, api_path, lib_path
1544
1579
  )
1545
1580
  env_preview = build_server_env(python_path, api_path, lib_path)
@@ -1557,6 +1592,10 @@ def main():
1557
1592
  print()
1558
1593
  for line in zed_fmt.split("\n"):
1559
1594
  print(f" {line}")
1595
+ print(f"\n {cyan('OpenCode format')} (add to ~/.config/opencode/opencode.json or a project opencode.json):")
1596
+ print()
1597
+ for line in opencode_fmt.split("\n"):
1598
+ print(f" {line}")
1560
1599
  print(f"\n {cyan('JetBrains IDEs')} (IntelliJ, WebStorm, PyCharm, etc.):")
1561
1600
  print(f" Settings → Tools → AI Assistant → Model Context Protocol (MCP)")
1562
1601
  print(f" Add server with command: {python_path} {server_path}")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.56.1",
3
+ "version": "2.57.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.56.1"
83
+ VERSION = "2.57.1"
84
84
  logger = logging.getLogger("davinci-resolve-mcp")
85
85
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
86
  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.56.1"
14
+ VERSION = "2.57.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -21543,6 +21543,234 @@ def _fusion_tool_names(comp):
21543
21543
  return [name for name, _ in _iter_fusion_tools(comp)]
21544
21544
 
21545
21545
 
21546
+ # Friendly mask aliases -> Fusion tool RegID, for add_fusion_mask (issue #73).
21547
+ _MASK_TOOL_ALIASES = {
21548
+ "rectangle": "RectangleMask",
21549
+ "rect": "RectangleMask",
21550
+ "rectanglemask": "RectangleMask",
21551
+ "ellipse": "EllipseMask",
21552
+ "ellipsemask": "EllipseMask",
21553
+ "circle": "EllipseMask",
21554
+ }
21555
+
21556
+ # Friendly param name -> Fusion input id, for add_fusion_mask. Center is handled
21557
+ # separately because it is a Point input (see _fusion_set_point_input).
21558
+ _MASK_INPUT_ALIASES = {
21559
+ "corner_radius": "CornerRadius", # RectangleMask only
21560
+ "width": "Width",
21561
+ "height": "Height",
21562
+ "angle": "Angle",
21563
+ "soft_edge": "SoftEdge",
21564
+ "border_width": "BorderWidth",
21565
+ "invert": "Invert",
21566
+ }
21567
+
21568
+
21569
+ def _fusion_set_point_input(tool, input_id, value):
21570
+ """Set a Fusion Point input (e.g. a mask Center).
21571
+
21572
+ Point inputs do not accept a plain scalar; the fusionscript bridge variously
21573
+ accepts a 2-element list or a 1-indexed table depending on platform/version.
21574
+ Try the known encodings and report which one took (or the last error).
21575
+ """
21576
+ if isinstance(value, dict):
21577
+ candidates = [value]
21578
+ else:
21579
+ try:
21580
+ x, y = value[0], value[1]
21581
+ except (TypeError, KeyError, IndexError):
21582
+ return False, f"{input_id} must be [x, y] or {{1:x, 2:y}} (got {value!r})", None
21583
+ candidates = [[x, y], {1: x, 2: y}, {"1": x, "2": y}]
21584
+ last_err = None
21585
+ for cand in candidates:
21586
+ try:
21587
+ tool.SetInput(input_id, cand)
21588
+ return True, None, cand
21589
+ except Exception as exc: # noqa: BLE401 - bridge raises bare exceptions
21590
+ last_err = str(exc)
21591
+ return False, last_err, None
21592
+
21593
+
21594
+ def _fusion_add_mask(comp, p: Dict[str, Any]) -> Dict[str, Any]:
21595
+ """Add and configure a Rectangle/Ellipse mask in one call (issue #73).
21596
+
21597
+ Equivalent to add_tool + a sequence of set_input calls, plus optional wiring
21598
+ of the mask into a downstream tool's mask input. Each input is applied
21599
+ independently so a single unsupported parameter does not abort the rest.
21600
+ """
21601
+ raw_type = str(p.get("mask_type", "Rectangle")).strip().lower()
21602
+ tool_type = _MASK_TOOL_ALIASES.get(raw_type)
21603
+ if not tool_type:
21604
+ return _err(
21605
+ "mask_type must be one of: Rectangle, Ellipse "
21606
+ f"(got {p.get('mask_type')!r})"
21607
+ )
21608
+
21609
+ x = p.get("x", -1)
21610
+ y = p.get("y", -1)
21611
+ readback = bool(p.get("readback", True))
21612
+
21613
+ comp.Lock()
21614
+ try:
21615
+ tool = comp.AddTool(tool_type, x, y)
21616
+ if not tool:
21617
+ return _err(
21618
+ f"Failed to add {tool_type}. Verify the comp is editable "
21619
+ "(switch to the Fusion page or target a clip with a Fusion comp)."
21620
+ )
21621
+ name = p.get("name")
21622
+ if name:
21623
+ tool.SetAttrs({"TOOLS_Name": str(name)})
21624
+ attrs = tool.GetAttrs() or {}
21625
+ tool_name = attrs.get("TOOLS_Name", "")
21626
+
21627
+ results: List[Dict[str, Any]] = []
21628
+
21629
+ # Center: accept center=[x,y]/{1:x,2:y}, or center_x / center_y.
21630
+ center = p.get("center")
21631
+ cx, cy = p.get("center_x"), p.get("center_y")
21632
+ if center is None and (cx is not None or cy is not None):
21633
+ center = [cx if cx is not None else 0.5, cy if cy is not None else 0.5]
21634
+ if center is not None:
21635
+ ok, err, applied = _fusion_set_point_input(tool, "Center", center)
21636
+ rec = {"input": "Center", "value": center, "success": ok}
21637
+ if not ok:
21638
+ rec["error"] = err
21639
+ elif readback:
21640
+ try:
21641
+ rec["readback"] = _ser(tool.GetInput("Center"))
21642
+ except Exception as exc:
21643
+ rec["readback_error"] = str(exc)
21644
+ results.append(rec)
21645
+
21646
+ # Scalar inputs (friendly aliases) + any raw passthrough inputs.
21647
+ to_set: List[tuple] = []
21648
+ for friendly, fusion_id in _MASK_INPUT_ALIASES.items():
21649
+ if friendly in p:
21650
+ to_set.append((fusion_id, p[friendly]))
21651
+ raw_inputs = p.get("inputs")
21652
+ if isinstance(raw_inputs, dict):
21653
+ for k, v in raw_inputs.items():
21654
+ to_set.append((str(k), v))
21655
+
21656
+ for fusion_id, value in to_set:
21657
+ rec = {"input": fusion_id, "value": value}
21658
+ try:
21659
+ tool.SetInput(fusion_id, value)
21660
+ rec["success"] = True
21661
+ if readback:
21662
+ try:
21663
+ rec["readback"] = _ser(tool.GetInput(fusion_id))
21664
+ except Exception as exc:
21665
+ rec["readback_error"] = str(exc)
21666
+ except Exception as exc:
21667
+ rec["success"] = False
21668
+ rec["error"] = str(exc)
21669
+ results.append(rec)
21670
+
21671
+ out: Dict[str, Any] = {
21672
+ "success": True,
21673
+ "tool_name": tool_name,
21674
+ "tool_type": attrs.get("TOOLS_RegID", tool_type),
21675
+ "inputs_set": results,
21676
+ }
21677
+
21678
+ # Optional wiring: connect this mask into a tool's mask input.
21679
+ connect_to = p.get("connect_to")
21680
+ if connect_to:
21681
+ input_name = p.get("connect_input", "EffectMask")
21682
+ target = comp.FindTool(str(connect_to))
21683
+ if not target:
21684
+ out["connection"] = {
21685
+ "success": False,
21686
+ "error": f"connect_to tool '{connect_to}' not found",
21687
+ }
21688
+ else:
21689
+ try:
21690
+ ok = bool(target.ConnectInput(input_name, tool))
21691
+ out["connection"] = {
21692
+ "success": ok,
21693
+ "target": str(connect_to),
21694
+ "input_name": input_name,
21695
+ }
21696
+ except Exception as exc:
21697
+ out["connection"] = {"success": False, "error": str(exc)}
21698
+ return out
21699
+ finally:
21700
+ comp.Unlock()
21701
+
21702
+
21703
+ def _fusion_find_text_tool(comp, p: Dict[str, Any]):
21704
+ """Find a Text+ (TextPlus) tool in the comp, or the named tool. (issue #73)"""
21705
+ name = p.get("tool_name")
21706
+ if name:
21707
+ tool = comp.FindTool(str(name))
21708
+ if not tool:
21709
+ return None, _err(f"Tool '{name}' not found")
21710
+ return tool, None
21711
+ # Auto-detect the first Text+ tool — this is what a Fusion title template
21712
+ # (e.g. "Deep") exposes its editable copy through.
21713
+ for name_, tool in _iter_fusion_tools(comp):
21714
+ try:
21715
+ reg = (tool.GetAttrs() or {}).get("TOOLS_RegID", "")
21716
+ except Exception:
21717
+ reg = ""
21718
+ if reg in ("TextPlus", "Text+") or "Text" in reg:
21719
+ return tool, None
21720
+ return None, _err(
21721
+ "No Text+ tool found in this comp. Pass tool_name explicitly, or call "
21722
+ "get_tool_list to inspect the node graph."
21723
+ )
21724
+
21725
+
21726
+ def _fusion_set_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
21727
+ """Set the text of a Fusion Text+ tool / title template. (issue #73)"""
21728
+ text = p.get("text")
21729
+ if not isinstance(text, str):
21730
+ return _err("set_text_plus requires params.text (string)")
21731
+ tool, err = _fusion_find_text_tool(comp, p)
21732
+ if err:
21733
+ return err
21734
+ input_id = p.get("input_name", "StyledText")
21735
+ readback = bool(p.get("readback", True))
21736
+ comp.Lock()
21737
+ try:
21738
+ try:
21739
+ tool.SetInput(input_id, text)
21740
+ except Exception as exc:
21741
+ return _err(f"SetInput({input_id!r}) failed: {exc}")
21742
+ out = {
21743
+ "success": True,
21744
+ "tool_name": (tool.GetAttrs() or {}).get("TOOLS_Name", ""),
21745
+ "input_name": input_id,
21746
+ }
21747
+ if readback:
21748
+ try:
21749
+ out["readback"] = _ser(tool.GetInput(input_id))
21750
+ except Exception as exc:
21751
+ out["readback_error"] = str(exc)
21752
+ return out
21753
+ finally:
21754
+ comp.Unlock()
21755
+
21756
+
21757
+ def _fusion_get_text_plus(comp, p: Dict[str, Any]) -> Dict[str, Any]:
21758
+ """Read the text of a Fusion Text+ tool / title template. (issue #73)"""
21759
+ tool, err = _fusion_find_text_tool(comp, p)
21760
+ if err:
21761
+ return err
21762
+ input_id = p.get("input_name", "StyledText")
21763
+ try:
21764
+ val = tool.GetInput(input_id)
21765
+ except Exception as exc:
21766
+ return _err(f"GetInput({input_id!r}) failed: {exc}")
21767
+ return {
21768
+ "tool_name": (tool.GetAttrs() or {}).get("TOOLS_Name", ""),
21769
+ "input_name": input_id,
21770
+ "text": _ser(val),
21771
+ }
21772
+
21773
+
21546
21774
  @mcp.tool()
21547
21775
  def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
21548
21776
  """Fusion composition node graph operations.
@@ -21598,6 +21826,17 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
21598
21826
  safe_set_inputs(tool_name, inputs, readback?) -> {success, results}
21599
21827
  safe_connect_tools(target_tool, input_name, source_tool, dry_run?) -> {success}
21600
21828
  fusion_boundary_report(include_io?) -> {capabilities, composition}
21829
+ add_fusion_mask(mask_type?, name?, center?|center_x?|center_y?, width?, height?,
21830
+ corner_radius?, angle?, soft_edge?, border_width?, invert?, inputs?, x?, y?,
21831
+ connect_to?, connect_input?, readback?) -> {success, tool_name, inputs_set, connection?}
21832
+ mask_type: "Rectangle" (default) or "Ellipse". One call = add the mask tool,
21833
+ set its params (corner_radius/width/height/center are 0..1), and optionally
21834
+ wire it into connect_to's mask input (connect_input default "EffectMask").
21835
+ set_text_plus(text, tool_name?, input_name?, readback?) -> {success, tool_name, readback?}
21836
+ Set the text of a Fusion Text+ tool / title template (e.g. a "Deep" Fusion
21837
+ title). Auto-finds the Text+ tool if tool_name is omitted; input_name
21838
+ defaults to "StyledText".
21839
+ get_text_plus(tool_name?, input_name?) -> {tool_name, input_name, text}
21601
21840
 
21602
21841
  Common tool_type values: Merge, Background, TextPlus, Transform, Blur,
21603
21842
  ColorCorrector, RectangleMask, EllipseMask, Tracker, MediaIn, MediaOut,
@@ -21637,6 +21876,12 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
21637
21876
  return _safe_connect_fusion_tools(comp, p)
21638
21877
  elif action == "fusion_boundary_report":
21639
21878
  return _fusion_boundary_report(comp, p)
21879
+ elif action == "add_fusion_mask":
21880
+ return _fusion_add_mask(comp, p)
21881
+ elif action == "set_text_plus":
21882
+ return _fusion_set_text_plus(comp, p)
21883
+ elif action == "get_text_plus":
21884
+ return _fusion_get_text_plus(comp, p)
21640
21885
 
21641
21886
  # --- Node Management ---
21642
21887
  if action == "add_tool":
@@ -22020,6 +22265,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
22020
22265
  "get_position","set_position","copy_tool","auto_arrange",
22021
22266
  "bulk_set_inputs",
22022
22267
  "bulk_set_expressions",
22268
+ "add_fusion_mask","set_text_plus","get_text_plus",
22023
22269
  *_FUSION_GROUP_KERNEL_ACTIONS,
22024
22270
  *_FUSION_KERNEL_ACTIONS,
22025
22271
  ])
@@ -158,6 +158,28 @@ API_TRUTH: List[Dict[str, Any]] = [
158
158
  "need a comp to operate on.",
159
159
  "tags": ["fusion", "timeline"],
160
160
  },
161
+ {
162
+ "symbol": "Source Track Selector / destination track for Insert*IntoTimeline",
163
+ "object": "Timeline",
164
+ "reality": "There is no API to read or set the Source/Auto Track Selector "
165
+ "(the Edit-page patch panel that picks the destination track). "
166
+ "InsertTitleIntoTimeline, InsertFusionTitleIntoTimeline, "
167
+ "InsertGeneratorIntoTimeline, InsertFusionGeneratorIntoTimeline, "
168
+ "InsertOFXGeneratorIntoTimeline and "
169
+ "InsertFusionCompositionIntoTimeline take no trackIndex and "
170
+ "always drop the clip on the selector's current target (V1 in "
171
+ "practice). Locking lower video tracks does NOT redirect the "
172
+ "insert — verified live on 21.0.0: locking V1 makes the insert "
173
+ "FAIL rather than land on V2. Titles/generators also can't be "
174
+ "moved afterward (no MediaPoolItem, so AppendToTimeline clipInfo "
175
+ "and MoveClips don't apply).",
176
+ "recommended": "Accept the limitation for titles/generators (insert lands "
177
+ "on V1). For clips that DO have a MediaPoolItem, target a "
178
+ "track with MediaPool.AppendToTimeline's clipInfo 'trackIndex' "
179
+ "instead (exposed as media_pool append_to_timeline clip_infos). "
180
+ "See issue #74.",
181
+ "tags": ["missing-method", "timeline", "title", "generator", "track"],
182
+ },
161
183
  {
162
184
  "symbol": "subprocess inheriting stdin under the MCP stdio server",
163
185
  "object": "(server runtime)",