davinci-resolve-mcp 2.87.2 → 2.89.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,128 @@
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.89.0
6
+
7
+ The build gates this server already enforced are now gates an agent can ask
8
+ about, and the capability probe those gates run on no longer lies. Issue #132,
9
+ reported by @magwa101.
10
+
11
+ ### Added
12
+
13
+ - **`get_version` reports what the connected build is missing.** A new `build`
14
+ block carries `unavailable_on_this_build` — every recorded API surface this
15
+ build does not have — plus `known_gates` and the caveat that an absence from
16
+ that list is not a promise a method exists. #132 happened in a session that
17
+ opened with `get_version` and was told a number and nothing the number ruled
18
+ out. `get_resolve_version_fields` gains the same list on the granular server.
19
+ - **The version-gate registry went from 7 recorded surfaces to 41.**
20
+ `_requires_method(obj, "GetLayoutPresetList", "21.0.4")` appears 44 times
21
+ across the two servers and gates 31 distinct symbols, but
22
+ `check_version_support` — the call `resolve-session` step 2 tells an agent to
23
+ make — answered from a ledger that knew seven of them. So the one question an
24
+ agent is instructed to ask returned `unknown` for surfaces this server was
25
+ already routing on. On Studio 19.1.3.7 the session preflight named 7 missing
26
+ surfaces; it now names 40. The new floors are labelled `documented` rather
27
+ than `measured`: they come from Blackmagic's release documentation, not a live
28
+ bisect here, and an agent relaying one should be able to say which.
29
+ - **`tests/test_version_gate_drift.py`** fails when a call site and the ledger
30
+ disagree, and when a bare method name is gated at two different builds on two
31
+ different classes (which would make a bare-name lookup a coin flip). A table
32
+ copied by hand is exactly what drifts back apart.
33
+ - **`src/utils/resolve_probe.py`** — `has_method` and `api_constant`, with the
34
+ measurement behind them. A companion guard fails the suite on a bare `hasattr`
35
+ with a Resolve-shaped attribute name.
36
+
37
+ ### Fixed
38
+
39
+ - **29 capability probes across `src/` used `hasattr` on Resolve API objects,
40
+ where it is a constant `True`** (measured on Studio 19.1.3.7 across 42 checks,
41
+ recorded in `api_truth`; re-confirmed live for this release —
42
+ `hasattr(project, 'GenerateSpeech')` returns `True` on a build that has no
43
+ such method). It failed in two shapes that look nothing alike:
44
+ - `if not hasattr(clip, "RemoveMotionBlur")` is a dead branch, so the
45
+ "requires Resolve 21+" refusal never fired and the call below it raised
46
+ `AttributeError` on an older build. Eleven of these were the granular
47
+ server's Resolve 21 AI guards.
48
+ - `getattr(r, n) if hasattr(r, n) else n` never reaches its `else`, so a build
49
+ without the constant got `None` where the author wrote a string fallback —
50
+ no exception, no refusal, just a `None` travelling on into `Export()`. This
51
+ reached granular timeline export and `ExportLUT`, and three constant
52
+ lookups on the compound server. Confirmed live: a name Resolve does not
53
+ define returned `None` under the old form and now falls back correctly.
54
+ The fallback keys on `is None`, not truthiness, because `EXPORT_AAF` is
55
+ genuinely `0.0`.
56
+ - **The granular AI tools reported a missing Extras pack as success.** Resolve's
57
+ AI methods return the reason as a *string* when the pack is absent, and
58
+ `bool("Required package ... is not installed.")` is `True`. The compound
59
+ server has normalized that since the 21.0.2.4 measurement; the granular one
60
+ had not, so folder and clip audio classification returned `{"success": true}`
61
+ for a call that did not run, and `RemoveMotionBlur` / `GenerateSpeech` walked
62
+ a string into `.GetName()`. All now route through `_ai_result`, which reports
63
+ the failure and carries Resolve's own reason.
64
+ - **`Project.ApplyFairlightPresetToCurrentTimeline` was recorded on `Timeline`.**
65
+ The shipped README lists it under Project and the server calls it there; the
66
+ method's name is what made the wrong attribution look right.
67
+
68
+ ### Validation
69
+
70
+ Suite 2560 → 2580. Live-checked against the running Studio 19.1.3.7 for the
71
+ probe semantics, the refusal path, the constant fallback, and the `get_version`
72
+ preflight (40 of 41 gates unavailable, as expected on that build). The Resolve 21
73
+ and 21.0.4 surfaces themselves remain untested here — this machine cannot run
74
+ them — and the Extras-pack failure paths are pinned by a stub that reproduces
75
+ the measured attribute-fabrication behaviour rather than by a live 21 build.
76
+
77
+ ## What's New in v2.88.0
78
+
79
+ The twelve Resolve 21.0.4 surfaces that only existed on the compound server now
80
+ exist on the granular one too. Issue #140, PR #142 by @legionsound.
81
+
82
+ ### Added
83
+
84
+ - **Twelve granular tools closing the 21.0.4 delta.** `get_layout_preset_list`,
85
+ `get_burn_in_preset_list` and `delete_burn_in_preset`; the six
86
+ `*_user_preferences_preset` tools; `get_project_attributes_in_current_folder`;
87
+ `get_clip_timeline`; and `get_selected_timeline_items`. Each is guarded with
88
+ `_requires_method` at 21.0.4, so an older build gets a named version error
89
+ rather than an attribute crash, and each returns the same shape its compound
90
+ counterpart does. `get_selected_timeline_items` is deliberately not called
91
+ `get_selected_clips` — that name belongs to the Media Pool selection tool, and
92
+ the collision was the confusion the issue reported. The granular server is now
93
+ 353 tools.
94
+ - **`load_user_preferences_preset` carries the SESSION-WIDE warning** in its
95
+ docstring, matching the compound action. It swaps the user's global Resolve
96
+ preferences, not a project setting.
97
+
98
+ ### Fixed
99
+
100
+ - **Four places still said 341 after the count moved to 353, and the guard that
101
+ exists to catch exactly that was not looking at any of them.** The literal in
102
+ `tests/test_import.py` was the one that bit: that file is pytest-style, so
103
+ `unittest discover` never collects it, and a green 2560-test run said nothing
104
+ while `python tests/test_import.py` — the smoke step in the publish workflow
105
+ and step one of the release process — failed. The other three were the startup
106
+ log line in `src/resolve_mcp_server.py`, `docs/install.md` (which had also been
107
+ quoting 32 compound tools since the compound server reached 34), and the badge,
108
+ server-modes table and metrics table in `README.zh-CN.md`. All six files are in
109
+ `test_doc_tool_counts` now, along with the English README's Tools badge, so the
110
+ next count change fails offline instead of at publish time.
111
+
112
+ ### Validation
113
+
114
+ - 2560 offline tests OK; `python tests/test_import.py` exits 0; agent-rules
115
+ drift check in sync.
116
+ - The twelve 21.0.4 methods were exercised live on Studio 21.0.4.5 by the
117
+ contributor — full round-trips for the layout, burn-in and user-preferences
118
+ preset families, project attributes across 8 projects, and both branches of
119
+ `get_clip_timeline`. `LoadUserPreferencesPreset` was deliberately not executed,
120
+ by them or here. The validation machine for this repo runs 19.1.3, so the live
121
+ results stay attributed to the reporter in `docs/reference/api-coverage.md`.
122
+ - What could be checked live here was: against a running Studio 19.1.3.7, the
123
+ new tools return their named `requires DaVinci Resolve 21.0.4+` error rather
124
+ than crashing on a missing attribute — the guard path exercised against a real
125
+ Resolve object, not a stub.
126
+
5
127
  ## What's New in v2.87.2
6
128
 
7
129
  A refused `SetSetting` now says why, when the ledger already knows. Issue #141,
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.87.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.89.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
- [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#server-modes)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(353%20full)-blue.svg)](#server-modes)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#server-modes)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
@@ -100,7 +100,7 @@ The command starts a localhost server and opens the control panel in your browse
100
100
  | Mode | Entry point | Tools | Best for |
101
101
  |------|-------------|-------|----------|
102
102
  | Compound | `src/server.py` | 34 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
103
- | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 341 | Power users who want one MCP tool per Resolve API method. |
103
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 353 | Power users who want one MCP tool per Resolve API method. |
104
104
 
105
105
  The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
106
106
 
@@ -262,7 +262,7 @@ The default server is a local stdio process launched by your MCP client; it does
262
262
 
263
263
  | Metric | Value |
264
264
  |--------|-------|
265
- | MCP Tools | **34** compound / **341** granular (live server) |
265
+ | MCP Tools | **34** compound / **353** granular (live server) |
266
266
  | Advanced (offline) tools | **18** — .drp/.drt/.drx + DB authoring, no Resolve running |
267
267
  | Kernel Actions | **136** guarded workflow actions across 9 compound tools |
268
268
  | API Methods Covered | **361/361** (100%) |
package/README.zh-CN.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.87.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.89.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
- [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(341%20full)-blue.svg)](#服务器模式)
8
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-34%20(353%20full)-blue.svg)](#服务器模式)
9
9
  [![Advanced](https://img.shields.io/badge/Advanced%20(offline)-18%20tools-blueviolet.svg)](#服务器模式)
10
10
  [![Tested](https://img.shields.io/badge/Live%20Tested-93.6%25-green.svg)](docs/reference/api-coverage.md#test-results)
11
11
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.87.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.89.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -76,7 +76,7 @@ venv/bin/python -m src.control_panel
76
76
  | 模式 | 入口 | 工具数 | 适合谁 |
77
77
  |------|------|--------|--------|
78
78
  | Compound(复合) | `src/server.py` | 34 | 大多数助手的默认模式。相关的 Resolve 操作按 action 参数分组,压低上下文占用。 |
79
- | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 341 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
79
+ | Full / granular(细粒度) | `src/server.py --full` 或 `src/resolve_mcp_server.py` | 353 | 想要"一个 Resolve API 方法 = 一个 MCP 工具"的重度用户。 |
80
80
 
81
81
  除非你明确需要一方法一工具的细粒度界面,否则推荐复合模式。
82
82
 
@@ -190,7 +190,7 @@ DRX 调色写入**针对 Resolve Studio 做过实机校准**:调色参数默
190
190
 
191
191
  | 指标 | 数值 |
192
192
  |------|------|
193
- | MCP 工具 | **34** 复合 / **341** 细粒度(实时服务器) |
193
+ | MCP 工具 | **34** 复合 / **353** 细粒度(实时服务器) |
194
194
  | Advanced(离线)工具 | **18**——.drp/.drt/.drx + 数据库创作,无需 Resolve 运行 |
195
195
  | 内核 action | 9 个复合工具下 **136** 个带护栏的工作流 action |
196
196
  | API 方法覆盖 | **361/361**(100%) |
package/docs/SKILL.md CHANGED
@@ -158,7 +158,7 @@ before mutating Resolve state.
158
158
  | Mode | Entry point | Tool count | Use when |
159
159
  |---|---|---|---|
160
160
  | Compound (default) | `src/server.py` | 34 tools | Most workflows — keeps context lean |
161
- | Granular (full) | `src/server.py --full` | 341 tools | Power users needing one tool per API method |
161
+ | Granular (full) | `src/server.py --full` | 353 tools | Power users needing one tool per API method |
162
162
 
163
163
  This skill document covers the **compound server** (the default). Each compound
164
164
  tool accepts an `action` string and an optional `params` object.
@@ -489,7 +489,16 @@ you are on the correct page first.
489
489
  Key actions:
490
490
  - `launch` — connect to or start Resolve; call this first if any tool returns a
491
491
  "Not connected" error
492
- - `get_version` — returns `{product, version, version_string}`
492
+ - `get_version` — returns `{product, version, version_string, build, mcp}`.
493
+ `build.unavailable_on_this_build` lists every recorded API surface this build
494
+ does **not** have; read it before offering anything version-gated. An absence
495
+ from that list is not a promise a method exists — most of the API has never
496
+ been version-bisected, so `check_version_support` answers `unknown` for it,
497
+ and `unknown` means probe with `name in dir(obj)`, never bare `hasattr`
498
+ (constant `True` on Resolve objects)
499
+ - `check_version_support(symbol?, resolve_version?)` — is one named symbol on
500
+ this build? Without `symbol`, the same missing-surface list `get_version`
501
+ carries. No connection needed when `resolve_version` is passed
493
502
  - `api_truth(query?)` — look up behaviorally-verified facts about quirky/unreliable
494
503
  Resolve API behavior (no connection needed); filter by substring
495
504
  - `verification_stats` — readback-verification tally (verified/contradicted/
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
64
64
  ├── install.py # Universal installer (macOS/Windows/Linux)
65
65
  ├── src/
66
66
  │ ├── server.py # Compound MCP server — 34 tools (default)
67
- │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 341 tools
67
+ │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 353 tools
68
68
  │ ├── granular/ # Modular full-server implementation
69
69
  │ └── utils/ # Platform detection, Resolve connection helpers
70
70
  ├── tests/ # offline suite (test_*.py) + live harnesses (live_*.py):
package/docs/install.md CHANGED
@@ -132,8 +132,8 @@ The MCP server comes in two modes:
132
132
 
133
133
  | Mode | File | Tools | Best For |
134
134
  |------|------|-------|----------|
135
- | **Compound** (default) | `src/server.py` | 32 | Most users — fast, clean, low context usage |
136
- | **Full** | `src/resolve_mcp_server.py` | 341 | Power users who want one tool per API method |
135
+ | **Compound** (default) | `src/server.py` | 34 | Most users — fast, clean, low context usage |
136
+ | **Full** | `src/resolve_mcp_server.py` | 353 | Power users who want one tool per API method |
137
137
 
138
138
  The compound server's `timeline_item` tool includes dedicated actions for common workflows:
139
139
 
@@ -148,7 +148,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
148
148
 
149
149
  The installer uses the compound server by default. To use the full server:
150
150
  ```bash
151
- python src/server.py --full # Launch full 341-tool server
151
+ python src/server.py --full # Launch full 353-tool server
152
152
  # Or point your MCP config directly at src/resolve_mcp_server.py
153
153
  ```
154
154
 
@@ -6,7 +6,7 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
6
6
 
7
7
  | Metric | Value |
8
8
  |--------|-------|
9
- | MCP Tools | **34** compound (default) / **341** granular |
9
+ | MCP Tools | **34** compound (default) / **353** granular |
10
10
  | Kernel Actions | **136** guarded MCP workflow actions across 9 compound tools |
11
11
  | API Methods Covered | **361/361** (100%) |
12
12
  | Methods Live Tested | **338/361** (93.6%) |
@@ -17,7 +17,7 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
17
17
 
18
18
  ## API Coverage
19
19
 
20
- Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **34 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **341 individual tools** for power users. Both modes cover all 13 API object classes. MCP-level kernel actions are tracked separately in [Kernel Action Coverage](../kernels/README.md).
20
+ Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **34 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **353 individual tools** for power users. Both modes cover all 13 API object classes. MCP-level kernel actions are tracked separately in [Kernel Action Coverage](../kernels/README.md).
21
21
 
22
22
  The 34th compound tool is `timeline_versioning` (C6) — an MCP-level workflow
23
23
  tool, not a wrapper around a Resolve API method. It surfaces the
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.87.2"
39
+ VERSION = "2.89.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.87.2",
3
+ "version": "2.89.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -45,6 +45,7 @@ from src.utils.media_analysis_jobs import (
45
45
  )
46
46
  from src.utils.platform import setup_environment
47
47
  from src.utils.resolve_connection import connect_resolve
48
+ from src.utils.resolve_probe import has_method
48
49
  from src.utils.analysis_memory import read_panel_state, write_panel_state
49
50
  from src.utils import brain_edits as _brain_edits
50
51
  from src.utils import timeline_versioning as _timeline_versioning
@@ -11917,7 +11918,7 @@ HTML = HTML.replace("/* CONTROL_PANEL_I18N */", localization_script())
11917
11918
 
11918
11919
 
11919
11920
  def _safe_call(obj: Any, method_name: str, *args: Any) -> Tuple[Any, Optional[str]]:
11920
- if obj is None or not hasattr(obj, method_name):
11921
+ if not has_method(obj, method_name):
11921
11922
  return None, f"{method_name} unavailable"
11922
11923
  try:
11923
11924
  return getattr(obj, method_name)(*args), None
@@ -49,6 +49,8 @@ from src.utils.render_ids import (
49
49
  render_format_id_from_formats,
50
50
  )
51
51
  from src.utils.resolve_connection import connect_resolve
52
+ from src.utils.resolve_probe import api_constant as _api_constant, has_method
53
+ from src.utils.resolve_versions import availability, gates_unavailable_on
52
54
  from src.utils.project_properties import (
53
55
  get_all_project_properties,
54
56
  get_color_settings,
@@ -85,7 +87,7 @@ if not logging.getLogger().handlers:
85
87
  handlers=[logging.StreamHandler()],
86
88
  )
87
89
 
88
- VERSION = "2.87.2"
90
+ VERSION = "2.89.0"
89
91
  logger = logging.getLogger("davinci-resolve-mcp")
90
92
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
93
  logger.info(f"Detected platform: {get_platform()}")
@@ -741,11 +743,43 @@ def _get_timeline_item(track_type="video", track_index=1, item_index=0):
741
743
  return items[item_index], None
742
744
 
743
745
  def _has_method(obj, method_name):
744
- return callable(getattr(obj, method_name, None))
746
+ # `hasattr` is a constant True on Resolve objects — see src/utils/resolve_probe.
747
+ return has_method(obj, method_name)
745
748
 
746
749
  def _requires_method(obj, method_name, min_version):
747
750
  if _has_method(obj, method_name):
748
751
  return None
749
752
  return {"error": f"{method_name} requires DaVinci Resolve {min_version}+"}
750
753
 
754
+ def _ai_result(returned):
755
+ """Normalize a Resolve 21 AI-method return into (ok, message).
756
+
757
+ The AI methods do not agree on how they report a missing Extras pack, and
758
+ one of the two shapes is a trap. Verified live on Studio 21.0.2.4 with only
759
+ AI Motion Deblur installed:
760
+
761
+ - `AnalyzeForSlate` -> False
762
+ - `AnalyzeForIntellisearch` -> "Required package 'AI Intellisearch -
763
+ Faster' is not installed."
764
+ - `GenerateSpeech` -> "Required Package, 'AI Speech Generator' is not
765
+ Installed."
766
+
767
+ A non-empty string is truthy, so `bool(returned)` reports success for a call
768
+ that definitively did not run, and treating the return as a MediaPoolItem
769
+ raises AttributeError. This is the compound server's `_ai_result`, kept
770
+ identical on purpose: the granular tools called the same AI methods with a
771
+ bare `bool()` and inherited the whole trap.
772
+ """
773
+ if isinstance(returned, str):
774
+ return False, returned.strip() or None
775
+ return bool(returned), None
776
+
777
+ def _ai_result_payload(returned):
778
+ """`{"success": ...}` plus the Resolve-supplied reason when there is one."""
779
+ ok, message = _ai_result(returned)
780
+ payload = {"success": ok}
781
+ if message:
782
+ payload["error"] = message
783
+ return payload
784
+
751
785
  __all__ = [name for name in globals() if not name.startswith("__")]
@@ -325,9 +325,10 @@ def folder_perform_audio_classification(folder_path: str = "") -> Dict[str, Any]
325
325
  folder, err = _resolve_folder(mp, folder_path)
326
326
  if err:
327
327
  return err
328
- if not hasattr(folder, "PerformAudioClassification"):
329
- return {"error": "PerformAudioClassification requires DaVinci Resolve 21+"}
330
- return {"success": bool(folder.PerformAudioClassification())}
328
+ missing = _requires_method(folder, "PerformAudioClassification", "21.0")
329
+ if missing:
330
+ return missing
331
+ return _ai_result_payload(folder.PerformAudioClassification())
331
332
 
332
333
 
333
334
  @mcp.tool()
@@ -343,9 +344,10 @@ def folder_clear_audio_classification(folder_path: str = "") -> Dict[str, Any]:
343
344
  folder, err = _resolve_folder(mp, folder_path)
344
345
  if err:
345
346
  return err
346
- if not hasattr(folder, "ClearAudioClassification"):
347
- return {"error": "ClearAudioClassification requires DaVinci Resolve 21+"}
348
- return {"success": bool(folder.ClearAudioClassification())}
347
+ missing = _requires_method(folder, "ClearAudioClassification", "21.0")
348
+ if missing:
349
+ return missing
350
+ return _ai_result_payload(folder.ClearAudioClassification())
349
351
 
350
352
 
351
353
  @mcp.tool()
@@ -363,9 +365,10 @@ def folder_analyze_for_intellisearch(folder_path: str = "", identify_faces: bool
363
365
  folder, err = _resolve_folder(mp, folder_path)
364
366
  if err:
365
367
  return err
366
- if not hasattr(folder, "AnalyzeForIntellisearch"):
367
- return {"error": "AnalyzeForIntellisearch requires DaVinci Resolve 21+"}
368
- return {"success": bool(folder.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))}
368
+ missing = _requires_method(folder, "AnalyzeForIntellisearch", "21.0")
369
+ if missing:
370
+ return missing
371
+ return _ai_result_payload(folder.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))
369
372
 
370
373
 
371
374
  @mcp.tool()
@@ -383,11 +386,12 @@ def folder_analyze_for_slate(folder_path: str = "", marker_color: str = "Blue")
383
386
  folder, err = _resolve_folder(mp, folder_path)
384
387
  if err:
385
388
  return err
386
- if not hasattr(folder, "AnalyzeForSlate"):
387
- return {"error": "AnalyzeForSlate requires DaVinci Resolve 21+"}
389
+ missing = _requires_method(folder, "AnalyzeForSlate", "21.0")
390
+ if missing:
391
+ return missing
388
392
  if marker_color not in _MARKER_COLORS:
389
393
  return {"error": f"Invalid marker_color '{marker_color}'. Valid: {', '.join(_MARKER_COLORS)}"}
390
- return {"success": bool(folder.AnalyzeForSlate(marker_color))}
394
+ return _ai_result_payload(folder.AnalyzeForSlate(marker_color))
391
395
 
392
396
 
393
397
  @mcp.tool()
@@ -407,9 +411,16 @@ def folder_remove_motion_blur(folder_path: str = "", deblur_option: Optional[Dic
407
411
  folder, err = _resolve_folder(mp, folder_path)
408
412
  if err:
409
413
  return err
410
- if not hasattr(folder, "RemoveMotionBlur"):
411
- return {"error": "RemoveMotionBlur requires DaVinci Resolve 21+"}
414
+ missing = _requires_method(folder, "RemoveMotionBlur", "21.0")
415
+ if missing:
416
+ return missing
412
417
  result = folder.RemoveMotionBlur(deblur_option or {})
418
+ # A missing Extras pack comes back as an error STRING, which is truthy and
419
+ # also iterable — the loop below would walk it character by character and
420
+ # report success. Normalize before either.
421
+ ok, message = _ai_result(result)
422
+ if not ok:
423
+ return {"success": False, "error": message} if message else {"success": False}
413
424
  created = []
414
425
  for pair in (result or []):
415
426
  try:
@@ -417,4 +428,4 @@ def folder_remove_motion_blur(folder_path: str = "", deblur_option: Optional[Dic
417
428
  created.append({"original": orig.GetName(), "new": new.GetName(), "new_id": new.GetUniqueId()})
418
429
  except Exception:
419
430
  continue
420
- return {"success": bool(result), "created": created}
431
+ return {"success": True, "created": created}
@@ -17,13 +17,13 @@ def _invalidate_analysis_registry_for_clip(project, clip, *, reason: str) -> Opt
17
17
  if project is None or clip is None:
18
18
  return None
19
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
20
+ project_name = project.GetName() if _has_method(project, "GetName") else None
21
+ project_id = project.GetUniqueId() if _has_method(project, "GetUniqueId") else None
22
+ clip_id = clip.GetUniqueId() if _has_method(clip, "GetUniqueId") else None
23
+ media_id = clip.GetMediaId() if _has_method(clip, "GetMediaId") else None
24
24
  source_file = None
25
25
  try:
26
- source_file = clip.GetClipProperty("File Path") if hasattr(clip, "GetClipProperty") else None
26
+ source_file = clip.GetClipProperty("File Path") if _has_method(clip, "GetClipProperty") else None
27
27
  except Exception:
28
28
  source_file = None
29
29
  return _mark_analysis_registry_stale(
@@ -930,9 +930,10 @@ def perform_clip_audio_classification(clip_id: str) -> Dict[str, Any]:
930
930
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
931
931
  if not clip:
932
932
  return {"error": f"Clip {clip_id} not found"}
933
- if not hasattr(clip, "PerformAudioClassification"):
934
- return {"error": "PerformAudioClassification requires DaVinci Resolve 21+"}
935
- return {"success": bool(clip.PerformAudioClassification())}
933
+ missing = _requires_method(clip, "PerformAudioClassification", "21.0")
934
+ if missing:
935
+ return missing
936
+ return _ai_result_payload(clip.PerformAudioClassification())
936
937
 
937
938
 
938
939
  @mcp.tool()
@@ -948,9 +949,10 @@ def clear_clip_audio_classification(clip_id: str) -> Dict[str, Any]:
948
949
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
949
950
  if not clip:
950
951
  return {"error": f"Clip {clip_id} not found"}
951
- if not hasattr(clip, "ClearAudioClassification"):
952
- return {"error": "ClearAudioClassification requires DaVinci Resolve 21+"}
953
- return {"success": bool(clip.ClearAudioClassification())}
952
+ missing = _requires_method(clip, "ClearAudioClassification", "21.0")
953
+ if missing:
954
+ return missing
955
+ return _ai_result_payload(clip.ClearAudioClassification())
954
956
 
955
957
 
956
958
  @mcp.tool()
@@ -968,9 +970,10 @@ def analyze_clip_for_intellisearch(clip_id: str, identify_faces: bool = False, i
968
970
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
969
971
  if not clip:
970
972
  return {"error": f"Clip {clip_id} not found"}
971
- if not hasattr(clip, "AnalyzeForIntellisearch"):
972
- return {"error": "AnalyzeForIntellisearch requires DaVinci Resolve 21+"}
973
- return {"success": bool(clip.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))}
973
+ missing = _requires_method(clip, "AnalyzeForIntellisearch", "21.0")
974
+ if missing:
975
+ return missing
976
+ return _ai_result_payload(clip.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))
974
977
 
975
978
 
976
979
  @mcp.tool()
@@ -988,11 +991,12 @@ def analyze_clip_for_slate(clip_id: str, marker_color: str = "Blue") -> Dict[str
988
991
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
989
992
  if not clip:
990
993
  return {"error": f"Clip {clip_id} not found"}
991
- if not hasattr(clip, "AnalyzeForSlate"):
992
- return {"error": "AnalyzeForSlate requires DaVinci Resolve 21+"}
994
+ missing = _requires_method(clip, "AnalyzeForSlate", "21.0")
995
+ if missing:
996
+ return missing
993
997
  if marker_color not in _MARKER_COLORS:
994
998
  return {"error": f"Invalid marker_color '{marker_color}'. Valid: {', '.join(_MARKER_COLORS)}"}
995
- return {"success": bool(clip.AnalyzeForSlate(marker_color))}
999
+ return _ai_result_payload(clip.AnalyzeForSlate(marker_color))
996
1000
 
997
1001
 
998
1002
  @mcp.tool()
@@ -1012,9 +1016,51 @@ def remove_clip_motion_blur(clip_id: str, deblur_option: Optional[Dict[str, Any]
1012
1016
  clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
1013
1017
  if not clip:
1014
1018
  return {"error": f"Clip {clip_id} not found"}
1015
- if not hasattr(clip, "RemoveMotionBlur"):
1016
- return {"error": "RemoveMotionBlur requires DaVinci Resolve 21+"}
1019
+ missing = _requires_method(clip, "RemoveMotionBlur", "21.0")
1020
+ if missing:
1021
+ return missing
1017
1022
  new_clip = clip.RemoveMotionBlur(deblur_option or {})
1018
- if not new_clip:
1019
- return {"success": False}
1023
+ # An error string is truthy, so a bare `if not new_clip` let a missing
1024
+ # Extras pack through to .GetName() and raised AttributeError.
1025
+ ok, message = _ai_result(new_clip)
1026
+ if not ok:
1027
+ return {"success": False, "error": message} if message else {"success": False}
1020
1028
  return {"success": True, "new": new_clip.GetName(), "new_id": new_clip.GetUniqueId()}
1029
+
1030
+
1031
+ @mcp.tool()
1032
+ def get_clip_timeline(clip_id: str) -> Dict[str, Any]:
1033
+ """Resolve a Media Pool timeline entry to its timeline summary (Resolve 21.0.4+).
1034
+
1035
+ Calls MediaPoolItem.GetTimeline(). Returns is_timeline=false for ordinary
1036
+ clips — that is an answer, not a failure.
1037
+
1038
+ Args:
1039
+ clip_id: Unique ID of the Media Pool item.
1040
+ """
1041
+ _, mp, err = _get_mp()
1042
+ if err:
1043
+ return err
1044
+ clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
1045
+ if not clip:
1046
+ return {"error": f"Clip {clip_id} not found"}
1047
+ missing = _requires_method(clip, "GetTimeline", "21.0.4")
1048
+ if missing:
1049
+ return missing
1050
+ try:
1051
+ tl_obj = clip.GetTimeline()
1052
+ except Exception as exc:
1053
+ return {"error": f"GetTimeline failed: {exc}"}
1054
+ if not tl_obj:
1055
+ return {"is_timeline": False, "timeline": None,
1056
+ "note": "This media pool item is not a timeline entry."}
1057
+ summary = {}
1058
+ for getter, key in (("GetName", "name"), ("GetUniqueId", "unique_id"),
1059
+ ("GetStartFrame", "start_frame"), ("GetEndFrame", "end_frame")):
1060
+ method = getattr(tl_obj, getter, None)
1061
+ if callable(method):
1062
+ try:
1063
+ summary[key] = method()
1064
+ except Exception:
1065
+ pass
1066
+ return {"is_timeline": True, "timeline": summary}