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 +63 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/destructive_hook.py +90 -12
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
|
-
[](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.5.
|
|
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.
|
|
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
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.5.
|
|
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
|
@@ -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
|
|
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
|
|
952
|
-
|
|
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
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
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
|
-
|
|
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
|
|