davinci-resolve-mcp 2.28.1 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,81 @@
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.30.0
6
+
7
+ Adds the **Resolve 21 AI-ops ledger** — usage/time/file accounting for the
8
+ Resolve-local AI operations added in v2.29.0 (audio classification, IntelliSearch,
9
+ slate, motion-deblur, speech generation). These run on Resolve's own GPU/AI engine
10
+ and do **not** consume the Claude-side analysis token budget, so they get their
11
+ own ledger instead of being metered by the analysis-caps layer.
12
+
13
+ **What's tracked.** Every run of the five 21.0 ops records: op name, op class
14
+ (`analysis` vs `render`), clip id, success/failure, wall-clock time, and — for the
15
+ two media-creating ops (`remove_motion_blur`, `generate_speech`) — the output
16
+ file path and byte size. The reliable signal is invocation counts + the
17
+ file/disk accounting for the creators; durations for the bool-returning analysis
18
+ ops reflect the script-call time (some queue work inside Resolve).
19
+
20
+ - **New table** `resolve_ai_op_usage` (timeline_brain DB schema v7).
21
+ - **New module** `src/utils/resolve_ai_ledger.py` — `timed()` context manager +
22
+ `record_op` / `get_usage` / `get_summary`. All writes are best-effort and never
23
+ block or mask the underlying Resolve op.
24
+ - **Instrumentation** wraps the consolidated `folder` / `media_pool_item`
25
+ `perform_audio_classification` / `clear_audio_classification` /
26
+ `analyze_for_intellisearch` / `analyze_for_slate` / `remove_motion_blur`
27
+ handlers and `project_settings.generate_speech`.
28
+ - **New MCP action** `media_analysis(action="get_resolve_ai_usage", session_only?, op?, limit?)`
29
+ returns the per-op summary + recent runs.
30
+ - **Control panel**: a read-only "Resolve 21 AI ops" card (`/api/resolve_ai_usage`)
31
+ shows runs, success/fail, total time, and files/bytes created.
32
+
33
+ Phase 1 of a staged build (ledger → interactive console → governance). Granular
34
+ `--full` server instrumentation is deferred — the ledger covers the consolidated
35
+ server, which is the default surface. Validated live against Resolve Studio 21.0.0.47.
36
+
37
+ ## What's New in v2.29.0
38
+
39
+ Adds the **DaVinci Resolve 21.0** scripting-API additions. Every new method is
40
+ runtime-detected (`_requires_method`/capability flags), so the tools stay inert
41
+ on older Resolve builds and activate automatically on Resolve 21+.
42
+
43
+ **New AI analysis actions** on the `folder` and `media_pool_item` compound tools
44
+ (and mirrored as granular `--full` tools):
45
+
46
+ - `perform_audio_classification` / `clear_audio_classification` — classify clip
47
+ audio into categories and subcategories.
48
+ - `analyze_for_intellisearch(identify_faces?, is_better_mode?)` — IntelliSearch
49
+ analysis with optional face identification. Requires the *AI IntelliSearch* Extra.
50
+ - `analyze_for_slate(marker_color?)` — slate/clapboard detection that drops a
51
+ marker of the chosen color (validated against the 16 Resolve marker colors).
52
+ Requires the *AI Slate ID* Extra.
53
+ - `remove_motion_blur(deblur_option?)` — renders motion-deblurred copies. This
54
+ **creates new media files** (source media is never modified) and is therefore
55
+ **confirm-token gated**: the first call returns a preview + token, the second
56
+ call (with the token) runs.
57
+
58
+ **Speaker-detection transcription.** `transcribe_audio` now accepts an optional
59
+ `use_speaker_detection` boolean (Resolve 21+); omit it to use the project's
60
+ Speech Recognition setting.
61
+
62
+ **Speech generation.** `project_settings(action="generate_speech", ...)` wraps
63
+ `Project.GenerateSpeech` (AI text-to-speech). It creates a new audio item and
64
+ optionally places it on the timeline, so it is also confirm-token gated.
65
+ Requires the *AI Speech Generator* Extra. Granular `--full` tool: `generate_speech`.
66
+
67
+ **Session control.** `resolve_control(action="disable_background_tasks_for_current_session")`
68
+ wraps `Resolve.DisableBackgroundTasksForCurrentResolveSession()` to quiet
69
+ background work during heavy scripted runs.
70
+
71
+ **Capability surface.** The `media_analysis` transcription-capability report and
72
+ the control panel's boot payload (`resolve.ai_features`) now list which 21.0 AI
73
+ methods are available and which Extras each gated method needs.
74
+
75
+ Notes: these are Resolve-local GPU/AI operations and do not consume the
76
+ Claude-side analysis token budget, so they are not metered by the analysis-caps
77
+ layer; the derivative-creating ones are protected by the confirm-token gate
78
+ instead. The granular `--full` server grew from 329 to 341 tools.
79
+
5
80
  ## What's New in v2.28.1
