davinci-resolve-mcp 2.56.0 → 2.57.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 +40 -0
- package/README.md +2 -2
- package/docs/SKILL.md +8 -0
- package/docs/install.md +1 -0
- package/install.py +49 -10
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +271 -6
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.0
|
|
6
|
+
|
|
7
|
+
Community feature requests #72 and #73.
|
|
8
|
+
|
|
9
|
+
- **Added** OpenCode to the installer's supported MCP clients (issue #72). The
|
|
10
|
+
installer now writes/merges `~/.config/opencode/opencode.json` using OpenCode's
|
|
11
|
+
own schema (`type`/`enabled`, a combined `command` array, and an `environment`
|
|
12
|
+
block), and `--manual` prints a ready-to-paste OpenCode snippet.
|
|
13
|
+
- **Added** `fusion_comp(action="add_fusion_mask", ...)` (issue #73): a one-call
|
|
14
|
+
Rectangle/Ellipse mask — adds the mask tool, sets its params (`corner_radius`,
|
|
15
|
+
`width`, `height`, `center`/`center_x`/`center_y`, etc., all 0..1), and
|
|
16
|
+
optionally wires it into a tool's mask input (`EffectMask` by default). Each
|
|
17
|
+
input is applied independently so one unsupported parameter never aborts the rest.
|
|
18
|
+
- **Added** `fusion_comp(action="set_text_plus" / "get_text_plus", ...)` (issue
|
|
19
|
+
#73): read/write the text of a Fusion `Text+` tool or Fusion title template
|
|
20
|
+
(e.g. a "Deep" title), auto-finding the `Text+` tool when `tool_name` is
|
|
21
|
+
omitted. Complements `timeline(action="set_title_text")` for generator titles.
|
|
22
|
+
- **Note** (issue #73) per-clip audio Stereo↔Mono conversion is not in
|
|
23
|
+
Blackmagic's scripting API and was not added. The supported surface is already
|
|
24
|
+
exposed: `timeline get_track_sub_type` (query channel format), `add_track` with
|
|
25
|
+
`audioType` (create mono/stereo tracks), `convert_to_stereo` (timeline-wide),
|
|
26
|
+
and `timeline_item get_source_audio_channel_mapping`.
|
|
27
|
+
|
|
28
|
+
## What's New in v2.56.1
|
|
29
|
+
|
|
30
|
+
Final reliability batch from the exhaustive audit (Wave B + P2/P3 selections).
|
|
31
|
+
|
|
32
|
+
- **Fixed** (EX11) `audio_track_probe` reported `available: true` for track index 0
|
|
33
|
+
(and negatives); audio tracks are 1-indexed, so it now requires
|
|
34
|
+
`1 <= index <= track_count`.
|
|
35
|
+
- **Fixed** (EX10) `_find_timeline_item_by_id` no longer silently skips a whole
|
|
36
|
+
track type when `GetTrackCount` errors — it logs the failure so a real API error
|
|
37
|
+
isn't mistaken for "item not found".
|
|
38
|
+
- **Fixed** (P3) the raw `timeline_item_color.set_cdl` now validates the ASC-CDL
|
|
39
|
+
payload (shape/ranges) before `SetCDL`, matching its safe twin — malformed CDL
|
|
40
|
+
returns a structured error instead of being silently rejected by Resolve.
|
|
41
|
+
- **Fixed** (P2) `media_pool.delete_timelines` is read-back verified: it reports
|
|
42
|
+
`verified` from the project's timeline count dropping, not the unreliable boolean.
|
|
43
|
+
`import_folder` now validates its required `path`.
|
|
44
|
+
|
|
5
45
|
## What's New in v2.56.0
|
|
6
46
|
|
|
7
47
|
Destructive-action registry audit (EX-REG) — a systemic version of the EX2 bug.
|
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)
|
|
@@ -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
|
|
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.
|
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.
|
|
38
|
+
VERSION = "2.57.0"
|
|
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
|
-
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.
|
|
83
|
+
VERSION = "2.57.0"
|
|
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.
|
|
14
|
+
VERSION = "2.57.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -1891,7 +1891,10 @@ def _find_timeline_item_by_id(tl, timeline_item_id) -> Optional[Any]:
|
|
|
1891
1891
|
for track_type in ("video", "audio", "subtitle"):
|
|
1892
1892
|
try:
|
|
1893
1893
|
track_count = int(tl.GetTrackCount(track_type) or 0)
|
|
1894
|
-
except Exception:
|
|
1894
|
+
except Exception as exc:
|
|
1895
|
+
# Don't silently skip a whole track type on API error — log it so a
|
|
1896
|
+
# genuine API failure isn't mistaken for "item not found" (EX10).
|
|
1897
|
+
logger.warning("_find_timeline_item_by_id: GetTrackCount(%s) failed, skipping: %s", track_type, exc)
|
|
1895
1898
|
continue
|
|
1896
1899
|
for track_index in range(1, track_count + 1):
|
|
1897
1900
|
for item in (tl.GetItemListInTrack(track_type, track_index) or []):
|
|
@@ -5691,8 +5694,11 @@ def _audio_capabilities():
|
|
|
5691
5694
|
def _audio_track_probe(tl, p: Dict[str, Any]):
|
|
5692
5695
|
track_index = int(p.get("track_index", 1))
|
|
5693
5696
|
track_count = int(tl.GetTrackCount("audio") or 0)
|
|
5694
|
-
|
|
5695
|
-
|
|
5697
|
+
# Resolve audio tracks are 1-indexed; track_index < 1 is invalid (EX11 — the
|
|
5698
|
+
# old `track_index <= track_count` reported available:true for index 0).
|
|
5699
|
+
out = {"track_index": track_index, "track_count": track_count,
|
|
5700
|
+
"available": 1 <= track_index <= track_count}
|
|
5701
|
+
if not (1 <= track_index <= track_count):
|
|
5696
5702
|
return out
|
|
5697
5703
|
for key, getter, args in (
|
|
5698
5704
|
("sub_type", "GetTrackSubType", ("audio", track_index)),
|
|
@@ -15158,7 +15164,13 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
15158
15164
|
blocked = _consume_confirm_token(action="media_pool.delete_timelines", params=p)
|
|
15159
15165
|
if blocked:
|
|
15160
15166
|
return blocked
|
|
15161
|
-
|
|
15167
|
+
# Read-back: DeleteTimelines' bool is unreliable; verify by the project's
|
|
15168
|
+
# timeline count dropping (P2).
|
|
15169
|
+
before_n = proj.GetTimelineCount()
|
|
15170
|
+
raw = bool(mp.DeleteTimelines(timelines))
|
|
15171
|
+
after_n = proj.GetTimelineCount()
|
|
15172
|
+
return {"success": raw, "verified": after_n < before_n,
|
|
15173
|
+
"timelines_before": before_n, "timelines_after": after_n}
|
|
15162
15174
|
elif action == "append_to_timeline":
|
|
15163
15175
|
if p.get("clip_infos") is not None:
|
|
15164
15176
|
raw = p["clip_infos"]
|
|
@@ -15321,6 +15333,8 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
|
|
|
15321
15333
|
return _err("Clip not found")
|
|
15322
15334
|
return {"success": bool(mp.DeleteClipMattes(clip, p["paths"]))}
|
|
15323
15335
|
elif action == "import_folder":
|
|
15336
|
+
if not p.get("path"):
|
|
15337
|
+
return _err("import_folder requires path")
|
|
15324
15338
|
return {"success": bool(mp.ImportFolderFromFile(p["path"], p.get("source_clips_path", "")))}
|
|
15325
15339
|
elif action == "ingest_capabilities":
|
|
15326
15340
|
return _media_pool_ingest_capabilities()
|
|
@@ -20377,7 +20391,12 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
20377
20391
|
elif action == "grade_boundary_report":
|
|
20378
20392
|
return _grade_boundary_report(proj, item, p)
|
|
20379
20393
|
elif action == "set_cdl":
|
|
20380
|
-
|
|
20394
|
+
# Validate ASC-CDL shape before SetCDL (malformed CDL is silently rejected
|
|
20395
|
+
# by Resolve); the raw action shares the safe twin's validator now (P3 #22).
|
|
20396
|
+
validation, err = _validate_cdl_payload(p.get("cdl"))
|
|
20397
|
+
if err:
|
|
20398
|
+
return err
|
|
20399
|
+
return {"success": bool(item.SetCDL(_normalize_cdl(validation["cdl"])))}
|
|
20381
20400
|
elif action == "copy_grades":
|
|
20382
20401
|
# Find target items by IDs
|
|
20383
20402
|
_, tl, _ = _get_tl()
|
|
@@ -21524,6 +21543,234 @@ def _fusion_tool_names(comp):
|
|
|
21524
21543
|
return [name for name, _ in _iter_fusion_tools(comp)]
|
|
21525
21544
|
|
|
21526
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
|
+
|
|
21527
21774
|
@mcp.tool()
|
|
21528
21775
|
def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
21529
21776
|
"""Fusion composition node graph operations.
|
|
@@ -21579,6 +21826,17 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
21579
21826
|
safe_set_inputs(tool_name, inputs, readback?) -> {success, results}
|
|
21580
21827
|
safe_connect_tools(target_tool, input_name, source_tool, dry_run?) -> {success}
|
|
21581
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}
|
|
21582
21840
|
|
|
21583
21841
|
Common tool_type values: Merge, Background, TextPlus, Transform, Blur,
|
|
21584
21842
|
ColorCorrector, RectangleMask, EllipseMask, Tracker, MediaIn, MediaOut,
|
|
@@ -21618,6 +21876,12 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
21618
21876
|
return _safe_connect_fusion_tools(comp, p)
|
|
21619
21877
|
elif action == "fusion_boundary_report":
|
|
21620
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)
|
|
21621
21885
|
|
|
21622
21886
|
# --- Node Management ---
|
|
21623
21887
|
if action == "add_tool":
|
|
@@ -22001,6 +22265,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
22001
22265
|
"get_position","set_position","copy_tool","auto_arrange",
|
|
22002
22266
|
"bulk_set_inputs",
|
|
22003
22267
|
"bulk_set_expressions",
|
|
22268
|
+
"add_fusion_mask","set_text_plus","get_text_plus",
|
|
22004
22269
|
*_FUSION_GROUP_KERNEL_ACTIONS,
|
|
22005
22270
|
*_FUSION_KERNEL_ACTIONS,
|
|
22006
22271
|
])
|