davinci-resolve-mcp 4.8.3 → 4.8.5
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 +117 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +4 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +36 -11
- package/src/resolve_mcp_server.py +2 -1
- package/src/server.py +70 -2
- package/src/utils/api_truth.py +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,123 @@
|
|
|
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 v4.8.5 — the offline suite no longer reaches Resolve through the granular server
|
|
6
|
+
|
|
7
|
+
No tool or action changed. One runtime change outside the tests: importing
|
|
8
|
+
`src.granular` no longer connects to Resolve, and the granular launchers now
|
|
9
|
+
connect explicitly at startup instead.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **With Resolve open, the offline suite connected to it through the granular server.**
|
|
14
|
+
`src/granular/common.py` ran `import DaVinciResolveScript` and `connect_resolve()`
|
|
15
|
+
at import time, so any test that imported a granular module called
|
|
16
|
+
`scriptapp("Resolve")` on the real `fusionscript.so`. `tests/offline_guard.py`
|
|
17
|
+
swapped `_launch_resolve`, `get_resolve` and `resolve_is_running` on `src.server`
|
|
18
|
+
only. The granular `get_resolve()` still fell through to its own `_launch_resolve()`.
|
|
19
|
+
Whether the real module loaded at all came down to import order, because the test
|
|
20
|
+
modules that `sys.modules.setdefault()` a stub only win when they run first.
|
|
21
|
+
`tests/test_live_api.py`, which pytest collects, calls `scriptapp` whenever the
|
|
22
|
+
real module wins. The compound server leaked the same way: the execution-lifecycle
|
|
23
|
+
state provider calls `src.server._try_connect()` directly before tool calls, and
|
|
24
|
+
the guard never swapped that function. Measured with a full
|
|
25
|
+
`python -m unittest discover -s tests -t .` run of v4.8.4, with a tripwire
|
|
26
|
+
standing in for the native library. The test process called `scriptapp("Resolve")`
|
|
27
|
+
1,165 times, 1,162 of them from `_get_resolve_lifecycle_state`. It also tried
|
|
28
|
+
once to `open` the application, through the granular `ResolveProxy` →
|
|
29
|
+
`get_resolve()` → `_launch_resolve()`. The same run on this release makes
|
|
30
|
+
neither call.
|
|
31
|
+
- Before it imports `src.server`, the guard installs a `sys.meta_path` finder that
|
|
32
|
+
answers `DaVinciResolveScript` and `fusionscript` with an empty stub. It is a
|
|
33
|
+
finder rather than a `sys.modules` entry, so a test that pops the module cannot
|
|
34
|
+
let the next import reach the real library. The stub has no `scriptapp`, so
|
|
35
|
+
`connect_resolve()` raises before its bridge fallback instead of falling
|
|
36
|
+
through to it.
|
|
37
|
+
- `_try_connect`, `_launch_resolve` and `get_resolve` in `src.granular.common`
|
|
38
|
+
are swapped the same way as the compound server's. The swap covers every
|
|
39
|
+
granular module that holds one: `src/granular/__init__.py` imports each tool
|
|
40
|
+
module, and each binds its own copy through `from src.granular.common import *`
|
|
41
|
+
before the guard can swap `common`. The originals stay reachable as
|
|
42
|
+
`_*_unpatched`.
|
|
43
|
+
- For the duration of the run, the in-app bridge client points at a config file
|
|
44
|
+
that does not exist. `DAVINCI_RESOLVE_BRIDGE=1` in a developer's shell therefore
|
|
45
|
+
cannot open a socket to a bridge running inside Resolve.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **`src/granular/common.py` no longer connects at import.** It still imports
|
|
50
|
+
DaVinciResolveScript, with the same diagnostics when that fails. The connection
|
|
51
|
+
moved to `connect_at_startup()`, which `src/resolve_mcp_server.py` and
|
|
52
|
+
`src/server.py --full` call right after importing the package. Starting the
|
|
53
|
+
granular server behaves as before: it connects to a Resolve that is already
|
|
54
|
+
open, logs it, and never launches one. Launching stays with `get_resolve()` on
|
|
55
|
+
the first tool call. Code that only imports the package no longer talks to
|
|
56
|
+
Resolve.
|
|
57
|
+
|
|
58
|
+
### Validation
|
|
59
|
+
|
|
60
|
+
- New `tests/test_offline_guard_granular.py` puts a module shaped like Blackmagic's
|
|
61
|
+
loader first on `sys.path` and checks that a path import would load it. It then
|
|
62
|
+
asserts that neither a fresh import of `src.granular.common` nor
|
|
63
|
+
`connect_at_startup()` ever executes that module. It also pins the stand-ins in
|
|
64
|
+
every granular module, the stub's missing `scriptapp`, and the bridge redirect.
|
|
65
|
+
With the finder disabled it fails with
|
|
66
|
+
`['imported DaVinciResolveScript', "scriptapp ('Resolve',)"]`.
|
|
67
|
+
`tests/test_0000_offline_bootstrap.py` now also asserts that the finder is in
|
|
68
|
+
place before `src.server` imports.
|
|
69
|
+
- Full suite, `python -m unittest discover -s tests -t .`: 3,749 tests. The
|
|
70
|
+
errors are the same 11 as on v4.8.4 in this environment (no `numpy` or
|
|
71
|
+
`requests` in the venv, plus `test_offline_fallback` and
|
|
72
|
+
`test_lut_file_controls`). pytest was not run locally.
|
|
73
|
+
- Not covered: the control-panel tests (`test_control_panel_ipv6_loopback` and
|
|
74
|
+
one in `test_open_control_panel`) start the real `src/analysis_dashboard.py`
|
|
75
|
+
as a child process. An in-process guard cannot reach a child, and the child
|
|
76
|
+
still calls `scriptapp("Resolve")` read-only: 5 calls from 3 children in the
|
|
77
|
+
run above, the same as on v4.8.4.
|
|
78
|
+
- **Live-validated** on Resolve Studio 21.1.0.14, with Resolve already open. Both
|
|
79
|
+
launchers log `Connected to DaVinci Resolve: DaVinci Resolve Studio 21.1.0.14` at
|
|
80
|
+
startup, and neither started anything:
|
|
81
|
+
- `src/resolve_mcp_server.py` logs it before `Starting DaVinci Resolve MCP Server
|
|
82
|
+
v4.8.5 (389 granular tools)`, where the import-time connect used to log it.
|
|
83
|
+
- `src/server.py --full` logs it right after the granular import, before
|
|
84
|
+
`Threaded tool dispatch installed for 389 tools`.
|
|
85
|
+
- Not measured: startup with Resolve closed. That path logs `Failed to get Resolve
|
|
86
|
+
object` and leaves launching to the first tool call. The offline suite was never
|
|
87
|
+
run against the live Resolve.
|
|
88
|
+
|
|
89
|
+
## What's New in v4.8.4 — a Fusion nest control is refused with the controls it folds named
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- **`fusion_comp add_keyframe` on a nest control (`Softness1`, the Follower's
|
|
94
|
+
`TransformSize`, `Size1`, …) answered a generic `FUSION_ADD_MODIFIER_FAILED` with no
|
|
95
|
+
way forward.** ([#253](https://github.com/samuelgursky/davinci-resolve-mcp/issues/253), reported by @artpavelalex-ux as a follow-up to #250)
|
|
96
|
+
**Measured on Studio 19.1.3.7:** some entries `GetInputList()` returns are not
|
|
97
|
+
values at all. Inputs whose `INPID_InputControl` is `NestControl` (`INPB_Passive`
|
|
98
|
+
true) are the fold-down group headers the Fusion UI draws — `TextPlus Softness1`,
|
|
99
|
+
and on the text Follower `TransformSize` (display name "Size"), `Softness1` and
|
|
100
|
+
`Size1`. `Tool.AddModifier` returns False for them on every modifier type
|
|
101
|
+
(BezierSpline, Path, TextScramble all measured), so nothing could ever keyframe
|
|
102
|
+
them; this is not Follower-specific. The controls a header folds are the next
|
|
103
|
+
`INPI_LabelControl_NumInputs` entries in `GetInputList()` order —
|
|
104
|
+
`Softness1` → `SoftnessX1`, `SoftnessY1`, `SoftnessOnFillColorToo1`, `SoftnessGlow1`,
|
|
105
|
+
`SoftnessBlend1`; `TransformSize` → `LineSizeX/Y`, `WordSizeX/Y`, `CharacterSizeX/Y`;
|
|
106
|
+
`Size1` → `SizeX1`, `SizeY1` — and those take a spline normally.
|
|
107
|
+
- `add_keyframe` and `add_modifier` now detect a nest control before touching Fusion
|
|
108
|
+
and refuse it with **`FUSION_INPUT_IS_NEST_CONTROL`**, naming the folded controls
|
|
109
|
+
in the remediation and in `error.state.nest_members` (`_fusion_nest_members`).
|
|
110
|
+
- New `api_truth` entry `Tool.AddModifier (NestControl inputs)`, mapped on
|
|
111
|
+
`add_keyframe` and `add_modifier` results as a `known_limitation`.
|
|
112
|
+
- **Live-validated on landing through the real actions** on a disposable timeline:
|
|
113
|
+
the refusal named exactly those members on the Follower and on TextPlus;
|
|
114
|
+
`add_modifier` on `TransformSize` refused the same way; `SoftnessX1`,
|
|
115
|
+
`CharacterSizeX` and `Delay` keyframed and read back. Unit tests in
|
|
116
|
+
`tests/test_fusion_nest_control.py` against fakes whose input list is handed back
|
|
117
|
+
unsorted, so the member order is proven to come from the list order, not luck.
|
|
118
|
+
- The report itself arrived as an empty template with only its title; the
|
|
119
|
+
measurement was made from the title. Not measured: nests on tools other than
|
|
120
|
+
TextPlus and the Follower, and builds other than 19.1.3.7.
|
|
121
|
+
|
|
5
122
|
## What's New in v4.8.3 — nested folder ids resolve for delete and move
|
|
6
123
|
|
|
7
124
|
### Fixed
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [简体中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v4.8.
|
|
15
|
+
> 本翻译对应 v4.8.5 版 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
|
@@ -2047,7 +2047,10 @@ Key actions:
|
|
|
2047
2047
|
- `get_inputs(tool_name)` / `get_outputs(tool_name)`
|
|
2048
2048
|
- `set_attrs(tool_name, attrs)` / `get_attrs(tool_name)`
|
|
2049
2049
|
- `add_keyframe(tool_name, input_name, time, value, modifier?)` — attaches a
|
|
2050
|
-
BezierSpline (or `modifier`, e.g. `Path` for Point inputs) on first use
|
|
2050
|
+
BezierSpline (or `modifier`, e.g. `Path` for Point inputs) on first use. A nest
|
|
2051
|
+
control (a fold-down group header like `Softness1` or the Follower's
|
|
2052
|
+
`TransformSize`) is refused with `FUSION_INPUT_IS_NEST_CONTROL` naming the
|
|
2053
|
+
controls it folds (`SoftnessX1`/`SoftnessY1`, `CharacterSizeX`/`Y`, ...); keyframe those
|
|
2051
2054
|
- `add_modifier(tool_name, input_name, modifier)` → `{modifier_tool, modifier_type}`
|
|
2052
2055
|
— attach any modifier and get back the tool Fusion created, so a text modifier
|
|
2053
2056
|
(`Follower` on a TextPlus `StyledText`) can be driven with `set_input` /
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.8.
|
|
40
|
+
VERSION = "4.8.5"
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
93
93
|
handlers=[logging.StreamHandler()],
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
VERSION = "4.8.
|
|
96
|
+
VERSION = "4.8.5"
|
|
97
97
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
98
98
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
99
99
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -297,16 +297,13 @@ _OPTIONAL_DEPENDENCY_CONTRACT = (
|
|
|
297
297
|
"DaVinciResolveScript: always routed through connect_resolve(), which is None-tolerant"
|
|
298
298
|
)
|
|
299
299
|
|
|
300
|
+
# Loading the module is not connecting. `scriptapp` is what reaches a running
|
|
301
|
+
# Resolve, and it waits for `connect_at_startup()` or the first `get_resolve()`.
|
|
302
|
+
# Connecting here made *importing* this module talk to whatever Resolve was
|
|
303
|
+
# open, including from the offline test suite, where any `import src.granular.*`
|
|
304
|
+
# connected before a single test could stop it.
|
|
300
305
|
try:
|
|
301
306
|
import DaVinciResolveScript as dvr_script # type: ignore
|
|
302
|
-
|
|
303
|
-
resolve = connect_resolve(dvr_script)
|
|
304
|
-
if resolve:
|
|
305
|
-
logger.info(
|
|
306
|
-
f"Connected to DaVinci Resolve: {resolve.GetProductName()} {resolve.GetVersionString()}"
|
|
307
|
-
)
|
|
308
|
-
else:
|
|
309
|
-
logger.error("Failed to get Resolve object. Is DaVinci Resolve running?")
|
|
310
307
|
except ImportError as exc:
|
|
311
308
|
logger.error(f"Failed to import DaVinciResolveScript: {exc}")
|
|
312
309
|
logger.error("Check that DaVinci Resolve is installed and running.")
|
|
@@ -314,10 +311,38 @@ except ImportError as exc:
|
|
|
314
311
|
logger.error(f"RESOLVE_SCRIPT_LIB: {RESOLVE_LIB_PATH}")
|
|
315
312
|
logger.error(f"RESOLVE_MODULES_PATH: {RESOLVE_MODULES_PATH}")
|
|
316
313
|
logger.error(f"sys.path: {sys.path}")
|
|
317
|
-
|
|
314
|
+
dvr_script = None
|
|
318
315
|
except Exception as exc:
|
|
319
316
|
logger.error(f"Unexpected error initializing Resolve: {exc}")
|
|
320
|
-
|
|
317
|
+
dvr_script = None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def connect_at_startup():
|
|
321
|
+
"""Connect to a running Resolve as the granular server starts, and log it.
|
|
322
|
+
|
|
323
|
+
This is the connection that used to run at import. The launchers
|
|
324
|
+
(`src/resolve_mcp_server.py` and `src/server.py --full`) call it right after
|
|
325
|
+
importing the package, so starting the server behaves as before: it
|
|
326
|
+
connects to a Resolve that is already open and never launches one. Launching
|
|
327
|
+
is still left to `get_resolve()` on the first tool call. A missing
|
|
328
|
+
DaVinciResolveScript was already reported above and skips the attempt, as
|
|
329
|
+
the import failure did before.
|
|
330
|
+
"""
|
|
331
|
+
global resolve
|
|
332
|
+
if dvr_script is None:
|
|
333
|
+
return None
|
|
334
|
+
try:
|
|
335
|
+
resolve = connect_resolve(dvr_script)
|
|
336
|
+
if resolve:
|
|
337
|
+
logger.info(
|
|
338
|
+
f"Connected to DaVinci Resolve: {resolve.GetProductName()} {resolve.GetVersionString()}"
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
logger.error("Failed to get Resolve object. Is DaVinci Resolve running?")
|
|
342
|
+
except Exception as exc:
|
|
343
|
+
logger.error(f"Unexpected error initializing Resolve: {exc}")
|
|
344
|
+
resolve = None
|
|
345
|
+
return resolve
|
|
321
346
|
|
|
322
347
|
|
|
323
348
|
def _normalize_cdl(cdl):
|
|
@@ -26,13 +26,14 @@ if modules_path and modules_path not in sys.path:
|
|
|
26
26
|
sys.path.append(modules_path)
|
|
27
27
|
|
|
28
28
|
from src.granular import VERSION, mcp
|
|
29
|
-
from src.granular.common import logger
|
|
29
|
+
from src.granular.common import connect_at_startup, logger
|
|
30
30
|
from src.utils.mcp_stdio import run_fastmcp_stdio
|
|
31
31
|
from src.utils.update_check import start_background_update_check
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
if __name__ == "__main__":
|
|
35
35
|
try:
|
|
36
|
+
connect_at_startup()
|
|
36
37
|
start_background_update_check(VERSION, project_dir, logger)
|
|
37
38
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (389 granular tools)")
|
|
38
39
|
run_fastmcp_stdio(mcp)
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 377-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "4.8.
|
|
14
|
+
VERSION = "4.8.5"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -30530,6 +30530,61 @@ def _fusion_modifier_id(name: Any) -> str:
|
|
|
30530
30530
|
return _FUSION_MODIFIER_IDS.get(text.lower(), text)
|
|
30531
30531
|
|
|
30532
30532
|
|
|
30533
|
+
def _fusion_nest_members(tool, input_name: str):
|
|
30534
|
+
"""(is_nest, member_ids) for an input that is a Fusion NestControl header.
|
|
30535
|
+
|
|
30536
|
+
Measured on Studio 19.1.3.7 (issue #253): inputs whose `INPID_InputControl`
|
|
30537
|
+
is "NestControl" (`INPB_Passive` true) are the fold-down group headers the
|
|
30538
|
+
Fusion UI draws — TextPlus `Softness1`, Follower `TransformSize`, `Softness1`,
|
|
30539
|
+
`Size1` — not animatable values. `Tool.AddModifier` returns False for them on
|
|
30540
|
+
every modifier type, and so did every attempt to keyframe them. The controls
|
|
30541
|
+
the header folds are the next `INPI_LabelControl_NumInputs` entries in
|
|
30542
|
+
`GetInputList()` order (Softness1 -> SoftnessX1, SoftnessY1,
|
|
30543
|
+
SoftnessOnFillColorToo1, SoftnessGlow1, SoftnessBlend1; TransformSize ->
|
|
30544
|
+
Line/Word/CharacterSize X and Y), and those take a spline normally.
|
|
30545
|
+
"""
|
|
30546
|
+
try:
|
|
30547
|
+
attrs = tool[input_name].GetAttrs() or {}
|
|
30548
|
+
except Exception:
|
|
30549
|
+
return False, []
|
|
30550
|
+
if attrs.get("INPID_InputControl") != "NestControl":
|
|
30551
|
+
return False, []
|
|
30552
|
+
try:
|
|
30553
|
+
count = int(attrs.get("INPI_LabelControl_NumInputs") or 0)
|
|
30554
|
+
except (TypeError, ValueError):
|
|
30555
|
+
count = 0
|
|
30556
|
+
members: List[str] = []
|
|
30557
|
+
try:
|
|
30558
|
+
input_list = tool.GetInputList() or {}
|
|
30559
|
+
keys = list(input_list.keys())
|
|
30560
|
+
try:
|
|
30561
|
+
keys.sort(key=float)
|
|
30562
|
+
except (TypeError, ValueError):
|
|
30563
|
+
pass
|
|
30564
|
+
ids = [((input_list[k].GetAttrs() or {}).get("INPS_ID") or "") for k in keys]
|
|
30565
|
+
if input_name in ids:
|
|
30566
|
+
start = ids.index(input_name) + 1
|
|
30567
|
+
members = [i for i in ids[start:start + count] if i]
|
|
30568
|
+
except Exception:
|
|
30569
|
+
members = []
|
|
30570
|
+
return True, members
|
|
30571
|
+
|
|
30572
|
+
|
|
30573
|
+
def _fusion_nest_control_error(tool_name: str, input_name: str, members: List[str], verb: str):
|
|
30574
|
+
listed = ", ".join(members) if members else "see get_inputs(tool_name)"
|
|
30575
|
+
return _err(
|
|
30576
|
+
f"'{input_name}' on '{tool_name}' is a nest control (a group header), "
|
|
30577
|
+
f"not an animatable input; it cannot be {verb}.",
|
|
30578
|
+
code="FUSION_INPUT_IS_NEST_CONTROL", category="invalid_input", retryable=False,
|
|
30579
|
+
reason="Fusion NestControl inputs are passive headers that fold a group of "
|
|
30580
|
+
"controls. Tool.AddModifier returns False for them on every modifier "
|
|
30581
|
+
"type (measured on Studio 19.1.3.7: TextPlus Softness1, Follower "
|
|
30582
|
+
"TransformSize / Softness1 / Size1).",
|
|
30583
|
+
remediation=f"Target one of the controls the nest folds instead: {listed}.",
|
|
30584
|
+
state={"nest_control": input_name, "nest_members": members},
|
|
30585
|
+
)
|
|
30586
|
+
|
|
30587
|
+
|
|
30533
30588
|
def _fusion_input_spline(inp):
|
|
30534
30589
|
"""The modifier/spline tool driving `inp`, or None when it is not animated.
|
|
30535
30590
|
|
|
@@ -30686,7 +30741,10 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
30686
30741
|
add_keyframe(tool_name, input_name, time, value, modifier?) -> {success}
|
|
30687
30742
|
Attaches a BezierSpline (or `modifier`, e.g. 'Path' for Point inputs)
|
|
30688
30743
|
the first time an input is animated. Modifier names are mapped to
|
|
30689
|
-
their registry ID ('Follower' -> 'StyledTextFollower').
|
|
30744
|
+
their registry ID ('Follower' -> 'StyledTextFollower'). A nest control
|
|
30745
|
+
(a fold-down group header such as Softness1 or TransformSize) is refused
|
|
30746
|
+
with FUSION_INPUT_IS_NEST_CONTROL naming the controls it folds
|
|
30747
|
+
(SoftnessX1/SoftnessY1, ...): keyframe those.
|
|
30690
30748
|
add_modifier(tool_name, input_name, modifier) -> {success, modifier_tool, modifier_type}
|
|
30691
30749
|
Attach any modifier and return the tool Fusion created for it, so a
|
|
30692
30750
|
TEXT modifier (Follower on a TextPlus StyledText) can then be driven
|
|
@@ -30966,6 +31024,10 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
30966
31024
|
except Exception:
|
|
30967
31025
|
_already_animated = False
|
|
30968
31026
|
if not _already_animated:
|
|
31027
|
+
_is_nest, _members = _fusion_nest_members(tool, p["input_name"])
|
|
31028
|
+
if _is_nest:
|
|
31029
|
+
return _fusion_nest_control_error(
|
|
31030
|
+
p["tool_name"], p["input_name"], _members, "keyframed")
|
|
30969
31031
|
# AddModifier reports through the Lua bridge, which resolves an
|
|
30970
31032
|
# unknown attribute to None rather than raising, so the return is
|
|
30971
31033
|
# not reliable evidence on its own. The readback below is: if the
|
|
@@ -31012,6 +31074,10 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
|
|
|
31012
31074
|
if not inp:
|
|
31013
31075
|
return _err(f"Input '{input_name}' not found on tool '{p['tool_name']}'")
|
|
31014
31076
|
modifier_id = _fusion_modifier_id(requested)
|
|
31077
|
+
is_nest, members = _fusion_nest_members(tool, input_name)
|
|
31078
|
+
if is_nest:
|
|
31079
|
+
return _fusion_nest_control_error(p["tool_name"], input_name, members,
|
|
31080
|
+
"given a modifier")
|
|
31015
31081
|
try:
|
|
31016
31082
|
existing = inp.GetConnectedOutput()
|
|
31017
31083
|
except Exception:
|
|
@@ -33064,7 +33130,9 @@ if __name__ == "__main__":
|
|
|
33064
33130
|
logger.info("Starting full 377-tool granular server...")
|
|
33065
33131
|
sys.argv = [arg for arg in sys.argv if arg != "--full"]
|
|
33066
33132
|
from src.granular import mcp as granular_mcp
|
|
33133
|
+
from src.granular.common import connect_at_startup
|
|
33067
33134
|
|
|
33135
|
+
connect_at_startup()
|
|
33068
33136
|
_install_threaded_tool_dispatch(granular_mcp)
|
|
33069
33137
|
run_fastmcp_stdio(granular_mcp)
|
|
33070
33138
|
sys.exit(0)
|
package/src/utils/api_truth.py
CHANGED
|
@@ -3440,6 +3440,34 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
3440
3440
|
"only StyledTextFollower attached (new tool Follower1).",
|
|
3441
3441
|
"mitigation": ["fusion_comp.add_modifier", "fusion_comp.add_keyframe"],
|
|
3442
3442
|
},
|
|
3443
|
+
{
|
|
3444
|
+
"symbol": "Tool.AddModifier (NestControl inputs)",
|
|
3445
|
+
"object": "Fusion Tool",
|
|
3446
|
+
"signature": "(inputName, modifierRegID) -> bool",
|
|
3447
|
+
"reality": "Some inputs GetInputList() returns are not values at all: those whose "
|
|
3448
|
+
"INPID_InputControl attribute is 'NestControl' (INPB_Passive true) are "
|
|
3449
|
+
"the fold-down group headers the Fusion UI draws. AddModifier returns "
|
|
3450
|
+
"False for them on every modifier type, and assigning at a time sets "
|
|
3451
|
+
"nothing. Measured on TextPlus Softness1 and on the text Follower's "
|
|
3452
|
+
"TransformSize, Softness1 and Size1. The controls a header folds are "
|
|
3453
|
+
"the next INPI_LabelControl_NumInputs entries in GetInputList() order "
|
|
3454
|
+
"(Softness1 -> SoftnessX1, SoftnessY1, SoftnessOnFillColorToo1, "
|
|
3455
|
+
"SoftnessGlow1, SoftnessBlend1; TransformSize -> Line/Word/Character "
|
|
3456
|
+
"Size X and Y), and those take a BezierSpline normally.",
|
|
3457
|
+
"recommended": "Keyframe the folded controls, never the header. fusion_comp "
|
|
3458
|
+
"add_keyframe and add_modifier refuse a nest control with "
|
|
3459
|
+
"FUSION_INPUT_IS_NEST_CONTROL and list its members.",
|
|
3460
|
+
"tags": ["fusion", "silent-failure", "naming"],
|
|
3461
|
+
"verified_on": "DaVinci Resolve Studio 19.1.3.7",
|
|
3462
|
+
"measured": "2026-09-19 on a disposable timeline: Follower via add_modifier, then "
|
|
3463
|
+
"add_keyframe on TransformSize / Softness1 (FUSION_ADD_MODIFIER_FAILED, "
|
|
3464
|
+
"raw AddModifier False for BezierSpline, Path, TextScramble) versus "
|
|
3465
|
+
"Size / Opacity1 / Delay / SoftnessX1 / SoftnessY1 / SizeX1 / "
|
|
3466
|
+
"CharacterSizeX (BezierSpline attached); TextPlus Softness1 refused too. "
|
|
3467
|
+
"GetAttrs diff: INPID_InputControl NestControl vs SliderControl, "
|
|
3468
|
+
"INPB_Passive true, INPI_LabelControl_NumInputs 6 / 5 / 2.",
|
|
3469
|
+
"mitigation": ["fusion_comp.add_keyframe", "fusion_comp.add_modifier"],
|
|
3470
|
+
},
|
|
3443
3471
|
|
|
3444
3472
|
]
|
|
3445
3473
|
|
|
@@ -3500,7 +3528,8 @@ ACTION_SYMBOLS: Dict[Tuple[str, str], List[str]] = {
|
|
|
3500
3528
|
("timeline_item_color", "safe_export_lut"): ["TimelineItem.ExportLUT"],
|
|
3501
3529
|
("timeline", "duplicate"): ["Timeline.DuplicateTimeline"],
|
|
3502
3530
|
("project_manager", "archive"): ["ProjectManager.ArchiveProject"],
|
|
3503
|
-
("fusion_comp", "add_modifier"): ["Tool.AddModifier"],
|
|
3531
|
+
("fusion_comp", "add_modifier"): ["Tool.AddModifier", "Tool.AddModifier (NestControl inputs)"],
|
|
3532
|
+
("fusion_comp", "add_keyframe"): ["Tool.AddModifier (NestControl inputs)"],
|
|
3504
3533
|
("project_manager", "safe_project_archive"): ["ProjectManager.ArchiveProject"],
|
|
3505
3534
|
}
|
|
3506
3535
|
|