davinci-resolve-mcp 2.215.0 → 2.215.2
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 +69 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/README.md +2 -0
- package/docs/reference/resolve-211-typed-api.md +78 -0
- package/docs/reference/resolve_scripting_changelog_21.1.md +132 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/audit_typed_api.py +116 -0
- package/src/granular/common.py +1 -1
- package/src/granular/timeline_item.py +30 -11
- package/src/server.py +43 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
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.215.2 — the granular server accepts Resolve 21.1's lowercase item types; the 21.1 typed API ships as reference
|
|
6
|
+
|
|
7
|
+
Both contributed by @legionsound (#204, #205), measured on Studio 21.1.0.14.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Granular timeline-item tools rejected ordinary video clips on Resolve
|
|
12
|
+
21.1.** Studio 21.1 returns lowercase `video` / `audio` from
|
|
13
|
+
`TimelineItem.GetType()`, as its shipped typed API declares, and the
|
|
14
|
+
granular guards compared against `Video` / `Audio` — so every transform,
|
|
15
|
+
crop, composite, stabilization and keyframe-mode write refused a plain video
|
|
16
|
+
clip, and the property resource omitted the type-specific sections. Type
|
|
17
|
+
values are now normalised for the checks, title-case still passes, a
|
|
18
|
+
missing or non-string type is unknown rather than a clip, and the optional
|
|
19
|
+
`GetMediaType` accessor is only called when it is callable (on the probed
|
|
20
|
+
21.1 clip it is not). Public `type` values are unchanged. The compound
|
|
21
|
+
server has no title-case comparison of its own, so the class was confined
|
|
22
|
+
to the granular layer. Seven regression tests drive the real handlers; the
|
|
23
|
+
contributor's live write on a synthetic clip changed the rendered frame as
|
|
24
|
+
expected. (#204)
|
|
25
|
+
- **On landing:** the accompanying live script selected a clip with
|
|
26
|
+
`item.GetMediaPoolItem()`, which on Studio 19.1.3.7 resolves to `None` on a
|
|
27
|
+
conform timeline's items and raised before the read; it now checks the
|
|
28
|
+
accessor is callable first.
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
- **Blackmagic's 21.1 `DaVinciResolveScript.pyi` is bundled unmodified** under
|
|
33
|
+
`docs/reference/`, with its SHA-256, the shipped 21.1 scripting changelog,
|
|
34
|
+
and a provenance note; the legacy README snapshot and every
|
|
35
|
+
`resolve_scripting_api.txt line N` anchor are untouched, as the #197 review
|
|
36
|
+
asked. `scripts/audit_typed_api.py` inventories the stub's 410 methods and
|
|
37
|
+
46 option dictionaries against executable references in `src/`, marking
|
|
38
|
+
same-name methods on different classes as receiver-unresolved rather than
|
|
39
|
+
covered; it reports candidate gaps and makes no coverage claim. (#205)
|
|
40
|
+
|
|
41
|
+
### Validation
|
|
42
|
+
|
|
43
|
+
- Both PRs' tests plus the full offline suite, drift guards and the advanced
|
|
44
|
+
Node suite. The lowercase behaviour is the contributor's measurement on
|
|
45
|
+
21.1.0.14 and is recorded as such; title-case compatibility is pinned by
|
|
46
|
+
the unit tests, not by a live read here.
|
|
47
|
+
|
|
48
|
+
## What's New in v2.215.1 — single-frame capture survives per-clip render mode and a vanished stills folder
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **`timeline_frame capture` forces Single-clip render mode and restores it.**
|
|
53
|
+
Measured 2026-09-09 on a project whose delivery preset was "Individual
|
|
54
|
+
clips": every single-frame capture reported success, wrote no file, and took
|
|
55
|
+
30+ seconds, because in that mode Resolve ignores `CustomName`, renders the
|
|
56
|
+
whole clip under its own naming, and the frame this helper waits for never
|
|
57
|
+
appears. The helper now reads `GetCurrentRenderMode()`, switches to single
|
|
58
|
+
clip (1) for the render, puts the mode back afterwards, and refuses with
|
|
59
|
+
`RENDER_MODE_REFUSED` before adding a job if the switch fails.
|
|
60
|
+
- **The shared stills folder is recreated before every directory listing.**
|
|
61
|
+
Every sandbox path redirects to one `~/Documents/resolve-stills`, and the
|
|
62
|
+
helper's own cleanup removes it once it empties, so a concurrent capture (or
|
|
63
|
+
anything else) can take it away between the makedirs at the top and the
|
|
64
|
+
`os.listdir` that follows. Frame 81 of a 214-frame QC batch died on exactly
|
|
65
|
+
that. Recreate, do not assume.
|
|
66
|
+
|
|
67
|
+
### Measured (not code)
|
|
68
|
+
|
|
69
|
+
- A per-clip `.drx` carries the clip node graph losslessly (applying a clip's
|
|
70
|
+
own emitted grade back onto it re-renders bit-identically, PSNR 99 on 5/5),
|
|
71
|
+
but NOT colour-group pre/post grades, NOT Colour-page input sizing, and with
|
|
72
|
+
`grade_mode` 0 NOT keyframes. A traced conform whose source used groups needs
|
|
73
|
+
the group grades carried separately.
|
|
5
74
|
## What's New in v2.215.0 — mutating operations write a structured operation log; the free-edition bridge is version-qualified for Resolve 21.1
|
|
6
75
|
|
|
7
76
|
### Added
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v2.215.
|
|
15
|
+
> 本翻译对应 v2.215.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/docs/README.md
CHANGED
|
@@ -22,6 +22,8 @@ ignored scratch folders such as `docs/_scratch/`.
|
|
|
22
22
|
correction guidance and Resolve color API boundaries.
|
|
23
23
|
- [Resolve Scripting API Reference](reference/resolve_scripting_api.txt) — bundled
|
|
24
24
|
Resolve scripting API text used for parity checks.
|
|
25
|
+
- [Resolve 21.1 Typed API](reference/resolve-211-typed-api.md) — vendor stub,
|
|
26
|
+
scripting changelog, provenance and an executable-reference inventory.
|
|
25
27
|
- [Contributing and Project Layout](contributing.md) — contribution workflow,
|
|
26
28
|
platform support, security notes, and repository structure.
|
|
27
29
|
- [Release Process](process/release-process.md) — maintainer release checklist.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Resolve 21.1 typed API snapshot
|
|
2
|
+
|
|
3
|
+
The vendor-shipped [DaVinciResolveScript.pyi](DaVinciResolveScript.pyi) preserves
|
|
4
|
+
Blackmagic's typed Resolve API separately from the legacy
|
|
5
|
+
[README snapshot](resolve_scripting_api.txt). The legacy file and its line
|
|
6
|
+
anchors are unchanged. The accompanying
|
|
7
|
+
[scripting changelog](resolve_scripting_changelog_21.1.md) lists the 21.1 additions.
|
|
8
|
+
|
|
9
|
+
## Provenance
|
|
10
|
+
|
|
11
|
+
Collected September 9, 2026 from the installed official MCP's
|
|
12
|
+
`get_scripting_api(as_file=True)` on macOS, DaVinci Resolve Studio **21.1.0.14**.
|
|
13
|
+
The changelog was copied from the installed `Developer/Scripting/CHANGELOG.md`
|
|
14
|
+
and is dated September 1, 2026. The stub is unmodified vendor reference material;
|
|
15
|
+
it is not authored by this project. Its SHA-256 is
|
|
16
|
+
`00078fa1256851b9807621a4eea5e670f4266b0e7003763cba4a62655543f0ec`.
|
|
17
|
+
|
|
18
|
+
This supplies the typed snapshot requested in
|
|
19
|
+
[the review of #197](https://github.com/samuelgursky/davinci-resolve-mcp/pull/197#issuecomment-5584959121).
|
|
20
|
+
Future refreshes should replace the whole vendor file, retain provenance and
|
|
21
|
+
review the signature/option changes, rather than patch selected methods into it.
|
|
22
|
+
|
|
23
|
+
## Diagnostic inventory
|
|
24
|
+
|
|
25
|
+
Run from the repository root:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
python scripts/audit_typed_api.py
|
|
29
|
+
python scripts/audit_typed_api.py --json > /tmp/resolve-api-inventory.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The JSON includes every public class method, its signatures and source line,
|
|
33
|
+
all TypedDict option fields and their descriptions, and source references split
|
|
34
|
+
between compound, granular and helper files. An alternate installed stub can be
|
|
35
|
+
passed with `--stub`; `--source-root` selects an alternate source tree.
|
|
36
|
+
|
|
37
|
+
The 21.1 snapshot has **410 unique class-qualified public methods** and **46
|
|
38
|
+
TypedDicts containing 513 fields**. Compared with the 361 methods parsed from
|
|
39
|
+
the legacy README, 57 are newly represented and eight legacy names are absent
|
|
40
|
+
from the stub. That does **not** mean 57 newly introduced runtime functions or
|
|
41
|
+
eight removed functions: newly documented existing methods and deprecated
|
|
42
|
+
aliases are part of the difference. For example, the 21.1 README deprecates
|
|
43
|
+
`GetSetting`/`SetSetting` in favor of `GetSettings`/`SetSettings` and
|
|
44
|
+
`GetProperty`/`SetProperty` in favor of their plural forms. The special
|
|
45
|
+
four-argument Super Scale `SetSetting` form is explicitly not deprecated.
|
|
46
|
+
|
|
47
|
+
At main `91d03a52c139002061e325824004162366f0902c`, 50 method names had no
|
|
48
|
+
executable attribute or literal-getattr reference under `src/`. These are
|
|
49
|
+
**candidate gaps**, not a claim that 50 user-facing features are missing.
|
|
50
|
+
|
|
51
|
+
## What this audit proves, and what it does not
|
|
52
|
+
|
|
53
|
+
- Comments and docstrings cannot count as executable coverage. In particular,
|
|
54
|
+
`CreateMulticamClip` mentioned in the API-truth ledger is not a wrapper.
|
|
55
|
+
- Class identity is preserved in the inventory, but Python receiver types are
|
|
56
|
+
**not inferred**. A `pm.GetCurrentProject()` reference does not prove that
|
|
57
|
+
the new `Resolve.GetCurrentProject()` utility is used. Matching names carry
|
|
58
|
+
`unresolved_receiver`, not `covered`.
|
|
59
|
+
- A literal `getattr` can be a capability probe rather than an invocation.
|
|
60
|
+
Attribute references and direct calls have separate kinds in the report.
|
|
61
|
+
- A helper reference does not prove that both public server layers expose it,
|
|
62
|
+
or that all arguments/options are forwarded. Follow each candidate through
|
|
63
|
+
action dispatch, parameter validation, version guards and response shaping.
|
|
64
|
+
- Computed dispatch and equivalent older APIs require manual review.
|
|
65
|
+
- The script only parses files. It does not import source modules, connect to
|
|
66
|
+
Resolve, mutate projects, or establish behavior.
|
|
67
|
+
|
|
68
|
+
Exit zero means the inventory completed, **not** that API parity passed. Empty
|
|
69
|
+
or malformed inputs fail rather than reporting a false clean inventory. The
|
|
70
|
+
existing `audit_api_parity.py` guard remains unchanged; promoting this diagnostic
|
|
71
|
+
to a coverage gate requires an explicitly reviewed method-to-action manifest.
|
|
72
|
+
|
|
73
|
+
## Validation
|
|
74
|
+
|
|
75
|
+
`python -m unittest tests.test_typed_api_audit` covers comment-only false
|
|
76
|
+
coverage, class-name collisions, source layers, call versus getattr references,
|
|
77
|
+
option-field descriptions, overload preservation and invalid input. No Resolve
|
|
78
|
+
behavior is changed, so no live mutation test is required for this contribution.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# DaVinci Resolve Scripting API Changelog
|
|
2
|
+
|
|
3
|
+
*Last Updated: 1 Sep 2026*
|
|
4
|
+
|
|
5
|
+
This file documents scripting changes by version. For the overall DaVinci Resolve Scripting API, please refer to
|
|
6
|
+
Developer/Scripting/README.md and DaVinciResolveScript.pyi.
|
|
7
|
+
|
|
8
|
+
## 21.1
|
|
9
|
+
|
|
10
|
+
Notes:
|
|
11
|
+
|
|
12
|
+
- Python 2 is no longer supported.
|
|
13
|
+
- Built-in Python support for script menu and console scripts.
|
|
14
|
+
- Overloaded function signatures are deprecated.
|
|
15
|
+
|
|
16
|
+
Added:
|
|
17
|
+
|
|
18
|
+
- Resolve Keyboard Presets - Load/Delete/Import/Export/GetCurrent/Get List for KeyboardPreset.
|
|
19
|
+
- Project Settings Presets - Set/Delete/Import/Export/Update/Save/Get List for ProjectSettingsPreset.
|
|
20
|
+
- Render Presets - Project.UpdateRenderPreset, SetQuickExportEnabledForRenderPreset.
|
|
21
|
+
- Audio Render APIs - Project.GetAudioRenderCodecs, GetAudioRenderFormats.
|
|
22
|
+
- Media Transcription - MediaPoolItem.GetTranscription.
|
|
23
|
+
- Multicam APIs - MediaPool.CreateMulticamClip, TimelineItem.FlattenMulticam, PerformMulticamSmartSwitch, Timeline.AutoAlignClips.
|
|
24
|
+
- TimelineItem properties - Get/Set Property includes native audio properties and enabled states.
|
|
25
|
+
- TimelineItem properties - Get/Set Fades, Speed, OutputBlanking, UseTimelineForOutputBlanking and GetType.
|
|
26
|
+
- Transition API - TimelineItem.AddTransition.
|
|
27
|
+
- Timeline Output blanking - Timeline.Get/SetOutputBlanking.
|
|
28
|
+
- Timeline Audio Normalization - Timeline.GetNormalizeAudioModes, NormalizeAudioLevel.
|
|
29
|
+
- Set Audio Mappings - MediaPoolItem.SetAudioMapping, TimelineItem.SetSourceAudioChannelMapping.
|
|
30
|
+
- Media Storage Clone - MediaStorage.StartCloneMedia, StopCloneMedia, GetCloneStatus, SetCloneToolSettings.
|
|
31
|
+
- DCTL development - resolve.ValidateDCTL, EncryptDCTL.
|
|
32
|
+
- Utility functions - resolve.Get CurrentProject/CurrentTimeline/MediaPool/Gallery.
|
|
33
|
+
|
|
34
|
+
## 21.0.4
|
|
35
|
+
|
|
36
|
+
Added:
|
|
37
|
+
|
|
38
|
+
- Timeline.GetSelectedClips
|
|
39
|
+
- Project.SetRenderSettings - options for handles, full extents and data burn.
|
|
40
|
+
|
|
41
|
+
## 21.0.3
|
|
42
|
+
|
|
43
|
+
Addressed:
|
|
44
|
+
|
|
45
|
+
- Incorrect audio mapping results in some scenarios.
|
|
46
|
+
|
|
47
|
+
## 21.0.1
|
|
48
|
+
|
|
49
|
+
Addressed:
|
|
50
|
+
|
|
51
|
+
- GenerateSpeech API character limit issue.
|
|
52
|
+
|
|
53
|
+
## 21.0
|
|
54
|
+
|
|
55
|
+
Added:
|
|
56
|
+
|
|
57
|
+
- Optional useSpeakerDetection argument for Folder.TranscribeAudio and MediaPoolItem.TranscribeAudio.
|
|
58
|
+
- PerformAudioClassification and ClearAudioClassification for Folder and MediaPoolItem.
|
|
59
|
+
- RemoveMotionBlur({deblurOption}) for Folder and MediaPoolItem.
|
|
60
|
+
- AnalyzeForIntellisearch(identifyFaces, isBetterMode) for Folder and MediaPoolItem.
|
|
61
|
+
- New Marker color enumerated type - currently used only in AnalyzeForSlate APIs.
|
|
62
|
+
- AnalyzeForSlate(markerColor) for Folder and MediaPoolItem.
|
|
63
|
+
- Resolve.DisableBackgroundTasksForCurrentResolveSession
|
|
64
|
+
- Project.GenerateSpeech({speechGenerationSettings}, timecode)
|
|
65
|
+
|
|
66
|
+
## 20.3.2
|
|
67
|
+
|
|
68
|
+
Addressed:
|
|
69
|
+
|
|
70
|
+
- AppendToTimeline failure when no media pool clip is selected.
|
|
71
|
+
|
|
72
|
+
## 20.2.2
|
|
73
|
+
|
|
74
|
+
Added:
|
|
75
|
+
|
|
76
|
+
- ProjectManager.CreateProject supports optional mediaLocationPath parameter.
|
|
77
|
+
- Resolve.GetFairlightPresets
|
|
78
|
+
- Project.ApplyFairlightPresetToCurrentTimeline(preset_name)
|
|
79
|
+
|
|
80
|
+
Addressed:
|
|
81
|
+
|
|
82
|
+
- GetRenderCodecs(renderFormat) missing some valid codecs.
|
|
83
|
+
- SetVoiceIsolationState incorrect behavior in some scenarios.
|
|
84
|
+
- AppendToTimeline validating trackIndex as video track indices in 'Audio Only' mode.
|
|
85
|
+
|
|
86
|
+
## 20.2.1
|
|
87
|
+
|
|
88
|
+
Addressed:
|
|
89
|
+
|
|
90
|
+
- TimelineItem.GetName not returning subtitle text content.
|
|
91
|
+
- Project.SetSetting('colorAcesODT', value) not working for some values.
|
|
92
|
+
|
|
93
|
+
## 20.2
|
|
94
|
+
|
|
95
|
+
Added:
|
|
96
|
+
|
|
97
|
+
- TimelineItem and MediaPoolItem support for SetName.
|
|
98
|
+
- Render Settings: ExportSubtitle (Bool) and SubtitleFormat (string) options.
|
|
99
|
+
- TimelineItem.ResetAllNodeColors
|
|
100
|
+
|
|
101
|
+
## 20.1
|
|
102
|
+
|
|
103
|
+
Added:
|
|
104
|
+
|
|
105
|
+
- Timeline.GetVoiceIsolationState(trackIndex)
|
|
106
|
+
- Timeline.SetVoiceIsolationState(trackIndex, {VoiceIsolationState})
|
|
107
|
+
- TimelineItem.GetVoiceIsolationState
|
|
108
|
+
- TimelineItem.EnableVoiceIsolationState({VoiceIsolationState})
|
|
109
|
+
|
|
110
|
+
Addressed:
|
|
111
|
+
|
|
112
|
+
- Interlaced clip frame offset issues in multiple APIs.
|
|
113
|
+
|
|
114
|
+
## 20.0.1
|
|
115
|
+
|
|
116
|
+
Addressed:
|
|
117
|
+
|
|
118
|
+
- MediaPool.AppendToTimeline not working in some scenarios.
|
|
119
|
+
|
|
120
|
+
## 20.0
|
|
121
|
+
|
|
122
|
+
Added:
|
|
123
|
+
|
|
124
|
+
- MediaPoolItem.LinkFullResolutionMedia(fullResMediaPath)
|
|
125
|
+
- MediaPoolItem.ReplaceClipPreserveSubClip(filePath)
|
|
126
|
+
- MediaPoolItem.MonitorGrowingFile
|
|
127
|
+
|
|
128
|
+
Addressed:
|
|
129
|
+
|
|
130
|
+
- TimelineItem.GetRightExtents return (it now returns offset between right edit point and media extents end).
|
|
131
|
+
- Occasional lags for some scripting APIs.
|
|
132
|
+
- AddFusionComp sometimes showing incorrect composition.
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.215.
|
|
40
|
+
VERSION = "2.215.2"
|
|
41
41
|
# Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
|
|
42
42
|
# Resolve's scripting bridge loads into newer interpreters on recent builds
|
|
43
43
|
# (Python 3.14 verified against Resolve Studio 20.3.2). Older Resolve builds
|
package/package.json
CHANGED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Inventory a shipped Resolve .pyi against executable source references.
|
|
3
|
+
|
|
4
|
+
Diagnostic only: absence is a candidate gap, presence is NOT wrapper coverage.
|
|
5
|
+
Receiver types are not inferred, and arbitrary dynamic dispatch is unresolved.
|
|
6
|
+
This complements the legacy README audit without changing its baseline.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import ast
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def inventory_stub(text: str) -> dict:
|
|
19
|
+
"""Preserve class identity, signatures, docs, and typed option fields."""
|
|
20
|
+
methods, options = {}, {}
|
|
21
|
+
for cls in ast.parse(text).body:
|
|
22
|
+
if not isinstance(cls, ast.ClassDef):
|
|
23
|
+
continue
|
|
24
|
+
for node in cls.body:
|
|
25
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and not node.name.startswith('_'):
|
|
26
|
+
key = f'{cls.name}.{node.name}'
|
|
27
|
+
signature = f'{node.name}({ast.unparse(node.args)})'
|
|
28
|
+
if node.returns is not None:
|
|
29
|
+
signature += f' -> {ast.unparse(node.returns)}'
|
|
30
|
+
row = methods.setdefault(key, {'signatures': [], 'line': node.lineno,
|
|
31
|
+
'description': ast.get_docstring(node)})
|
|
32
|
+
row['signatures'].append(signature)
|
|
33
|
+
if any(ast.unparse(base).endswith('TypedDict') for base in cls.bases):
|
|
34
|
+
fields = {}
|
|
35
|
+
for index, node in enumerate(cls.body):
|
|
36
|
+
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
|
37
|
+
doc = None
|
|
38
|
+
if index + 1 < len(cls.body):
|
|
39
|
+
following = cls.body[index + 1]
|
|
40
|
+
if (isinstance(following, ast.Expr) and isinstance(following.value, ast.Constant)
|
|
41
|
+
and isinstance(following.value.value, str)):
|
|
42
|
+
doc = following.value.value
|
|
43
|
+
fields[node.target.id] = {'type': ast.unparse(node.annotation),
|
|
44
|
+
'line': node.lineno, 'description': doc}
|
|
45
|
+
options[cls.name] = fields
|
|
46
|
+
if not methods:
|
|
47
|
+
raise ValueError('No public class methods found; refusing an empty API inventory')
|
|
48
|
+
return {'methods': methods, 'option_types': options}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def source_references(text: str) -> list[dict]:
|
|
52
|
+
"""Record executable syntax, never docstrings/comments as fake coverage."""
|
|
53
|
+
tree = ast.parse(text)
|
|
54
|
+
parents = {child: parent for parent in ast.walk(tree) for child in ast.iter_child_nodes(parent)}
|
|
55
|
+
refs = []
|
|
56
|
+
for node in ast.walk(tree):
|
|
57
|
+
if isinstance(node, ast.Attribute):
|
|
58
|
+
parent = parents.get(node)
|
|
59
|
+
refs.append({'method_name': node.attr, 'line': node.lineno,
|
|
60
|
+
'receiver': ast.unparse(node.value),
|
|
61
|
+
'kind': 'call' if isinstance(parent, ast.Call) and parent.func is node else 'attribute'})
|
|
62
|
+
elif (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
|
|
63
|
+
and node.func.id == 'getattr' and len(node.args) > 1
|
|
64
|
+
and isinstance(node.args[1], ast.Constant) and isinstance(node.args[1].value, str)):
|
|
65
|
+
refs.append({'method_name': node.args[1].value, 'line': node.lineno,
|
|
66
|
+
'receiver': ast.unparse(node.args[0]), 'kind': 'getattr'})
|
|
67
|
+
return refs
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def build_report(stub_text: str, sources: dict[str, str]) -> dict:
|
|
71
|
+
inventory = inventory_stub(stub_text)
|
|
72
|
+
by_name = {}
|
|
73
|
+
for path, text in sorted(sources.items()):
|
|
74
|
+
layer = ('compound' if path == 'src/server.py' else
|
|
75
|
+
'granular' if path.startswith('src/granular/') else 'helper')
|
|
76
|
+
for ref in source_references(text):
|
|
77
|
+
by_name.setdefault(ref['method_name'], []).append({'path': path, 'layer': layer, **ref})
|
|
78
|
+
for key, method in inventory['methods'].items():
|
|
79
|
+
refs = by_name.get(key.rsplit('.', 1)[1], [])
|
|
80
|
+
method['name_references'] = refs
|
|
81
|
+
method['status'] = 'unresolved_receiver' if refs else 'no_executable_name_reference'
|
|
82
|
+
candidates = [key for key, row in inventory['methods'].items() if not row['name_references']]
|
|
83
|
+
return {'schema_version': 1, 'method_count': len(inventory['methods']),
|
|
84
|
+
'candidate_count': len(candidates), 'candidate_gaps': candidates,
|
|
85
|
+
'limitations': ['References are matched by method name, not receiver type.',
|
|
86
|
+
'A call/reference does not prove public action or parameter coverage.',
|
|
87
|
+
'Literal getattr may only probe availability.',
|
|
88
|
+
'Computed dispatch and equivalent older methods need manual review.',
|
|
89
|
+
'This inventory does not invoke Resolve or validate behavior.'],
|
|
90
|
+
**inventory}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main() -> int:
|
|
94
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
95
|
+
parser.add_argument('--stub', type=Path, default=ROOT / 'docs/reference/DaVinciResolveScript.pyi')
|
|
96
|
+
parser.add_argument('--source-root', type=Path, default=ROOT / 'src')
|
|
97
|
+
parser.add_argument('--json', action='store_true', help='Print the full method/option/reference inventory')
|
|
98
|
+
args = parser.parse_args()
|
|
99
|
+
paths = sorted(args.source_root.rglob('*.py'))
|
|
100
|
+
if not paths:
|
|
101
|
+
parser.error('No Python source files found; refusing an empty source inventory')
|
|
102
|
+
sources = {'src/' + path.relative_to(args.source_root).as_posix(): path.read_text(encoding='utf-8')
|
|
103
|
+
for path in paths}
|
|
104
|
+
report = build_report(args.stub.read_text(encoding='utf-8'), sources)
|
|
105
|
+
if args.json:
|
|
106
|
+
print(json.dumps(report, indent=2))
|
|
107
|
+
else:
|
|
108
|
+
print(f"{report['method_count']} class-qualified methods; {report['candidate_count']} candidate gaps")
|
|
109
|
+
for name in report['candidate_gaps']:
|
|
110
|
+
print(name)
|
|
111
|
+
print('Diagnostic only: name references are not proof of wrapper coverage or live behavior.')
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == '__main__':
|
|
116
|
+
raise SystemExit(main())
|
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.215.
|
|
90
|
+
VERSION = "2.215.2"
|
|
91
91
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
92
92
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
93
93
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -5,6 +5,25 @@ from src.utils.clip_colors import clip_color_refusal
|
|
|
5
5
|
|
|
6
6
|
resolve = ResolveProxy()
|
|
7
7
|
|
|
8
|
+
|
|
9
|
+
def _item_type(item, method="GetType"):
|
|
10
|
+
"""Normalize type values without assuming an optional API is callable.
|
|
11
|
+
|
|
12
|
+
Resolve 21.1 documents lowercase types. Keep title-case compatibility,
|
|
13
|
+
and treat missing/non-string results as unknown rather than as a clip.
|
|
14
|
+
"""
|
|
15
|
+
getter = getattr(item, method, None)
|
|
16
|
+
if not callable(getter):
|
|
17
|
+
return ""
|
|
18
|
+
value = getter()
|
|
19
|
+
return value.lower() if isinstance(value, str) else ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _has_audio_type(item):
|
|
23
|
+
return (_item_type(item) == "audio"
|
|
24
|
+
or _item_type(item, "GetMediaType") == "audio")
|
|
25
|
+
|
|
26
|
+
|
|
8
27
|
@mcp.resource("resolve://timeline-item/{timeline_item_id}")
|
|
9
28
|
def get_timeline_item_properties(timeline_item_id: str) -> Dict[str, Any]:
|
|
10
29
|
"""Get properties of a specific timeline item by ID.
|
|
@@ -65,7 +84,7 @@ def get_timeline_item_properties(timeline_item_id: str) -> Dict[str, Any]:
|
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
# Get additional properties if it's a video item
|
|
68
|
-
if timeline_item
|
|
87
|
+
if _item_type(timeline_item) == "video":
|
|
69
88
|
# Transform properties
|
|
70
89
|
properties["transform"] = {
|
|
71
90
|
"position": {
|
|
@@ -118,7 +137,7 @@ def get_timeline_item_properties(timeline_item_id: str) -> Dict[str, Any]:
|
|
|
118
137
|
}
|
|
119
138
|
|
|
120
139
|
# Audio-specific properties
|
|
121
|
-
if
|
|
140
|
+
if _has_audio_type(timeline_item):
|
|
122
141
|
properties["audio"] = {
|
|
123
142
|
"volume": timeline_item.GetProperty("Volume"),
|
|
124
143
|
"pan": timeline_item.GetProperty("Pan"),
|
|
@@ -239,7 +258,7 @@ def set_timeline_item_transform(timeline_item_id: str,
|
|
|
239
258
|
if not timeline_item:
|
|
240
259
|
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
241
260
|
|
|
242
|
-
if timeline_item
|
|
261
|
+
if _item_type(timeline_item) != "video":
|
|
243
262
|
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
244
263
|
|
|
245
264
|
# Set the property
|
|
@@ -299,7 +318,7 @@ def set_timeline_item_crop(timeline_item_id: str,
|
|
|
299
318
|
if not timeline_item:
|
|
300
319
|
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
301
320
|
|
|
302
|
-
if timeline_item
|
|
321
|
+
if _item_type(timeline_item) != "video":
|
|
303
322
|
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
304
323
|
|
|
305
324
|
# Set the property
|
|
@@ -368,7 +387,7 @@ def set_timeline_item_composite(timeline_item_id: str,
|
|
|
368
387
|
if not timeline_item:
|
|
369
388
|
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
370
389
|
|
|
371
|
-
if timeline_item
|
|
390
|
+
if _item_type(timeline_item) != "video":
|
|
372
391
|
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
373
392
|
|
|
374
393
|
success = True
|
|
@@ -529,7 +548,7 @@ def set_timeline_item_stabilization(timeline_item_id: str,
|
|
|
529
548
|
if not timeline_item:
|
|
530
549
|
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
531
550
|
|
|
532
|
-
if timeline_item
|
|
551
|
+
if _item_type(timeline_item) != "video":
|
|
533
552
|
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
534
553
|
|
|
535
554
|
success = True
|
|
@@ -635,7 +654,7 @@ def set_timeline_item_audio(timeline_item_id: str,
|
|
|
635
654
|
return f"Error: Timeline item with ID '{timeline_item_id}' not found"
|
|
636
655
|
|
|
637
656
|
# Check if the item has audio capabilities
|
|
638
|
-
if not is_audio and timeline_item
|
|
657
|
+
if not is_audio and not _has_audio_type(timeline_item):
|
|
639
658
|
return f"Error: Timeline item with ID '{timeline_item_id}' does not have audio properties"
|
|
640
659
|
|
|
641
660
|
success = True
|
|
@@ -737,7 +756,7 @@ def get_timeline_item_keyframes(timeline_item_id: str, property_name: str) -> Di
|
|
|
737
756
|
audio_properties = ['Volume', 'Pan']
|
|
738
757
|
|
|
739
758
|
# Check if it's a video item
|
|
740
|
-
if timeline_item
|
|
759
|
+
if _item_type(timeline_item) == "video":
|
|
741
760
|
# Check each property to see if it has keyframes
|
|
742
761
|
for prop in video_properties:
|
|
743
762
|
if timeline_item.GetKeyframeCount(prop) > 0:
|
|
@@ -758,7 +777,7 @@ def get_timeline_item_keyframes(timeline_item_id: str, property_name: str) -> Di
|
|
|
758
777
|
})
|
|
759
778
|
|
|
760
779
|
# Check if it has audio properties (could be video with audio or audio-only)
|
|
761
|
-
if
|
|
780
|
+
if _has_audio_type(timeline_item):
|
|
762
781
|
# Check each audio property for keyframes
|
|
763
782
|
for prop in audio_properties:
|
|
764
783
|
if timeline_item.GetKeyframeCount(prop) > 0:
|
|
@@ -877,7 +896,7 @@ def add_keyframe(timeline_item_id: str, property_name: str, frame: int, value: f
|
|
|
877
896
|
if is_audio and property_name not in audio_properties:
|
|
878
897
|
return f"Error: Property '{property_name}' is not available for audio items"
|
|
879
898
|
|
|
880
|
-
if not is_audio and property_name not in video_properties and timeline_item
|
|
899
|
+
if not is_audio and property_name not in video_properties and _item_type(timeline_item) != "video":
|
|
881
900
|
return f"Error: Property '{property_name}' is not available for this item type"
|
|
882
901
|
|
|
883
902
|
# Validate frame is within the item's range
|
|
@@ -1241,7 +1260,7 @@ def enable_keyframes(timeline_item_id: str, keyframe_mode: str = "All") -> str:
|
|
|
1241
1260
|
if not timeline_item:
|
|
1242
1261
|
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
1243
1262
|
|
|
1244
|
-
if timeline_item
|
|
1263
|
+
if _item_type(timeline_item) != "video":
|
|
1245
1264
|
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
1246
1265
|
|
|
1247
1266
|
# Set the keyframe mode
|
package/src/server.py
CHANGED
|
@@ -11,7 +11,7 @@ Usage:
|
|
|
11
11
|
python src/server.py --full # Start the 353-tool granular server instead
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
VERSION = "2.215.
|
|
14
|
+
VERSION = "2.215.2"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -14963,6 +14963,18 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
|
|
|
14963
14963
|
original_fc = proj.GetCurrentRenderFormatAndCodec()
|
|
14964
14964
|
except Exception:
|
|
14965
14965
|
pass
|
|
14966
|
+
# Render MODE is project state too, and it decides whether the capture can
|
|
14967
|
+
# work at all. In "Individual clips" mode (0) Resolve ignores CustomName,
|
|
14968
|
+
# renders the WHOLE clip under the frame's own file naming, and the
|
|
14969
|
+
# single-frame file this helper waits for never appears — measured
|
|
14970
|
+
# 2026-09-09 on a project whose delivery preset was per-clip: every capture
|
|
14971
|
+
# reported success, wrote no file, and took 30+ s rendering the clip.
|
|
14972
|
+
# Force single clip (1) for the capture and put the mode back afterwards.
|
|
14973
|
+
original_mode = None
|
|
14974
|
+
try:
|
|
14975
|
+
original_mode = proj.GetCurrentRenderMode()
|
|
14976
|
+
except Exception:
|
|
14977
|
+
original_mode = None
|
|
14966
14978
|
# Rendering pulls Resolve onto the Deliver page and moves the playhead;
|
|
14967
14979
|
# measured leaving the user on Deliver at a different frame. Both are ours
|
|
14968
14980
|
# to put back.
|
|
@@ -15014,6 +15026,16 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
|
|
|
15014
15026
|
|
|
15015
15027
|
job = None
|
|
15016
15028
|
try:
|
|
15029
|
+
if original_mode is not None and original_mode != 1:
|
|
15030
|
+
if not proj.SetCurrentRenderMode(1):
|
|
15031
|
+
return _err(
|
|
15032
|
+
"Resolve refused to switch the render mode to single clip; in "
|
|
15033
|
+
"individual-clips mode a single-frame capture renders the whole clip "
|
|
15034
|
+
"under Resolve's own naming and the captured file never appears",
|
|
15035
|
+
code="RENDER_MODE_REFUSED", category="api_error",
|
|
15036
|
+
remediation="render(action='set_mode', params={'mode': 1}) then retry.",
|
|
15037
|
+
state={"render_mode": original_mode},
|
|
15038
|
+
)
|
|
15017
15039
|
codecs = proj.GetRenderCodecs("JPEG" if fmt == "jpg" else fmt.upper()) or {}
|
|
15018
15040
|
codec = list(codecs.values())[0] if codecs else fmt
|
|
15019
15041
|
if not proj.SetCurrentRenderFormatAndCodec(fmt, codec):
|
|
@@ -15040,6 +15062,13 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
|
|
|
15040
15062
|
job = proj.AddRenderJob()
|
|
15041
15063
|
if not job:
|
|
15042
15064
|
return _err("AddRenderJob returned nothing", code="RENDER_JOB_FAILED", category="api_error")
|
|
15065
|
+
# The folder is shared (every sandbox path redirects to one
|
|
15066
|
+
# ~/Documents/resolve-stills) and the cleanup below removes it when it
|
|
15067
|
+
# empties, so another capture — or anything else — can take it away
|
|
15068
|
+
# between the makedirs above and here. Measured 2026-09-09: frame 81 of a
|
|
15069
|
+
# 214-frame QC batch died in os.listdir on the missing folder. Recreate,
|
|
15070
|
+
# don't assume.
|
|
15071
|
+
os.makedirs(folder, exist_ok=True)
|
|
15043
15072
|
before = set(os.listdir(folder))
|
|
15044
15073
|
# Positional on purpose: the free-edition bridge proxies method calls
|
|
15045
15074
|
# positionally, and a keyword argument dies inside _BoundMethod with
|
|
@@ -15087,6 +15116,19 @@ def _playhead_frame_render(proj, tl, p: Dict[str, Any]):
|
|
|
15087
15116
|
proj.DeleteRenderJob(job)
|
|
15088
15117
|
except Exception:
|
|
15089
15118
|
pass
|
|
15119
|
+
if original_mode is not None and original_mode != 1:
|
|
15120
|
+
# Same teardown contract as the format/codec restore below.
|
|
15121
|
+
try:
|
|
15122
|
+
restored_mode = bool(proj.SetCurrentRenderMode(original_mode))
|
|
15123
|
+
except Exception as exc:
|
|
15124
|
+
restored_mode, mode_exc = False, exc
|
|
15125
|
+
else:
|
|
15126
|
+
mode_exc = None
|
|
15127
|
+
if not restored_mode:
|
|
15128
|
+
logger.warning(
|
|
15129
|
+
"frame capture could not restore render mode %r: %s",
|
|
15130
|
+
original_mode, mode_exc or "SetCurrentRenderMode returned False",
|
|
15131
|
+
)
|
|
15090
15132
|
if original_fc:
|
|
15091
15133
|
# A failed restore leaves the Deliver page on the capture's format
|
|
15092
15134
|
# and codec, which the user's next render would silently inherit.
|