davinci-resolve-mcp 2.37.2 → 2.37.3
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 +28 -0
- package/README.md +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +3 -6
- package/src/granular/common.py +1 -1
- package/src/granular/resolve_control.py +1 -1
- package/src/server.py +30 -17
- package/src/utils/update_check.py +10 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
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.37.3
|
|
6
|
+
|
|
7
|
+
Deep-audit fixes, rounds two and three: agent-facing action discovery,
|
|
8
|
+
crash-safe user-state persistence, and resource hygiene.
|
|
9
|
+
|
|
10
|
+
- **Fixed** three tools' unknown-action error lists drifting from their real
|
|
11
|
+
dispatch: `timeline` omitted `clip_where` and `action_help`;
|
|
12
|
+
`timeline_item_color` and `graph` omitted `action_help`. Agents recovering
|
|
13
|
+
from a typo now see the full action set. A static AST test keeps every
|
|
14
|
+
tool's advertised list in both-way sync with its dispatch (and verifies
|
|
15
|
+
docstring-advertised actions are real).
|
|
16
|
+
- **Fixed** user-state files being vulnerable to truncation by a crash
|
|
17
|
+
mid-write. Affected files all reset to `{}` on corruption, so the next
|
|
18
|
+
save would silently wipe remaining user data. Writers now use atomic
|
|
19
|
+
temp-file + `os.replace`: `media-analysis-preferences.json` (all
|
|
20
|
+
analysis/caps/governance/update defaults — including the
|
|
21
|
+
`set_resolution_tier` and `set_caps_preset` paths, now routed through the
|
|
22
|
+
shared writer), `update-check.json`, the dashboard's `analysis.json`
|
|
23
|
+
transcription patch, and `transcript-corrections.json`.
|
|
24
|
+
- **Fixed** `_safe_project_delete` discarding `SaveProject()`'s result when
|
|
25
|
+
closing the current project before deletion; a failed save now surfaces
|
|
26
|
+
as a warning in the response.
|
|
27
|
+
- **Fixed** a file-descriptor leak: the parent kept its copy of the control
|
|
28
|
+
panel's log handle open after spawning the detached child.
|
|
29
|
+
- Audit classes verified clean: bare excepts, mutable default arguments,
|
|
30
|
+
`asyncio.run` inside running loops, subprocess timeouts, docstring phantom
|
|
31
|
+
actions, silent-success swallows in metadata/marker/archive write paths.
|
|
32
|
+
|
|
5
33
|
## What's New in v2.37.2
|
|
6
34
|
|
|
7
35
|
Static-audit fixes — four undefined-name references that silently fell back
|
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
|
@@ -35,7 +35,7 @@ from src.utils.update_check import (
|
|
|
35
35
|
|
|
36
36
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
37
37
|
|
|
38
|
-
VERSION = "2.37.
|
|
38
|
+
VERSION = "2.37.3"
|
|
39
39
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
40
40
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
41
41
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ from typing import Any, Dict, List, Optional, Tuple
|
|
|
21
21
|
from urllib.parse import parse_qs, unquote, urlparse
|
|
22
22
|
|
|
23
23
|
from src.utils.media_analysis import (
|
|
24
|
+
_write_json as _atomic_write_json,
|
|
24
25
|
analysis_index_status,
|
|
25
26
|
analysis_root_coverage,
|
|
26
27
|
build_analysis_index,
|
|
@@ -12605,9 +12606,7 @@ def regenerate_clip_transcript(
|
|
|
12605
12606
|
if payload.get("words"):
|
|
12606
12607
|
updated_report["transcription"]["words"] = payload["words"]
|
|
12607
12608
|
try:
|
|
12608
|
-
|
|
12609
|
-
json.dump(updated_report, handle, indent=2)
|
|
12610
|
-
handle.write("\n")
|
|
12609
|
+
_atomic_write_json(artifacts["analysis_json"], updated_report)
|
|
12611
12610
|
except Exception as exc:
|
|
12612
12611
|
return {"success": False, "error": f"transcript written but analysis.json patch failed: {exc}"}
|
|
12613
12612
|
word_segment_count = sum(1 for seg in (payload.get("segments") or []) if isinstance(seg, dict) and seg.get("words"))
|
|
@@ -12779,9 +12778,7 @@ def save_clip_transcript_corrections(project_root: str, clip_id: str, body: Dict
|
|
|
12779
12778
|
},
|
|
12780
12779
|
}
|
|
12781
12780
|
try:
|
|
12782
|
-
|
|
12783
|
-
json.dump(payload, handle, indent=2)
|
|
12784
|
-
handle.write("\n")
|
|
12781
|
+
_atomic_write_json(path, payload)
|
|
12785
12782
|
except Exception as exc:
|
|
12786
12783
|
return {"success": False, "error": str(exc)}
|
|
12787
12784
|
return {
|
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.37.
|
|
83
|
+
VERSION = "2.37.3"
|
|
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()}")
|
|
@@ -534,5 +534,5 @@ def quit_resolve() -> Dict[str, Any]:
|
|
|
534
534
|
resolve = get_resolve()
|
|
535
535
|
if resolve is None:
|
|
536
536
|
return {"error": "Not connected to DaVinci Resolve"}
|
|
537
|
-
|
|
537
|
+
resolve.Quit()
|
|
538
538
|
return {"success": True, "message": "DaVinci Resolve is quitting"}
|
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.37.
|
|
14
|
+
VERSION = "2.37.3"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -6944,11 +6944,22 @@ def _read_media_analysis_preferences() -> Dict[str, Any]:
|
|
|
6944
6944
|
|
|
6945
6945
|
|
|
6946
6946
|
def _write_media_analysis_preferences(preferences: Dict[str, Any]) -> None:
|
|
6947
|
+
# Atomic replace: a crash mid-write must not truncate the file, because the
|
|
6948
|
+
# reader resets to {} on corruption and the next save would then wipe every
|
|
6949
|
+
# remaining user preference.
|
|
6947
6950
|
path = _media_analysis_preferences_path()
|
|
6948
6951
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
+
tmp_path = f"{path}.tmp-{os.getpid()}-{threading.get_ident()}-{time.time_ns()}"
|
|
6953
|
+
try:
|
|
6954
|
+
with open(tmp_path, "w", encoding="utf-8") as handle:
|
|
6955
|
+
json.dump(preferences, handle, indent=2, sort_keys=True)
|
|
6956
|
+
handle.write("\n")
|
|
6957
|
+
os.replace(tmp_path, path)
|
|
6958
|
+
finally:
|
|
6959
|
+
try:
|
|
6960
|
+
os.remove(tmp_path)
|
|
6961
|
+
except OSError:
|
|
6962
|
+
pass
|
|
6952
6963
|
|
|
6953
6964
|
|
|
6954
6965
|
def _setup_text_key(value: Any) -> str:
|
|
@@ -11553,6 +11564,11 @@ def _open_control_panel(p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
11553
11564
|
)
|
|
11554
11565
|
except (OSError, FileNotFoundError) as exc:
|
|
11555
11566
|
return _err(f"Failed to launch control panel: {type(exc).__name__}: {exc}")
|
|
11567
|
+
finally:
|
|
11568
|
+
# The child holds its own copy of the fd; keeping ours open would leak
|
|
11569
|
+
# one descriptor per panel launch.
|
|
11570
|
+
if log_handle is not subprocess.DEVNULL:
|
|
11571
|
+
log_handle.close()
|
|
11556
11572
|
|
|
11557
11573
|
# Verify the child actually came up. Bind errors (port in use), import
|
|
11558
11574
|
# failures, etc. would otherwise leave us reporting "launched" while the
|
|
@@ -12270,11 +12286,14 @@ def _safe_project_delete(pm, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
12270
12286
|
if current_name == name:
|
|
12271
12287
|
if not p.get("close_current", False):
|
|
12272
12288
|
return _err("Refusing to delete the currently open project; pass close_current=True")
|
|
12273
|
-
if p.get("save_current", True)
|
|
12274
|
-
pm.SaveProject()
|
|
12289
|
+
saved = bool(pm.SaveProject()) if p.get("save_current", True) else None
|
|
12275
12290
|
closed = bool(pm.CloseProject(current))
|
|
12276
12291
|
if not closed:
|
|
12277
12292
|
return _err(f"Failed to close current project '{name}' before delete")
|
|
12293
|
+
result = {"success": bool(pm.DeleteProject(name))}
|
|
12294
|
+
if saved is False:
|
|
12295
|
+
result["warning"] = "SaveProject returned False before close; unsaved changes may have been discarded"
|
|
12296
|
+
return result
|
|
12278
12297
|
return {"success": bool(pm.DeleteProject(name))}
|
|
12279
12298
|
|
|
12280
12299
|
|
|
@@ -14759,10 +14778,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
14759
14778
|
prefs["resolve_ai_governance_preset"] = preset
|
|
14760
14779
|
if isinstance(p.get("overrides"), dict):
|
|
14761
14780
|
prefs["resolve_ai_governance_overrides"] = p["overrides"]
|
|
14762
|
-
|
|
14763
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
14764
|
-
with open(path, "w", encoding="utf-8") as fh:
|
|
14765
|
-
json.dump(prefs, fh, indent=2)
|
|
14781
|
+
_write_media_analysis_preferences(prefs)
|
|
14766
14782
|
return {"success": True, "tier": preset, "overrides": prefs.get("resolve_ai_governance_overrides") or {}}
|
|
14767
14783
|
if action == "set_caps_preset":
|
|
14768
14784
|
preset = (p.get("preset") or "").strip().lower()
|
|
@@ -14774,10 +14790,7 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
|
|
|
14774
14790
|
prefs["analysis_caps_preset"] = preset
|
|
14775
14791
|
if isinstance(p.get("overrides"), dict):
|
|
14776
14792
|
prefs["analysis_caps_overrides"] = p["overrides"]
|
|
14777
|
-
|
|
14778
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
14779
|
-
with open(path, "w", encoding="utf-8") as fh:
|
|
14780
|
-
json.dump(prefs, fh, indent=2)
|
|
14793
|
+
_write_media_analysis_preferences(prefs)
|
|
14781
14794
|
return {"success": True, "preset": preset, "overrides": prefs.get("analysis_caps_overrides") or {}}
|
|
14782
14795
|
if action == "get_usage":
|
|
14783
14796
|
from src.utils import analysis_caps as _ac
|
|
@@ -16099,7 +16112,7 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
16099
16112
|
if mp_err:
|
|
16100
16113
|
return mp_err
|
|
16101
16114
|
return _fairlight_boundary_report(proj, mp, tl, p)
|
|
16102
|
-
return _unknown(action, ["list","get_current","set_current","get_name","set_name","get_start_frame","get_end_frame","get_start_timecode","set_start_timecode","get_track_count","add_track","delete_track","get_track_sub_type","set_track_enable","get_track_enabled","set_track_lock","get_track_locked","get_track_name","set_track_name","get_items","delete_clips","set_clips_linked","duplicate","duplicate_clips","copy_clips","move_clips","copy_range","duplicate_range","overwrite_range","lift_range","story_spine_report","create_variant_from_ranges","bulk_set_item_properties","apply_look_to_items","thumbnail_contact_sheet","marker_thumbnail_review","edit_kernel_capabilities","probe_edit_kernel_item","title_property_scan","set_title_text","bulk_set_title_text","create_compound_clip","create_fusion_clip","import_into_timeline","export","get_setting","set_setting","insert_generator","insert_fusion_generator","insert_fusion_composition","insert_ofx_generator","insert_title","insert_fusion_title","get_unique_id","get_node_graph","get_media_pool_item","get_transcript","propose_cuts","apply_cuts","get_mark_in_out","set_mark_in_out","clear_mark_in_out","convert_to_stereo","get_items_in_track","get_voice_isolation_state","set_voice_isolation_state","extract_source_frame_ranges","audio_mix_capability_report",*_TIMELINE_CONFORM_KERNEL_ACTIONS,*_TIMELINE_AUDIO_KERNEL_ACTIONS])
|
|
16115
|
+
return _unknown(action, ["list","get_current","set_current","get_name","set_name","get_start_frame","get_end_frame","get_start_timecode","set_start_timecode","get_track_count","add_track","delete_track","get_track_sub_type","set_track_enable","get_track_enabled","set_track_lock","get_track_locked","get_track_name","set_track_name","get_items","delete_clips","set_clips_linked","duplicate","duplicate_clips","copy_clips","move_clips","copy_range","duplicate_range","overwrite_range","lift_range","story_spine_report","create_variant_from_ranges","bulk_set_item_properties","apply_look_to_items","thumbnail_contact_sheet","marker_thumbnail_review","edit_kernel_capabilities","probe_edit_kernel_item","title_property_scan","set_title_text","bulk_set_title_text","create_compound_clip","create_fusion_clip","import_into_timeline","export","get_setting","set_setting","insert_generator","insert_fusion_generator","insert_fusion_composition","insert_ofx_generator","insert_title","insert_fusion_title","get_unique_id","get_node_graph","get_media_pool_item","get_transcript","propose_cuts","apply_cuts","get_mark_in_out","set_mark_in_out","clear_mark_in_out","convert_to_stereo","get_items_in_track","get_voice_isolation_state","set_voice_isolation_state","extract_source_frame_ranges","audio_mix_capability_report","clip_where","action_help",*_TIMELINE_CONFORM_KERNEL_ACTIONS,*_TIMELINE_AUDIO_KERNEL_ACTIONS])
|
|
16103
16116
|
|
|
16104
16117
|
|
|
16105
16118
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -18018,7 +18031,7 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
|
|
|
18018
18031
|
return {"success": bool(item.CreateMagicMask(p.get("mode", "F")))}
|
|
18019
18032
|
elif action == "regenerate_magic_mask":
|
|
18020
18033
|
return {"success": bool(item.RegenerateMagicMask())}
|
|
18021
|
-
return _unknown(action, ["set_cdl","copy_grades","add_version","get_current_version","get_version_names","load_version","rename_version","delete_version","get_node_graph","get_color_group","assign_color_group","remove_from_color_group","export_lut","get_color_cache","set_color_cache","get_fusion_cache","set_fusion_cache","reset_all_node_colors","stabilize","smart_reframe","create_magic_mask","regenerate_magic_mask",*_COLOR_GRADE_KERNEL_ACTIONS])
|
|
18034
|
+
return _unknown(action, ["set_cdl","copy_grades","add_version","get_current_version","get_version_names","load_version","rename_version","delete_version","get_node_graph","get_color_group","assign_color_group","remove_from_color_group","export_lut","get_color_cache","set_color_cache","get_fusion_cache","set_fusion_cache","reset_all_node_colors","stabilize","smart_reframe","create_magic_mask","regenerate_magic_mask","action_help",*_COLOR_GRADE_KERNEL_ACTIONS])
|
|
18022
18035
|
|
|
18023
18036
|
|
|
18024
18037
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -18402,7 +18415,7 @@ def graph(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any
|
|
|
18402
18415
|
if blocked:
|
|
18403
18416
|
return blocked
|
|
18404
18417
|
return {"success": bool(g.ResetAllGrades())}
|
|
18405
|
-
return _unknown(action, ["get_num_nodes","get_lut","set_lut","get_node_cache","set_node_cache","get_node_label","get_tools_in_node","set_node_enabled","apply_grade_from_drx","apply_arri_cdl_lut","reset_all_grades"])
|
|
18418
|
+
return _unknown(action, ["get_num_nodes","get_lut","set_lut","get_node_cache","set_node_cache","get_node_label","get_tools_in_node","set_node_enabled","apply_grade_from_drx","apply_arri_cdl_lut","reset_all_grades","action_help"])
|
|
18406
18419
|
|
|
18407
18420
|
|
|
18408
18421
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -631,13 +631,22 @@ def _read_state(path: Path) -> Dict[str, Any]:
|
|
|
631
631
|
|
|
632
632
|
|
|
633
633
|
def _write_state(path: Path, result: Mapping[str, Any]) -> None:
|
|
634
|
+
# Atomic replace: _read_state resets to {} on a corrupt file, so a crash
|
|
635
|
+
# mid-write would silently drop snooze/ignore preferences.
|
|
636
|
+
tmp_path = path.with_name(f"{path.name}.tmp-{os.getpid()}")
|
|
634
637
|
try:
|
|
635
638
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
636
|
-
with
|
|
639
|
+
with tmp_path.open("w", encoding="utf-8") as handle:
|
|
637
640
|
json.dump(result, handle, indent=2, sort_keys=True)
|
|
638
641
|
handle.write("\n")
|
|
642
|
+
os.replace(tmp_path, path)
|
|
639
643
|
except OSError:
|
|
640
644
|
return
|
|
645
|
+
finally:
|
|
646
|
+
try:
|
|
647
|
+
os.remove(tmp_path)
|
|
648
|
+
except OSError:
|
|
649
|
+
pass
|
|
641
650
|
|
|
642
651
|
|
|
643
652
|
def _set_cached_status(result: Mapping[str, Any]) -> None:
|