davinci-resolve-mcp 2.76.0 → 2.77.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,48 @@
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.77.0
6
+
7
+ Folder addressing fails loud instead of quietly answering about whichever bin the
8
+ UI happens to have open. Contributed by [@billcarroll](https://github.com/billcarroll)
9
+ in [#116](https://github.com/samuelgursky/davinci-resolve-mcp/pull/116).
10
+
11
+ ### Fixed
12
+
13
+ - **An unresolvable folder address no longer falls back to the current bin.**
14
+ `media_pool add_subfolder` and `media_pool get_timeline_mattes` resolved their
15
+ folder argument with `_navigate_folder(...) or fallback`, so a typo'd path was
16
+ dropped and the action proceeded against the current bin (or root) with a
17
+ `success` envelope. For a read that is a wrong answer indistinguishable from a
18
+ right one; for `add_subfolder` it creates the folder wherever the UI happens to
19
+ be pointed. All three sites now share one resolver that returns
20
+ `FOLDER_NOT_FOUND` / `invalid_input` when a supplied address does not resolve,
21
+ with remediation naming `get_subfolders`.
22
+ - **A bad folder path came back marked retryable.** The `folder` tool did already
23
+ error on an unresolvable `path`, but with a bare message, so the envelope
24
+ defaulted to `resolve_api_failed` and told the caller to retry an address that
25
+ would never resolve. It is now `invalid_input`, non-retryable — the caller's to
26
+ fix.
27
+
28
+ ### Added
29
+
30
+ - **`folder_id` is accepted as a folder address**, alongside `path`, on the
31
+ `folder` tool, `media_pool add_subfolder`, and `media_pool get_timeline_mattes`.
32
+ `get_subfolders` hands out ids, and having no way to spend them is what invited
33
+ agents to guess a `folder_id` argument that no action read — which was silently
34
+ dropped, returned the current bin's contents with `success`, and made the tools
35
+ look like they ignored their arguments. Omitting every address still means what
36
+ it did before: the current folder for the `folder` tool, the root folder for the
37
+ two `media_pool` actions.
38
+
39
+ ### Known limitation
40
+
41
+ Only `path`/`folder_path`/`folderPath` and `folder_id`/`folderId` are recognised
42
+ as addresses. Any other invented key (`id`, `bin`, `folderName`) is still dropped,
43
+ and the action still answers about its default folder with `success`. This release
44
+ narrows the silent-wrong-folder class to a known key set rather than closing it;
45
+ closing it needs unknown-parameter rejection at the dispatch layer.
46
+
5
47
  ## What's New in v2.76.0
6
48
 
7
49
  Three AAF conform-fidelity fixes found by placing a full 83-minute Avid turnover and
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.76.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.77.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/docs/SKILL.md CHANGED
@@ -617,9 +617,25 @@ switching, and flattening remain Resolve UI workflows; see
617
617
  Note: `folder path` arguments use slash notation like `"Master/SubFolder"`.
618
618
  `"Master"` or `"/"` refers to the root folder.
619
619
 
620
+ Address a folder either by `path` or by `folder_id` — the id `get_subfolders`
621
+ returns for each entry (v2.77.0+; the same pair works for `media_pool
622
+ add_subfolder` via `parent_path`/`folder_id` and for `media_pool
623
+ get_timeline_mattes` via `folder_path`/`folder_id`). Omit both to get the
624
+ action's default: the current folder for the `folder` tool, the root folder for
625
+ those two `media_pool` actions. An address that is supplied but does not resolve
626
+ is a `FOLDER_NOT_FOUND` / `invalid_input` error — it never quietly falls back to
627
+ the current bin.
628
+
629
+ That fallback is what these tools used to do, so treat a pre-v2.77.0 server as
630
+ unable to tell you when it answered about the wrong folder. Note also that only
631
+ `path`/`folder_path`/`folderPath` and `folder_id`/`folderId` are recognised as
632
+ addresses: any other key you invent (`id`, `bin`, `folderName`) is still
633
+ silently dropped, and the action still answers about its default folder with
634
+ `success`. Use the documented names.
635
+
620
636
  **`folder`** — Operations on a specific Media Pool folder.
621
637
 
622
- Key actions: `get_clips(path?)`, `get_subfolders(path?)`, `export(path?, export_path)`,
638
+ Key actions: `get_clips(path?|folder_id?)`, `get_subfolders(path?|folder_id?)`, `export(path?, export_path)`,
623
639
  `transcribe_audio(path?, use_speaker_detection?)`, `clear_transcription(path?)`,
624
640
  `perform_audio_classification(path?)`, `analyze_for_intellisearch(path?, identify_faces?, is_better_mode?)`,
625
641
  `analyze_for_slate(path?, marker_color?)`, `remove_motion_blur(path?, deblur_option?)` (Resolve 21+;
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.76.0"
39
+ VERSION = "2.77.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.76.0",
3
+ "version": "2.77.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.76.0"
88
+ VERSION = "2.77.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.76.0"
14
+ VERSION = "2.77.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -2579,6 +2579,54 @@ def _find_clip_with_parent(folder, clip_id, _parent=None):
2579
2579
  return found_clip, found_parent
2580
2580
  return None, None
2581
2581
 
2582
+ def _find_folder_by_id(folder, folder_id):
2583
+ if folder.GetUniqueId() == folder_id:
2584
+ return folder
2585
+ for sub in (folder.GetSubFolderList() or []):
2586
+ found = _find_folder_by_id(sub, folder_id)
2587
+ if found:
2588
+ return found
2589
+ return None
2590
+
2591
+
2592
+ def _folder_from_params(mp, p, *path_keys, no_address="current"):
2593
+ """Resolve the folder an action was aimed at. Returns (folder, error).
2594
+
2595
+ Naming no folder falls back to the action's documented default (see
2596
+ `no_address`). What must never happen is the middle case: an addressing argument was
2597
+ supplied, did not resolve, and the action answered about the current bin
2598
+ anyway. That reports success for a different question than the caller asked,
2599
+ and it is indistinguishable from the tool working. It cost one session an
2600
+ afternoon: `folder_id` is not a key any action read, so it was dropped, the
2601
+ current bin's clips came back, and the tools were written off as broken.
2602
+
2603
+ So: unresolvable-but-supplied is an error, and the id that `get_subfolders`
2604
+ hands out is accepted as an address, since being given an id and having no
2605
+ way to use it is what invited the guess.
2606
+
2607
+ `no_address` preserves each action's historical no-argument default: "current"
2608
+ for the folder tool, "root" for the media_pool actions (whose old code hit
2609
+ `_navigate_folder(mp, "")`, which returns root). This fix must not also
2610
+ change what omitting the address means.
2611
+ """
2612
+ remediation = ("List folders with folder get_subfolders (walking down from "
2613
+ "path=\"Master\") and address by exact path or folder_id.")
2614
+ path = _first_param(p, *path_keys)
2615
+ if path:
2616
+ f = _navigate_folder(mp, path)
2617
+ return (f, None) if f else (None, _err(
2618
+ f"Folder not found: {path}", code="FOLDER_NOT_FOUND",
2619
+ category="invalid_input", remediation=remediation))
2620
+ folder_id = _first_param(p, "folder_id", "folderId")
2621
+ if folder_id:
2622
+ f = _find_folder_by_id(mp.GetRootFolder(), str(folder_id))
2623
+ return (f, None) if f else (None, _err(
2624
+ f"Folder not found: {folder_id}", code="FOLDER_NOT_FOUND",
2625
+ category="invalid_input", remediation=remediation))
2626
+ f = mp.GetRootFolder() if no_address == "root" else mp.GetCurrentFolder()
2627
+ return (f, None) if f else (None, _err("No current Media Pool folder"))
2628
+
2629
+
2582
2630
  def _navigate_folder(mp, path):
2583
2631
  root = mp.GetRootFolder()
2584
2632
  if not path or path in ("Master", "/", ""):
@@ -16666,7 +16714,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
16666
16714
  return _err(f"Folder not found: {p.get('path')}")
16667
16715
  return {"success": bool(mp.SetCurrentFolder(f))}
16668
16716
  elif action == "add_subfolder":
16669
- parent = _navigate_folder(mp, p.get("parent_path", "")) or mp.GetCurrentFolder()
16717
+ parent, folder_err = _folder_from_params(mp, p, "parent_path", "parentPath", no_address="root")
16718
+ if folder_err:
16719
+ return folder_err
16670
16720
  f = mp.AddSubFolder(parent, p["name"])
16671
16721
  return _ok(name=f.GetName(), id=f.GetUniqueId()) if f else _err("Failed to create subfolder")
16672
16722
  elif action == "delete_folders":
@@ -16973,7 +17023,9 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
16973
17023
  clip = _find_clip(root, p["clip_id"])
16974
17024
  return {"mattes": mp.GetClipMatteList(clip)} if clip else _err("Clip not found")
16975
17025
  elif action == "get_timeline_mattes":
16976
- folder = _navigate_folder(mp, p.get("folder_path", "")) or mp.GetCurrentFolder()
17026
+ folder, folder_err = _folder_from_params(mp, p, "folder_path", "folderPath", no_address="root")
17027
+ if folder_err:
17028
+ return folder_err
16977
17029
  result = mp.GetTimelineMatteList(folder)
16978
17030
  return {"mattes": len(result) if result else 0}
16979
17031
  elif action == "delete_clip_mattes":
@@ -17037,6 +17089,11 @@ def media_pool(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str
17037
17089
  def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
17038
17090
  """Operations on Media Pool folders.
17039
17091
 
17092
+ Address a folder with `path` ("Master/SubFolder") or with `folder_id` (the id
17093
+ get_subfolders returns). Omit both for the current folder. An address that is
17094
+ supplied but does not resolve is an error — it never falls back to the current
17095
+ folder.
17096
+
17040
17097
  Actions:
17041
17098
  get_clips(path?) -> {clips} — path like "Master/SubFolder", omit for current
17042
17099
  get_name(path?) -> {name}
@@ -17057,10 +17114,9 @@ def folder(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
17057
17114
  if err:
17058
17115
  return err
17059
17116
 
17060
- folder_path = p.get("path", "")
17061
- f = _navigate_folder(mp, folder_path) if folder_path else mp.GetCurrentFolder()
17062
- if not f:
17063
- return _err(f"Folder not found: {folder_path}")
17117
+ f, folder_err = _folder_from_params(mp, p, "path", "folder_path", "folderPath")
17118
+ if folder_err:
17119
+ return folder_err
17064
17120
 
17065
17121
  if action == "get_clips":
17066
17122
  clips = f.GetClipList() or []