davinci-resolve-mcp 4.8.0 → 4.8.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,41 @@
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.1 — text modifiers (Follower) attach to TextPlus inputs
6
+
7
+ ### Added
8
+
9
+ - **`fusion_comp add_modifier(tool_name, input_name, modifier)`** — attach any modifier
10
+ to a Fusion input and get back the tool Fusion created for it
11
+ (`modifier_tool`, `modifier_type`), so a TEXT modifier can then be driven with
12
+ `set_input` / `add_keyframe` on that tool. ([#250](https://github.com/samuelgursky/davinci-resolve-mcp/issues/250), reported by @artpavelalex-ux — the first report filed through this server's own `report_issue` action)
13
+ The reporter wanted a Follower on a TextPlus `StyledText` for per-character
14
+ typewriter animation, and `add_keyframe(modifier="Follower")` failed with
15
+ `FUSION_ADD_MODIFIER_FAILED` because Fusion rejects the attach. **Measured on
16
+ Studio 19.1.3.7:** `Tool.AddModifier` wants the modifier's REGISTRY ID, not its
17
+ display name — `AddModifier("StyledText", "Follower")` and `"TextFollower"` return
18
+ False and attach nothing; `"StyledTextFollower"` returns True, creates a `Follower1`
19
+ tool of that ID and connects it to the input. The mapping now lives in
20
+ `_FUSION_MODIFIER_IDS` and both `add_modifier` and `add_keyframe`'s `modifier`
21
+ parameter use it, so `Follower` works as written. Attachment is verified by
22
+ readback (the input's connected output), never by AddModifier's bool, which is
23
+ unreliable through the Lua bridge. An input that already has a modifier is refused
24
+ with `FUSION_INPUT_ALREADY_CONNECTED` and the existing tool named; a modifier
25
+ Fusion rejects is reported with the registry-ID remediation rather than claimed.
26
+ - Rated a LOW bounded reversible edit and registered in the destructive hook, so it
27
+ gets safe-mode, an audit row, and the timeline archive copy before the edit like
28
+ every other compound write; `add_keyframe` itself stays on the ratchet backlog.
29
+ - New `api_truth` entry `Tool.AddModifier` (`verified_on` 19.1.3.7) mapped through
30
+ `ACTION_SYMBOLS`, so `add_modifier` results carry the fact as a `known_limitation`.
31
+ - **Live-validated on landing through the real action** on a disposable timeline:
32
+ `add_modifier(Follower)` → `Follower1` / `StyledTextFollower`; `set_input(Delay=5)`
33
+ on the returned tool read back `5.0`; a second attach → `FUSION_INPUT_ALREADY_CONNECTED`
34
+ naming `Follower1`; a bogus modifier → `FUSION_ADD_MODIFIER_FAILED`. Unit tests in
35
+ `tests/test_fusion_add_modifier.py` against fakes that accept only the registry ID.
36
+ - Not measured: other text modifiers (the mapping table holds only the Follower),
37
+ builds other than 19.1.3.7, and whether the per-character transforms the reporter
38
+ wants animate as expected once driven — that is theirs to confirm.
39
+
5
40
  ## What's New in v4.8.0 — one read before planning: `project_manager snapshot`
6
41
 
7
42
  ### Added
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-4.8.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.1-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-37%20(389%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-4.8.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.8.1-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-37%20(389%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
- > 本翻译对应 v4.8.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.8.1 版 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
@@ -2038,7 +2038,13 @@ Key actions:
2038
2038
  `get_input(tool_name, input_name, time?)`
2039
2039
  - `get_inputs(tool_name)` / `get_outputs(tool_name)`
2040
2040
  - `set_attrs(tool_name, attrs)` / `get_attrs(tool_name)`
2041
- - `add_keyframe(tool_name, input_name, time, value)`
2041
+ - `add_keyframe(tool_name, input_name, time, value, modifier?)` — attaches a
2042
+ BezierSpline (or `modifier`, e.g. `Path` for Point inputs) on first use
2043
+ - `add_modifier(tool_name, input_name, modifier)` → `{modifier_tool, modifier_type}`
2044
+ — attach any modifier and get back the tool Fusion created, so a text modifier
2045
+ (`Follower` on a TextPlus `StyledText`) can be driven with `set_input` /
2046
+ `add_keyframe` on that tool (e.g. `Delay`). Fusion wants the registry ID
2047
+ (`StyledTextFollower`, measured on Studio 19.1.3.7); `Follower` is mapped for you
2042
2048
  - `get_position(tool_name)` / `set_position(tool_name, x, y)` — read/write a node's
2043
2049
  position on the FlowView canvas; `set_position` returns a position read-back
2044
2050
  - `copy_tool(tool_name, name?, x?, y?)` — duplicate a node (settings copied via a
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Read/Write Symmetry Audit
4
4
 
5
- - write-style action occurrences scanned: **124**
5
+ - write-style action occurrences scanned: **125**
6
6
  - write-style action occurrences with a matching read: **76**
7
7
  - distinct high-signal `set_` actions without a direct/known readback: **4**
8
8
 
@@ -13,6 +13,6 @@
13
13
  - `set_keyframe_interpolation`
14
14
  - `set_node_enabled`
15
15
 
16
- ## Low-signal (create/add/insert/apply/import — usually expected): 43 distinct names
16
+ ## Low-signal (create/add/insert/apply/import — usually expected): 44 distinct names
17
17
 
18
- `add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_multicam_clip`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
18
+ `add_clip_mattes`, `add_comp`, `add_fusion_mask`, `add_modifier`, `add_subfolder`, `add_sync_event_markers`, `add_timeline_mattes`, `add_track`, `add_transition`, `add_version`, `apply_arri_cdl_lut`, `apply_cuts`, `apply_fairlight_preset`, `apply_grade_from_drx`, `apply_look_to_items`, `apply_spec`, `apply_trace_plan`, `create_compound_clip`, `create_fusion_clip`, `create_magic_mask`, `create_multicam_clip`, `create_stereo_clip`, `create_subtitles`, `create_timeline`, `create_timeline_from_clips`, `create_variant_from_ranges`, `import_comp`, `import_folder`, `import_from_drp`, `import_into_timeline`, `import_media`, `import_preset`, `import_project`, `import_render`, `import_timeline`, `import_timeline_checked`, `import_to_pool`, `insert_audio`, `insert_fusion_composition`, `insert_fusion_generator`, `insert_fusion_title`, `insert_generator`, `insert_ofx_generator`, `insert_title`
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.0"
40
+ VERSION = "4.8.1"
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": "4.8.0",
3
+ "version": "4.8.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
93
93
  handlers=[logging.StreamHandler()],
94
94
  )
95
95
 
96
- VERSION = "4.8.0"
96
+ VERSION = "4.8.1"
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()}")
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.0"
14
+ VERSION = "4.8.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -30412,6 +30412,25 @@ def _fusion_keyframe_frames(inp) -> List[float]:
30412
30412
  return sorted(float(frame) for frame in kfs.values())
30413
30413
 
30414
30414
 
30415
+ #: Modifier names callers use -> the registry ID `Tool.AddModifier` wants.
30416
+ #: Measured on Studio 19.1.3.7 (issue #250) on a TextPlus StyledText input:
30417
+ #: AddModifier("StyledText", "Follower") and "TextFollower" return False and
30418
+ #: attach nothing; "StyledTextFollower" returns True, creates a `Follower1` tool
30419
+ #: of that ID and connects it to the input. Spline modifiers already go by their
30420
+ #: registry ID (BezierSpline, Path). Keys are compared case-insensitively.
30421
+ _FUSION_MODIFIER_IDS = {
30422
+ "follower": "StyledTextFollower",
30423
+ "textfollower": "StyledTextFollower",
30424
+ "styledtextfollower": "StyledTextFollower",
30425
+ }
30426
+
30427
+
30428
+ def _fusion_modifier_id(name: Any) -> str:
30429
+ """The registry ID for a modifier a caller named; unknown names pass through."""
30430
+ text = str(name or "").strip()
30431
+ return _FUSION_MODIFIER_IDS.get(text.lower(), text)
30432
+
30433
+
30415
30434
  def _fusion_input_spline(inp):
30416
30435
  """The modifier/spline tool driving `inp`, or None when it is not animated.
30417
30436
 
@@ -30565,7 +30584,17 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
30565
30584
  get_input(tool_name, input_name, time?) -> {value}
30566
30585
  set_attrs(tool_name, attrs) -> {success}
30567
30586
  get_attrs(tool_name) -> {attrs}
30568
- add_keyframe(tool_name, input_name, time, value) -> {success}
30587
+ add_keyframe(tool_name, input_name, time, value, modifier?) -> {success}
30588
+ Attaches a BezierSpline (or `modifier`, e.g. 'Path' for Point inputs)
30589
+ the first time an input is animated. Modifier names are mapped to
30590
+ their registry ID ('Follower' -> 'StyledTextFollower').
30591
+ add_modifier(tool_name, input_name, modifier) -> {success, modifier_tool, modifier_type}
30592
+ Attach any modifier and return the tool Fusion created for it, so a
30593
+ TEXT modifier (Follower on a TextPlus StyledText) can then be driven
30594
+ with set_input / add_keyframe on that tool (e.g. Delay). Refuses an
30595
+ input that already has a modifier. Measured on Studio 19.1.3.7:
30596
+ Fusion wants the registry ID ('StyledTextFollower'); 'Follower' is
30597
+ mapped for you.
30569
30598
  get_keyframes(tool_name, input_name) -> {keyframes}
30570
30599
  delete_keyframe(tool_name, input_name, time) -> {success, time, remaining_keyframes}
30571
30600
  Deletes on the spline attached to the input. Structured errors when the
@@ -30844,7 +30873,8 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
30844
30873
  # input still has no connected output, the assignment on the next
30845
30874
  # line would set a STATIC value and _ok() would report a keyframe
30846
30875
  # that does not exist.
30847
- tool.AddModifier(p["input_name"], p.get("modifier", "BezierSpline"))
30876
+ _modifier_id = _fusion_modifier_id(p.get("modifier", "BezierSpline"))
30877
+ tool.AddModifier(p["input_name"], _modifier_id)
30848
30878
  try:
30849
30879
  attached = tool[p["input_name"]].GetConnectedOutput() is not None
30850
30880
  except Exception:
@@ -30860,14 +30890,86 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
30860
30890
  "Reporting success here would claim a keyframe that "
30861
30891
  "does not exist.",
30862
30892
  remediation="Check the input accepts the modifier type "
30863
- f"({p.get('modifier', 'BezierSpline')!r}); Point "
30864
- "inputs take 'Path'.",
30893
+ f"({_modifier_id!r}); Point inputs take 'Path'. "
30894
+ "A TEXT modifier (Follower) is not a spline: "
30895
+ "attach it with add_modifier, then drive the "
30896
+ "modifier tool it returns.",
30865
30897
  )
30866
30898
  tool[p["input_name"]][p["time"]] = p["value"]
30867
30899
  return _ok()
30868
30900
  finally:
30869
30901
  comp.EndUndo(True)
30870
30902
 
30903
+ elif action == "add_modifier":
30904
+ tool = comp.FindTool(p["tool_name"])
30905
+ if not tool:
30906
+ return _err(f"Tool '{p['tool_name']}' not found")
30907
+ input_name = p.get("input_name")
30908
+ requested = p.get("modifier")
30909
+ if not input_name or not requested:
30910
+ return _err("add_modifier requires input_name and modifier",
30911
+ category="invalid_input")
30912
+ inp = tool[input_name]
30913
+ if not inp:
30914
+ return _err(f"Input '{input_name}' not found on tool '{p['tool_name']}'")
30915
+ modifier_id = _fusion_modifier_id(requested)
30916
+ try:
30917
+ existing = inp.GetConnectedOutput()
30918
+ except Exception:
30919
+ existing = None
30920
+ if existing is not None:
30921
+ existing_tool = existing.GetTool()
30922
+ attrs = (existing_tool.GetAttrs() or {}) if existing_tool else {}
30923
+ return _err(
30924
+ f"'{p['tool_name']}.{input_name}' already has a modifier attached.",
30925
+ code="FUSION_INPUT_ALREADY_CONNECTED", category="precondition",
30926
+ retryable=False,
30927
+ state={"modifier_tool": attrs.get("TOOLS_Name", ""),
30928
+ "modifier_type": attrs.get("TOOLS_RegID", "")},
30929
+ remediation="Drive the existing modifier tool, or "
30930
+ "disconnect(tool_name, input_name) first.",
30931
+ )
30932
+ # Same undo bracket as add_keyframe; no comp.Lock() — a lock is for
30933
+ # value writes and this is a graph edit, but see _FUSION_VALUE_WRITE_NOTE
30934
+ # for why writes under a lock are not trusted here.
30935
+ comp.StartUndo(f"Add modifier {modifier_id} to {input_name}")
30936
+ try:
30937
+ tool.AddModifier(input_name, modifier_id)
30938
+ # AddModifier's bool is unreliable through the Lua bridge (an unknown
30939
+ # attribute resolves to None). The readback is the evidence: the
30940
+ # input must now be connected to a modifier tool.
30941
+ try:
30942
+ out = tool[input_name].GetConnectedOutput()
30943
+ except Exception:
30944
+ out = None
30945
+ modifier_tool = out.GetTool() if out else None
30946
+ if modifier_tool is None:
30947
+ return _err(
30948
+ f"Could not attach {modifier_id!r} to "
30949
+ f"'{p['tool_name']}.{input_name}': no modifier is connected "
30950
+ "after AddModifier.",
30951
+ code="FUSION_ADD_MODIFIER_FAILED", category="api_error",
30952
+ retryable=False,
30953
+ reason="Fusion rejects a modifier whose registry ID it does not "
30954
+ "know for that input type, and reports it only as False.",
30955
+ remediation="Pass the modifier's REGISTRY ID, not its display "
30956
+ "name: the text Follower is 'StyledTextFollower' "
30957
+ "(add_modifier maps 'Follower' to it; measured on "
30958
+ "Studio 19.1.3.7). Spline modifiers are "
30959
+ "'BezierSpline' and, for Point inputs, 'Path'.",
30960
+ )
30961
+ attrs = modifier_tool.GetAttrs() or {}
30962
+ return _ok(
30963
+ tool_name=p["tool_name"], input_name=input_name,
30964
+ requested=requested, modifier_id=modifier_id,
30965
+ modifier_tool=attrs.get("TOOLS_Name", ""),
30966
+ modifier_type=attrs.get("TOOLS_RegID", modifier_id),
30967
+ next="Drive the modifier tool with set_input / add_keyframe "
30968
+ "(e.g. its Delay input for a per-character stagger).",
30969
+ )
30970
+ finally:
30971
+ comp.EndUndo(True)
30972
+
30871
30973
  elif action == "get_keyframes":
30872
30974
  tool = comp.FindTool(p["tool_name"])
30873
30975
  if not tool:
@@ -31057,7 +31159,7 @@ def fusion_comp(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[st
31057
31159
  "add_tool","delete_tool","get_tool_list","find_tool",
31058
31160
  "connect","disconnect","get_inputs","get_outputs",
31059
31161
  "set_input","get_input","set_attrs","get_attrs",
31060
- "add_keyframe","get_keyframes","delete_keyframe",
31162
+ "add_keyframe","add_modifier","get_keyframes","delete_keyframe",
31061
31163
  "get_comp_info","set_frame_range","get_frame_range","render",
31062
31164
  "start_undo","end_undo",
31063
31165
  "get_position","set_position","copy_tool","auto_arrange",
@@ -3414,6 +3414,32 @@ API_TRUTH: List[Dict[str, Any]] = [
3414
3414
  "mitigation": ["project_manager.archive", "project_manager.safe_project_archive",
3415
3415
  "archive_project"],
3416
3416
  },
3417
+ {
3418
+ "symbol": "Tool.AddModifier",
3419
+ "object": "Fusion Tool",
3420
+ "signature": "(inputName, modifierRegID) -> bool",
3421
+ "reality": "The second argument is the modifier's REGISTRY ID, not its "
3422
+ "display name. On a TextPlus StyledText input, "
3423
+ "AddModifier('StyledText', 'Follower') and 'TextFollower' return "
3424
+ "False and attach nothing; 'StyledTextFollower' returns True, "
3425
+ "creates a tool named Follower1 of that ID and connects it to the "
3426
+ "input. Spline modifiers already go by registry ID (BezierSpline, "
3427
+ "Path). Through the Lua bridge the bool is not reliable evidence; "
3428
+ "the input's connected-output readback is.",
3429
+ "recommended": "Pass the registry ID. fusion_comp add_modifier maps 'Follower' "
3430
+ "to 'StyledTextFollower', verifies by readback and returns the "
3431
+ "created modifier tool so it can be driven with set_input / "
3432
+ "add_keyframe (Delay for a per-character stagger).",
3433
+ "tags": ["fusion", "naming", "silent-failure"],
3434
+ "verified_on": "DaVinci Resolve Studio 19.1.3.7",
3435
+ "measured": "2026-09-19 on a disposable timeline: "
3436
+ "InsertFusionCompositionIntoTimeline, AddTool('TextPlus'), then "
3437
+ "AddModifier('StyledText', X) for X in Follower / "
3438
+ "StyledTextFollower / TextFollower, with a GetToolList diff and "
3439
+ "StyledText.GetConnectedOutput().GetTool() readback after each; "
3440
+ "only StyledTextFollower attached (new tool Follower1).",
3441
+ "mitigation": ["fusion_comp.add_modifier", "fusion_comp.add_keyframe"],
3442
+ },
3417
3443
 
3418
3444
  ]
3419
3445
 
@@ -3474,6 +3500,7 @@ ACTION_SYMBOLS: Dict[Tuple[str, str], List[str]] = {
3474
3500
  ("timeline_item_color", "safe_export_lut"): ["TimelineItem.ExportLUT"],
3475
3501
  ("timeline", "duplicate"): ["Timeline.DuplicateTimeline"],
3476
3502
  ("project_manager", "archive"): ["ProjectManager.ArchiveProject"],
3503
+ ("fusion_comp", "add_modifier"): ["Tool.AddModifier"],
3477
3504
  ("project_manager", "safe_project_archive"): ["ProjectManager.ArchiveProject"],
3478
3505
  }
3479
3506
 
@@ -96,7 +96,7 @@ DESTRUCTIVE_ACTIONS_BY_TOOL: Dict[str, FrozenSet[str]] = {
96
96
  # them. Plus the 21.0 AI deblur, rated MEDIUM: it creates media, and is
97
97
  # registered so it is audited and its dry run is honest.
98
98
  "folder": frozenset({"remove_motion_blur"}),
99
- "fusion_comp": frozenset({"delete_keyframe", "delete_tool"}),
99
+ "fusion_comp": frozenset({"add_modifier", "delete_keyframe", "delete_tool"}),
100
100
  "gallery_stills": frozenset({"delete_stills"}),
101
101
  "media_pool_item": frozenset({"remove_motion_blur"}),
102
102
  "media_pool_item_markers": frozenset({
@@ -203,6 +203,9 @@ class RiskClassificationHook(LifecycleHook):
203
203
  #: them unrecognised, i.e. it warns that the risk is unestablished for the
204
204
  #: actions whose risk is the best established of any we dispatch.
205
205
  _LOW_RISK_ACTIONS: Set[Tuple[str, str]] = {
206
+ # Attaching a modifier to one Fusion input: one undo step, and
207
+ # disconnect(tool, input) removes it. (#250)
208
+ ("fusion_comp", "add_modifier"),
206
209
  ("dctl", "encrypt_native"), # Creates a new file; never replaces existing content.
207
210
  ("timeline_markers", "add"),
208
211
  ("timeline_markers", "update_custom_data"),