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 +30 -0
- package/README.md +1 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +52 -5
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
|
-
[](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
|
@@ -36,7 +36,7 @@ from src.utils.update_check import (
|
|
|
36
36
|
|
|
37
37
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
38
38
|
|
|
39
|
-
VERSION = "2.
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
85
85
|
handlers=[logging.StreamHandler()],
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
VERSION = "2.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|