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 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
- [![Version](https://img.shields.io/badge/version-2.215.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.215.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.215.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.215.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
6
6
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
7
7
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
8
8
  [![Tools](https://img.shields.io/badge/MCP%20Tools-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.215.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
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.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.215.0",
3
+ "version": "2.215.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -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())
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.215.0"
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.GetType() == "Video":
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 timeline_item.GetType() == "Audio" or timeline_item.GetMediaType() == "Audio":
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.GetType() != "Video":
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.GetType() != "Video":
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.GetType() != "Video":
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.GetType() != "Video":
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.GetMediaType() != "Audio":
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.GetType() == "Video":
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 timeline_item.GetType() == "Audio" or timeline_item.GetMediaType() == "Audio":
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.GetType() != "Video":
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.GetType() != "Video":
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.0"
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.