davinci-resolve-mcp 4.4.0 → 4.4.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 +98 -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/option_errors.py +77 -0
- package/src/utils/resolve211_alignment.py +4 -2
- package/src/utils/resolve211_normalization.py +8 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,104 @@
|
|
|
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.4.2 — a refused option now says which one, and why
|
|
6
|
+
|
|
7
|
+
Reported as [#232](https://github.com/samuelgursky/davinci-resolve-mcp/issues/232):
|
|
8
|
+
`timeline.normalize_audio_level` "rejects every documented option schema". It does
|
|
9
|
+
not, and a test now pins all seven documented `NormalizeAudioOptions` shapes reaching
|
|
10
|
+
the native call. The defect was the refusal itself.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **One error message covered two unrelated failures.** `Unknown normalization
|
|
15
|
+
options or non-dictionary options` named neither the offending key nor the type
|
|
16
|
+
actually received, and listed nothing that *would* have been accepted — so a typo
|
|
17
|
+
and a malformed payload were indistinguishable, to the caller and to the bug
|
|
18
|
+
report. The only way to produce that message while passing documented keys is an
|
|
19
|
+
`options` that arrived as a **JSON string**, which some MCP clients produce when
|
|
20
|
+
they serialise a nested object. That caller is looking at a payload that appears
|
|
21
|
+
correct, so the refusal now says so in as many words:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
normalization options must be an object with any of normalizationMode,
|
|
25
|
+
targetLevel, targetLoudness, setLevelMode; received a string. It looks like a
|
|
26
|
+
JSON string — send options as a nested object, not as encoded text.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
An unknown key reads differently, because the cause and the fix are different:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
Unknown normalization option 'normalisationMode'; accepted keys are
|
|
33
|
+
normalizationMode, targetLevel, targetLoudness, setLevelMode.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **`auto_align_clips` carried the identical conflation** and now shares the same
|
|
37
|
+
builder, `src/utils/option_errors.py`.
|
|
38
|
+
|
|
39
|
+
### Validation
|
|
40
|
+
|
|
41
|
+
- Full offline suite: **3,655 passed, 1 skipped, 0 failed**, 1,276 subtests.
|
|
42
|
+
- **No behaviour change to accepted input.** The same options are accepted and reach
|
|
43
|
+
the same native call; a test asserts each of the seven documented shapes arrives at
|
|
44
|
+
`NormalizeAudioLevel`, and that a JSON-string payload is refused *without* reaching
|
|
45
|
+
it. No Resolve live run: nothing about the native call changed.
|
|
46
|
+
|
|
47
|
+
### Still unconfirmed
|
|
48
|
+
|
|
49
|
+
The reporter has not replied, so the JSON-string diagnosis remains the most likely
|
|
50
|
+
cause rather than a measured one. If their payload was something else, the new
|
|
51
|
+
message will now say what — which is the actual fix here.
|
|
52
|
+
|
|
53
|
+
## What's New in v4.4.1 — the safety ratchet stops scanning only half the project
|
|
54
|
+
|
|
55
|
+
The write-enforcement ratchet read `src/server.py` and nothing else. The granular
|
|
56
|
+
server's 387 tools were covered by no guard at all — not a risk table, not the
|
|
57
|
+
destructive registry, not the ratchet. That is how `ti_copy_grades` reached
|
|
58
|
+
`TimelineItem.CopyGrades`, which replaces a node graph with no recovery version,
|
|
59
|
+
behind nothing. v4.3.0 fixed that one tool by hand; nothing would have caught the
|
|
60
|
+
next one.
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- **`tests/test_write_enforcement_ratchet.py` now scans both servers.** They are
|
|
65
|
+
built differently, so the granular tests claim different things and the module
|
|
66
|
+
docstring says which is which:
|
|
67
|
+
|
|
68
|
+
- **Enforcement.** A granular tool that calls a symbol the ledger marks
|
|
69
|
+
`destroys_prior_work` must be gated — `acknowledge_trap` plus a confirm token —
|
|
70
|
+
and must be hinted destructive, so a client that refuses destructive tools never
|
|
71
|
+
reaches the confirmation at all. `TRAP_METHODS` is derived from `API_TRUTH`
|
|
72
|
+
rather than written out, so flagging a new entry extends this guard without
|
|
73
|
+
anyone remembering that this file exists.
|
|
74
|
+
- **Visibility.** The other **131** destructive-hinted granular tools are frozen
|
|
75
|
+
in a backlog that can only shrink. This does **not** make them safe: the
|
|
76
|
+
granular server has no enforcement hook — `@_destructive_op` wraps an
|
|
77
|
+
`(action, params)` signature granular tools do not have — so there is no
|
|
78
|
+
archive, no safe-mode refusal and no audit row behind any of them. The backlog
|
|
79
|
+
makes the number known, and makes the 132nd fail the suite.
|
|
80
|
+
|
|
81
|
+
### Fixed
|
|
82
|
+
|
|
83
|
+
- **The first draft of the gate detector could be fooled by dead code.** It looked
|
|
84
|
+
for the string `CONFIRM_TOKENS` in the function body, so deleting the token
|
|
85
|
+
*redemption* while leaving the *issuance* behind still read as gated — and issuing
|
|
86
|
+
a token nobody checks is exactly the regression worth catching. Gating is now
|
|
87
|
+
detected as AST call nodes (`issue` **and** `consume` on `CONFIRM_TOKENS`) plus
|
|
88
|
+
the real `acknowledge_trap` and `confirm_token` parameters.
|
|
89
|
+
|
|
90
|
+
### Validation
|
|
91
|
+
|
|
92
|
+
- Five regressions re-introduced deliberately, each confirming a guard fires rather
|
|
93
|
+
than passing vacuously: delete the redemption, drop `acknowledge_trap`, drop the
|
|
94
|
+
destructive annotation, add a new ungated destructive tool, and gate a tool still
|
|
95
|
+
on the backlog. **Two of the five passed against the first draft** — the dead-code
|
|
96
|
+
hole above, and a probe that silently did nothing because `ast.unparse` drops
|
|
97
|
+
comments. Both the guard and the probes were fixed until all five failed on
|
|
98
|
+
demand and passed on restore.
|
|
99
|
+
- Full offline suite: **3,644 passed, 1 skipped, 0 failed**, 1,269 subtests. All
|
|
100
|
+
release drift guards green.
|
|
101
|
+
- Tests only; no server behaviour changed and no Resolve call was made.
|
|
102
|
+
|
|
5
103
|
## What's New in v4.4.0 — 85 granular tools stop lying to clients about what they do
|
|
6
104
|
|
|
7
105
|
Granular tools infer their MCP safety annotation from the leading verb in the tool
|
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.4.
|
|
15
|
+
> 本翻译对应 v4.4.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.4.
|
|
40
|
+
VERSION = "4.4.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
|
@@ -92,7 +92,7 @@ if not logging.getLogger().handlers:
|
|
|
92
92
|
handlers=[logging.StreamHandler()],
|
|
93
93
|
)
|
|
94
94
|
|
|
95
|
-
VERSION = "4.4.
|
|
95
|
+
VERSION = "4.4.2"
|
|
96
96
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
97
97
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
98
98
|
logger.info(f"Detected platform: {get_platform()}")
|
package/src/server.py
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Say which option was wrong, and how — not merely that something was.
|
|
2
|
+
|
|
3
|
+
Issue #232 reported that `timeline.normalize_audio_level` "rejects every documented
|
|
4
|
+
option schema". It does not: every shape in `NormalizeAudioOptions` is accepted. But
|
|
5
|
+
the refusal read
|
|
6
|
+
|
|
7
|
+
Unknown normalization options or non-dictionary options
|
|
8
|
+
|
|
9
|
+
which folds two unrelated failures into one sentence, names neither the offending
|
|
10
|
+
key nor the type actually received, and lists nothing that *would* be accepted. A
|
|
11
|
+
caller whose MCP client had serialised `options` to a JSON string — the one way to
|
|
12
|
+
produce that message while passing documented keys — had no way to tell that from a
|
|
13
|
+
typo, and neither did the report.
|
|
14
|
+
|
|
15
|
+
So the message is the bug worth fixing. These builders make a refusal answer three
|
|
16
|
+
questions: what was wrong, what was received, and what would have been accepted.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, Iterable, Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _type_name(value: Any) -> str:
|
|
25
|
+
"""A type name a caller will recognise from their own client's payload."""
|
|
26
|
+
return {
|
|
27
|
+
str: "a string",
|
|
28
|
+
list: "a list",
|
|
29
|
+
tuple: "a list",
|
|
30
|
+
int: "a number",
|
|
31
|
+
float: "a number",
|
|
32
|
+
bool: "a boolean",
|
|
33
|
+
type(None): "null",
|
|
34
|
+
}.get(type(value), f"a {type(value).__name__}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def reject_option_keys(
|
|
38
|
+
options: Any,
|
|
39
|
+
accepted: Iterable[str],
|
|
40
|
+
label: str,
|
|
41
|
+
*,
|
|
42
|
+
allow_empty: bool = True,
|
|
43
|
+
) -> Optional[str]:
|
|
44
|
+
"""Refuse a malformed options mapping, or return None when it is usable.
|
|
45
|
+
|
|
46
|
+
The two failures are reported separately because they have different causes and
|
|
47
|
+
different fixes: a non-mapping is almost always the client serialising a nested
|
|
48
|
+
object, while an unknown key is a typo or a stale field name.
|
|
49
|
+
"""
|
|
50
|
+
accepted = tuple(accepted)
|
|
51
|
+
if not isinstance(options, dict):
|
|
52
|
+
hint = ""
|
|
53
|
+
if isinstance(options, str):
|
|
54
|
+
# By far the likeliest way to reach here with correct keys: some MCP
|
|
55
|
+
# clients JSON-encode nested objects. Say so, because the caller is
|
|
56
|
+
# looking at a payload that appears correct.
|
|
57
|
+
hint = (
|
|
58
|
+
" It looks like a JSON string — send options as a nested object, "
|
|
59
|
+
"not as encoded text."
|
|
60
|
+
)
|
|
61
|
+
return (
|
|
62
|
+
f"{label} options must be an object with any of "
|
|
63
|
+
f"{', '.join(accepted)}; received {_type_name(options)}.{hint}"
|
|
64
|
+
)
|
|
65
|
+
if not options and not allow_empty:
|
|
66
|
+
return (
|
|
67
|
+
f"{label} options must not be empty; accepted keys are "
|
|
68
|
+
f"{', '.join(accepted)}."
|
|
69
|
+
)
|
|
70
|
+
unknown = sorted(set(options) - set(accepted))
|
|
71
|
+
if unknown:
|
|
72
|
+
return (
|
|
73
|
+
f"Unknown {label} option{'s' if len(unknown) > 1 else ''} "
|
|
74
|
+
f"{', '.join(repr(k) for k in unknown)}; accepted keys are "
|
|
75
|
+
f"{', '.join(accepted)}."
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Native alignment with strict ID resolution before any timeline mutation."""
|
|
2
|
+
from src.utils.option_errors import reject_option_keys
|
|
2
3
|
from src.utils.resolve211_multicam import resolve_constant
|
|
3
4
|
|
|
4
5
|
OPTIONS = {
|
|
@@ -12,8 +13,9 @@ def auto_align(r, timeline, item_ids, options):
|
|
|
12
13
|
return {'error':'item_ids must be a non-empty list of timeline item unique IDs'}
|
|
13
14
|
if len(set(item_ids))!=len(item_ids):
|
|
14
15
|
return {'error':'item_ids must not contain duplicates'}
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
error = reject_option_keys(options, tuple(OPTIONS), 'alignment')
|
|
17
|
+
if error:
|
|
18
|
+
return {'error': error}
|
|
17
19
|
normalized={}
|
|
18
20
|
for key,value in options.items():
|
|
19
21
|
normalized[key],error=resolve_constant(r,value,OPTIONS[key])
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
"""Native normalization: validate explicit audio-item selection before writes."""
|
|
2
2
|
import math
|
|
3
3
|
|
|
4
|
+
from src.utils.option_errors import reject_option_keys
|
|
5
|
+
|
|
6
|
+
#: The full NormalizeAudioOptions surface, per the shipped typed API stub.
|
|
7
|
+
OPTION_KEYS = ('normalizationMode', 'targetLevel', 'targetLoudness', 'setLevelMode')
|
|
8
|
+
|
|
4
9
|
|
|
5
10
|
def finite_number(value):
|
|
6
11
|
try:
|
|
@@ -14,8 +19,9 @@ def normalize_audio(r, timeline, item_ids, options):
|
|
|
14
19
|
return {'error':'item_ids must be a non-empty list of audio timeline item unique IDs'}
|
|
15
20
|
if len(set(item_ids))!=len(item_ids):
|
|
16
21
|
return {'error':'item_ids must not contain duplicates'}
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
error = reject_option_keys(options, OPTION_KEYS, 'normalization')
|
|
23
|
+
if error:
|
|
24
|
+
return {'error': error}
|
|
19
25
|
normalized=dict(options)
|
|
20
26
|
for key,value in options.items():
|
|
21
27
|
if key=='normalizationMode':
|