davinci-resolve-mcp 2.82.1 → 2.86.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.
@@ -8,12 +8,12 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
8
8
  |--------|-------|
9
9
  | MCP Tools | **34** compound (default) / **341** granular |
10
10
  | Kernel Actions | **136** guarded MCP workflow actions across 9 compound tools |
11
- | API Methods Covered | **349/349** (100%) |
12
- | Methods Live Tested | **338/349** (96.8%) |
11
+ | API Methods Covered | **351/351** (100%) |
12
+ | Methods Live Tested | **338/351** (96.3%) |
13
13
  | Live Test Pass Rate | **338/338** (100%) |
14
14
  | API Object Classes | 13 |
15
- | Tested Against | DaVinci Resolve 19.1.3 Studio + Resolve 20.3.2 Studio + Resolve 21.0.2 Studio |
16
- | Compatibility Note | Resolve 19.1.3 remains the compatibility baseline; Resolve 20.x scripting calls are additive, version-guarded, and live-tested on 20.3.2; Resolve 21.0 additions are version-guarded and live-tested on 21.0.2 (see the Resolve 21 delta row below — five wrappers need AI Extras packs and stay untested without one, and one is deliberately not executed) |
15
+ | Tested Against | DaVinci Resolve 19.1.3 Studio + Resolve 20.3.2 Studio + Resolve 21.0.2 Studio + Resolve 21.0.3 **free** (via the in-app bridge) |
16
+ | Compatibility Note | Resolve 19.1.3 remains the compatibility baseline; Resolve 20.x scripting calls are additive, version-guarded, and live-tested on 20.3.2; Resolve 21.0 additions are version-guarded and live-tested on 21.0.2 (see the Resolve 21 delta row below — five wrappers need AI Extras packs and stay untested without one, and one is deliberately not executed). The Resolve 21.0.4 additions are version-guarded and unit-tested, but not live-tested here — the validation machine carries Studio 19.1.3 and free 21.0.3, neither of which is 21.0.4. Note the free build is reachable only through the in-app bridge, since the free edition refuses external scripting |
17
17
 
18
18
  ## API Coverage
19
19
 
@@ -109,7 +109,7 @@ source, these summaries are downstream of it:
109
109
  | Phase 5 | 6/6 | 100% | Scene cuts, subtitles from audio, graph node cache/tools/enable |
110
110
  | Resolve 20 delta | 12/12 | 100% | Resolve 20.0-20.2.2 scripting additions live-tested on 20.3.2 |
111
111
  | Resolve 21 delta | 7/7 | 100% | Resolve 21.0 additions that could be executed, live-tested on Studio 21.0.2.4 (`tests/live_resolve21_validation.py`). The other 6 need an AI Extras pack or are unsafe to run — counted as untested, not as passes |
112
- | **Total** | **338/338** | **100%** | **96.8% of the 349 covered methods tested live** |
112
+ | **Total** | **338/338** | **100%** | **96.3% of the 351 covered methods tested live** |
113
113
 
114
114
  #### Resolve 21 delta detail
115
115
 
@@ -130,7 +130,7 @@ marked 🔬 rather than ⚠️.
130
130
  | `Project.GenerateSpeech` | 🔬 | Requires AI Speech Generator; returned an error **string**, not a MediaPoolItem |
131
131
  | `Resolve.DisableBackgroundTasksForCurrentResolveSession` | 🔬 | Present in `dir()`; **not executed** — session-wide, returns None, and has no `Enable...` counterpart, so there is no undo short of restarting Resolve |
132
132
 
133
- ### Untested Methods (11 of 349)
133
+ ### Untested Methods (13 of 351)
134
134
 
135
135
  Every ☁️ and 🔬 row from the reference tables, listed here so the count is
136
136
  checkable rather than asserted.
@@ -148,10 +148,15 @@ checkable rather than asserted.
148
148
  | `MPI.AnalyzeForSlate` | Requires the AI Slate ID Extra | Yes |
149
149
  | `Project.GenerateSpeech` | Requires the AI Speech Generator Extra | Yes |
150
150
  | `Resolve.DisableBackgroundTasksForCurrentResolveSession` | Deliberately not executed: session-wide, returns `None`, and has no `Enable...` counterpart, so there is no undo short of restarting Resolve | No |
151
+ | `MPI.GetTimeline` | Requires a Resolve 21.0.4 build; the validation machine runs Studio 19.1.3 and free 21.0.3 | Yes |
152
+ | `TL.GetSelectedClips` | Requires a Resolve 21.0.4 build; the validation machine runs 19.1.3 | Yes |
151
153
 
152
154
  The five AI Extras rows are untested for want of a downloadable pack, not because
153
155
  the wrappers are suspect — a report from anyone who has the packs installed would
154
- close them. The last one is a decision rather than a gap: it is reachable, and
156
+ close them. The two 21.0.4 rows are untested for want of the build: this machine
157
+ runs 19.1.3, so they are guarded and unit-tested against stubs and carry a
158
+ contributor's report rather than a result of ours. The remaining one is a
159
+ decision rather than a gap: it is reachable, and
155
160
  running it during a validation sweep would disable background tasks for every
156
161
  project open in that Resolve instance.
157
162
 
@@ -192,7 +197,7 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
192
197
  | 19 | `ExportBurnInPreset(presetName, exportPath)` | ⚠️ | API accepts; needs valid preset name |
193
198
  | 20 | `GetKeyframeMode()` | ✅ | Returns keyframe mode |
194
199
  | 21 | `SetKeyframeMode(keyframeMode)` | ⚠️ | API accepts; mode must match valid enum |
195
- | 22 | `GetFairlightPresets()` | ✅ | Resolve 20.3.2 live test returns preset map |
200
+ | 22 | `GetFairlightPresets()` | ✅ | Returns a preset map. Live on 20.3.2 Studio and 21.0.3 free; **absent below 20.2.2** — confirmed missing on 19.1.3, so the version floor is verified from both sides |
196
201
  | 23 | `DisableBackgroundTasksForCurrentResolveSession()` | 🔬 | Resolve 21.0. Present in `dir()`; **not executed** — session-wide, returns `None`, no `Enable...` counterpart, so no undo short of restarting Resolve |
