davinci-resolve-mcp 2.35.0 → 2.35.1

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,15 @@
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.35.1
6
+
7
+ - **Added** `media_pool_item(action="extract_frames", clip_id, timestamps, output_dir?)`
8
+ extracts still JPEGs from a clip's source media at the given timestamps (seconds)
9
+ via ffmpeg. Source-safe: it reads the source and writes only to a scratch output
10
+ directory — it never modifies, transcodes, or proxies the source. (Closes a
11
+ read/write-symmetry gap: the analysis sampler existed internally but had no
12
+ standalone frame-extraction tool.)
13
+
5
14
  ## What's New in v2.35.0
6
15
 
7
16
  The Cut-IR executor — transcript-driven editing now closes the loop onto the
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.35.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.35.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-32%20(341%20full)-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.35.0"
38
+ VERSION = "2.35.1"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.35.0",
3
+ "version": "2.35.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.35.0"
83
+ VERSION = "2.35.1"
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.35.0"
14
+ VERSION = "2.35.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -5510,6 +5510,55 @@ def _audio_mapping_report(mp, tl, p: Dict[str, Any]):
5510
5510
  return {"timeline_items": timeline_items, "media_pool_items": clip_rows}
5511
5511
 
5512
5512
 
5513
+ def _extract_clip_frames(clip, p: Dict[str, Any]) -> Dict[str, Any]:
5514
+ """Extract still frames from a clip's source media at given timestamps.
5515
+
5516
+ Source-safe: reads the source file with ffmpeg and writes JPEGs to a scratch
5517
+ output directory only — it never modifies, transcodes, or proxies the source.
5518
+ `timestamps` is a list of seconds. Returns the written frame paths.
5519
+ """
5520
+ try:
5521
+ src = clip.GetClipProperty("File Path")
5522
+ except Exception:
5523
+ src = None
5524
+ if not src or not os.path.isfile(src):
5525
+ return _err("Clip has no readable source file (File Path)")
5526
+ timestamps = p.get("timestamps")
5527
+ if not isinstance(timestamps, list) or not timestamps:
5528
+ return _err("extract_frames requires 'timestamps' (a non-empty list of seconds)")
5529
+ if not shutil.which("ffmpeg"):
5530
+ return _err("ffmpeg not found on PATH")
5531
+ out_dir = p.get("output_dir") or tempfile.mkdtemp(prefix="mcp_frames_")
5532
+ try:
5533
+ os.makedirs(out_dir, exist_ok=True)
5534
+ except OSError as exc:
5535
+ return _err(f"Could not create output_dir: {exc}")
5536
+ paths, errors = [], []
5537
+ for i, ts in enumerate(timestamps):
5538
+ out_path = os.path.join(out_dir, f"frame_{i:04d}.jpg")
5539
+ try:
5540
+ safe_run(
5541
+ ["ffmpeg", "-y", "-ss", str(float(ts)), "-i", src,
5542
+ "-frames:v", "1", "-q:v", "2", out_path],
5543
+ capture_output=True, timeout=60,
5544
+ )
5545
+ except (OSError, subprocess.SubprocessError, ValueError) as exc:
5546
+ errors.append({"timestamp": ts, "error": str(exc)})
5547
+ continue
5548
+ if os.path.isfile(out_path):
5549
+ paths.append(out_path)
5550
+ else:
5551
+ errors.append({"timestamp": ts, "error": "no frame written (timestamp out of range?)"})
5552
+ return {
5553
+ "success": bool(paths),
5554
+ "source": src,
5555
+ "output_dir": out_dir,
5556
+ "frame_paths": paths,
5557
+ "count": len(paths),
5558
+ "errors": errors,
5559
+ }
5560
+
5561
+
5513
5562
  def _clip_name(clip):
5514
5563
  try:
5515
5564
  return clip.GetName()
@@ -13797,6 +13846,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
13797
13846
  get_transcription(clip_id) -> {text, truncated, status, has_transcription}
13798
13847
  Read a clip's transcription. `truncated` flags when Resolve's preview
13799
13848
  property cut the text off (the full transcript is longer).
13849
+ extract_frames(clip_id, timestamps, output_dir?) -> {frame_paths, output_dir, count, errors}
13850
+ Extract still JPEGs from the clip's source at the given timestamps (seconds)
13851
+ via ffmpeg. Source-safe: reads source, writes only to a scratch dir.
13800
13852
  perform_audio_classification(clip_id) -> {success} — Resolve 21+
13801
13853
  clear_audio_classification(clip_id) -> {success} — Resolve 21+
13802
13854
  analyze_for_intellisearch(clip_id, identify_faces?, is_better_mode?) -> {success} — Resolve 21+, AI IntelliSearch Extra
@@ -14012,6 +14064,8 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
14012
14064
  "status": status or None,
14013
14065
  "has_transcription": bool(text.strip()),
14014
14066
  }
14067
+ elif action == "extract_frames":
14068
+ return _extract_clip_frames(clip, p)
14015
14069
  elif action == "perform_audio_classification":
14016
14070
  missing = _requires_method(clip, "PerformAudioClassification", "21.0")
14017
14071
  if missing:
@@ -14096,7 +14150,7 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
14096
14150
  return {"success": bool(clip.SetMarkInOut(clean["mark_in"], clean["mark_out"], p.get("type", "all")))}
14097
14151
  elif action == "clear_mark_in_out":
14098
14152
  return {"success": bool(clip.ClearMarkInOut(p.get("type", "all")))}
14099
- return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer"])
14153
+ return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","extract_frames","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer"])
14100
14154
 
14101
14155
 
14102
14156
  # ═══════════════════════════════════════════════════════════════════════════════