davinci-resolve-mcp 2.54.0 → 2.54.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 +46 -0
- package/README.md +1 -1
- package/install.py +94 -6
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +16 -3
- package/src/utils/api_truth.py +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
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 v2.54.2
|
|
6
|
+
|
|
7
|
+
A destructive-overwrite bug in the installer (issue #71): the MCP client setup
|
|
8
|
+
step could wipe a user's entire editor settings file instead of merging into it.
|
|
9
|
+
|
|
10
|
+
- **Fixed** `install.py` silently destroyed existing client config files whose
|
|
11
|
+
contents weren't strict JSON. `read_json` swallowed `JSONDecodeError` and
|
|
12
|
+
returned `{}`, so the subsequent "merge" wrote a file containing *only* the
|
|
13
|
+
`davinci-resolve` server entry — wiping themes, terminal env vars, LSP
|
|
14
|
+
settings, keybindings, everything else. Zed was the reported victim because
|
|
15
|
+
its `settings.json` ships with `//` comments (JSONC), but the same latent
|
|
16
|
+
risk existed for **every** supported client — VS Code and Continue also accept
|
|
17
|
+
JSONC. The fix is centralized in the single read/merge path so all clients are
|
|
18
|
+
covered:
|
|
19
|
+
- `read_json` now best-effort strips JSONC `//` and `/* */` comments and
|
|
20
|
+
trailing commas (string-aware, so comment markers inside string values are
|
|
21
|
+
preserved), letting commented configs merge cleanly instead of being lost.
|
|
22
|
+
- When a config file exists but still can't be parsed after that, the
|
|
23
|
+
installer **refuses to overwrite it** and tells the user to add the entry
|
|
24
|
+
manually — rather than silently replacing their settings.
|
|
25
|
+
- **Added** regression tests covering JSONC merge, plain-JSON merge,
|
|
26
|
+
refuse-to-overwrite on unparseable files, fresh-file creation, and
|
|
27
|
+
string-aware comment stripping.
|
|
28
|
+
|
|
29
|
+
## What's New in v2.54.1
|
|
30
|
+
|
|
31
|
+
One more instance of the enum-keyed silent-failure class (issue #70), plus a
|
|
32
|
+
guard so the next one can't ship unnoticed.
|
|
33
|
+
|
|
34
|
+
- **Fixed** the raw `timeline.export` action passed `type`/`subtype` straight to
|
|
35
|
+
`Timeline.Export`, which needs resolved `resolve.EXPORT_*` enum *values* — a
|
|
36
|
+
JSON/MCP caller can't pass a live enum, so the action silently wrote nothing
|
|
37
|
+
for every caller. It now resolves friendly format names (and `EXPORT_*`
|
|
38
|
+
constant names) via the same `_timeline_export_spec` resolver that
|
|
39
|
+
`export_timeline_checked` uses, and reports the resolved `export_type`/
|
|
40
|
+
`export_subtype`.
|
|
41
|
+
- **Fixed** `export_timeline_checked` resolved enum constants against the module
|
|
42
|
+
global `resolve` (which can be `None` and silently degrade the `EXPORT_*` args
|
|
43
|
+
to strings); it now uses `get_resolve()`, matching the issue-#70 lesson.
|
|
44
|
+
- **Added** an `api_truth` ↔ mitigation guard test: every `enum`-tagged catalog
|
|
45
|
+
entry must declare a `mitigation` (the resolver/wrapper functions), each of
|
|
46
|
+
which must exist in `src.server`. The next raw enum passthrough — a documented
|
|
47
|
+
symbol with no real resolver, or a renamed/removed resolver — now fails CI.
|
|
48
|
+
Added a `Timeline.Export` catalog entry and wired `mitigation` onto the
|
|
49
|
+
`AutoSyncAudio`, `CreateSubtitlesFromAudio`, and CloudProject entries.
|
|
50
|
+
|
|
5
51
|
## What's New in v2.54.0
|
|
6
52
|
|
|
7
53
|
Hardens the rest of the enum-keyed settings APIs against the same silent-failure
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# DaVinci Resolve MCP Server
|
|
2
2
|
|
|
3
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
4
4
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
5
5
|
[](docs/reference/api-coverage.md)
|
|
6
6
|
[-blue.svg)](#server-modes)
|
package/install.py
CHANGED
|
@@ -17,6 +17,7 @@ import argparse
|
|
|
17
17
|
import json
|
|
18
18
|
import os
|
|
19
19
|
import platform
|
|
20
|
+
import re
|
|
20
21
|
import shutil
|
|
21
22
|
import subprocess
|
|
22
23
|
import sys
|
|
@@ -35,7 +36,7 @@ from src.utils.update_check import (
|
|
|
35
36
|
|
|
36
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
38
|
|
|
38
|
-
VERSION = "2.54.
|
|
39
|
+
VERSION = "2.54.2"
|
|
39
40
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
41
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
42
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
|
@@ -458,14 +459,91 @@ def build_zed_entry(python_path, server_path, api_path, lib_path, system=SYSTEM,
|
|
|
458
459
|
|
|
459
460
|
# ─── Config File Operations ──────────────────────────────────────────────────
|
|
460
461
|
|
|
462
|
+
class ConfigParseError(Exception):
|
|
463
|
+
"""Existing config file has content but could not be parsed.
|
|
464
|
+
|
|
465
|
+
Callers must NOT overwrite such a file -- doing so destroys the user's
|
|
466
|
+
settings (issue #71).
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _strip_jsonc(text):
|
|
471
|
+
"""Best-effort strip of // and /* */ comments and trailing commas.
|
|
472
|
+
|
|
473
|
+
Zed's ``settings.json`` (and several other clients) accept JSON-with-comments.
|
|
474
|
+
Python's ``json`` module rejects those, so we strip them before parsing.
|
|
475
|
+
The walk is string-aware, so a ``//`` or ``/*`` sequence inside a JSON
|
|
476
|
+
string value is preserved.
|
|
477
|
+
"""
|
|
478
|
+
out = []
|
|
479
|
+
i = 0
|
|
480
|
+
n = len(text)
|
|
481
|
+
in_string = False
|
|
482
|
+
escape = False
|
|
483
|
+
while i < n:
|
|
484
|
+
ch = text[i]
|
|
485
|
+
if in_string:
|
|
486
|
+
out.append(ch)
|
|
487
|
+
if escape:
|
|
488
|
+
escape = False
|
|
489
|
+
elif ch == "\\":
|
|
490
|
+
escape = True
|
|
491
|
+
elif ch == '"':
|
|
492
|
+
in_string = False
|
|
493
|
+
i += 1
|
|
494
|
+
continue
|
|
495
|
+
if ch == '"':
|
|
496
|
+
in_string = True
|
|
497
|
+
out.append(ch)
|
|
498
|
+
i += 1
|
|
499
|
+
continue
|
|
500
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "/":
|
|
501
|
+
i += 2
|
|
502
|
+
while i < n and text[i] not in "\r\n":
|
|
503
|
+
i += 1
|
|
504
|
+
continue
|
|
505
|
+
if ch == "/" and i + 1 < n and text[i + 1] == "*":
|
|
506
|
+
i += 2
|
|
507
|
+
while i + 1 < n and not (text[i] == "*" and text[i + 1] == "/"):
|
|
508
|
+
i += 1
|
|
509
|
+
i += 2
|
|
510
|
+
continue
|
|
511
|
+
out.append(ch)
|
|
512
|
+
i += 1
|
|
513
|
+
stripped = "".join(out)
|
|
514
|
+
# Drop trailing commas before a closing brace/bracket.
|
|
515
|
+
stripped = re.sub(r",(\s*[}\]])", r"\1", stripped)
|
|
516
|
+
return stripped
|
|
517
|
+
|
|
518
|
+
|
|
461
519
|
def read_json(path):
|
|
462
|
-
"""Read a JSON file
|
|
520
|
+
"""Read a JSON/JSONC config file.
|
|
521
|
+
|
|
522
|
+
Returns an empty dict when the file is absent or empty (safe to create).
|
|
523
|
+
Raises :class:`ConfigParseError` when the file has content that cannot be
|
|
524
|
+
parsed even after stripping JSONC comments and trailing commas -- callers
|
|
525
|
+
must refuse to overwrite such a file, or they would wipe the user's
|
|
526
|
+
existing settings (issue #71: Zed's settings.json ships with comments).
|
|
527
|
+
"""
|
|
463
528
|
try:
|
|
464
529
|
with open(path, "r") as f:
|
|
465
|
-
|
|
466
|
-
except
|
|
530
|
+
raw = f.read()
|
|
531
|
+
except FileNotFoundError:
|
|
467
532
|
return {}
|
|
468
533
|
|
|
534
|
+
if not raw.strip():
|
|
535
|
+
return {}
|
|
536
|
+
|
|
537
|
+
try:
|
|
538
|
+
return json.loads(raw)
|
|
539
|
+
except json.JSONDecodeError:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
return json.loads(_strip_jsonc(raw))
|
|
544
|
+
except json.JSONDecodeError as exc:
|
|
545
|
+
raise ConfigParseError(str(exc)) from exc
|
|
546
|
+
|
|
469
547
|
|
|
470
548
|
def write_json(path, data):
|
|
471
549
|
"""Write JSON to file, creating parent directories."""
|
|
@@ -501,8 +579,18 @@ def write_client_config(client, python_path, server_path, api_path, lib_path, dr
|
|
|
501
579
|
preview = {config_key: {"davinci-resolve": server_entry}}
|
|
502
580
|
return True, f"Would write to {config_path}:\n{json.dumps(preview, indent=2)}"
|
|
503
581
|
|
|
504
|
-
# Read existing config and merge
|
|
505
|
-
|
|
582
|
+
# Read existing config and merge. If the file exists but cannot be parsed,
|
|
583
|
+
# refuse to overwrite it -- silently replacing it would wipe the user's
|
|
584
|
+
# settings (issue #71, especially Zed's commented settings.json).
|
|
585
|
+
try:
|
|
586
|
+
existing = read_json(config_path)
|
|
587
|
+
except ConfigParseError as exc:
|
|
588
|
+
return False, (
|
|
589
|
+
f"{config_path} exists but could not be parsed ({exc}). "
|
|
590
|
+
f"Refusing to overwrite to avoid data loss. Add the "
|
|
591
|
+
f'"{config_key}" entry manually using the manual config output '
|
|
592
|
+
f"(run with --manual)."
|
|
593
|
+
)
|
|
506
594
|
|
|
507
595
|
if config_key not in existing:
|
|
508
596
|
existing[config_key] = {}
|
package/package.json
CHANGED
package/src/granular/common.py
CHANGED
|
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
|
|
|
80
80
|
handlers=[logging.StreamHandler()],
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
-
VERSION = "2.54.
|
|
83
|
+
VERSION = "2.54.2"
|
|
84
84
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
85
85
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
86
86
|
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 341-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.54.
|
|
14
|
+
VERSION = "2.54.2"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -5042,7 +5042,10 @@ def _export_timeline_checked(tl, p: Dict[str, Any]):
|
|
|
5042
5042
|
folder = os.path.dirname(os.path.abspath(path))
|
|
5043
5043
|
if folder:
|
|
5044
5044
|
os.makedirs(folder, exist_ok=True)
|
|
5045
|
-
|
|
5045
|
+
# Resolve enum constants via get_resolve(), not the module global `resolve`
|
|
5046
|
+
# (which can be None and silently degrade the EXPORT_* args to strings — the
|
|
5047
|
+
# same failure class as issue #70).
|
|
5048
|
+
spec = _timeline_export_spec(p, get_resolve())
|
|
5046
5049
|
if p.get("dry_run"):
|
|
5047
5050
|
return _ok(path=path, would_export=True, spec={k: v for k, v in spec.items() if k != "export_type"})
|
|
5048
5051
|
with long_resolve_op("timeline.export_timeline_checked"):
|
|
@@ -17851,8 +17854,18 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
17851
17854
|
elif action == "import_into_timeline":
|
|
17852
17855
|
return {"success": bool(tl.ImportIntoTimeline(p["path"], p.get("options", {})))}
|
|
17853
17856
|
elif action == "export":
|
|
17857
|
+
# Timeline.Export needs resolved resolve.EXPORT_* enum *values*, which a
|
|
17858
|
+
# JSON/MCP caller cannot pass — handing it a string silently fails. Resolve
|
|
17859
|
+
# friendly type/subtype names (or EXPORT_* constant names) the same way
|
|
17860
|
+
# export_timeline_checked does. This raw action keeps no path sandbox.
|
|
17861
|
+
spec = _timeline_export_spec(p, get_resolve())
|
|
17854
17862
|
with long_resolve_op("timeline.export"):
|
|
17855
|
-
|
|
17863
|
+
success = bool(tl.Export(p["path"], spec["export_type"], spec["export_subtype"]))
|
|
17864
|
+
return {
|
|
17865
|
+
"success": success,
|
|
17866
|
+
"export_type": spec["export_type_name"],
|
|
17867
|
+
"export_subtype": spec["export_subtype_name"],
|
|
17868
|
+
}
|
|
17856
17869
|
elif action == "get_setting":
|
|
17857
17870
|
return {"settings": _ser(tl.GetSetting(p.get("name", "")))}
|
|
17858
17871
|
elif action == "set_setting":
|
package/src/utils/api_truth.py
CHANGED
|
@@ -28,6 +28,7 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
28
28
|
"resolve handle, and verify by reading each clip's "
|
|
29
29
|
"'Synced Audio' property (see verify_by_readback).",
|
|
30
30
|
"tags": ["unreliable-return", "silent-failure", "audio", "enum"],
|
|
31
|
+
"mitigation": ["_normalize_auto_sync_settings", "_safe_auto_sync_audio"],
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
"symbol": "Timeline.CreateSubtitlesFromAudio",
|
|
@@ -43,6 +44,7 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
43
44
|
"and verify by reading the timeline's subtitle track count "
|
|
44
45
|
"before/after (server._safe_create_subtitles).",
|
|
45
46
|
"tags": ["unreliable-return", "silent-failure", "subtitle", "enum"],
|
|
47
|
+
"mitigation": ["_normalize_auto_caption_settings", "_safe_create_subtitles"],
|
|
46
48
|
},
|
|
47
49
|
{
|
|
48
50
|
"symbol": "ProjectManager CloudProject family (Create/Load/Import/RestoreCloudProject)",
|
|
@@ -57,6 +59,23 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
57
59
|
"before calling, and treat the bool return from "
|
|
58
60
|
"Import/RestoreCloudProject as advisory.",
|
|
59
61
|
"tags": ["silent-failure", "project", "cloud", "enum"],
|
|
62
|
+
"mitigation": ["_normalize_cloud_settings"],
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"symbol": "Timeline.Export",
|
|
66
|
+
"object": "Timeline",
|
|
67
|
+
"signature": "(fileName, exportType, exportSubtype) -> bool",
|
|
68
|
+
"reality": "exportType/exportSubtype must be resolve.EXPORT_* enum *values* "
|
|
69
|
+
"resolved from the live handle. A JSON/MCP caller cannot pass a "
|
|
70
|
+
"live enum, and a plain string ('fcpxml', or even the constant "
|
|
71
|
+
"name 'EXPORT_FCPXML_1_10') is silently rejected with no file "
|
|
72
|
+
"written.",
|
|
73
|
+
"recommended": "Map a friendly format/subtype to the EXPORT_* constant and "
|
|
74
|
+
"resolve it against the live handle "
|
|
75
|
+
"(server._timeline_export_spec) before calling; verify the "
|
|
76
|
+
"output file exists afterward.",
|
|
77
|
+
"tags": ["silent-failure", "timeline", "export", "enum"],
|
|
78
|
+
"mitigation": ["_timeline_export_spec", "_timeline_export_value"],
|
|
60
79
|
},
|
|
61
80
|
{
|
|
62
81
|
"symbol": "Composition.Paste",
|