197
202
 
198
203
  ### ProjectManager
@@ -250,7 +255,7 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
250
255
  | 19 | `LoadRenderPreset(presetName)` | ✅ | Loads render preset |
251
256
  | 20 | `SaveAsNewRenderPreset(presetName)` | ✅ | Creates render preset |
252
257
  | 21 | `DeleteRenderPreset(presetName)` | ✅ | Deletes render preset |
253
- | 22 | `SetRenderSettings({settings})` | ✅ | Applies render settings; Resolve 20.2 adds `ExportSubtitle` and `SubtitleFormat` keys |
258
+ | 22 | `SetRenderSettings({settings})` | ✅ | Applies render settings; Resolve 20.2 adds `ExportSubtitle` and `SubtitleFormat` keys; Resolve 21.0.4 adds `UseFullExtents`, `AddFrameHandles` and `DataBurnIn` — `AddFrameHandles` is accepted and then ignored while `UseFullExtents` is true, so the server returns it as a warning |
254
259
  | 23 | `GetRenderJobStatus(jobId)` | ✅ | Returns `{JobStatus, CompletionPercentage}` |
255
260
  | 24 | `GetQuickExportRenderPresets()` | ✅ | Returns preset names |
256
261
  | 25 | `RenderWithQuickExport(preset, {params})` | ✅ | Initiates quick export |
@@ -271,7 +276,7 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
271
276
  | 40 | `GetColorGroupsList()` | ✅ | Returns color group list |
272
277
  | 41 | `AddColorGroup(groupName)` | ✅ | Returns ColorGroup object |
273
278
  | 42 | `DeleteColorGroup(colorGroup)` | ✅ | Deletes color group |
274
- | 43 | `ApplyFairlightPresetToCurrentTimeline(presetName)` | ⚠️ | Resolve 20.3.2 live test accepts call; returns `False` without a named preset |
279
+ | 43 | `ApplyFairlightPresetToCurrentTimeline(presetName)` | ⚠️ | Accepts the call and returns `False` when handed a name that does not exist. **Never exercised against a genuinely saved preset**, so a `True` path has not been observed — the API has no way to create one, and `GetFairlightPresets` reported an empty map on both test machines. To clear this: save a mix as a Fairlight preset in the UI, then apply it by that name. Requires 20.2.2+ |
275
280
  | 44 | `GenerateSpeech({speechGenerationSettings}, timecode)` | 🔬 | Resolve 21.0. Requires the AI Speech Generator Extra; without it returns an error **string**, not a MediaPoolItem |
276
281
  | 45 | `ResetIntellisearchAnalysis()` | ✅ | Resolve 21.0.2 live test returns `True` |
277
282
 
@@ -382,6 +387,7 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
382
387
  | 39 | `AnalyzeForIntellisearch(identifyFaces, isBetterMode)` | 🔬 | Resolve 21.0. Requires the AI IntelliSearch Extra; without it returns an error **string**, not `False` |
383
388
  | 40 | `AnalyzeForSlate(markerColor)` | 🔬 | Resolve 21.0. Requires the AI Slate ID Extra. Documented `resolve.MARKER_*` constants do not exist on the handle |
384
389
  | 41 | `RemoveMotionBlur({deblurOption})` | ✅ | Resolve 21.0.2 live test returns the new MediaPoolItem; source media path unchanged. Requires the AI Motion Deblur Extra |
390
+ | 42 | `GetTimeline()` | 🔬 | Resolve 21.0.4. Not executed here — no 21.0.4 build on hand; reported working on Studio 21.0.4.5 in issue #131 |
385
391
 
386
392
  ### Timeline
387
393
 
@@ -445,6 +451,7 @@ Every method in the DaVinci Resolve Scripting API and its test status. Methods a
445
451
  | 56 | `ClearMarkInOut(type)` | ✅ | Clears mark in/out |
446
452
  | 57 | `GetVoiceIsolationState(trackIndex)` | ✅ | Resolve 20.3.2 live test returns voice isolation state |
447
453
  | 58 | `SetVoiceIsolationState(trackIndex, {state})` | ✅ | Resolve 20.3.2 live test sets voice isolation state |
454
+ | 59 | `GetSelectedClips()` | 🔬 | Resolve 21.0.4. First name tried by the selection probe, ahead of the two legacy speculative names. Not executed here — no 21.0.4 build on hand; reported working on Studio 21.0.4.5 in issue #131 |
448
455
 
449
456
  ### TimelineItem
450
457
 
@@ -182,7 +182,7 @@ equivalent, blocking full automation.
182
182
 
183
183
  - **Object:** `TimelineItem / Timeline / Project`
184
184
  - **Behavior:** There is no API method to set or query subtitle font family, font size, text color, background color, outline, shadow, position, alignment, or to apply/query subtitle style presets. TimelineItem.GetProperty() on subtitle items returns only transform/composite keys. Timeline.GetSetting() and Project.GetSetting() return None for all probed subtitle-style keys (e.g. 'subtitleFontName', 'subtitleFontSize', 'subtitleTextColor', 'subtitleBackgroundColor', 'subtitlePosition', 'subtitleAlignment', 'subtitlePreset', 'subtitleStyle'). Verified via dir(), GetProperty(), and GetSetting() on Resolve 21.0.0.48.
