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 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
- [![Version](https://img.shields.io/badge/version-4.4.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.4.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.4.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.4.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.4.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
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.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "4.4.0",
3
+ "version": "4.4.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -92,7 +92,7 @@ if not logging.getLogger().handlers:
92
92
  handlers=[logging.StreamHandler()],
93
93
  )
94
94
 
95
- VERSION = "4.4.0"
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
@@ -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.4.0"
14
+ VERSION = "4.4.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -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
- if not isinstance(options,dict) or set(options)-set(OPTIONS):
16
- return {'error':'options must contain only SyncUsing and/or UseTrack'}
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
- if not isinstance(options,dict) or set(options)-{'normalizationMode','targetLevel','targetLoudness','setLevelMode'}:
18
- return {'error':'Unknown normalization options or non-dictionary options'}
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':