davinci-resolve-mcp 2.87.1 → 2.88.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 +84 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/docs/SKILL.md +1 -1
- package/docs/contributing.md +1 -1
- package/docs/install.md +3 -3
- package/docs/reference/api-coverage.md +2 -2
- package/docs/reference/api-limitations.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/granular/media_pool_item.py +38 -0
- package/src/granular/project.py +18 -0
- package/src/granular/resolve_control.py +169 -0
- package/src/granular/timeline.py +30 -0
- package/src/resolve_mcp_server.py +1 -1
- package/src/server.py +68 -8
- package/src/utils/api_truth.py +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
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.88.0
|
|
6
|
+
|
|
7
|
+
The twelve Resolve 21.0.4 surfaces that only existed on the compound server now
|
|
8
|
+
exist on the granular one too. Issue #140, PR #142 by @legionsound.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Twelve granular tools closing the 21.0.4 delta.** `get_layout_preset_list`,
|
|
13
|
+
`get_burn_in_preset_list` and `delete_burn_in_preset`; the six
|
|
14
|
+
`*_user_preferences_preset` tools; `get_project_attributes_in_current_folder`;
|
|
15
|
+
`get_clip_timeline`; and `get_selected_timeline_items`. Each is guarded with
|
|
16
|
+
`_requires_method` at 21.0.4, so an older build gets a named version error
|
|
17
|
+
rather than an attribute crash, and each returns the same shape its compound
|
|
18
|
+
counterpart does. `get_selected_timeline_items` is deliberately not called
|
|
19
|
+
`get_selected_clips` — that name belongs to the Media Pool selection tool, and
|
|
20
|
+
the collision was the confusion the issue reported. The granular server is now
|
|
21
|
+
353 tools.
|
|
22
|
+
- **`load_user_preferences_preset` carries the SESSION-WIDE warning** in its
|
|
23
|
+
docstring, matching the compound action. It swaps the user's global Resolve
|
|
24
|
+
preferences, not a project setting.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **Four places still said 341 after the count moved to 353, and the guard that
|
|
29
|
+
exists to catch exactly that was not looking at any of them.** The literal in
|
|
30
|
+
`tests/test_import.py` was the one that bit: that file is pytest-style, so
|
|
31
|
+
`unittest discover` never collects it, and a green 2560-test run said nothing
|
|
32
|
+
while `python tests/test_import.py` — the smoke step in the publish workflow
|
|
33
|
+
and step one of the release process — failed. The other three were the startup
|
|
34
|
+
log line in `src/resolve_mcp_server.py`, `docs/install.md` (which had also been
|
|
35
|
+
quoting 32 compound tools since the compound server reached 34), and the badge,
|
|
36
|
+
server-modes table and metrics table in `README.zh-CN.md`. All six files are in
|
|
37
|
+
`test_doc_tool_counts` now, along with the English README's Tools badge, so the
|
|
38
|
+
next count change fails offline instead of at publish time.
|
|
39
|
+
|
|
40
|
+
### Validation
|
|
41
|
+
|
|
42
|
+
- 2560 offline tests OK; `python tests/test_import.py` exits 0; agent-rules
|
|
43
|
+
drift check in sync.
|
|
44
|
+
- The twelve 21.0.4 methods were exercised live on Studio 21.0.4.5 by the
|
|
45
|
+
contributor — full round-trips for the layout, burn-in and user-preferences
|
|
46
|
+
preset families, project attributes across 8 projects, and both branches of
|
|
47
|
+
`get_clip_timeline`. `LoadUserPreferencesPreset` was deliberately not executed,
|
|
48
|
+
by them or here. The validation machine for this repo runs 19.1.3, so the live
|
|
49
|
+
results stay attributed to the reporter in `docs/reference/api-coverage.md`.
|
|
50
|
+
- What could be checked live here was: against a running Studio 19.1.3.7, the
|
|
51
|
+
new tools return their named `requires DaVinci Resolve 21.0.4+` error rather
|
|
52
|
+
than crashing on a missing attribute — the guard path exercised against a real
|
|
53
|
+
Resolve object, not a stub.
|
|
54
|
+
|
|
55
|
+
## What's New in v2.87.2
|
|
56
|
+
|
|
57
|
+
A refused `SetSetting` now says why, when the ledger already knows. Issue #141,
|
|
58
|
+
reported by @jus-kim.
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- **`project_settings set_setting` returned a bare `{"success": false}` for a
|
|
63
|
+
key that can never be written.** `Project.SetSetting('timelinePlaybackFrameRate')`
|
|
64
|
+
refuses every value form, before and after a timeline exists — measured in
|
|
65
|
+
PR #99, written into `api_truth`, published in `api-limitations.md`, and
|
|
66
|
+
invisible at the one moment it mattered. A bare `false` reads as *your value
|
|
67
|
+
was wrong*, which sends a caller into retrying string, int, and float for a
|
|
68
|
+
key with no writable path at all. A refusal now carries the ledger entry for
|
|
69
|
+
that key: what is really happening, and the UI step that is the way through.
|
|
70
|
+
`timeline set_setting` gets the same treatment.
|
|
71
|
+
- The match is deliberately narrow. It requires the exact quoted key on the
|
|
72
|
+
right object — `Project.SetSetting('x')` will not be handed to a `Timeline`
|
|
73
|
+
refusal, and a substring like `timeline` will not collect the
|
|
74
|
+
`timelinePlaybackFrameRate` entry. An unmeasured refusal stays bare, because
|
|
75
|
+
inventing an explanation for a failure nobody measured is the thing this
|
|
76
|
+
ledger exists to prevent. The write is always attempted first, so a key that
|
|
77
|
+
starts working in a later build reports plain success.
|
|
78
|
+
|
|
79
|
+
### Documentation
|
|
80
|
+
|
|
81
|
+
- **The `timelinePlaybackFrameRate` ledger entry carries the second report.**
|
|
82
|
+
Issue #141 confirms it independently on **Resolve 20.2**, against a freshly
|
|
83
|
+
created project whose timeline rate already read 60 — so a matching
|
|
84
|
+
`timelineFrameRate` does not unlock the write, which the PR #99 measurement
|
|
85
|
+
alone left open. The reporter's workaround is now recorded too: for repeat
|
|
86
|
+
setups, duplicate a project that already carries the wanted playback rate
|
|
87
|
+
rather than creating one and trying to write it.
|
|
88
|
+
|
|
5
89
|
## What's New in v2.87.1
|
|
6
90
|
|
|
7
91
|
Follow-up evidence from @legionsound on PR #139, plus the process fix for the
|
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.88.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.
|
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
|
|
@@ -44,8 +44,8 @@ equivalent, blocking full automation.
|
|
|
44
44
|
### Project.SetSetting('timelinePlaybackFrameRate')
|
|
45
45
|
|
|
46
46
|
- **Object:** `Project`
|
|
47
|
-
- **Behavior:** Returns False for every value form tried (string, int, float), both before and after a timeline exists, so the playback frame rate cannot be set from the API at all. Reported by a community contributor against Resolve Studio while assembling a vertical timeline (PR #99).
|
|
48
|
-
- **Workaround / current handling:** Ask the user to set it in Project Settings > Master Settings > Playback frame rate as a SETUP step, before any timeline exists. Read it back to confirm; do not report it as set on the strength of the call alone.
|
|
47
|
+
- **Behavior:** Returns False for every value form tried (string, int, float), both before and after a timeline exists, so the playback frame rate cannot be set from the API at all. Reported by a community contributor against Resolve Studio while assembling a vertical timeline (PR #99), and independently on Resolve 20.2 against a freshly created project whose timeline rate already read 60 (issue #141) — so a matching timelineFrameRate does not unlock the write.
|
|
48
|
+
- **Workaround / current handling:** Ask the user to set it in Project Settings > Master Settings > Playback frame rate as a SETUP step, before any timeline exists. Read it back to confirm; do not report it as set on the strength of the call alone. The issue #141 reporter's workaround is worth passing on for repeat setups: duplicate a project that already carries the wanted playback rate rather than creating one and trying to write it.
|
|
49
49
|
- **Tags:** project-settings, silent-failure, timeline
|
|
50
50
|
|
|
51
51
|
### Timeline.GetCurrentClipThumbnailImage (Color page only)
|
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.88.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.88.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()}")
|
|
@@ -1018,3 +1018,41 @@ def remove_clip_motion_blur(clip_id: str, deblur_option: Optional[Dict[str, Any]
|
|
|
1018
1018
|
if not new_clip:
|
|
1019
1019
|
return {"success": False}
|
|
1020
1020
|
return {"success": True, "new": new_clip.GetName(), "new_id": new_clip.GetUniqueId()}
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
@mcp.tool()
|
|
1024
|
+
def get_clip_timeline(clip_id: str) -> Dict[str, Any]:
|
|
1025
|
+
"""Resolve a Media Pool timeline entry to its timeline summary (Resolve 21.0.4+).
|
|
1026
|
+
|
|
1027
|
+
Calls MediaPoolItem.GetTimeline(). Returns is_timeline=false for ordinary
|
|
1028
|
+
clips — that is an answer, not a failure.
|
|
1029
|
+
|
|
1030
|
+
Args:
|
|
1031
|
+
clip_id: Unique ID of the Media Pool item.
|
|
1032
|
+
"""
|
|
1033
|
+
_, mp, err = _get_mp()
|
|
1034
|
+
if err:
|
|
1035
|
+
return err
|
|
1036
|
+
clip = _find_clip_by_id(mp.GetRootFolder(), clip_id)
|
|
1037
|
+
if not clip:
|
|
1038
|
+
return {"error": f"Clip {clip_id} not found"}
|
|
1039
|
+
missing = _requires_method(clip, "GetTimeline", "21.0.4")
|
|
1040
|
+
if missing:
|
|
1041
|
+
return missing
|
|
1042
|
+
try:
|
|
1043
|
+
tl_obj = clip.GetTimeline()
|
|
1044
|
+
except Exception as exc:
|
|
1045
|
+
return {"error": f"GetTimeline failed: {exc}"}
|
|
1046
|
+
if not tl_obj:
|
|
1047
|
+
return {"is_timeline": False, "timeline": None,
|
|
1048
|
+
"note": "This media pool item is not a timeline entry."}
|
|
1049
|
+
summary = {}
|
|
1050
|
+
for getter, key in (("GetName", "name"), ("GetUniqueId", "unique_id"),
|
|
1051
|
+
("GetStartFrame", "start_frame"), ("GetEndFrame", "end_frame")):
|
|
1052
|
+
method = getattr(tl_obj, getter, None)
|
|
1053
|
+
if callable(method):
|
|
1054
|
+
try:
|
|
1055
|
+
summary[key] = method()
|
|
1056
|
+
except Exception:
|
|
1057
|
+
pass
|
|
1058
|
+
return {"is_timeline": True, "timeline": summary}
|
package/src/granular/project.py
CHANGED
|
@@ -1667,3 +1667,21 @@ def generate_speech(text_input: str, voice_model: str = "", timecode: str = "",
|
|
|
1667
1667
|
if not new_item:
|
|
1668
1668
|
return {"success": False, "error": "GenerateSpeech returned no media item"}
|
|
1669
1669
|
return {"success": True, "new": new_item.GetName(), "new_id": new_item.GetUniqueId()}
|
|
1670
|
+
|
|
1671
|
+
|
|
1672
|
+
@mcp.tool()
|
|
1673
|
+
def get_project_attributes_in_current_folder() -> Dict[str, Any]:
|
|
1674
|
+
"""Get per-project attributes for every project in the current folder (Resolve 21.0.4+).
|
|
1675
|
+
|
|
1676
|
+
Calls ProjectManager.GetProjectAttributesInCurrentFolder(). Returns a dict
|
|
1677
|
+
keyed by project name with 'lastModifiedDate', 'creationDate', 'notes' and
|
|
1678
|
+
'liveCollaborationMode' — without loading any project.
|
|
1679
|
+
"""
|
|
1680
|
+
project_manager = get_project_manager()
|
|
1681
|
+
if not project_manager:
|
|
1682
|
+
return {"error": "Failed to get Project Manager"}
|
|
1683
|
+
missing = _requires_method(project_manager, "GetProjectAttributesInCurrentFolder", "21.0.4")
|
|
1684
|
+
if missing:
|
|
1685
|
+
return missing
|
|
1686
|
+
attributes = project_manager.GetProjectAttributesInCurrentFolder()
|
|
1687
|
+
return {"projects": attributes if attributes else {}}
|
|
@@ -536,3 +536,172 @@ def quit_resolve() -> Dict[str, Any]:
|
|
|
536
536
|
return {"error": "Not connected to DaVinci Resolve"}
|
|
537
537
|
resolve.Quit()
|
|
538
538
|
return {"success": True, "message": "DaVinci Resolve is quitting"}
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
@mcp.tool()
|
|
542
|
+
def get_layout_preset_list() -> Dict[str, Any]:
|
|
543
|
+
"""Get the names of all saved UI layout presets (Resolve 21.0.4+).
|
|
544
|
+
|
|
545
|
+
Calls Resolve.GetLayoutPresetList(). These are the names accepted by
|
|
546
|
+
load_layout_preset_tool, update_layout_preset, export_layout_preset_tool
|
|
547
|
+
and delete_layout_preset_tool.
|
|
548
|
+
"""
|
|
549
|
+
resolve = get_resolve()
|
|
550
|
+
if resolve is None:
|
|
551
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
552
|
+
missing = _requires_method(resolve, "GetLayoutPresetList", "21.0.4")
|
|
553
|
+
if missing:
|
|
554
|
+
return missing
|
|
555
|
+
presets = resolve.GetLayoutPresetList()
|
|
556
|
+
return {"presets": presets if presets else []}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@mcp.tool()
|
|
560
|
+
def get_burn_in_preset_list() -> Dict[str, Any]:
|
|
561
|
+
"""Get the names of all saved data burn-in presets (Resolve 21.0.4+).
|
|
562
|
+
|
|
563
|
+
Calls Resolve.GetBurnInPresetList(). These are the names accepted by the
|
|
564
|
+
'DataBurnIn' render setting, project load_burn_in_preset, and
|
|
565
|
+
export_burn_in_preset.
|
|
566
|
+
"""
|
|
567
|
+
resolve = get_resolve()
|
|
568
|
+
if resolve is None:
|
|
569
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
570
|
+
missing = _requires_method(resolve, "GetBurnInPresetList", "21.0.4")
|
|
571
|
+
if missing:
|
|
572
|
+
return missing
|
|
573
|
+
presets = resolve.GetBurnInPresetList()
|
|
574
|
+
return {"presets": presets if presets else []}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
@mcp.tool()
|
|
578
|
+
def delete_burn_in_preset(preset_name: str) -> Dict[str, Any]:
|
|
579
|
+
"""Delete a data burn-in preset by name (Resolve 21.0.4+).
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
preset_name: Name of the burn-in preset to delete.
|
|
583
|
+
"""
|
|
584
|
+
resolve = get_resolve()
|
|
585
|
+
if resolve is None:
|
|
586
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
587
|
+
missing = _requires_method(resolve, "DeleteBurnInPreset", "21.0.4")
|
|
588
|
+
if missing:
|
|
589
|
+
return missing
|
|
590
|
+
result = resolve.DeleteBurnInPreset(preset_name)
|
|
591
|
+
return {"success": bool(result), "preset_name": preset_name}
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@mcp.tool()
|
|
595
|
+
def get_user_preferences_preset_list() -> Dict[str, Any]:
|
|
596
|
+
"""Get the names of all saved user-preferences presets (Resolve 21.0.4+).
|
|
597
|
+
|
|
598
|
+
Calls Resolve.GetUserPreferencesPresetList().
|
|
599
|
+
"""
|
|
600
|
+
resolve = get_resolve()
|
|
601
|
+
if resolve is None:
|
|
602
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
603
|
+
missing = _requires_method(resolve, "GetUserPreferencesPresetList", "21.0.4")
|
|
604
|
+
if missing:
|
|
605
|
+
return missing
|
|
606
|
+
presets = resolve.GetUserPreferencesPresetList()
|
|
607
|
+
return {"presets": presets if presets else []}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@mcp.tool()
|
|
611
|
+
def save_user_preferences_preset(preset_name: str) -> Dict[str, Any]:
|
|
612
|
+
"""Save the current user preferences as a named preset (Resolve 21.0.4+).
|
|
613
|
+
|
|
614
|
+
Args:
|
|
615
|
+
preset_name: Name for the new user-preferences preset.
|
|
616
|
+
"""
|
|
617
|
+
resolve = get_resolve()
|
|
618
|
+
if resolve is None:
|
|
619
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
620
|
+
missing = _requires_method(resolve, "SaveUserPreferencesPreset", "21.0.4")
|
|
621
|
+
if missing:
|
|
622
|
+
return missing
|
|
623
|
+
result = resolve.SaveUserPreferencesPreset(preset_name)
|
|
624
|
+
return {"success": bool(result), "preset_name": preset_name}
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@mcp.tool()
|
|
628
|
+
def load_user_preferences_preset(preset_name: str) -> Dict[str, Any]:
|
|
629
|
+
"""Load a user-preferences preset (Resolve 21.0.4+).
|
|
630
|
+
|
|
631
|
+
SESSION-WIDE: this swaps the user's global Resolve preferences, not a
|
|
632
|
+
project setting. It affects every project open in this Resolve instance.
|
|
633
|
+
Only call when the user explicitly asked for the switch.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
preset_name: Name of the user-preferences preset to load.
|
|
637
|
+
"""
|
|
638
|
+
resolve = get_resolve()
|
|
639
|
+
if resolve is None:
|
|
640
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
641
|
+
missing = _requires_method(resolve, "LoadUserPreferencesPreset", "21.0.4")
|
|
642
|
+
if missing:
|
|
643
|
+
return missing
|
|
644
|
+
result = resolve.LoadUserPreferencesPreset(preset_name)
|
|
645
|
+
return {"success": bool(result), "preset_name": preset_name}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
@mcp.tool()
|
|
649
|
+
def delete_user_preferences_preset(preset_name: str) -> Dict[str, Any]:
|
|
650
|
+
"""Delete a user-preferences preset by name (Resolve 21.0.4+).
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
preset_name: Name of the user-preferences preset to delete.
|
|
654
|
+
"""
|
|
655
|
+
resolve = get_resolve()
|
|
656
|
+
if resolve is None:
|
|
657
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
658
|
+
missing = _requires_method(resolve, "DeleteUserPreferencesPreset", "21.0.4")
|
|
659
|
+
if missing:
|
|
660
|
+
return missing
|
|
661
|
+
result = resolve.DeleteUserPreferencesPreset(preset_name)
|
|
662
|
+
return {"success": bool(result), "preset_name": preset_name}
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
@mcp.tool()
|
|
666
|
+
def import_user_preferences_preset(import_path: str, preset_name: str = None) -> Dict[str, Any]:
|
|
667
|
+
"""Import a user-preferences preset from a file (Resolve 21.0.4+).
|
|
668
|
+
|
|
669
|
+
The imported preset is NOT auto-loaded; it takes its name from the file
|
|
670
|
+
when preset_name is omitted (measured on Studio 21.0.4.5). Follow with
|
|
671
|
+
load_user_preferences_preset to activate it.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
import_path: Absolute path to the preset file to import.
|
|
675
|
+
preset_name: Name to save the imported preset as (filename if None).
|
|
676
|
+
"""
|
|
677
|
+
resolve = get_resolve()
|
|
678
|
+
if resolve is None:
|
|
679
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
680
|
+
missing = _requires_method(resolve, "ImportUserPreferencesPreset", "21.0.4")
|
|
681
|
+
if missing:
|
|
682
|
+
return missing
|
|
683
|
+
if preset_name:
|
|
684
|
+
result = resolve.ImportUserPreferencesPreset(import_path, preset_name)
|
|
685
|
+
else:
|
|
686
|
+
result = resolve.ImportUserPreferencesPreset(import_path)
|
|
687
|
+
preset_name = os.path.splitext(os.path.basename(import_path))[0]
|
|
688
|
+
return {"success": bool(result), "preset_name": preset_name, "import_path": import_path,
|
|
689
|
+
"note": "The imported preset is not auto-loaded; use load_user_preferences_preset to activate it."}
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
@mcp.tool()
|
|
693
|
+
def export_user_preferences_preset(preset_name: str, export_path: str) -> Dict[str, Any]:
|
|
694
|
+
"""Export a user-preferences preset to a file (Resolve 21.0.4+).
|
|
695
|
+
|
|
696
|
+
Args:
|
|
697
|
+
preset_name: Name of the user-preferences preset to export.
|
|
698
|
+
export_path: Absolute path where the preset file will be saved.
|
|
699
|
+
"""
|
|
700
|
+
resolve = get_resolve()
|
|
701
|
+
if resolve is None:
|
|
702
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
703
|
+
missing = _requires_method(resolve, "ExportUserPreferencesPreset", "21.0.4")
|
|
704
|
+
if missing:
|
|
705
|
+
return missing
|
|
706
|
+
result = resolve.ExportUserPreferencesPreset(preset_name, export_path)
|
|
707
|
+
return {"success": bool(result), "preset_name": preset_name, "export_path": export_path}
|
package/src/granular/timeline.py
CHANGED
|
@@ -1087,3 +1087,33 @@ def set_timeline_setting(setting_name: str, setting_value: str) -> Dict[str, Any
|
|
|
1087
1087
|
return err
|
|
1088
1088
|
result = tl.SetSetting(setting_name, setting_value)
|
|
1089
1089
|
return {"success": bool(result), "setting_name": setting_name, "setting_value": setting_value}
|
|
1090
|
+
|
|
1091
|
+
|
|
1092
|
+
@mcp.tool()
|
|
1093
|
+
def get_selected_timeline_items() -> Dict[str, Any]:
|
|
1094
|
+
"""Get the timeline items currently selected in the timeline (Resolve 21.0.4+).
|
|
1095
|
+
|
|
1096
|
+
Calls Timeline.GetSelectedClips() on the current timeline. An empty list
|
|
1097
|
+
means nothing is selected — that is an answer, not a failure. (Distinct
|
|
1098
|
+
from get_selected_clips, which reads the Media Pool selection.)
|
|
1099
|
+
"""
|
|
1100
|
+
_, tl, err = _get_timeline()
|
|
1101
|
+
if err:
|
|
1102
|
+
return err
|
|
1103
|
+
missing = _requires_method(tl, "GetSelectedClips", "21.0.4")
|
|
1104
|
+
if missing:
|
|
1105
|
+
return missing
|
|
1106
|
+
items = tl.GetSelectedClips() or []
|
|
1107
|
+
summaries = []
|
|
1108
|
+
for item in items:
|
|
1109
|
+
entry = {}
|
|
1110
|
+
for getter, key in (("GetName", "name"), ("GetUniqueId", "unique_id"),
|
|
1111
|
+
("GetStart", "start"), ("GetEnd", "end")):
|
|
1112
|
+
method = getattr(item, getter, None)
|
|
1113
|
+
if callable(method):
|
|
1114
|
+
try:
|
|
1115
|
+
entry[key] = method()
|
|
1116
|
+
except Exception:
|
|
1117
|
+
pass
|
|
1118
|
+
summaries.append(entry)
|
|
1119
|
+
return {"count": len(summaries), "items": summaries}
|
|
@@ -34,7 +34,7 @@ from src.utils.update_check import start_background_update_check
|
|
|
34
34
|
if __name__ == "__main__":
|
|
35
35
|
try:
|
|
36
36
|
start_background_update_check(VERSION, project_dir, logger)
|
|
37
|
-
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (
|
|
37
|
+
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION} (353 granular tools)")
|
|
38
38
|
run_fastmcp_stdio(mcp)
|
|
39
39
|
except KeyboardInterrupt:
|
|
40
40
|
logger.info("Server shutdown requested")
|
package/src/server.py
CHANGED
|
@@ -8,10 +8,10 @@ Each tool groups related operations via an 'action' parameter.
|
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
python src/server.py # Start the MCP server
|
|
11
|
-
python src/server.py --full # Start the
|
|
11
|
+
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.
|
|
14
|
+
VERSION = "2.88.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -16017,6 +16017,37 @@ def project_manager_database(action: str, params: Optional[Dict[str, Any]] = Non
|
|
|
16017
16017
|
# TOOL 8: project_settings
|
|
16018
16018
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
16019
16019
|
|
|
16020
|
+
def _setting_limitation(name: Any, obj: str = "Project") -> Optional[Dict[str, Any]]:
|
|
16021
|
+
"""The api_truth entry for a settings key on `obj`, when one exists.
|
|
16022
|
+
|
|
16023
|
+
`SetSetting` reports a refusal as a bare `False` with no reason, and for
|
|
16024
|
+
several keys this repo has already measured the reason and written it down —
|
|
16025
|
+
`timelinePlaybackFrameRate` returns False for every value form, before and
|
|
16026
|
+
after a timeline exists (issue #141, PR #99). A caller who gets
|
|
16027
|
+
`{"success": false}` has no way to tell "you passed a bad value" from "this
|
|
16028
|
+
key cannot be written from the API at all", and the second one is a
|
|
16029
|
+
different task: it has to go to the user as a UI step.
|
|
16030
|
+
|
|
16031
|
+
Matched narrowly on purpose. The entry must name this exact key *and* be
|
|
16032
|
+
`obj.SetSetting`, because attaching an unrelated explanation to a failure is
|
|
16033
|
+
worse than attaching none — it reads as a diagnosis. `Project` and
|
|
16034
|
+
`Timeline` both have a `SetSetting` and their keys overlap by name, so the
|
|
16035
|
+
object is part of the match rather than assumed.
|
|
16036
|
+
"""
|
|
16037
|
+
if not isinstance(name, str) or not name:
|
|
16038
|
+
return None
|
|
16039
|
+
prefix = f"{obj}.SetSetting"
|
|
16040
|
+
quoted = f"'{name}'"
|
|
16041
|
+
for entry in lookup_api_truth(name):
|
|
16042
|
+
symbol = entry.get("symbol", "")
|
|
16043
|
+
# The quoted form is what makes this an exact key match: `name in
|
|
16044
|
+
# symbol` would hand the timelinePlaybackFrameRate entry to anything
|
|
16045
|
+
# that is a substring of it, "timeline" included.
|
|
16046
|
+
if symbol.startswith(prefix) and quoted in symbol:
|
|
16047
|
+
return entry
|
|
16048
|
+
return None
|
|
16049
|
+
|
|
16050
|
+
|
|
16020
16051
|
@mcp.tool()
|
|
16021
16052
|
@_guard_missing_params
|
|
16022
16053
|
def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
@@ -16026,7 +16057,9 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
16026
16057
|
get_name() -> {name}
|
|
16027
16058
|
set_name(name) -> {success}
|
|
16028
16059
|
get_setting(name?) -> {settings} — omit name for all settings
|
|
16029
|
-
set_setting(name, value) -> {success}
|
|
16060
|
+
set_setting(name, value) -> {success, known_limitation?}
|
|
16061
|
+
A refusal carries the api_truth entry for that key when one exists —
|
|
16062
|
+
several settings cannot be written from the API at all.
|
|
16030
16063
|
get_unique_id() -> {id}
|
|
16031
16064
|
get_presets() -> {presets}
|
|
16032
16065
|
set_preset(name) -> {success}
|
|
@@ -16062,7 +16095,20 @@ def project_settings(action: str, params: Optional[Dict[str, Any]] = None) -> Di
|
|
|
16062
16095
|
return _err("set_setting requires name")
|
|
16063
16096
|
if "value" not in p:
|
|
16064
16097
|
return _err("set_setting requires value")
|
|
16065
|
-
|
|
16098
|
+
if bool(proj.SetSetting(p["name"], p["value"])):
|
|
16099
|
+
return {"success": True}
|
|
16100
|
+
known = _setting_limitation(p["name"])
|
|
16101
|
+
if not known:
|
|
16102
|
+
return {"success": False}
|
|
16103
|
+
return {
|
|
16104
|
+
"success": False,
|
|
16105
|
+
"known_limitation": {
|
|
16106
|
+
"symbol": known.get("symbol"),
|
|
16107
|
+
"reality": known.get("reality"),
|
|
16108
|
+
"recommended": known.get("recommended"),
|
|
16109
|
+
"ledger_verified_on": _API_TRUTH_VERIFIED_ON,
|
|
16110
|
+
},
|
|
16111
|
+
}
|
|
16066
16112
|
elif action == "get_unique_id":
|
|
16067
16113
|
return {"id": proj.GetUniqueId()}
|
|
16068
16114
|
elif action == "get_presets":
|
|
@@ -21402,7 +21448,8 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
21402
21448
|
export(path, type, subtype?, background?) -> {success | job_id} — type: AAF, EDL, FCPXML, etc.
|
|
21403
21449
|
UNSAFE. No path sandboxing. Prefer export_timeline_checked.
|
|
21404
21450
|
get_setting(name?) -> {settings}
|
|
21405
|
-
set_setting(name, value) -> {success}
|
|
21451
|
+
set_setting(name, value) -> {success, known_limitation?}
|
|
21452
|
+
A refusal carries the api_truth entry for that key when one exists.
|
|
21406
21453
|
insert_generator(name) -> {success}
|
|
21407
21454
|
insert_fusion_generator(name) -> {success}
|
|
21408
21455
|
insert_fusion_composition() -> {success}
|
|
@@ -21762,7 +21809,20 @@ def timeline(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str,
|
|
|
21762
21809
|
elif action == "get_setting":
|
|
21763
21810
|
return {"settings": _ser(tl.GetSetting(p.get("name", "")))}
|
|
21764
21811
|
elif action == "set_setting":
|
|
21765
|
-
|
|
21812
|
+
if bool(tl.SetSetting(p["name"], p["value"])):
|
|
21813
|
+
return {"success": True}
|
|
21814
|
+
known = _setting_limitation(p["name"], obj="Timeline")
|
|
21815
|
+
if not known:
|
|
21816
|
+
return {"success": False}
|
|
21817
|
+
return {
|
|
21818
|
+
"success": False,
|
|
21819
|
+
"known_limitation": {
|
|
21820
|
+
"symbol": known.get("symbol"),
|
|
21821
|
+
"reality": known.get("reality"),
|
|
21822
|
+
"recommended": known.get("recommended"),
|
|
21823
|
+
"ledger_verified_on": _API_TRUTH_VERIFIED_ON,
|
|
21824
|
+
},
|
|
21825
|
+
}
|
|
21766
21826
|
elif action == "insert_generator":
|
|
21767
21827
|
r = tl.InsertGeneratorIntoTimeline(p["name"])
|
|
21768
21828
|
return _ok() if r else _err("Failed to insert generator")
|
|
@@ -27736,9 +27796,9 @@ if __name__ == "__main__":
|
|
|
27736
27796
|
start_background_update_check(VERSION, project_dir, logger, env=_setup_update_env())
|
|
27737
27797
|
_install_threaded_tool_dispatch(mcp)
|
|
27738
27798
|
|
|
27739
|
-
# Support --full flag to run the
|
|
27799
|
+
# Support --full flag to run the 353-tool granular server instead
|
|
27740
27800
|
if "--full" in sys.argv:
|
|
27741
|
-
logger.info("Starting full
|
|
27801
|
+
logger.info("Starting full 353-tool granular server...")
|
|
27742
27802
|
sys.argv = [arg for arg in sys.argv if arg != "--full"]
|
|
27743
27803
|
from src.granular import mcp as granular_mcp
|
|
27744
27804
|
|
package/src/utils/api_truth.py
CHANGED
|
@@ -124,11 +124,18 @@ API_TRUTH: List[Dict[str, Any]] = [
|
|
|
124
124
|
"float), both before and after a timeline exists, so the "
|
|
125
125
|
"playback frame rate cannot be set from the API at all. "
|
|
126
126
|
"Reported by a community contributor against Resolve Studio "
|
|
127
|
-
"while assembling a vertical timeline (PR #99)
|
|
127
|
+
"while assembling a vertical timeline (PR #99), and "
|
|
128
|
+
"independently on Resolve 20.2 against a freshly created "
|
|
129
|
+
"project whose timeline rate already read 60 (issue #141) — "
|
|
130
|
+
"so a matching timelineFrameRate does not unlock the write.",
|
|
128
131
|
"recommended": "Ask the user to set it in Project Settings > Master "
|
|
129
132
|
"Settings > Playback frame rate as a SETUP step, before "
|
|
130
133
|
"any timeline exists. Read it back to confirm; do not "
|
|
131
|
-
"report it as set on the strength of the call alone."
|
|
134
|
+
"report it as set on the strength of the call alone. The "
|
|
135
|
+
"issue #141 reporter's workaround is worth passing on "
|
|
136
|
+
"for repeat setups: duplicate a project that already "
|
|
137
|
+
"carries the wanted playback rate rather than creating "
|
|
138
|
+
"one and trying to write it.",
|
|
132
139
|
"tags": ["project-settings", "silent-failure", "timeline"],
|
|
133
140
|
"submit": "missing",
|
|
134
141
|
},
|