185
- - **Workaround / current handling:** No API workaround exists, but the style IS reachable below the API: it lives in Sm2TiTrack.FieldsBlob for Type=2 tracks, as an EffectFiltersBA payload whose effect 136 carries a Qt QFont descriptor (param 18) and a normalised position vector (param 17). Exposed as project_db list_subtitle_styles / set_subtitle_style (font family/size/weight/italic + position). Confirmed live on Studio 19.1.3 (2026-08-06): Resolve opens a patched track and re-serialises it back to its own zstd form with the patched values intact, so it genuinely parses the write. Caveats: whole-TRACK style not per-caption, project must be CLOSED, Resolve must be fully quit and relaunched afterwards, and the track must already carry a style blob (a freshly added subtitle track has none until it is styled once in the UI). Burn-in overlays via Fusion titles remain a visual alternative but do not produce subtitle tracks.
185
+ - **Workaround / current handling:** No API workaround exists, but the style IS reachable below the API: it lives in Sm2TiTrack.FieldsBlob for Type=2 tracks, as an EffectFiltersBA payload whose effect 136 carries a Qt QFont descriptor (param 18) and a normalised position vector (param 17). Exposed as project_db list_subtitle_styles / set_subtitle_style (font family/size/weight/italic + position). Confirmed live on BOTH editions 2026-08-06 — Studio 19.1.3 and free 21.0.3, identical behaviour: Resolve opens a patched track and re-serialises it back to its own zstd form with the patched values intact, so it genuinely parses the write. Caveats: whole-TRACK style not per-caption, project must be CLOSED, Resolve must be fully quit and relaunched afterwards, and the track must already carry a style blob (a freshly added subtitle track has none until it is styled once in the UI). Burn-in overlays via Fusion titles remain a visual alternative but do not produce subtitle tracks.
186
186
  - **Tags:** missing-method, subtitle, style, preset
187
187
 
188
188
  ### Speech recognition engine selection and SRT import
@@ -226,7 +226,7 @@ equivalent, blocking full automation.
226
226
 
227
227
  - **Object:** `TimelineItem / MediaPoolItem`
228
228
  - **Signature:** `(colorName) -> bool`
229
- - **Behavior:** SetClipColor accepts exactly 16 names — the Edit-page clip-colour palette — and refuses everything else with a bare False, no exception and no other signal. Enumerated live on Studio 19.1.3.7 (2026-08-06) against both objects; the accepted set is IDENTICAL on TimelineItem and MediaPoolItem: Orange, Apricot, Yellow, Lime, Olive, Green, Teal, Navy, Blue, Purple, Violet, Pink, Tan, Beige, Brown, Chocolate. The empty string is refused too. The trap is that the scripting reference documents colorName as a bare string with no enumerated values, while exporting the MARKER palette as constants (resolve.MARKER_ROSE, resolve.MARKER_FUCHSIA, ...) — so the only colour vocabulary reachable from the API surface is the wrong one. Cyan, Red, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa and Cream are all marker-only and all refused. Five names overlap both palettes (Blue, Green, Yellow, Pink, Purple), which is why the decoy survives: reasoning from the marker constants scores 5 of 16 and looks like the right vocabulary with a few gaps.
229
+ - **Behavior:** SetClipColor accepts exactly 16 names — the Edit-page clip-colour palette — and refuses everything else with a bare False, no exception and no other signal. Enumerated live on Studio 19.1.3.7 (2026-08-06) against both objects; the accepted set is IDENTICAL on TimelineItem and MediaPoolItem: Orange, Apricot, Yellow, Lime, Olive, Green, Teal, Navy, Blue, Purple, Violet, Pink, Tan, Beige, Brown, Chocolate. The empty string is refused too. The trap is that the scripting reference documents colorName as a bare string with no enumerated values, while exporting the MARKER palette as constants (resolve.MARKER_ROSE, resolve.MARKER_FUCHSIA, ...) — so the only colour vocabulary reachable from the API surface is the wrong one. Cyan, Red, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa and Cream are all marker-only and all refused. Five names overlap both palettes (Blue, Green, Yellow, Pink, Purple), which is why the decoy survives: reasoning from the marker constants scores 5 of 16 and looks like the right vocabulary with a few gaps. The reporter's field experience sharpens this — the trap is not that a decoy vocabulary exists, it is that the decoy HALF-WORKS. Intermittent successes read as an unreliable API rather than as a wrong vocabulary, so the wrong conclusion is the natural one; a clean 0-for-8 would have exposed the mechanism immediately.
230
230
  - **Workaround / current handling:** Pass only the 16 clip-colour names; on False, treat it as an invalid name rather than an item/lock/page problem. The set is pinned in utils/clip_colors.py and named in the refusal remediation, but deliberately NOT enforced — it was measured on one build and a later Resolve could extend it.
