davinci-resolve-mcp 4.5.1 → 4.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,69 @@
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.5.2 — granular safety stops guessing, and the audit log stops lying
6
+
7
+ Two findings from a review of the v4.5.0 enforcement hook, both measured before and
8
+ after rather than reasoned about.
9
+
10
+ ### Fixed
11
+
12
+ - **The verb table was the sole authority on risk, and it disagreed with the
13
+ compound server on 20 tools.** The same operation is exposed on both servers under
14
+ the same action name, and the compound tables are where someone actually assessed
15
+ it. Three granular tools were rated **below** their compound assessment — the
16
+ direction that matters, because safe mode then lets them through:
17
+
18
+ | tool | was | now |
19
+ |---|---|---|
20
+ | `ti_copy_grades` | medium | **high** |
21
+ | `timeline_delete_clips` | high | **critical** |
22
+ | `timeline_detect_scene_cuts` | medium | **high** |
23
+
24
+ `ti_copy_grades` is the tool this entire effort began with — the one that reaches
25
+ `TimelineItem.CopyGrades` and replaces a node graph with no recovery version. The
26
+ verb table called it MEDIUM, because `copy` appears in no table, so safe mode did
27
+ not stop it.
28
+
29
+ The other seventeen were rated **above** their compound assessment: `clear_*` and
30
+ `set_*` tools called HIGH here while compound rates them LOW. That is not the safe
31
+ direction either — `_safe_mode_allows` documents why at length. A gate that refuses
32
+ work the compound server considers low-risk teaches people to switch safe mode off,
33
+ and a setting left off protects nothing.
34
+
35
+ Rating order is now: a symbol the ledger marks `destroys_prior_work`, then the
36
+ compound server's established rating for the same action name, then the verb.
37
+ Most-severe-wins where two compound tools rate one name differently, because a gate
38
+ should resolve ambiguity by refusing more rather than less.
39
+
40
+ - **The audit log misreported two of its three outcomes.** It is this surface's only
41
+ record of what ran — there is no archive behind it — so a row that overstates, or
42
+ is simply absent, is the whole artifact failing:
43
+
44
+ - a first call that only minted a confirm token was recorded `allowed`, claiming a
45
+ mutation that had not happened. It is now `pending_confirmation`, matching what
46
+ the compound hook records for the same case.
47
+ - **an exception wrote no row at all**, so the log went silent exactly when
48
+ something broke. It is now `failed`, carrying the exception type. The hook is a
49
+ witness, not a handler: the exception is re-raised untouched.
50
+
51
+ ### Changed
52
+
53
+ - Four existing tests asserted the old verb ratings. `ti_clear_flags` is LOW now
54
+ because compound rates `clear_flags` LOW — the change working, not a regression —
55
+ so the two behavioural tests moved to `ti_delete_version` as their HIGH exemplar,
56
+ and namespace stripping is asserted directly rather than through a rating that may
57
+ now come from the compound tables.
58
+
59
+ ### Validation
60
+
61
+ - Full offline suite: **3,693 passed, 1 skipped, 0 failed**, 1,419 subtests.
62
+ - Each fix was reverted in turn to confirm its guard fails rather than passing
63
+ vacuously — 9, 1 and 1 failures respectively, all green on restore.
64
+ - A guard now walks every decorated tool and fails if any is rated *below* the
65
+ compound server's established assessment, so the class cannot return one tool at a
66
+ time. A trap-symbol tool may still be raised above it.
67
+
5
68
  ## What's New in v4.5.1 — the safe-mode refusal reaches the caller on 27 more tools
6
69
 
