davinci-resolve-mcp 2.70.4 → 2.71.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,36 @@
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.71.0
6
+
7
+ Keyed metadata getters honor a list of keys, and `delete_timelines` names the
8
+ parameter it wants. Reported and fixed by @billcarroll in #113, from live
9
+ cataloguing work.
10
+
11
+ ### Keyed getters silently returned everything
12
+
13
+ Resolve's keyed getters take one string. Handed a list they ignore it and return
14
+ the full dict, so a caller asking for three fields silently received all of them
15
+ with no signal that the request had been dropped — the kind of thing that reads
16
+ as working until someone counts.
17
+
18
+ `get_metadata`, `get_third_party_metadata` and `get_clip_property` now share a
19
+ `_keyed_get` helper that subsets locally, since there is no batch getter to
20
+ delegate to. An empty or non-string list is a clear error rather than a silent
21
+ superset.
22
+
23
+ Missing keys deliberately report differently through the two forms: the list
24
+ form maps them to `null`, which separates "absent" from "present but empty"; the
25
+ string form still returns Resolve's `""`. All three actions document both the
26
+ list form and that divergence.
27
+
28
+ ### `delete_timelines` leaked a KeyError
29
+
30
+ Called with `timeline_names`, or without `timeline_ids` at all, it raised a bare
31
+ `KeyError('timeline_ids')`. It now returns a proper error naming the expected
32
+ parameter, and when `timeline_names` was passed it says explicitly that
33
+ timelines are matched by unique ID rather than by name.
34
+
5
35
  ## What's New in v2.70.4
6
36
 