231
231
  - **Reference:** [issue #124](https://github.com/samuelgursky/davinci-resolve-mcp/issues/124)
232
232
  - **Tags:** timeline-item, media-pool, silent-failure, undocumented-enum
@@ -385,8 +385,8 @@ values, or automation-hostile modal prompts.
385
385
  ### hasattr() / getattr() on Resolve API objects (attribute fabrication)
386
386
 
387
387
  - **Object:** `(all Resolve scripting objects)`
388
- - **Behavior:** UNRESOLVED — the two measurements do not test the same thing. On 21.0.0 the bridge was recorded as returning a callable for ANY attribute name, making capability detection by hasattr impossible; the evidence was REAL API method names borrowed from other object types (SetStart, Razor, AddNode, GenerateProxy, AddSmartBin reported present on objects that do not have them). A 21.0.2.4 control probe of the invented name 'TotallyMadeUpMethod_xyz123' returned getattr-callable False on all eight object types, matching dir() in every case. That does NOT refute the 21.0.0 record: if the bridge resolves any name known to the RemoteObject method table rather than literally any string, an invented name is correctly rejected on both builds and the probe never exercised the failing case. Re-running the probe with those five real names is what would settle it; until then, assume fabrication is possible.
389
- - **Workaround / current handling:** Use dir(obj) membership for capability probes. It is correct on every build measured, and it is the only form not affected by whichever way this resolves. server._has_method uses hasattr/getattr and so may over-report on builds where fabrication is live — that is the case _requires_method gates guard, so it matters most exactly where it is least tested. Calling a fabricated method typically returns None/False with no error.
388
+ - **Behavior:** UNRESOLVED — the two measurements do not test the same thing. On 21.0.0 the bridge was recorded as returning a callable for ANY attribute name, making capability detection by hasattr impossible; the evidence was REAL API method names borrowed from other object types (SetStart, Razor, AddNode, GenerateProxy, AddSmartBin reported present on objects that do not have them). A 21.0.2.4 control probe of the invented name 'TotallyMadeUpMethod_xyz123' returned getattr-callable False on all eight object types, matching dir() in every case. That does NOT refute the 21.0.0 record: if the bridge resolves any name known to the RemoteObject method table rather than literally any string, an invented name is correctly rejected on both builds and the probe never exercised the failing case. PARTLY SETTLED 2026-08-06 on Studio 19.1.3.7, DIRECT connection (not the bridge): the five real borrowed names were re-run as the entry asked, across Resolve, ProjectManager, Project, MediaPool, Timeline, Folder and TimelineItem — 42 checks. Every one returned getattr-callable False and dir()-absent, agreeing in all 42 cases, exactly like the invented control. So NO fabrication on the direct path on this build, and the borrowed-name case the 21.0.2.4 probe missed is now covered there. What the same run DID establish is narrower and worse than expected: bare hasattr() returns True for EVERY name on every object — real, borrowed or invented — while getattr returns None. hasattr alone is therefore not a weak probe, it is a constant True and carries no information at all. STILL OPEN: the 21.0.0 record was against the BRIDGE, which this run did not exercise, so bridge-side fabrication remains untested and the recommendation below stands unchanged.
389
+ - **Workaround / current handling:** Use dir(obj) membership for capability probes. It is correct on every build measured, and it is the only form not affected by whichever way this resolves. NEVER use bare hasattr(): it is a constant True on Resolve objects and cannot distinguish anything. server._has_method uses callable(getattr(...)), which agreed with dir() in all 42 direct-path checks on 19.1.3.7 but may still over-report on a bridge build where fabrication is live — that is the case _requires_method gates guard, so it matters most exactly where it is least tested. Calling a fabricated method typically returns None/False with no error.
390
390
  - **Tags:** bridge, introspection, silent-failure
391
391
 
392
392
  ### Resolve 21 AI methods (AnalyzeForIntellisearch, GenerateSpeech, AnalyzeForSlate) — inconsistent failure return type
package/install.py CHANGED
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.82.1"
39
+ VERSION = "2.86.2"
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
@@ -1748,8 +1748,8 @@ def main():
1748
1748
  " python scripts/install_resolve_bridge.py"
1749
1749
  )
1750
1750
  print(
1751
- " then Workspace > Scripts > resolve_bridge, "
1752
- "and set DAVINCI_RESOLVE_BRIDGE=1"
1751
+ " then Workspace > Scripts > resolve_bridge "
1752
+ "(used automatically once running)"
1753
1753
  )
1754
1754
  else:
1755
1755
  print(f" Resolve: {yellow('Not running')} — start Resolve to use MCP tools")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "2.82.1",
3
+ "version": "2.86.2",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -18,6 +18,29 @@ const require = createRequire(import.meta.url);
18
18
 
19
19
  export const DISK_DB_ROOT = path.join(os.homedir(), 'Library/Application Support/Blackmagic Design/DaVinci Resolve/Resolve Disk Database/Resolve Projects');
20
20
 