7
70
  v4.5.0 gave the granular server a working safe-mode gate. On 27 tools it then threw
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.5.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.5.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(387%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.5.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.5.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-37%20(387%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.5.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.5.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.5.1"
40
+ VERSION = "4.5.2"
41
41
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
42
42
  # Resolve's scripting bridge loads into newer interpreters on recent builds
43
43
  # (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "4.5.1",
3
+ "version": "4.5.2",
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.5.1"
96
+ VERSION = "4.5.2"
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.5.1"
14
+ VERSION = "4.5.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -38,7 +38,12 @@ from typing import Any, Callable, Dict, FrozenSet, Optional, Tuple
38
38
  from src.utils import analysis_runs, brain_edits, media_pool_changes, timeline_versioning
39
39
  from src.utils.api_truth import API_TRUTH, traps_for, trap_notice
40
40
  from src.utils.bool_params import coerce_bool, explicit_bool_param
41
- from src.utils.execution_lifecycle import RiskAssessment, RiskLevel, classify_operation_risk
41
+ from src.utils.execution_lifecycle import (
42
+ RiskAssessment,
43
+ RiskClassificationHook,
44
+ RiskLevel,
45
+ classify_operation_risk,
46
+ )
42
47
 
43
48
  logger = logging.getLogger("resolve-mcp.destructive-hook")
44
49
 
@@ -947,22 +952,72 @@ def _reaches_trap_symbol(fn: Callable[..., Any]) -> bool:
947
952
  return False
948
953
 
949
954
 
955
+ _RISK_ORDER = {
956
+ RiskLevel.LOW.value: 0,
957
+ RiskLevel.MEDIUM.value: 1,
958
+ RiskLevel.HIGH.value: 2,
959
+ RiskLevel.CRITICAL.value: 3,
960
+ }
961
+
962
+
963
+ @functools.lru_cache(maxsize=1)
964
+ def _established_compound_ratings() -> Dict[str, str]:
965
+ """Compound risk ratings keyed by action name, most severe wins.
966
+
967
+ The same operation is often exposed on both servers under the same action
968
+ name — `clear_flags`, `delete_clips`, `copy_grades` — and the compound tables
969
+ are where someone actually assessed it. Guessing from the verb when a real
970
+ rating exists produced 20 disagreements, three of them UNDER-rating: the verb
971
+ table called `ti_copy_grades` MEDIUM (so safe mode let it through) while the
972
+ compound tables rate `copy_grades` HIGH, and `delete_clips` is CRITICAL there
973
+ against HIGH here.
974
+
975
+ Most-severe-wins because a name rated differently by two compound tools is
976
+ ambiguous, and a gate should resolve ambiguity by refusing more, not less.
977
+ """
978
+ ratings: Dict[str, str] = {}
979
+ for table, level in (
980
+ ("_CRITICAL_ACTIONS", RiskLevel.CRITICAL.value),
981
+ ("_HIGH_RISK_ACTIONS", RiskLevel.HIGH.value),
982
+ ("_MEDIUM_RISK_ACTIONS", RiskLevel.MEDIUM.value),
983
+ ("_LOW_RISK_ACTIONS", RiskLevel.LOW.value),
984
+ ):
985
+ for _tool, action in getattr(RiskClassificationHook, table, ()) or ():
986
+ current = ratings.get(action)
987
+ if current is None or _RISK_ORDER[level] > _RISK_ORDER[current]:
988
+ ratings[action] = level
989
+ return ratings
990
+
991
+
992
+ def _granular_action_name(tool_name: str) -> str:
993
+ """The tool name with its namespace removed — the compound action name."""
994
+ name = (tool_name or "").lower()
995
+ for prefix in _GRANULAR_NAMESPACES:
996
+ if name.startswith(prefix):
997
+ return name[len(prefix):]
998
+ return name
999
+
1000
+
950
1001
  def granular_risk_level(tool_name: str, fn: Optional[Callable[..., Any]] = None) -> str:
951
- """Rate a granular tool: HIGH from the ledger if it reaches a trap symbol,
952
- otherwise from its verb. Unknown verbs are MEDIUM, never HIGH.
1002
+ """Rate a granular tool, preferring an assessment over a guess.
1003
+
1004
+ In order: HIGH if the body reaches a symbol the ledger marks
1005
+ `destroys_prior_work`; else the compound server's established rating for the
1006
+ same action name; else the verb. Unknown verbs are MEDIUM, never HIGH.
953
1007
 
954
1008
  Rating an unrecognised verb HIGH would let safe mode block writes nobody has
955
1009
  assessed, which is the failure `_safe_mode_allows` documents: over-blocking
956
1010
  teaches people to switch the setting off, and a setting left off protects
957
- nothing.
1011
+ nothing. The compound lookup cuts both ways for the same reason — it raises
1012
+ `copy_grades` to HIGH, and it lowers the seventeen `clear_*`/`set_*` tools the
1013
+ verb table called HIGH while compound rates them LOW.
958
1014
  """
959
1015
  if fn is not None and _reaches_trap_symbol(fn):
960
1016
  return RiskLevel.HIGH.value
961
- name = (tool_name or "").lower()
962
- for prefix in _GRANULAR_NAMESPACES:
963
- if name.startswith(prefix):
964
- name = name[len(prefix):]
965
- break
1017
+ name = _granular_action_name(tool_name)
1018
+ established = _established_compound_ratings().get(name)
1019
+ if established is not None:
1020
+ return established
966
1021
  return GRANULAR_RISK_BY_VERB.get(name.split("_", 1)[0], RiskLevel.MEDIUM.value)
967
1022
 
968
1023
 
@@ -1084,15 +1139,38 @@ def granular_destructive_op(
1084
1139
  override_hint=f"{GRANULAR_OVERRIDE_PARAM}=true",
1085
1140
  ))
1086
1141
 
1087
- result = fn(*args, **kwargs)
1142
+ try:
1143
+ result = fn(*args, **kwargs)
1144
+ except BaseException as exc:
1145
+ # A call that reached Resolve and then failed used to leave no row
1146
+ # at all. The audit log is this surface's only record of what ran,
1147
+ # so going silent precisely when something broke is the worst place
1148
+ # to have a gap. Record it, then let the exception continue
1149
+ # untouched — the hook is a witness, not a handler.
1150
+ _audit_security_event(
1151
+ operation_id=operation_id,
1152
+ tool_name="granular",
1153
+ action=action,
1154
+ risk_level=level,
1155
+ status="failed",
1156
+ params=params,
1157
+ reason=type(exc).__name__,
1158
+ )
1159
+ raise
1160
+
1161
+ # A first call that mints a confirm token has not mutated anything, and
1162
+ # recording it as "allowed" overstates what happened — the compound hook
1163
+ # distinguishes the same case via _PENDING_CONFIRM_CHECK.
1164
+ pending = (isinstance(result, dict)
1165
+ and result.get("status") == "confirmation_required")
1088
1166
  _audit_security_event(
1089
1167
  operation_id=operation_id,
1090
1168
  tool_name="granular",
1091
1169
  action=action,
1092
1170
  risk_level=level,
1093
- status="allowed",
1171
+ status="pending_confirmation" if pending else "allowed",
1094
1172
  params=params,
1095
- reason="no_archive_on_granular",
1173
+ reason="confirm_token_required" if pending else "no_archive_on_granular",
1096
1174
  )
1097
1175
  return _annotate_security(result, operation_id=operation_id, risk_level=level)
1098
1176