7
37
  Three silent-failure fixes from community reports, plus the Windows bridge
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.70.4-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.71.0-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-34%20(341%20full)-blue.svg)](#server-modes)
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.70.4"
39
+ VERSION = "2.71.0"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (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.70.4",
3
+ "version": "2.71.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.70.4"
88
+ VERSION = "2.71.0"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  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.70.4"
14
+ VERSION = "2.71.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -16626,11 +16626,18 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
16626
16626
 
16627
16627
  return _run_maybe_background("media_pool.import_timeline", p, _work)
16628
16628
  elif action == "delete_timelines":
16629
+ ids = p.get("timeline_ids")
16630
+ if not isinstance(ids, list) or not ids:
16631
+ hint = (" ('timeline_names' is not supported — timelines are matched"
16632
+ " by unique ID, e.g. from timeline.get_unique_id)"
16633
+ if "timeline_names" in p else "")
16634
+ return _err("delete_timelines requires 'timeline_ids', a non-empty"
16635
+ " list of timeline unique IDs" + hint)
16629
16636
  count = proj.GetTimelineCount()
16630
16637
  timelines = []
16631
16638
  for i in range(1, count + 1):
16632
16639
  tl = proj.GetTimelineByIndex(i)
16633
- if tl and tl.GetUniqueId() in p["timeline_ids"]:
16640
+ if tl and tl.GetUniqueId() in ids:
16634
16641
  timelines.append(tl)
16635
16642
  if not timelines:
16636
16643
  return _err("No timelines found")
@@ -17006,6 +17013,22 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
17006
17013
  # TOOL 13: media_pool_item
17007
17014
  # ═══════════════════════════════════════════════════════════════════════════════
17008
17015
 
17016
+ def _keyed_get(getter, key):
17017
+ """Resolve's keyed getters take one string key; passed a list they silently
17018
+ ignore it and return the full dict. Subset it ourselves instead.
17019
+
17020
+ Returns (value, error) — exactly one is non-None unless value is legitimately
17021
+ empty."""
17022
+ if isinstance(key, list):
17023
+ if not key or not all(isinstance(k, str) for k in key):
17024
+ return None, _err("'key' must be a string or a non-empty list of strings")
17025
+ full = getter("")
17026
+ if not isinstance(full, dict):
17027
+ full = {}
17028
+ return {k: full.get(k) for k in key}, None
17029
+ return getter(key), None
17030
+
17031
+
17009
17032
  @mcp.tool()
17010
17033
  @_guard_missing_params
17011
17034
  def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -17014,11 +17037,26 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17014
17037
  Actions:
17015
17038
  get_name(clip_id) -> {name}
17016
17039
  get_metadata(clip_id, key?) -> {metadata}
17040
+ — key: one string, or a list of strings to get just that subset.
17041
+ Missing keys: the list form maps them to null (distinguishing
17042
+ absent from empty); the string form passes Resolve's own answer
17043
+ through unchanged, which is "" or null depending on the getter
17044
+ and build (get_clip_property returns null on Studio 19.1.3.7).
17017
17045
  set_metadata(clip_id, key, value) OR set_metadata(clip_id, metadata) -> {success}
17018
17046
  get_third_party_metadata(clip_id, key?) -> {metadata}
17047
+ — key: one string, or a list of strings to get just that subset.
17048
+ Missing keys: the list form maps them to null (distinguishing
17049
+ absent from empty); the string form passes Resolve's own answer
17050
+ through unchanged, which is "" or null depending on the getter
17051
+ and build (get_clip_property returns null on Studio 19.1.3.7).
17019
17052
  set_third_party_metadata(clip_id, key, value) -> {success}
17020
17053
  get_media_id(clip_id) -> {media_id}
17021
17054
  get_clip_property(clip_id, key?) -> {properties}
17055
+ — key: one string, or a list of strings to get just that subset.
17056
+ Missing keys: the list form maps them to null (distinguishing
17057
+ absent from empty); the string form passes Resolve's own answer
17058
+ through unchanged, which is "" or null depending on the getter
17059
+ and build (get_clip_property returns null on Studio 19.1.3.7).
17022
17060
  set_clip_property(clip_id, key, value) -> {success}
17023
17061
  get_clip_color(clip_id) -> {color}
17024
17062
  set_clip_color(clip_id, color) -> {success}
@@ -17180,7 +17218,10 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17180
17218
  if action == "get_name":
17181
17219
  return {"name": clip.GetName()}
17182
17220
  elif action == "get_metadata":
17183
- return {"metadata": _ser(clip.GetMetadata(p.get("key", "")))}
17221
+ value, key_err = _keyed_get(clip.GetMetadata, p.get("key", ""))
17222
+ if key_err:
17223
+ return key_err
17224
+ return {"metadata": _ser(value)}
17184
17225
  elif action == "set_metadata":
17185
17226
  if "metadata" in p:
17186
17227
  ok = bool(clip.SetMetadata(p["metadata"]))
@@ -17196,13 +17237,19 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17196
17237
  return silent
17197
17238
  return {"success": ok}
17198
17239
  elif action == "get_third_party_metadata":
17199
- return {"metadata": _ser(clip.GetThirdPartyMetadata(p.get("key", "")))}
17240
+ value, key_err = _keyed_get(clip.GetThirdPartyMetadata, p.get("key", ""))
17241
+ if key_err:
17242
+ return key_err
17243
+ return {"metadata": _ser(value)}
17200
17244
  elif action == "set_third_party_metadata":
17201
17245
  return {"success": bool(clip.SetThirdPartyMetadata(p["key"], p["value"]))}
17202
17246
  elif action == "get_media_id":
17203
17247
  return {"media_id": clip.GetMediaId()}
17204
17248
  elif action == "get_clip_property":
17205
- return {"properties": _ser(clip.GetClipProperty(p.get("key", "")))}
17249
+ value, key_err = _keyed_get(clip.GetClipProperty, p.get("key", ""))
17250
+ if key_err:
17251
+ return key_err
17252
+ return {"properties": _ser(value)}
17206
17253
  elif action == "set_clip_property":
17207
17254
  ok = bool(clip.SetClipProperty(p["key"], p["value"]))
17208
17255
  if ok: