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 +122 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/SKILL.md +11 -2
- package/docs/contributing.md +1 -1
- package/docs/install.md +3 -3
- package/docs/reference/api-coverage.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/analysis_dashboard.py +2 -1
- package/src/granular/common.py +36 -2
- package/src/granular/folder.py +26 -15
- package/src/granular/media_pool_item.py +67 -21
- package/src/granular/project.py +29 -6
- package/src/granular/resolve_control.py +187 -4
- package/src/granular/timeline.py +37 -4
- package/src/granular/timeline_item.py +1 -1
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +40 -18
- package/src/utils/cloud_operations.py +5 -3
- package/src/utils/object_inspection.py +4 -2
- package/src/utils/project_properties.py +3 -1
- package/src/utils/resolve_probe.py +68 -0
- package/src/utils/resolve_versions.py +108 -6
- package/src/utils/timeline_kernel_live_probe.py +3 -1
- package/src/utils/timeline_versioning.py +4 -2
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
|
-
[-blue.svg)](#server-modes)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#server-modes)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](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` |
|
|
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 / **
|
|
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
|
-
[-blue.svg)](#服务器模式)
|
|
9
9
|
[-18%20tools-blueviolet.svg)](#服务器模式)
|
|
10
10
|
[](docs/reference/api-coverage.md#test-results)
|
|
11
11
|
[](https://www.blackmagicdesign.com/products/davinciresolve)
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.
|
|
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` |
|
|
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** 复合 / **
|
|
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` |
|
|
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/
|
package/docs/contributing.md
CHANGED
|
@@ -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 —
|
|
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` |
|
|
136
|
-
| **Full** | `src/resolve_mcp_server.py` |
|
|
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
|
|
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) / **
|
|
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 **
|
|
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.
|
|
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
|
@@ -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
|
|
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
|
package/src/granular/common.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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("__")]
|
package/src/granular/folder.py
CHANGED
|
@@ -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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
|
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
|
-
|
|
411
|
-
|
|
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":
|
|
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
|
|
21
|
-
project_id = project.GetUniqueId() if
|
|
22
|
-
clip_id = clip.GetUniqueId() if
|
|
23
|
-
media_id = clip.GetMediaId() if
|
|
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
|
|
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
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
992
|
-
|
|
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
|
|
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
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
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}
|