6
81
 
7
82
  Bug-fix release.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.28.1-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.30.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
- [![Tools](https://img.shields.io/badge/MCP%20Tools-32%20(329%20full)-blue.svg)](#server-modes)
6
+ [![Tools](https://img.shields.io/badge/MCP%20Tools-32%20(341%20full)-blue.svg)](#server-modes)
7
7
  [![Tested](https://img.shields.io/badge/Live%20Tested-98.5%25-green.svg)](docs/reference/api-coverage.md#test-results)
8
8
  [![DaVinci Resolve](https://img.shields.io/badge/DaVinci%20Resolve-18.5+-darkred.svg)](https://www.blackmagicdesign.com/products/davinciresolve)
9
9
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
@@ -50,7 +50,7 @@ The command starts a localhost server and opens the control panel in your browse
50
50
  | Mode | Entry point | Tools | Best for |
51
51
  |------|-------------|-------|----------|
52
52
  | Compound | `src/server.py` | 32 | Default mode for most assistants. Related Resolve operations are grouped behind action parameters to keep context usage low. |
53
- | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 329 | Power users who want one MCP tool per Resolve API method. |
53
+ | Full / granular | `src/server.py --full` or `src/resolve_mcp_server.py` | 341 | Power users who want one MCP tool per Resolve API method. |
54
54
 
55
55
  The compound server is recommended unless you specifically need the granular one-tool-per-method surface.
56
56
 
@@ -99,7 +99,7 @@ The default server is a local stdio process launched by your MCP client; it does
99
99
 
100
100
  | Metric | Value |
101
101
  |--------|-------|
102
- | MCP Tools | **32** compound / **329** granular |
102
+ | MCP Tools | **32** compound / **341** granular |
103
103
  | Kernel Actions | **136** guarded workflow actions across 9 compound tools |
104
104
  | API Methods Covered | **336/336** (100%) |
105
105
  | Methods Live Tested | **331/336** (98.5%) |
@@ -136,7 +136,7 @@ Extension authoring references live in [docs/authoring](docs/authoring/). Resolv
136
136
  - Python 3.10+ (3.10-3.12 is the lowest-risk range). Python 3.13/3.14 also work on recent Resolve builds (verified on Studio 20.3.2); older builds may fail to connect on 3.13+, in which case use 3.10-3.12.
137
137
  - Resolve external scripting set to **Local**.
138
138
 
139
- 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 beta APIs are intentionally deferred until stable.
139
+ 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 scripting additions (audio classification, speaker-detection transcription, IntelliSearch, slate analysis, motion-deblur, speech generation, session background-task control) are exposed behind runtime capability detection, so they stay inert on older builds and activate automatically on Resolve 21+.
140
140
 
141
141
  ## Development
142
142
 
package/docs/SKILL.md CHANGED
@@ -101,7 +101,7 @@ before mutating Resolve state.
101
101
  | Mode | Entry point | Tool count | Use when |
102
102
  |---|---|---|---|
103
103
  | Compound (default) | `src/server.py` | 32 tools | Most workflows — keeps context lean |
104
- | Granular (full) | `src/server.py --full` | 329 tools | Power users needing one tool per API method |
104
+ | Granular (full) | `src/server.py --full` | 341 tools | Power users needing one tool per API method |
105
105
 
106
106
  This skill document covers the **compound server** (the default). Each compound
107
107
  tool accepts an `action` string and an optional `params` object.
@@ -442,7 +442,10 @@ Note: `folder path` arguments use slash notation like `"Master/SubFolder"`.
442
442
  **`folder`** — Operations on a specific Media Pool folder.
443
443
 
444
444
  Key actions: `get_clips(path?)`, `get_subfolders(path?)`, `export(path?, export_path)`,
445
- `transcribe_audio(path?)`, `clear_transcription(path?)`
445
+ `transcribe_audio(path?, use_speaker_detection?)`, `clear_transcription(path?)`,
446
+ `perform_audio_classification(path?)`, `analyze_for_intellisearch(path?, identify_faces?, is_better_mode?)`,
447
+ `analyze_for_slate(path?, marker_color?)`, `remove_motion_blur(path?, deblur_option?)` (Resolve 21+;
448
+ the last three need AI Extras, and `remove_motion_blur` is confirm-token gated)
446
449
 
447
450
  **`media_pool_item`** — Read/write clip metadata and properties. All actions
448
451
  require a `clip_id` (the UUID returned by `GetUniqueId()`).
@@ -452,7 +455,10 @@ Key actions: `get_name`, `get_metadata(key?)`, `set_metadata(key, value)`,
452
455
  `set_clip_color(color)`, `link_proxy(proxy_path)`, `replace_clip(path)`,
453
456
  `set_name(name)`, `link_full_resolution_media(path)`,
454
457
  `replace_clip_preserve_sub_clip(path)`, `monitor_growing_file`,
455
- `transcribe_audio`, `get_audio_mapping`, `get_mark_in_out`, `set_mark_in_out`
458
+ `transcribe_audio(use_speaker_detection?)`, `perform_audio_classification`,
459
+ `analyze_for_intellisearch(identify_faces?, is_better_mode?)`, `analyze_for_slate(marker_color?)`,
460
+ `remove_motion_blur(deblur_option?)` (Resolve 21+; AI Extras / confirm-token gated as noted above),
461
+ `get_audio_mapping`, `get_mark_in_out`, `set_mark_in_out`
456
462
 
457
463
  **`media_pool_item_markers`** — Markers and flags on clips in the Media Pool.
458
464
  All actions require a `clip_id`.
@@ -662,6 +668,15 @@ effective values + a usage rollup (clip / job / day) with percent-consumed.
662
668
  counts for one scope. Usage is tracked in
663
669
  `<project>/_soul/timeline_brain.sqlite` (`analysis_token_usage` table).
664
670
 
671
+ Resolve 21's local AI ops (audio classification, IntelliSearch, slate,
672
+ motion-deblur, speech generation) run on Resolve's own GPU/AI engine and do NOT
673
+ spend the Claude analysis token budget — they are tracked separately in the
674
+ `resolve_ai_op_usage` table. Inspect with
675
+ `media_analysis(action="get_resolve_ai_usage", session_only?, op?, limit?)` →
676
+ `{summary, recent}` (invocation counts, wall-clock, and files/bytes created by
677
+ `remove_motion_blur` / `generate_speech`). The control panel shows the same as a
678
+ read-only "Resolve 21 AI ops" card.
679
+
665
680
  The caps layer:
666
681
  - Slices `frame_paths` to `frames_per_clip` before the host LLM sees them.
667
682
  - Downscales each sampled frame in place to `max_frame_dim_pixels` (Pillow;
@@ -64,7 +64,7 @@ davinci-resolve-mcp/
64
64
  ├── install.py # Universal installer (macOS/Windows/Linux)
65
65
  ├── src/
66
66
  │ ├── server.py # Compound MCP server — 32 tools (default)
67
- │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 329 tools
67
+ │ ├── resolve_mcp_server.py # Thin full-server entrypoint — 341 tools
68
68
  │ ├── granular/ # Modular full-server implementation
69
69
  │ └── utils/ # Platform detection, Resolve connection helpers
70
70
  ├── tests/ # 5-phase live API test suite + Resolve 20 delta (331/331 pass)
package/docs/install.md CHANGED
@@ -87,7 +87,7 @@ The MCP server comes in two modes:
87
87
  | Mode | File | Tools | Best For |
88
88
  |------|------|-------|----------|
89
89
  | **Compound** (default) | `src/server.py` | 32 | Most users — fast, clean, low context usage |
90
- | **Full** | `src/resolve_mcp_server.py` | 329 | Power users who want one tool per API method |
90
+ | **Full** | `src/resolve_mcp_server.py` | 341 | Power users who want one tool per API method |
91
91
 
92
92
  The compound server's `timeline_item` tool includes dedicated actions for common workflows:
93
93
 
@@ -102,7 +102,7 @@ The compound server's `timeline_item` tool includes dedicated actions for common
102
102
 
103
103
  The installer uses the compound server by default. To use the full server:
104
104
  ```bash
105
- python src/server.py --full # Launch full 329-tool server
105
+ python src/server.py --full # Launch full 341-tool server
106
106
  # Or point your MCP config directly at src/resolve_mcp_server.py
107
107
  ```
108
108
 
@@ -6,7 +6,7 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
6
6
 
7
7
  | Metric | Value |
8
8
  |--------|-------|
9
- | MCP Tools | **33** compound (default) / **329** granular |
9
+ | MCP Tools | **33** compound (default) / **341** granular |
10
10
  | Kernel Actions | **136** guarded MCP workflow actions across 9 compound tools |
11
11
  | API Methods Covered | **336/336** (100%) |
12
12
  | Methods Live Tested | **331/336** (98.5%) |
@@ -17,7 +17,7 @@ Complete Resolve scripting API coverage, live-test status, and method-by-method
17
17
 
18
18
  ## API Coverage
19
19
 
20
- Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **33 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **329 individual tools** for power users. Both modes cover all 13 API object classes. MCP-level kernel actions are tracked separately in [Kernel Action Coverage](../kernels/README.md).
20
+ Every non-deprecated method in the DaVinci Resolve Scripting API is covered. The default compound server exposes **33 tools** that group related operations by action parameter, keeping LLM context windows lean. The full granular server provides **341 individual tools** for power users. Both modes cover all 13 API object classes. MCP-level kernel actions are tracked separately in [Kernel Action Coverage](../kernels/README.md).
21
21
 
22
22
  The 33rd compound tool is `timeline_versioning` (C6) — an MCP-level workflow
23
23
  tool, not a wrapper around a Resolve API method. It surfaces the
@@ -1,4 +1,4 @@
1
- Last Updated: 7 Oct 2025
1
+ Last Updated: 5 May 2026
2
2
  -------------------------
3
3
  In this package, you will find a brief introduction to the Scripting API for DaVinci Resolve Studio. Apart from this README.txt file, this package contains folders containing the basic import
4
4
  modules for scripting access (DaVinciResolve.py) and some representative examples.
@@ -105,6 +105,7 @@ Resolve
105
105
  GetKeyframeMode() --> keyframeMode # Returns the currently set keyframe mode (int). Refer to section 'Keyframe Mode information' below for details.
106
106
  SetKeyframeMode(keyframeMode) --> Bool # Returns True when 'keyframeMode'(enum) is successfully set. Refer to section 'Keyframe Mode information' below for details.
107
107
  GetFairlightPresets() --> [presetNames...]. # Returns a list of Fairlight presets by name
108
+ DisableBackgroundTasksForCurrentResolveSession()--> None # Disables all background tasks for current Resolve session.
108
109
 
109
110
  ProjectManager
110
111
  ArchiveProject(projectName,
@@ -199,6 +200,7 @@ Project
199
200
  AddColorGroup(groupName) --> ColorGroup # Creates a new ColorGroup. groupName must be a unique string.
200
201
  DeleteColorGroup(colorGroup) --> Bool # Deletes the given color group and sets clips to ungrouped.
201
202
  ApplyFairlightPresetToCurrentTimeline(name) --> Bool # Apply Fairlight Preset of given name to the current timeline, returns True if successful, False otherwise.
203
+ GenerateSpeech({speechGenerationSettings}, timecode) --> MediaPoolItem # Generates an audio MediaPoolItem based on the given speechGenerationSettings and adds it to the timeline at the stated timecode if "AddToTimeline" is True. Returns the newly generated MediaPoolItem.
202
204
 
203
205
  MediaStorage
204
206
  GetMountedVolumeList() --> [paths...] # Returns list of folder paths corresponding to mounted volumes displayed in Resolve’s Media Storage.
@@ -263,8 +265,13 @@ Folder
263
265
  GetIsFolderStale() --> bool # Returns true if folder is stale in collaboration mode, false otherwise
264
266
  GetUniqueId() --> string # Returns a unique ID for the media pool folder
265
267
  Export(filePath) --> bool # Returns true if export of DRB folder to filePath is successful, false otherwise
266
- TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise
268
+ TranscribeAudio(useSpeakerDetection=None) --> Bool # Transcribes audio of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise
267
269
  ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItems within the folder and nested folders. Returns True if successful; False otherwise.
270
+ PerformAudioClassification() --> Bool # Analyzes and classifies the audio of the MediaPoolItems within the folder and nested folders into categories and subcategories.
271
+ ClearAudioClassification() --> Bool # Clears audio classification of the MediaPoolItems within the folder and nested folders.
272
+ RemoveMotionBlur({deblurOption}) --> [[MediaPoolItem, MediaPoolItem]...] # Applies motion deblur on MediaPoolItems in the folder. Returns a list of pairs mapping original to newly created MediaPoolItems.
273
+ AnalyzeForIntellisearch(identifyFaces, isBetterMode) --> Bool # Performs Intellisearch analysis on all MediaPoolItems in the folder. identifyFaces specifies whether to identify faces; isBetterMode specifies whether to use Better mode. Returns True if required packages are installed and analysis is successful.
274
+ AnalyzeForSlate(markerColor) --> Bool # Performs Slate analysis on all MediaPoolItems in the folder using the current settings and specified markerColor. Returns True if required packages are installed and analysis is successful.
268
275
 
269
276
  MediaPoolItem
270
277
  GetName() --> string # Returns the clip name.
@@ -304,8 +311,13 @@ MediaPoolItem
304
311
  ReplaceClip(filePath) --> Bool # Replaces the underlying asset and metadata of MediaPoolItem with the specified absolute clip path.
305
312
  ReplaceClipPreserveSubClip(filePath) --> Bool # Replaces the underlying asset and metadata of a video or audio clip with the specified absolute clip path, preserving original sub clip extents.
306
313
  GetUniqueId() --> string # Returns a unique ID for the media pool item
307
- TranscribeAudio() --> Bool # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise
314
+ TranscribeAudio(useSpeakerDetection=None) --> Bool # Transcribes audio of the MediaPoolItem. Returns True if successful; False otherwise
308
315
  ClearTranscription() --> Bool # Clears audio transcription of the MediaPoolItem. Returns True if successful; False otherwise.
316
+ PerformAudioClassification() --> Bool # Analyzes and classifies the audio of a MediaPoolItem into categories and subcategories.
317
+ ClearAudioClassification() --> Bool # Clears audio classification of the MediaPoolItem.
318
+ RemoveMotionBlur({deblurOption}) --> MediaPoolItem # Applies motion deblur on the MediaPoolItem. Returns the newly created MediaPoolItem.
319
+ AnalyzeForIntellisearch(identifyFaces, isBetterMode) --> Bool # Performs Intellisearch analysis on the MediaPoolItem. identifyFaces specifies whether to identify faces; isBetterMode specifies whether to use Better mode. Returns True if required packages are installed and analysis is successful.
320
+ AnalyzeForSlate(markerColor) --> Bool # Performs Slate analysis on the MediaPoolItem using the current settings and specified markerColor. Returns True if required packages are installed and analysis is successful.
309
321
  GetAudioMapping() --> json formatted string # Returns a string with MediaPoolItem's audio mapping information. Check 'Audio Mapping' section below for more information.
310
322
  GetMarkInOut() --> {mark} # Returns dict of in/out marks set (keys omitted if not set), example:
311
323
  # {'video': {'in': 0, 'out': 134}, 'audio': {'in': 0, 'out': 134}}
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.28.1"
38
+ VERSION = "2.30.0"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (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.28.1",
3
+ "version": "2.30.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -4370,6 +4370,24 @@ HTML = r"""<!doctype html>
4370
4370
  </div>
4371
4371
  </div>
4372
4372
 
4373
+ <div class="caps-section">
4374
+ <div class="caps-section-head">
4375
+ <div class="caps-section-title">Resolve 21 AI ops</div>
4376
+ <div class="caps-section-hint">Local Resolve AI operations (audio classification, IntelliSearch, slate, motion-deblur, speech). These run on Resolve's GPU/AI engine and do <strong>not</strong> consume the Claude analysis token budget above — tracked here for invocations, time, and files created.</div>
4377
+ </div>
4378
+ <div id="resolveAiOpsBlock" class="caps-usage-block">
4379
+ <div id="resolveAiOpsSummary" class="caps-section-hint">loading…</div>
4380
+ <table id="resolveAiOpsTable" class="resolve-ai-ops-table" style="display:none; width:100%; border-collapse:collapse; margin-top:8px; font-size:12px;">
4381
+ <thead><tr style="text-align:left; opacity:0.7;">
4382
+ <th style="padding:4px 6px;">Op</th><th style="padding:4px 6px;">Runs</th>
4383
+ <th style="padding:4px 6px;">OK</th><th style="padding:4px 6px;">Time</th>
4384
+ <th style="padding:4px 6px;">Files</th><th style="padding:4px 6px;">Created</th>
4385
+ </tr></thead>
4386
+ <tbody id="resolveAiOpsRows"></tbody>
4387
+ </table>
4388
+ </div>
4389
+ </div>
4390
+
4373
4391
  <div class="caps-section">
4374
4392
  <div class="caps-section-head">
4375
4393
  <div class="caps-section-title">Safety</div>
@@ -7040,6 +7058,59 @@ HTML = r"""<!doctype html>
7040
7058
  </svg>`;
7041
7059
  }
7042
7060
 
7061
+ // ─── Resolve 21 AI ops ledger (read-only) ───────────────────────
7062
+ function fmtMs(ms) {
7063
+ ms = ms || 0;
7064
+ if (ms < 1000) return ms + 'ms';
7065
+ const s = ms / 1000;
7066
+ return s < 60 ? s.toFixed(1) + 's' : (s / 60).toFixed(1) + 'm';
7067
+ }
7068
+ function fmtBytes(n) {
7069
+ n = n || 0;
7070
+ if (!n) return '—';
7071
+ const u = ['B', 'KB', 'MB', 'GB', 'TB'];
7072
+ let i = 0; let v = n;
7073
+ while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
7074
+ return v.toFixed(v < 10 && i > 0 ? 1 : 0) + ' ' + u[i];
7075
+ }
7076
+ async function refreshResolveAiOps() {
7077
+ const summaryEl = $('resolveAiOpsSummary');
7078
+ const tableEl = $('resolveAiOpsTable');
7079
+ const rowsEl = $('resolveAiOpsRows');
7080
+ if (!summaryEl || !tableEl || !rowsEl) return;
7081
+ const data = await api('/api/resolve_ai_usage').catch(() => ({ success: false }));
7082
+ if (!data || !data.success) {
7083
+ summaryEl.textContent = 'ledger unavailable';
7084
+ tableEl.style.display = 'none';
7085
+ return;
7086
+ }
7087
+ const totals = (data.summary && data.summary.totals) || {};
7088
+ const byOp = (data.summary && data.summary.by_op) || {};
7089
+ const ops = Object.keys(byOp).sort();
7090
+ if (!ops.length) {
7091
+ summaryEl.textContent = 'No Resolve AI ops recorded yet for this project.';
7092
+ tableEl.style.display = 'none';
7093
+ return;
7094
+ }
7095
+ summaryEl.innerHTML = `<strong>${totals.runs || 0}</strong> runs · `
7096
+ + `<strong>${totals.successes || 0}</strong> ok / ${totals.failures || 0} failed · `
7097
+ + `${fmtMs(totals.wall_clock_ms)} total · `
7098
+ + `<strong>${totals.files_created || 0}</strong> files (${fmtBytes(totals.bytes_created)}) created`;
7099
+ rowsEl.innerHTML = ops.map(op => {
7100
+ const b = byOp[op];
7101
+ const isRender = b.op_class === 'render';
7102
+ return `<tr style="border-top:1px solid rgba(255,255,255,0.06);">
7103
+ <td style="padding:4px 6px;">${escapeHtml(op)}${isRender ? ' <span style="opacity:0.6;">(media)</span>' : ''}</td>
7104
+ <td style="padding:4px 6px;">${b.runs}</td>
7105
+ <td style="padding:4px 6px;">${b.successes}</td>
7106
+ <td style="padding:4px 6px;">${fmtMs(b.wall_clock_ms)}</td>
7107
+ <td style="padding:4px 6px;">${b.files_created || '—'}</td>
7108
+ <td style="padding:4px 6px;">${b.bytes_created ? fmtBytes(b.bytes_created) : '—'}</td>
7109
+ </tr>`;
7110
+ }).join('');
7111
+ tableEl.style.display = '';
7112
+ }
7113
+
7043
7114
  // ─── Caps inspector + refusals + reset ──────────────────────────
7044
7115
  async function inspectCapsFromUI() {
7045
7116
  const clipId = ($('capsInspectClipId')?.value || '').trim();
@@ -9916,6 +9987,7 @@ HTML = r"""<!doctype html>
9916
9987
  refreshCapsWidget().catch(() => {});
9917
9988
  refreshCapsHistory().catch(() => {});
9918
9989
  refreshCapsRefusals().catch(() => {});
9990
+ refreshResolveAiOps().catch(() => {});
9919
9991
 
9920
9992
  // Caps inspector + reset
9921
9993
  $('capsInspectBtn')?.addEventListener('click', () => inspectCapsFromUI().catch(alertError));
@@ -10654,6 +10726,45 @@ def _current_resolve_project_id() -> Tuple[Optional[str], Optional[str]]:
10654
10726
 
10655
10727
 
10656
10728
  @_serialize_resolve
10729
+ def _resolve_ai_features(resolve: Any) -> Dict[str, Any]:
10730
+ """Report which Resolve 21.0 AI scripting methods are available on the
10731
+ connected build, plus the Extra each AI-gated method requires. Presence is
10732
+ detected via getattr (no Resolve round-trips beyond fetching the handles),
10733
+ so this stays cheap enough for the boot handshake.
10734
+ """
10735
+ def has(obj: Any, name: str) -> bool:
10736
+ return bool(obj) and callable(getattr(obj, name, None))
10737
+
10738
+ project = folder = None
10739
+ try:
10740
+ pm = resolve.GetProjectManager()
10741
+ project = pm.GetCurrentProject() if pm else None
10742
+ mp = project.GetMediaPool() if project else None
10743
+ folder = mp.GetRootFolder() if mp else None
10744
+ except Exception:
10745
+ pass
10746
+
10747
+ features = {
10748
+ "disable_background_tasks": has(resolve, "DisableBackgroundTasksForCurrentResolveSession"),
10749
+ "generate_speech": has(project, "GenerateSpeech"),
10750
+ "perform_audio_classification": has(folder, "PerformAudioClassification"),
10751
+ "clear_audio_classification": has(folder, "ClearAudioClassification"),
10752
+ "analyze_for_intellisearch": has(folder, "AnalyzeForIntellisearch"),
10753
+ "analyze_for_slate": has(folder, "AnalyzeForSlate"),
10754
+ "remove_motion_blur": has(folder, "RemoveMotionBlur"),
10755
+ }
10756
+ return {
10757
+ "features": features,
10758
+ "available_count": sum(1 for v in features.values() if v),
10759
+ # Methods that additionally need an Extras download to actually run.
10760
+ "requires_extra": {
10761
+ "analyze_for_intellisearch": "AI IntelliSearch",
10762
+ "analyze_for_slate": "AI Slate ID",
10763
+ "generate_speech": "AI Speech Generator",
10764
+ },
10765
+ }
10766
+
10767
+
10657
10768
  def _resolve_identity() -> Dict[str, Any]:
10658
10769
  resolve, error = _connect_resolve_read_only()
10659
10770
  if not resolve:
@@ -10668,6 +10779,7 @@ def _resolve_identity() -> Dict[str, Any]:
10668
10779
  "version_string": str(version_string) if version_string else None,
10669
10780
  "version": list(version_tuple) if isinstance(version_tuple, (list, tuple)) else None,
10670
10781
  "page": str(page) if page else None,
10782
+ "ai_features": _resolve_ai_features(resolve),
10671
10783
  }
10672
10784
 
10673
10785
 
@@ -13573,6 +13685,20 @@ class Handler(BaseHTTPRequestHandler):
13573
13685
  except Exception as exc:
13574
13686
  self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
13575
13687
  return
13688
+ if path == "/api/resolve_ai_usage":
13689
+ # Ledger of Resolve-local 21.0 AI ops (read straight from this
13690
+ # project's brain DB — no Resolve round-trip needed).
13691
+ try:
13692
+ from src.utils import resolve_ai_ledger as _ledger
13693
+ root = self.state.project_root
13694
+ self._json({
13695
+ "success": True,
13696
+ "summary": _ledger.get_summary(project_root=root),
13697
+ "recent": _ledger.get_usage(project_root=root, limit=50),
13698
+ })
13699
+ except Exception as exc:
13700
+ self._json({"success": False, "error": f"{type(exc).__name__}: {exc}"})
13701
+ return
13576
13702
  if path.startswith("/api/timeline_thumbnail/"):
13577
13703
  rel = unquote(path[len("/api/timeline_thumbnail/"):])
13578
13704
  # Path is <slug>/<vNN.png>; constrain it to live under _soul/timeline_versions
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.28.1"
83
+ VERSION = "2.30.0"
84
84
  logger = logging.getLogger("davinci-resolve-mcp")
85
85
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
86
  logger.info(f"Detected platform: {get_platform()}")
@@ -294,3 +294,127 @@ def folder_clear_transcription(folder_path: str = "") -> Dict[str, Any]:
294
294
  folder = mp.GetCurrentFolder()
295
295
  result = folder.ClearTranscription()
296
296
  return {"success": bool(result)}
297
+
298
+
299
+ def _resolve_folder(mp, folder_path):
300
+ """Return (folder, error). Empty path -> current folder."""
301
+ if folder_path:
302
+ folder = _navigate_to_folder(mp, folder_path)
303
+ if not folder:
304
+ return None, {"error": f"Folder '{folder_path}' not found"}
305
+ return folder, None
306
+ return mp.GetCurrentFolder(), None
307
+
308
+
309
+ _MARKER_COLORS = [
310
+ "Blue", "Cyan", "Green", "Yellow", "Red", "Pink", "Purple", "Fuchsia",
311
+ "Rose", "Lavender", "Sky", "Mint", "Lemon", "Sand", "Cocoa", "Cream",
312
+ ]
313
+
314
+
315
+ @mcp.tool()
316
+ def folder_perform_audio_classification(folder_path: str = "") -> Dict[str, Any]:
317
+ """Classify audio of all clips in a Media Pool folder into categories (Resolve 21+).
318
+
319
+ Args:
320
+ folder_path: Path from root. Empty for current folder.
321
+ """
322
+ _, mp, err = _get_mp()
323
+ if err:
324
+ return err
325
+ folder, err = _resolve_folder(mp, folder_path)
326
+ if err:
327
+ return err
328
+ if not hasattr(folder, "PerformAudioClassification"):
329
+ return {"error": "PerformAudioClassification requires DaVinci Resolve 21+"}
330
+ return {"success": bool(folder.PerformAudioClassification())}
331
+
332
+
333
+ @mcp.tool()
334
+ def folder_clear_audio_classification(folder_path: str = "") -> Dict[str, Any]:
335
+ """Clear audio classification for all clips in a Media Pool folder (Resolve 21+).
336
+
337
+ Args:
338
+ folder_path: Path from root. Empty for current folder.
339
+ """
340
+ _, mp, err = _get_mp()
341
+ if err:
342
+ return err
343
+ folder, err = _resolve_folder(mp, folder_path)
344
+ if err:
345
+ return err
346
+ if not hasattr(folder, "ClearAudioClassification"):
347
+ return {"error": "ClearAudioClassification requires DaVinci Resolve 21+"}
348
+ return {"success": bool(folder.ClearAudioClassification())}
349
+
350
+
351
+ @mcp.tool()
352
+ def folder_analyze_for_intellisearch(folder_path: str = "", identify_faces: bool = False, is_better_mode: bool = False) -> Dict[str, Any]:
353
+ """Run IntelliSearch analysis on all clips in a folder (Resolve 21+, requires AI IntelliSearch Extra).
354
+
355
+ Args:
356
+ folder_path: Path from root. Empty for current folder.
357
+ identify_faces: Whether to identify faces.
358
+ is_better_mode: Use Better mode (vs Faster).
359
+ """
360
+ _, mp, err = _get_mp()
361
+ if err:
362
+ return err
363
+ folder, err = _resolve_folder(mp, folder_path)
364
+ if err:
365
+ return err
366
+ if not hasattr(folder, "AnalyzeForIntellisearch"):
367
+ return {"error": "AnalyzeForIntellisearch requires DaVinci Resolve 21+"}
368
+ return {"success": bool(folder.AnalyzeForIntellisearch(bool(identify_faces), bool(is_better_mode)))}
369
+
370
+
371
+ @mcp.tool()
372
+ def folder_analyze_for_slate(folder_path: str = "", marker_color: str = "Blue") -> Dict[str, Any]:
373
+ """Run Slate analysis on all clips in a folder (Resolve 21+, requires AI Slate ID Extra).
374
+
375
+ Args:
376
+ folder_path: Path from root. Empty for current folder.
377
+ marker_color: Marker color for detected slates (Blue, Cyan, Green, Yellow, Red, Pink,
378
+ Purple, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa, Cream).
379
+ """
380
+ _, mp, err = _get_mp()
381
+ if err:
382
+ return err
383
+ folder, err = _resolve_folder(mp, folder_path)
384
+ if err:
385
+ return err
386
+ if not hasattr(folder, "AnalyzeForSlate"):
387
+ return {"error": "AnalyzeForSlate requires DaVinci Resolve 21+"}
388
+ if marker_color not in _MARKER_COLORS:
389
+ return {"error": f"Invalid marker_color '{marker_color}'. Valid: {', '.join(_MARKER_COLORS)}"}
390
+ return {"success": bool(folder.AnalyzeForSlate(marker_color))}
391
+
392
+
393
+ @mcp.tool()
394
+ def folder_remove_motion_blur(folder_path: str = "", deblur_option: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
395
+ """Render motion-deblurred copies of all clips in a folder (Resolve 21+).
396
+
397
+ Creates NEW media files; source media is not modified.
398
+
399
+ Args:
400
+ folder_path: Path from root. Empty for current folder.
401
+ deblur_option: Settings dict (FileName, Format, Codec, EncodingProfile,
402
+ UseExtremeMode, UseMarkInMarkOut, RenderAtSourceRes, UseMoreGpuMemory).
403
+ """
404
+ _, mp, err = _get_mp()
405
+ if err:
406
+ return err
407
+ folder, err = _resolve_folder(mp, folder_path)
408
+ if err:
409
+ return err
410
+ if not hasattr(folder, "RemoveMotionBlur"):
411
+ return {"error": "RemoveMotionBlur requires DaVinci Resolve 21+"}
412
+ result = folder.RemoveMotionBlur(deblur_option or {})
413
+ created = []
414
+ for pair in (result or []):
415
+ try:
416
+ orig, new = pair
417
+ created.append({"original": orig.GetName(), "new": new.GetName(), "new_id": new.GetUniqueId()})
418
+ except Exception:
419
+ continue
420
+ return {"success": bool(result), "created": created}