davinci-resolve-mcp 2.24.0 → 2.25.0

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.
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.24.0"
83
+ VERSION = "2.25.0"
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()}")
@@ -1,9 +1,42 @@
1
1
  """MediaPoolItem operations and metadata helpers."""
2
2
 
3
3
  from src.granular.common import * # noqa: F401,F403
4
+ from src.utils.media_analysis import mark_registry_stale_for_clip as _mark_analysis_registry_stale
4
5
 
5
6
  resolve = ResolveProxy()
6
7
 
8
+
9
+ def _invalidate_analysis_registry_for_clip(project, clip, *, reason: str) -> Optional[Dict[str, Any]]:
10
+ """Best-effort: mark cached analysis stale after a Resolve clip replace.
11
+
12
+ Called from replace_clip / replace_media_pool_clip / preserve_sub_clip
13
+ after the Resolve API confirms the mutation. Failures here must NEVER
14
+ block the Resolve mutation — we return the result for diagnostics but
15
+ swallow exceptions so registry trouble does not corrupt the user's edit.
16
+ """
17
+ if project is None or clip is None:
18
+ return None
19
+ try:
20
+ project_name = project.GetName() if hasattr(project, "GetName") else None
21
+ project_id = project.GetUniqueId() if hasattr(project, "GetUniqueId") else None
22
+ clip_id = clip.GetUniqueId() if hasattr(clip, "GetUniqueId") else None
23
+ media_id = clip.GetMediaId() if hasattr(clip, "GetMediaId") else None
24
+ source_file = None
25
+ try:
26
+ source_file = clip.GetClipProperty("File Path") if hasattr(clip, "GetClipProperty") else None
27
+ except Exception:
28
+ source_file = None
29
+ return _mark_analysis_registry_stale(
30
+ project_name=project_name,
31
+ project_id=project_id,
32
+ clip_id=clip_id,
33
+ media_id=media_id,
34
+ source_file=source_file,
35
+ reason=reason,
36
+ )
37
+ except Exception as exc: # noqa: BLE001 — never break a Resolve mutation
38
+ return {"success": False, "error": f"registry invalidation skipped: {type(exc).__name__}: {exc}"}
39
+
7
40
  @mcp.tool(annotations=EXTERNAL_DESTRUCTIVE_TOOL)
8
41
  def link_proxy_media(clip_name: str, proxy_file_path: str) -> str:
9
42
  """Link a proxy media file to a clip.
@@ -118,6 +151,9 @@ def replace_clip(clip_name: str, replacement_path: str) -> str:
118
151
  try:
119
152
  result = target_clip.ReplaceClip(replacement_path)
120
153
  if result:
154
+ _invalidate_analysis_registry_for_clip(
155
+ current_project, target_clip, reason="replace_clip"
156
+ )
121
157
  return f"Successfully replaced clip '{clip_name}' with '{replacement_path}'"
122
158
  else:
123
159
  return f"Failed to replace clip '{clip_name}'"
@@ -674,14 +710,21 @@ def replace_media_pool_clip(clip_id: str, new_file_path: str) -> Dict[str, Any]:
674
710
  clip_id: Unique ID of the clip to replace.
675
711
  new_file_path: Absolute path to the new media file.
676
712
  """
677
- _, mp, err = _get_mp()
713
+ project, mp, err = _get_mp()
678
714
  if err:
679
715
  return err
680
716
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
681
717
  if not clip:
682
718
  return {"error": f"Clip {clip_id} not found"}
683
719
  result = clip.ReplaceClip(new_file_path)
684
- return {"success": bool(result)}
720
+ response: Dict[str, Any] = {"success": bool(result)}
721
+ if result:
722
+ registry = _invalidate_analysis_registry_for_clip(
723
+ project, clip, reason="replace_media_pool_clip"
724
+ )
725
+ if registry is not None:
726
+ response["analysis_registry_invalidation"] = registry
727
+ return response
685
728
 
686
729
 
687
730
  @mcp.tool()
@@ -692,7 +735,7 @@ def replace_media_pool_clip_preserve_sub_clip(clip_id: str, file_path: str) -> D
692
735
  clip_id: Unique ID of the clip to replace.
693
736
  file_path: Absolute path to the replacement media file.
694
737
  """
695
- _, mp, err = _get_mp()
738
+ project, mp, err = _get_mp()
696
739
  if err:
697
740
  return err
698
741
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
@@ -702,7 +745,14 @@ def replace_media_pool_clip_preserve_sub_clip(clip_id: str, file_path: str) -> D
702
745
  if missing:
703
746
  return missing
704
747
  result = clip.ReplaceClipPreserveSubClip(file_path)
705
- return {"success": bool(result)}
748
+ response: Dict[str, Any] = {"success": bool(result)}
749
+ if result:
750
+ registry = _invalidate_analysis_registry_for_clip(
751
+ project, clip, reason="replace_media_pool_clip_preserve_sub_clip"
752
+ )
753
+ if registry is not None:
754
+ response["analysis_registry_invalidation"] = registry
755
+ return response
706
756
 
707
757
 
708
758
  @mcp.tool()