davinci-resolve-mcp 3.3.0 → 3.4.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,45 @@
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 v3.4.0 — review a bin one frame at a time in the control panel
6
+
7
+ Contributed by @tpellet (#230), their first contribution here.
8
+
9
+ ### Added
10
+
11
+ - **Serial source review in the control panel.** Reviewing a bin meant opening
12
+ each clip separately. The review bin now has an **Enlarge / review** button
13
+ per card that opens a full-size frame with previous/next navigation, so a bin
14
+ is walked once rather than clicked through a card at a time.
15
+ - **Include / Exclude / Unreviewed per clip, and an independent star rating.**
16
+ Keyboard-driven — `I`, `X`, `U` and the arrow keys — with Include and Exclude
17
+ advancing automatically. Selection and rating are separate fields: rating a
18
+ clip does not decide it, and excluding one does not discard its rating.
19
+ - **A selection filter** — all / non-excluded / excluded — alongside the
20
+ existing bin filter, and a selection chip on every card, so the state of a
21
+ pass is visible without opening anything.
22
+ - **Decisions survive a reload.** Both fields go through the existing
23
+ correction store, so they persist the way clip notes already did and are
24
+ visible to everything else that reads corrections. Notes are untouched.
25
+
26
+ ### Changed
27
+
28
+ - **Review thumbnails are letterboxed rather than cropped** (`object-fit:
29
+ cover` → `contain`). A cropped thumbnail hides exactly what a source review
30
+ is for: framing, headroom, and what is at the edges of frame.
31
+
32
+ ### Gating
33
+
34
+ - **Every save is verified, not assumed.** The panel re-reads the clip after
35
+ each write and refuses to advance if the value it reads back is not the one
36
+ it sent, so a failed save cannot be walked past. `apply_clip_correction`
37
+ validates server-side as well: `user.selection` must be one of the three
38
+ literals, and `user.rating` must be an `int` from 0 to 5 — `type(value) is
39
+ not int` deliberately, so a JSON `true` is rejected rather than silently
40
+ stored as a rating of 1.
41
+ - **No source media is touched and no Resolve edit is made.** Previews are
42
+ analyzed frames that already exist on disk; there is no conversion step.
43
+
5
44
  ## What's New in v3.3.0 — ask the server what the native Resolve API contains
6
45
 
7
46
  Contributed by @legionsound (#229), the second of the two branches queued in #207.
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-3.3.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-3.4.0-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-37%20(387%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-3.3.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-3.4.0-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-37%20(387%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
- > 本翻译对应 v3.3.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v3.4.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -101,6 +101,19 @@ detection has run (`detect_entities` + a one-frame-per-cluster confirmation
101
101
  in chat), a `Recurring across this bin` card lists the labeled people,
102
102
  places, and objects with their shot counts.
103
103
 
104
+ Use **Enlarge / review** on a card to inspect its complete analyzed frame without
105
+ cropping. Previous/Next (or arrow keys) follow the visible bin order. **Include**
106
+ (`I`) and **Exclude** (`X`) save the selection and advance after readback;
107
+ **Unreviewed** (`U`) clears the decision without advancing. Ratings save independently
108
+ and do not clear exclusions. The selection filter can show all, non-excluded,
109
+ or excluded clips; it applies to the bin browser, not search results.
110
+
111
+ Selections are stored as `user.selection` corrections alongside existing notes
112
+ and ratings. They express review preferences, not automatic timeline edits.
113
+ Previews use existing analyzed frames, so their resolution depends on the analysis;
114
+ this does not generate proxies or stream original media. Existing clip detail
115
+ controls remain available for notes and metadata edits.
116
+
104
117
  ### Clip detail
105
118
 
106
119
  ![Clip detail with shot strip](../images/control-panel/03-clip-detail.png)
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "3.3.0"
40
+ VERSION = "3.4.0"
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": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -1658,7 +1658,7 @@ HTML = r"""<!doctype html>
1658
1658
  aspect-ratio: 16 / 9;
1659
1659
  background: var(--lab-workspace-letterbox);
1660
1660
  border-radius: var(--radius-sm);
1661
- object-fit: cover;
1661
+ object-fit: contain;
1662
1662
  display: block;
1663
1663
  }
1664
1664
  .review-thumb.placeholder {
@@ -4265,6 +4265,11 @@ HTML = r"""<!doctype html>
4265
4265
  <select id="reviewBinFilter" aria-label="Filter by bin">
4266
4266
  <option value="">All bins</option>
4267
4267
  </select>
4268
+ <select id="sourceSelectionFilter" aria-label="Filter by source selection">
4269
+ <option value="all">All selections</option>
4270
+ <option value="non-excluded">Non-excluded</option>
4271
+ <option value="excluded">Excluded</option>
4272
+ </select>
4268
4273
  <div class="review-view-toggle" role="tablist" aria-label="View mode">
4269
4274
  <button id="reviewViewGridBtn" class="active" data-view-mode="grid" type="button">Grid</button>
4270
4275
  <button id="reviewViewListBtn" data-view-mode="list" type="button">List</button>
@@ -8822,11 +8827,159 @@ HTML = r"""<!doctype html>
8822
8827
  sorted.map(b => `<option value="${escapeHtml(b)}" ${b === current ? 'selected' : ''}>${escapeHtml(b)}</option>`).join('');
8823
8828
  }
8824
8829
 
8830
+ let sourceSelectionFilter = 'all';
8831
+ function matchesSourceSelection(clip) {
8832
+ const selection = clip.user_selection || 'Unreviewed';
8833
+ return sourceSelectionFilter === 'all'
8834
+ || (sourceSelectionFilter === 'excluded' ? selection === 'Exclude' : selection !== 'Exclude');
8835
+ }
8836
+
8837
+ // Source preview reviews the current bin/filter order using persisted corrections.
8838
+ const sourcePreview = document.createElement('dialog');
8839
+ sourcePreview.setAttribute('aria-label', 'Source preview');
8840
+ sourcePreview.style.cssText = 'max-width:96vw;max-height:96vh;background:var(--bg-elevated-1);color:var(--text-primary)';
8841
+ sourcePreview.innerHTML = `<button type="button" data-preview-close>Close</button>
8842
+ <h3 data-preview-title></h3><div data-preview-controls>
8843
+ <button type="button" data-preview-step="-1">← Previous</button>
8844
+ <button type="button" data-preview-selection="Include">Include & next (I)</button>
8845
+ <button type="button" data-preview-selection="Exclude">Exclude & next (X)</button>
8846
+ <button type="button" data-preview-selection="Unreviewed">Unreviewed (U)</button>
8847
+ <button type="button" data-preview-step="1">Next →</button>
8848
+ <span>Rating:</span>${[0,1,2,3,4,5].map(n => `<button type="button" data-preview-rating="${n}" aria-label="${n ? n + ' stars' : 'Clear stars'}">${n ? '★'.repeat(n) : 'Clear'}</button>`).join('')}
8849
+ </div><p data-preview-status role="status"></p>
8850
+ <img data-preview-image alt="" style="display:block;max-width:90vw;max-height:70vh;object-fit:contain">`;
8851
+ document.body.append(sourcePreview);
8852
+ const previewImage = sourcePreview.querySelector('[data-preview-image]');
8853
+ const previewStatus = sourcePreview.querySelector('[data-preview-status]');
8854
+ let previewQueue = [], previewIndex = -1, previewBusy = false, previewReady = false, previewGeneration = 0;
8855
+
8856
+ function lockSourcePreview(busy) {
8857
+ previewBusy = busy;
8858
+ sourcePreview.querySelectorAll('[data-preview-controls] button').forEach(button => {
8859
+ button.disabled = busy || (!button.dataset.previewStep && !previewReady);
8860
+ });
8861
+ }
8862
+
8863
+ function displaySourceReview(data, clip) {
8864
+ const selection = readCorrectionValue(data.corrections, 'clip', clip.clip_id, 'user.selection') || 'Unreviewed';
8865
+ const rating = readCorrectionValue(data.corrections, 'clip', clip.clip_id, 'user.rating') || 0;
8866
+ clip.user_selection = selection;
8867
+ sourcePreview.querySelectorAll('[data-preview-rating]').forEach(button => {
8868
+ button.setAttribute('aria-pressed', String(Number(button.dataset.previewRating) === rating));
8869
+ });
8870
+ sourcePreview.querySelectorAll('[data-preview-selection]').forEach(button => {
8871
+ button.setAttribute('aria-pressed', String(button.dataset.previewSelection === selection));
8872
+ });
8873
+ previewStatus.textContent = `${selection} · ${rating} stars — saved. ←/→ navigate; I include; X exclude; U clear selection.`;
8874
+ }
8875
+
8876
+ async function showSourcePreview(index) {
8877
+ if (previewBusy || index < 0 || index >= previewQueue.length) return;
8878
+ previewIndex = index;
8879
+ previewReady = false;
8880
+ const clip = previewQueue[index], generation = ++previewGeneration;
8881
+ lockSourcePreview(true);
8882
+ sourcePreview.querySelector('[data-preview-title]').textContent = `${index + 1} / ${previewQueue.length} — ${clip.clip_name || clip.clip_id}`;
8883
+ previewStatus.textContent = 'Loading preview and saved review…';
8884
+ previewImage.style.visibility = 'hidden';
8885
+ previewImage.alt = clip.clip_name || clip.clip_id;
8886
+ try {
8887
+ if (!clip.representative_frame_index) throw new Error('No analyzed frame available. Use Previous/Next to continue.');
8888
+ const loaded = new Promise((resolve, reject) => {
8889
+ previewImage.onload = resolve;
8890
+ previewImage.onerror = () => reject(new Error('Preview unavailable. Use Previous/Next to continue.'));
8891
+ });
8892
+ previewImage.src = `/api/clips/${encodeURIComponent(clip.clip_id)}/frames/${clip.representative_frame_index}`;
8893
+ const [data] = await Promise.all([api(`/api/clips/${encodeURIComponent(clip.clip_id)}`, {cache:'no-store'}), loaded]);
8894
+ if (generation !== previewGeneration) return;
8895
+ if (!data.success) throw new Error(data.error || 'Could not load saved review');
8896
+ displaySourceReview(data, clip);
8897
+ previewImage.style.visibility = 'visible';
8898
+ previewReady = true;
8899
+ } catch (error) {
8900
+ if (generation === previewGeneration) previewStatus.textContent = error.message;
8901
+ } finally {
8902
+ if (generation === previewGeneration) lockSourcePreview(false);
8903
+ }
8904
+ }
8905
+
8906
+ async function saveSourceReview(field, value) {
8907
+ if (previewBusy || !previewReady || previewIndex < 0) return;
8908
+ const clip = previewQueue[previewIndex], generation = previewGeneration;
8909
+ lockSourcePreview(true);
8910
+ previewStatus.textContent = 'Saving…';
8911
+ try {
8912
+ const result = await api(`/api/clips/${encodeURIComponent(clip.clip_id)}/corrections`, {
8913
+ method:'POST', body:JSON.stringify({entity_type:'clip', entity_uuid:clip.clip_id,
8914
+ field_path:field, new_value:value, author:'control_panel', reason:'source preview review'})});
8915
+ if (!result.success) throw new Error(result.error || 'Save failed');
8916
+ const data = await api(`/api/clips/${encodeURIComponent(clip.clip_id)}`, {cache:'no-store'});
8917
+ if (!data.success || readCorrectionValue(data.corrections, 'clip', clip.clip_id, field) !== value) {
8918
+ throw new Error('Saved review could not be verified; retry before continuing.');
8919
+ }
8920
+ clip.user_selection = readCorrectionValue(data.corrections, 'clip', clip.clip_id, 'user.selection') || 'Unreviewed';
8921
+ const listedClip = state.review.clipList?.clips?.find(item => item.clip_id === clip.clip_id);
8922
+ if (listedClip) listedClip.user_selection = clip.user_selection;
8923
+ renderReviewBin();
8924
+ if (generation !== previewGeneration) return;
8925
+ displaySourceReview(data, clip);
8926
+ lockSourcePreview(false);
8927
+ if (field === 'user.selection') {
8928
+ if (!matchesSourceSelection(clip)) {
8929
+ previewQueue.splice(previewIndex, 1);
8930
+ if (previewQueue.length) await showSourcePreview(Math.min(previewIndex, previewQueue.length - 1));
8931
+ else sourcePreview.close();
8932
+ } else if (value !== 'Unreviewed' && previewIndex + 1 < previewQueue.length) {
8933
+ await showSourcePreview(previewIndex + 1);
8934
+ }
8935
+ }
8936
+ } catch (error) {
8937
+ if (generation === previewGeneration) previewStatus.textContent = 'Save failed: ' + error.message;
8938
+ } finally {
8939
+ if (generation === previewGeneration) lockSourcePreview(false);
8940
+ }
8941
+ }
8942
+
8943
+ sourcePreview.querySelector('[data-preview-close]').onclick = () => sourcePreview.close();
8944
+ sourcePreview.addEventListener('close', () => {
8945
+ ++previewGeneration;
8946
+ previewReady = false;
8947
+ lockSourcePreview(false);
8948
+ renderReviewBin();
8949
+ });
8950
+ sourcePreview.addEventListener('click', event => {
8951
+ const button = event.target.closest('button');
8952
+ if (!button) return;
8953
+ if (button.dataset.previewStep) showSourcePreview(previewIndex + Number(button.dataset.previewStep));
8954
+ if (button.dataset.previewSelection) saveSourceReview('user.selection', button.dataset.previewSelection);
8955
+ if (button.dataset.previewRating != null) saveSourceReview('user.rating', Number(button.dataset.previewRating));
8956
+ });
8957
+ sourcePreview.addEventListener('keydown', event => {
8958
+ if (event.target.matches('input,textarea,select') || event.ctrlKey || event.metaKey || event.altKey) return;
8959
+ const actions = {
8960
+ ArrowLeft: () => showSourcePreview(previewIndex - 1),
8961
+ ArrowRight: () => showSourcePreview(previewIndex + 1),
8962
+ i: () => saveSourceReview('user.selection', 'Include'),
8963
+ x: () => saveSourceReview('user.selection', 'Exclude'),
8964
+ u: () => saveSourceReview('user.selection', 'Unreviewed'),
8965
+ };
8966
+ const action = actions[event.key] || actions[event.key.toLowerCase()];
8967
+ if (action) { event.preventDefault(); event.stopPropagation(); if (!event.repeat) action(); }
8968
+ });
8969
+ $('reviewBinGrid').addEventListener('click', event => {
8970
+ const button = event.target.closest('[data-source-preview]');
8971
+ if (!button) return;
8972
+ event.stopImmediatePropagation();
8973
+ previewQueue = filteredClips().slice();
8974
+ sourcePreview.showModal();
8975
+ showSourcePreview(previewQueue.findIndex(clip => clip.clip_id === button.dataset.sourcePreview));
8976
+ }, true);
8977
+
8825
8978
  function filteredClips() {
8826
8979
  const data = state.review.clipList;
8827
8980
  if (!data || !data.clips) return [];
8828
8981
  const binFilter = state.review.binFilter || '';
8829
- return data.clips.filter(c => !binFilter || c.bin_path === binFilter);
8982
+ return data.clips.filter(c => (!binFilter || c.bin_path === binFilter) && matchesSourceSelection(c));
8830
8983
  }
8831
8984
 
8832
8985
  function renderReviewBin() {
@@ -8850,8 +9003,8 @@ HTML = r"""<!doctype html>
8850
9003
  }
8851
9004
  const clips = filteredClips();
8852
9005
  if (!clips.length) {
8853
- if (state.review.binFilter) {
8854
- if (summary) summary.textContent = `No analyzed clips in bin "${state.review.binFilter}".`;
9006
+ if (state.review.binFilter || sourceSelectionFilter !== 'all') {
9007
+ if (summary) summary.textContent = 'No analyzed clips match these filters.';
8855
9008
  if (grid) grid.innerHTML = '';
8856
9009
  } else {
8857
9010
  if (summary) summary.textContent = 'Nothing analyzed yet.';
@@ -8868,8 +9021,8 @@ HTML = r"""<!doctype html>
8868
9021
  Array.from(selected).forEach(id => { if (!visibleIds.has(id)) selected.delete(id); });
8869
9022
  if (summary) {
8870
9023
  const total = data.clips.length;
8871
- const base = state.review.binFilter
8872
- ? `${clips.length} of ${total} analyzed clip${total === 1 ? '' : 's'} · bin: ${state.review.binFilter}`
9024
+ const base = state.review.binFilter || sourceSelectionFilter !== 'all'
9025
+ ? `${clips.length} of ${total} analyzed clip${total === 1 ? '' : 's'}${state.review.binFilter ? ' · bin: ' + state.review.binFilter : ''}`
8873
9026
  : `${total} analyzed clip${total === 1 ? '' : 's'} in this project\u2019s analysis root.`;
8874
9027
  if (selected.size > 0) {
8875
9028
  summary.innerHTML = `<span>${escapeHtml(base)}</span>
@@ -8901,8 +9054,9 @@ HTML = r"""<!doctype html>
8901
9054
  <span class="select-box">${isSelected ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>' : ''}</span>
8902
9055
  </button>
8903
9056
  ${thumb}
9057
+ <button type="button" class="secondary" data-source-preview="${escapeHtml(clip.clip_id || '')}">Enlarge / review</button>
8904
9058
  <div class="review-clip-card-name">${escapeHtml(clip.clip_name || '')}</div>
8905
- <div class="review-clip-card-meta"><span>${dur}</span><span>${shots}</span>${primaryUse}${chip}</div>
9059
+ <div class="review-clip-card-meta"><span>${dur}</span><span>${shots}</span>${primaryUse}${chip}<span class="review-chip">${escapeHtml(clip.user_selection || 'Unreviewed')}</span></div>
8906
9060
  ${clip.clip_summary_oneliner ? `<div class="review-clip-card-oneliner">${escapeHtml(clip.clip_summary_oneliner)}</div>` : ''}
8907
9061
  </div>`;
8908
9062
  }).join('');
@@ -11481,6 +11635,7 @@ HTML = r"""<!doctype html>
11481
11635
  openClipDetail(card.dataset.clipId).catch(alertError);
11482
11636
  });
11483
11637
  $('reviewBinGrid').addEventListener('keydown', event => {
11638
+ if (event.target.closest('[data-source-preview]')) return;
11484
11639
  if (event.key !== 'Enter' && event.key !== ' ') return;
11485
11640
  const card = event.target.closest('.review-clip-card');
11486
11641
  if (card && card.dataset.clipId) {
@@ -11886,6 +12041,10 @@ HTML = r"""<!doctype html>
11886
12041
  const q = $('reviewSearchInput').value.trim();
11887
12042
  if (q) runReviewSearch(q).catch(alertError);
11888
12043
  });
12044
+ $('sourceSelectionFilter').addEventListener('change', event => {
12045
+ sourceSelectionFilter = event.target.value;
12046
+ renderReviewBin();
12047
+ });
11889
12048
  // Bin dropdown.
11890
12049
  $('reviewBinFilter').addEventListener('change', event => {
11891
12050
  state.review.binFilter = event.target.value || '';
@@ -13433,7 +13592,10 @@ def _v2_clip_summary_card(clip_slug: str, clip_dir: str, report: Dict[str, Any])
13433
13592
  if not isinstance(oneliner, str) or not oneliner:
13434
13593
  oneliner = summary[:140] + ("…" if len(summary) > 140 else "")
13435
13594
  rep_index = _v2_pick_representative_frame_index(report)
13595
+ corrections = _v2_read_corrections_for_dir(clip_dir).get("current", {})
13596
+ selection = corrections.get(f"clip:{clip_block.get('clip_id')}:user.selection", {}).get("value", "Unreviewed")
13436
13597
  return {
13598
+ "user_selection": selection,
13437
13599
  "clip_id": clip_block.get("clip_id"),
13438
13600
  "clip_slug": clip_slug,
13439
13601
  "clip_dir": clip_dir,
@@ -13938,6 +14100,12 @@ def apply_clip_correction(project_root: str, clip_id: str, body: Dict[str, Any])
13938
14100
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
13939
14101
  if not clip_dir:
13940
14102
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
14103
+ field_path = body.get("field_path") or body.get("fieldPath")
14104
+ value = body.get("new_value") if "new_value" in body else body.get("newValue", body.get("value"))
14105
+ if field_path == "user.selection" and value not in ("Include", "Exclude", "Unreviewed"):
14106
+ return {"success": False, "error": "Selection must be Include, Exclude, or Unreviewed"}
14107
+ if field_path == "user.rating" and (type(value) is not int or not 0 <= value <= 5):
14108
+ return {"success": False, "error": "Rating must be an integer from 0 to 5"}
13941
14109
  from src.server import _v2_update_field
13942
14110
  entity_type = body.get("entity_type") or body.get("entityType") or "shot"
13943
14111
  params: Dict[str, Any] = dict(body)
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "3.3.0"
90
+ VERSION = "3.4.0"
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()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 377-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "3.3.0"
14
+ VERSION = "3.4.0"
15
15
 
16
16
  import base64
17
17
  import os