21
+ /**
22
+ * The FREE edition ships from the App Store and runs SANDBOXED, so its project
23
+ * library is not under Application Support at all — it lives inside the app's
24
+ * container, under a differently-named root ("Resolve Project Library", not
25
+ * "Resolve Disk Database"). Confirmed on free 21.0.3.7, macOS.
26
+ *
27
+ * Without this, `project_db` resolved by projectName could never find a
28
+ * free-edition project: it searched only the Studio root and reported "no
29
+ * Project.db found", sending exactly the users the in-app bridge exists to serve
30
+ * hunting for a path they have no reason to know.
31
+ *
32
+ * macOS-only: the sandbox container is an Apple construct. Where the free
33
+ * edition keeps its library on Windows and Linux is NOT verified here, so
34
+ * nothing is guessed for those platforms — pass `projectDb` explicitly there.
35
+ */
36
+ export const LITE_DB_ROOT = path.join(
37
+ os.homedir(),
38
+ 'Library/Containers/com.blackmagic-design.DaVinciResolveLite/Data/Library/Application Support/Resolve Project Library/Resolve Projects',
39
+ );
40
+
41
+ /** Every root searched when resolving a project by name, Studio first. */
42
+ export const DB_ROOTS = [DISK_DB_ROOT, LITE_DB_ROOT];
43
+
21
44
  export function loadSqlite() {
22
45
  try {
23
46
  return require('better-sqlite3');
@@ -26,7 +49,10 @@ export function loadSqlite() {
26
49
  }
27
50
  }
28
51
 
29
- /** Recursively locate <projectName>/Project.db under the Resolve Disk Database. */
52
+ /**
53
+ * Recursively locate <projectName>/Project.db under a project-library root.
54
+ * Called once per root by resolveDbPath; pass `root` to search just one.
55
+ */
30
56
  export function findProjectDb(projectName, root = DISK_DB_ROOT) {
31
57
  const hits = [];
32
58
  const walk = (dir, depth) => {
@@ -51,9 +77,22 @@ export function findProjectDb(projectName, root = DISK_DB_ROOT) {
51
77
  export function resolveDbPath({ projectDb, projectName }) {
52
78
  if (projectDb) return projectDb;
53
79
  if (!projectName) throw new Error('provide projectDb (path) or projectName');
54
- const hits = findProjectDb(projectName);
55
- if (!hits.length) throw new Error(`no Project.db found for project "${projectName}" under the Resolve Disk Database`);
56
- if (hits.length > 1) throw new Error(`multiple Project.db match "${projectName}": pass projectDb explicitly`);
80
+ // Deduplicate: a project present under both roots must not read as ambiguous
81
+ // just because the same file was found twice.
82
+ const hits = [...new Set(DB_ROOTS.flatMap((root) => findProjectDb(projectName, root)))];
83
+ if (!hits.length) {
84
+ throw new Error(
85
+ `no Project.db found for project "${projectName}". Searched the Studio library ` +
86
+ `(${DISK_DB_ROOT}) and the sandboxed free-edition library (${LITE_DB_ROOT}). ` +
87
+ 'If Resolve keeps its projects elsewhere — a relocated library, a network/Postgres ' +
88
+ 'database, or the free edition on Windows/Linux — pass projectDb with the full path.',
89
+ );
90
+ }
91
+ if (hits.length > 1) {
92
+ throw new Error(
93
+ `multiple Project.db match "${projectName}" (${hits.join(', ')}): pass projectDb explicitly`,
94
+ );
95
+ }
57
96
  return hits[0];
58
97
  }
59
98
 
@@ -167,11 +167,17 @@ function unwrapEffectFilters(raw) {
167
167
  * *compressor* (the bundled fzstd is decompress-only); the cost is a larger
168
168
  * blob, which does not matter here.
169
169
  *
170
- * Confirmed live against Resolve Studio 19.1.3 (2026-08-06): a subtitle track patched with
171
- * a 0x80 payload opens without error, and once Resolve next re-serialises that
172
- * track it writes the style back out as 0x81 zstd with the font descriptor and
173
- * position preserved exactly — i.e. Resolve genuinely parses the 0x80 form into
174
- * its in-memory model rather than passing the bytes through untouched.
170
+ * Confirmed live on BOTH editions (2026-08-06) — Studio 19.1.3 and free 21.0.3:
171
+ * a subtitle track patched with a 0x80 payload opens without error, and once
172
+ * Resolve next re-serialises that track it writes the style back out as 0x81
173
+ * zstd with the font descriptor and position preserved exactly. Resolve
174
+ * genuinely parses the 0x80 form into its in-memory model rather than passing
175
+ * the bytes through untouched. Both versions behaved identically, down to
176
+ * dropping the same neighbouring key on rewrite.
177
+ *
178
+ * Also identical across both: a freshly added subtitle track carries NO style
179
+ * blob (a ~111-byte NumLayers stub), so there is nothing to patch until the
180
+ * track has been styled once in the UI.
175
181
  */
176
182
  function wrapEffectFilters(protobuf, version = 2) {
177
183
  const head = Buffer.alloc(9);
package/scripts/doctor.py CHANGED
@@ -338,8 +338,10 @@ def bridge_checks(probe: dict[str, Any]) -> list[dict[str, str]]:
338
338
  elif installed_at:
339
339
  check(
340
340
  results, "INFO", "DAVINCI_RESOLVE_BRIDGE",
341
- "not set — the bridge is installed but will not be used. "
342
- "export DAVINCI_RESOLVE_BRIDGE=1 to enable it.",
341
+ "not set — the bridge is still used automatically if external scripting "
342
+ "is unavailable and the in-Resolve script is running. Set it to 1 to make "
343
+ "the bridge the only transport tried, so its faults surface directly "
344
+ "instead of falling back.",
343
345
  )
344
346
  return results
345
347
 
@@ -168,7 +168,8 @@ def main():
168
168
  print(" operations : %d" % len(resolve_bridge_ops.ResolveOperations.OPERATIONS))
169
169
  print(" generation : %d" % generation)
170
170
  print()
171
- print(" The MCP server connects with DAVINCI_RESOLVE_BRIDGE=1.")
171
+ print(" The MCP server uses this bridge automatically when external")
172
+ print(" scripting is unavailable. DAVINCI_RESOLVE_BRIDGE=1 forces it.")
172
173
  if model["blocking_required"]:
173
174
  print(" This script now HOLDS the listener open and will look busy until")
174
175
  print(" Resolve exits. That is expected — closing it stops the bridge.")
@@ -12023,8 +12023,8 @@ def _not_connected_message(bridge_on: bool) -> str:
12023
12023
  "Workspace > Scripts > resolve_bridge; launching Resolve cannot start it.")
12024
12024
  return ("DaVinci Resolve is not connected. On Studio, enable Preferences > General > "
12025
12025
  "'External scripting using' = Local. On the free edition, install the in-app "
12026
- "bridge, run Workspace > Scripts > resolve_bridge, and set "
12027
- "DAVINCI_RESOLVE_BRIDGE=1.")
12026
+ "bridge and run Workspace > Scripts > resolve_bridge — once running it is used "
12027
+ "automatically, no environment variable needed.")
12028
12028
 
12029
12029
 
12030
12030
  @_serialize_resolve
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.82.1"
88
+ VERSION = "2.86.2"
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()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.82.1"
14
+ VERSION = "2.86.2"
15
15
 
16
16
  import base64
17
17
  import os
@@ -47,6 +47,7 @@ from src.utils.cdl import normalize_cdl_payload
47
47
  from src.utils.mcp_stdio import run_fastmcp_stdio
48
48
  from src.utils.api_truth import lookup_api_truth, VERIFIED_ON as _API_TRUTH_VERIFIED_ON
49
49
  from src.utils import clip_colors as _clip_colors
50
+ from src.utils import resolve_versions as _resolve_versions
50
51
  from src.utils.contracts import validate as _validate_params
51
52
  from src.utils.cut_ir import build_cut_list as _build_cut_list
52
53
  from src.utils.page_lock import (
@@ -180,9 +181,10 @@ mcp = FastMCP(
180
181
  "if one is already running they never launch a second. "
181
182
  "On a connection error, read the error's own remediation field — it names the "
182
183
  "fix that applies. External scripting is Studio-only, but the free edition is "
183
- "reachable via the in-app bridge (Workspace > Scripts > resolve_bridge, with "
184
- "DAVINCI_RESOLVE_BRIDGE=1), so a connection error does NOT mean the free "
185
- "edition is unsupported."
184
+ "reachable via the in-app bridge (Workspace > Scripts > resolve_bridge — it is "
185
+ "used automatically when external scripting is unavailable; "
186
+ "DAVINCI_RESOLVE_BRIDGE=1 only forces it), so a connection error does NOT mean "
187
+ "the free edition is unsupported."
186
188
  ),
187
189
  )
188
190
 
@@ -937,8 +939,8 @@ def get_resolve():
937
939
  "DaVinci Resolve is already running but is not answering the scripting API, "
938
940
  "so it will NOT be launched again. Either enable Preferences > General > "
939
941
  "'External scripting using' = Local (Studio only), or, on the free edition, "
940
- "use the in-app bridge: install it, run Workspace > Scripts > resolve_bridge, "
941
- "and set DAVINCI_RESOLVE_BRIDGE=1."
942
+ "use the in-app bridge: install it and run Workspace > Scripts > resolve_bridge "
943
+ "— once running it is used automatically, no environment variable needed."
942
944
  )
943
945
  return None
944
946
  if already_running is None:
@@ -991,8 +993,10 @@ def _not_connected_error():
991
993
  reason="External scripting is a Studio feature; the free edition refuses it "
992
994
  "regardless of the preference. Resolve was NOT launched again.",
993
995
  remediation="On Studio: Preferences > General > 'External scripting using' = Local. "
994
- "On the free edition: install the in-app bridge, run "
995
- "Workspace > Scripts > resolve_bridge, and set DAVINCI_RESOLVE_BRIDGE=1.",
996
+ "On the free edition: install the in-app bridge and run "
997
+ "Workspace > Scripts > resolve_bridge — once it is running it is used "
998
+ "automatically, no environment variable needed "
999
+ "(DAVINCI_RESOLVE_BRIDGE=1 only forces it and disables this fallback).",
996
1000
  # Which Resolve is refusing matters to the reader: a headless render
997
1001
  # worker and the editor the user is looking at warrant different
998
1002
  # responses, and the in-app bridge is not an option for the former.
@@ -3542,7 +3546,9 @@ def _coerce_item_list(value):
3542
3546
 
3543
3547
  def _get_selected_timeline_items(tl):
3544
3548
  warnings = []
3545
- for method_name in ("GetSelectedTimelineItems", "GetSelectedItems", "GetSelectedClips"):
3549
+ # GetSelectedClips is the documented name since Resolve 21.0.4; the other two
3550
+ # are legacy speculative probes kept for older/renamed builds.
3551
+ for method_name in ("GetSelectedClips", "GetSelectedTimelineItems", "GetSelectedItems"):
3546
3552
  method = getattr(tl, method_name, None)
3547
3553
  if not callable(method):
3548
3554
  continue
@@ -7732,6 +7738,7 @@ _MEDIA_POOL_ITEM_METHODS = [
7732
7738
  "GetMarkInOut",
7733
7739
  "SetMarkInOut",
7734
7740
  "ClearMarkInOut",
7741
+ "GetTimeline",
7735
7742
  ]
7736
7743
 
7737
7744
  _MEDIA_POOL_METHODS = [
@@ -13547,8 +13554,18 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
13547
13554
  ignore_mcp_update() -> {success, version, update, decision}
13548
13555
  snooze_mcp_update(hours?) -> {success, version, update, decision}
13549
13556
  clear_mcp_update_preferences() -> {success, version, update, decision}
13550
- api_truth(query?) -> {verified_on, count, facts} — look up behaviorally-verified
13551
- facts about quirky/unreliable Resolve API behavior (no connection needed).
13557
+ api_truth(query?, resolve_version?) -> {verified_on, count, facts, live_version?}
13558
+ — look up behaviorally-verified facts about quirky/unreliable Resolve API
13559
+ behavior (no connection needed; never connects). When a build is already
13560
+ connected or resolve_version is passed, each fact carries a
13561
+ version_context saying whether it was measured on an older, newer or
13562
+ identical build — the API changes per PATCH release, so a fact from
13563
+ another build is a prior, not a finding.
13564
+ check_version_support(symbol?, resolve_version?) -> {live_version, support|unavailable_on_this_build}
13565
+ — does THIS build have that method? Answers "unknown" unless a gate is
13566
+ recorded, because most of the API has never been version-bisected and a
13567
+ false "available" is how an agent insists a missing method exists.
13568
+ Omit symbol for every recorded gate this build does not clear.
13552
13569
  verification_stats() -> {stats} — readback-verification tally
13553
13570
  (verified/contradicted/unverified) since server start (no connection needed).
13554
13571
  job_status(job_id) -> {id, label, status, result?, error?, started_at, ended_at}
@@ -13576,10 +13593,72 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
13576
13593
  """
13577
13594
  p = _params(params)
13578
13595
 
13579
- # api_truth is a static knowledge lookup — no Resolve connection needed.
13596
+ # api_truth is a static knowledge lookup — no Resolve connection needed, and
13597
+ # that property is worth keeping: it is the one call that still answers when
13598
+ # Resolve is down or unreachable. So the live build is used only if one is
13599
+ # ALREADY connected, or if the caller names it. Never connect for this.
13580
13600
  if action == "api_truth":
13581
13601
  facts = lookup_api_truth(p.get("query"))
13582
- return {"verified_on": _API_TRUTH_VERIFIED_ON, "count": len(facts), "facts": facts}
13602
+ live_version = p.get("resolve_version")
13603
+ if not live_version and resolve is not None:
13604
+ try:
13605
+ live_version = resolve.GetVersionString()
13606
+ except Exception:
13607
+ live_version = None
13608
+ result = {
13609
+ "verified_on": _API_TRUTH_VERIFIED_ON,
13610
+ "count": len(facts),
13611
+ "facts": _resolve_versions.annotate_facts(
13612
+ facts, live_version, ledger_verified_on=_API_TRUTH_VERIFIED_ON
13613
+ ),
13614
+ }
13615
+ if live_version:
13616
+ result["live_version"] = str(live_version)
13617
+ unavailable = _resolve_versions.gates_unavailable_on(live_version)
13618
+ if unavailable:
13619
+ result["unavailable_on_this_build"] = unavailable
13620
+ else:
13621
+ result["version_context"] = (
13622
+ "No live build known, so facts are unannotated. The scripting API "
13623
+ "changes per PATCH release, so a fact measured on another build is a "
13624
+ "prior rather than a finding. Pass resolve_version, or call "
13625
+ "get_version first."
13626
+ )
13627
+ return result
13628
+ if action == "check_version_support":
13629
+ # "Does THIS build have it?" asked directly, instead of inferred from
13630
+ # prose. Answers `unknown` unless a gate is recorded — see
13631
+ # utils/resolve_versions.py for why that default is the load-bearing one.
13632
+ live_version = p.get("resolve_version")
13633
+ if not live_version and resolve is not None:
13634
+ try:
13635
+ live_version = resolve.GetVersionString()
13636
+ except Exception:
13637
+ live_version = None
13638
+ if not live_version:
13639
+ return _err(
13640
+ "No Resolve version available",
13641
+ code="VERSION_UNKNOWN",
13642
+ category="invalid_input",
13643
+ reason="Nothing is connected and no resolve_version was passed.",
13644
+ remediation="Call get_version first, or pass resolve_version='21.0.4.5'.",
13645
+ )
13646
+ symbol = p.get("symbol")
13647
+ if symbol:
13648
+ return {
13649
+ "live_version": str(live_version),
13650
+ "support": _resolve_versions.availability(symbol, live_version),
13651
+ }
13652
+ return {
13653
+ "live_version": str(live_version),
13654
+ "unavailable_on_this_build": _resolve_versions.gates_unavailable_on(live_version),
13655
+ "known_gates": len(_resolve_versions.VERSION_GATES),
13656
+ "note": (
13657
+ "Only surfaces with recorded evidence appear here. Most of the "
13658
+ "scripting API has never been version-bisected, so an absence from "
13659
+ "this list is not a promise that a method exists."
13660
+ ),
13661
+ }
13583
13662
  if action == "verification_stats":
13584
13663
  # Process-level readback-verification tally — no connection needed.
13585
13664
  stats = _verification_stats()
@@ -13737,7 +13816,7 @@ def resolve_control(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
13737
13816
  return missing
13738
13817
  r.DisableBackgroundTasksForCurrentResolveSession()
13739
13818
  return _ok()
13740
- return _unknown(action, ["launch","runtime_mode","get_version","api_truth","verification_stats","job_status","list_jobs","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
13819
+ return _unknown(action, ["launch","runtime_mode","get_version","api_truth","check_version_support","verification_stats","job_status","list_jobs","mcp_update_status","set_mcp_update_policy","ignore_mcp_update","snooze_mcp_update","clear_mcp_update_preferences","get_page","open_page","get_keyframe_mode","set_keyframe_mode","quit","get_fairlight_presets","set_high_priority","disable_background_tasks_for_current_session","open_control_panel","control_panel_status","close_control_panel","save_state","restore_state"])
13741
13820
 
13742
13821
 
13743
13822
  # ─── V2 C4: Per-field corrections with provenance + changelog ────────────────
@@ -16028,6 +16107,9 @@ _RENDER_SETTING_KEYS = [
16028
16107
  "ReplaceExistingFilesInPlace",
16029
16108
  "ExportSubtitle",
16030
16109
  "SubtitleFormat",
16110
+ "UseFullExtents",
16111
+ "AddFrameHandles",
16112
+ "DataBurnIn",
16031
16113
  ]
16032
16114
 
16033
16115
  _RENDER_KERNEL_ACTIONS = [
@@ -16247,6 +16329,24 @@ def _render_preset_pin(proj, preset_name: str):
16247
16329
  return {"preset": preset_name, "loaded": True}, None
16248
16330
 
16249
16331
 
16332
+ def _render_settings_warnings(settings: Dict[str, Any]):
16333
+ """Inter-key combinations Resolve accepts and then silently ignores.
16334
+
16335
+ SetRenderSettings returns True for these, so nothing downstream would ever
16336
+ tell the caller the key did nothing — the same silent-no-op class as the
16337
+ unknown-key drop that `_filter_to_keys` covers on the set path.
16338
+ """
16339
+ warnings = []
16340
+ if not isinstance(settings, dict):
16341
+ return warnings
16342
+ if settings.get("UseFullExtents") is True and isinstance(settings.get("AddFrameHandles"), int) and settings["AddFrameHandles"] > 0:
16343
+ warnings.append(
16344
+ "AddFrameHandles is ignored while UseFullExtents is true: Resolve renders the "
16345
+ "clip's full extents and the handle count silently does nothing. Drop one of the two."
16346
+ )
16347
+ return warnings
16348
+
16349
+
16250
16350
  def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_target: bool = False):
16251
16351
  if not isinstance(settings, dict) or not settings:
16252
16352
  return None, _err("settings must be a non-empty object")
@@ -16262,15 +16362,20 @@ def _validate_render_settings_payload(settings: Dict[str, Any], *, require_temp_
16262
16362
  errors.append("TargetDir must be under the system temp directory for this safe operation")
16263
16363
  elif require_temp_target:
16264
16364
  errors.append("TargetDir is required for this safe operation")
16265
- for key in ("FormatWidth", "FormatHeight", "MarkIn", "MarkOut", "AudioBitDepth", "AudioSampleRate"):
16365
+ for key in ("FormatWidth", "FormatHeight", "MarkIn", "MarkOut", "AudioBitDepth", "AudioSampleRate", "AddFrameHandles"):
16266
16366
  if key in settings and not isinstance(settings[key], int):
16267
16367
  errors.append(f"{key} must be an integer")
16268
- for key in ("SelectAllFrames", "ExportVideo", "ExportAudio", "ExportAlpha", "MultiPassEncode", "NetworkOptimization", "ReplaceExistingFilesInPlace", "ExportSubtitle"):
16368
+ for key in ("SelectAllFrames", "ExportVideo", "ExportAudio", "ExportAlpha", "MultiPassEncode", "NetworkOptimization", "ReplaceExistingFilesInPlace", "ExportSubtitle", "UseFullExtents"):
16269
16369
  if key in settings and not isinstance(settings[key], bool):
16270
16370
  errors.append(f"{key} must be a boolean")
16371
+ if "AddFrameHandles" in settings and isinstance(settings["AddFrameHandles"], int) and settings["AddFrameHandles"] < 0:
16372
+ errors.append("AddFrameHandles must be >= 0")
16373
+ if "DataBurnIn" in settings and not isinstance(settings["DataBurnIn"], str):
16374
+ errors.append("DataBurnIn must be a string (a burn-in preset name, 'Same as project', or 'None')")
16271
16375
  if "MarkIn" in settings and "MarkOut" in settings and settings["MarkOut"] < settings["MarkIn"]:
16272
16376
  errors.append("MarkOut must be greater than or equal to MarkIn")
16273
- result = {"valid": not errors, "unknown_keys": unknown, "errors": errors, "settings": dict(settings)}
16377
+ result = {"valid": not errors, "unknown_keys": unknown, "errors": errors,
16378
+ "warnings": _render_settings_warnings(settings), "settings": dict(settings)}
16274
16379
  return result, None
16275
16380
 
16276
16381
 
@@ -16721,7 +16826,12 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
16721
16826
  set_mode(mode) -> {success}
16722
16827
  get_resolutions(format, codec) -> {resolutions}
16723
16828
  get_settings() -> {settings} (alias for set_render_settings with get)
16724
- set_settings(settings) -> {success}
16829
+ set_settings(settings) -> {success, ignored_settings?, warnings?}
16830
+ Resolve 21.0.4+ settings keys: UseFullExtents (bool),
16831
+ AddFrameHandles (int >= 0), DataBurnIn (burn-in preset name,
16832
+ "Same as project", or "None"). AddFrameHandles is ignored while
16833
+ UseFullExtents is true — the call still succeeds, so that pairing
16834
+ comes back in warnings rather than silently doing nothing.
16725
16835
  list_presets() -> {presets}
16726
16836
  load_preset(name) -> {success}
16727
16837
  save_preset(name) -> {success}
@@ -16845,6 +16955,11 @@ def render(action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, An
16845
16955
  result = {"success": bool(proj.SetRenderSettings(settings))}
16846
16956
  if ignored_settings:
16847
16957
  result["ignored_settings"] = ignored_settings
16958
+ # Accepted-then-ignored key combinations (21.0.4 AddFrameHandles under
16959
+ # UseFullExtents) read as a clean success without this.
16960
+ warnings = _render_settings_warnings(settings)
16961
+ if warnings:
16962
+ result["warnings"] = warnings
16848
16963
  return result
16849
16964
  elif action == "list_presets":
16850
16965
  return {"presets": proj.GetRenderPresetList()}
@@ -17672,6 +17787,9 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
17672
17787
  get_mark_in_out(clip_id) -> {mark}
17673
17788
  set_mark_in_out(clip_id, mark_in, mark_out, type?) -> {success}
17674
17789
  clear_mark_in_out(clip_id, type?) -> {success}
17790
+ get_timeline(clip_id) -> {is_timeline, timeline: {name, unique_id?, start_frame?, end_frame?}}
17791
+ — Resolve 21.0.4+. Resolves a Media Pool timeline entry to its timeline
17792
+ object summary; is_timeline=false for ordinary clips.
17675
17793
  open_in_viewer(clip_id, page?, mark_in_seconds?, mark_out_seconds?, clear_marks?) -> {success, clip_id, clip_name, folder_name, page, mark_set}
17676
17794
  — Switches to Media page (default) and selects the clip in the bin.
17677
17795
  Resolve auto-loads the selected clip into the source viewer on Media page.
@@ -18000,7 +18118,28 @@ def media_pool_item(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
18000
18118
  return {"success": bool(clip.SetMarkInOut(clean["mark_in"], clean["mark_out"], p.get("type", "all")))}
18001
18119
  elif action == "clear_mark_in_out":
18002
18120
  return {"success": bool(clip.ClearMarkInOut(p.get("type", "all")))}
18003
- return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","extract_frames","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer"])
18121
+ elif action == "get_timeline":
18122
+ missing = _requires_method(clip, "GetTimeline", "21.0.4")
18123
+ if missing:
18124
+ return missing
18125
+ try:
18126
+ tl_obj = clip.GetTimeline()
18127
+ except Exception as exc:
18128
+ return _err(f"GetTimeline failed: {exc}")
18129
+ if not tl_obj:
18130
+ return {"is_timeline": False, "timeline": None,
18131
+ "note": "This media pool item is not a timeline entry."}
18132
+ summary = {}
18133
+ for getter, key in (("GetName", "name"), ("GetUniqueId", "unique_id"),
18134
+ ("GetStartFrame", "start_frame"), ("GetEndFrame", "end_frame")):
18135
+ if _has_method(tl_obj, getter):
18136
+ try:
18137
+ summary[key] = getattr(tl_obj, getter)()
18138
+ except Exception:
18139
+ pass
18140
+ return {"is_timeline": True, "timeline": summary,
18141
+ "note": "Address this timeline by name via the timeline tool."}
18142
+ return _unknown(action, ["get_name","get_metadata","set_metadata","get_third_party_metadata","set_third_party_metadata","get_media_id","get_clip_property","set_clip_property","get_clip_color","set_clip_color","clear_clip_color","link_proxy","unlink_proxy","replace_clip","set_name","link_full_resolution_media","monitor_growing_file","replace_clip_preserve_sub_clip","get_unique_id","transcribe_audio","clear_transcription","get_transcription","extract_frames","perform_audio_classification","clear_audio_classification","analyze_for_intellisearch","analyze_for_slate","remove_motion_blur","get_audio_mapping","get_mark_in_out","set_mark_in_out","clear_mark_in_out","open_in_viewer","get_timeline"])
18004
18143
 
18005
18144
 
18006
18145
  # ═══════════════════════════════════════════════════════════════════════════════