davinci-resolve-mcp 4.15.5 → 4.16.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,96 @@
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 v4.16.0 — Calls name their timeline, and failures say so
6
+
7
+ Eight reports from a Studio 21.1 / Windows deployment (#320–#327) and the
8
+ open half of #318. The theme: a call that did something other than what the
9
+ caller asked, or failed, must not look like a success.
10
+
11
+ ### Added
12
+
13
+ - **Pin a call to a timeline** (#320). `timeline`, `timeline_item*`,
14
+ `timeline_markers`, `timeline_frame`, `timeline_ai`, `graph` and `render`
15
+ accept `timeline_id` or `timeline_name`. The server makes that timeline
16
+ current for the call and restores the caller's timeline afterwards,
17
+ verified by readback. Unknown ids (`TIMELINE_NOT_FOUND`), ambiguous names
18
+ (`TIMELINE_NAME_AMBIGUOUS`) and an id/name pair that disagree are refused
19
+ before the action runs. Before, a `timeline_id` nothing read was dropped,
20
+ and the write landed on whichever timeline was current.
21
+ - **Results name the timeline acted on** (#320): `_operation.timeline`
22
+ `{name, id}`, plus `pinned`, `restored_current` or `restore_failed` on a
23
+ pinned call.
24
+ - **Unread params are reported** (#320). A successful call carrying keys the
25
+ action never read returns `ignored_params` and an envelope warning. They
26
+ are reported after the call, not refused before it: the server has no
27
+ per-action schema, and a static scan of the reachable code could not bound
28
+ the key set for 25 of the 37 tools.
29
+ - **`timeline_item_id` on the timeline item tools** (#323). On 21.1
30
+ `item_index` counts transitions, so it shifts whenever one is added or
31
+ removed. Every tool that resolves an item through the shared lookup
32
+ accepts the item's `GetUniqueId()` and refuses an unknown id with
33
+ `ITEM_NOT_FOUND`.
34
+ - **`drx generate` can target the timeline** (#318).
35
+ `target: "timeline"` writes the grade into `<pTrackVer>`, the block a
36
+ timeline `ApplyGradeFromDRX` loads, in the shape the reporter applied on
37
+ Studio 21.1.1.10. The default stays `"clip"`.
38
+ - **`duplicate` takes `make_current`** and returns the copy's `id` and the
39
+ `current` timeline (#322).
40
+
41
+ ### Fixed
42
+
43
+ - **Failed calls set MCP `isError: true`** (#321). Every error-envelope
44
+ result used to arrive with `isError: false`, so spec-following hooks and
45
+ retry policies counted refusals as successes. The flag is set at the
46
+ protocol boundary; the JSON body is unchanged. `{"success": false}` without
47
+ an `error` key is not flagged.
48
+ - **`timeline.duplicate` leaves the original current** (#322) unless
49
+ `make_current: true`. Resolve makes the copy current, so "duplicate as a
50
+ backup, then edit" edited the backup.
51
+ - **FCPXML round trip on the server's own export** (#324).
52
+ `import_timeline_checked` accepts the bundle directory 21.1 writes for
53
+ FCPXML and imports the `Info.fcpxml` inside.
54
+ - **Online image sequences are not reported missing** (#325). A path like
55
+ `name.[0000-8999].jpeg` is checked by its first and last frames (printf and
56
+ `####` patterns by one matching frame). Before, `probe_timeline_structure`
57
+ said `file_exists: false`, and `detect_missing_media` and
58
+ `build_relink_plan` listed the clips for relink.
59
+ - **The bridge honours `allowed_media_roots` / `allowed_output_roots`**
60
+ (#326). `load_config` dropped both, so the policies were always the home
61
+ folder. The generic `call` path the MCP server uses was never gated by
62
+ them. The bridge's named path operations now follow the configured roots,
63
+ including the installer's default output root (`~/Movies`).
64
+
65
+ ### Performance
66
+
67
+ - **The bridge keeps one connection open** (#327). Each request is still
68
+ signed with its own nonce; only the TCP connection is reused, retired after
69
+ 20 s idle on the client and 30 s on the bridge. macOS loopback: 0.40 ms a
70
+ call per connection, 0.17 ms kept alive, and no TIME_WAIT socket per call.
71
+ Windows was not measured here. Re-run `scripts/install_resolve_bridge.py`
72
+ to update the in-Resolve copy; an older bridge keeps working, one
73
+ connection per call.
74
+
75
+ ### Validation
76
+
77
+ - Full offline suite: 4,289 tests, OK. 47 new tests across the error flag,
78
+ pinning, unread params, duplicate, item ids, FCPXML bundles, image
79
+ sequences, bridge roots and bridge keep-alive (over a real loopback
80
+ bridge).
81
+ - Advanced Node suite on Node 20 with a fresh `npm ci`: 1,206 tests, 0
82
+ failures (4 new for `target: "timeline"`).
83
+ - Live on Studio 19.1.3.7, over MCP stdio against a disposable project with
84
+ synthetic media: pinned reads and writes land on the named timeline and
85
+ the caller's timeline comes back; unknown and conflicting selectors
86
+ refuse; `ignored_params`; `timeline_item_id` and its `ITEM_NOT_FOUND`;
87
+ `duplicate` with and without `make_current` and pinned; `isError` on
88
+ failures and not on successes. 21 checks, 2 cycles. The first cycle found
89
+ a pinned `duplicate` reporting the pinned timeline as `current` after the
90
+ caller's had been restored; fixed before release.
91
+ - The 21.1-specific behaviour (transitions in `GetItemListInTrack`, FCPXML
92
+ bundles, timeline DRX applies) cannot be reproduced on 19.1.3.7 and is
93
+ not live-validated.
94
+
5
95
  ## What's New in v4.15.5 — DRX applies say which graph changed
6
96
 
7
97
  `graph.apply_grade_from_drx` and `timeline_item_color.safe_apply_drx` no
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-4.15.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.16.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(390%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-4.15.5-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.16.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(390%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
- > 本翻译对应 v4.15.5 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.16.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
package/docs/SKILL.md CHANGED
@@ -215,6 +215,30 @@ tool — did it happen, was it verified, what changed:
215
215
  not read a missing `changes` as a no-op.
216
216
  - **`warnings`** — present only when there are any.
217
217
  - **`execution_id`** — correlates one call across logs and transcripts.
218
+ - **`timeline`** — on timeline-scoped tools, `{name, id}` of the timeline the
219
+ call acted on. A pinned call (below) adds `pinned: true` and
220
+ `restored_current`, or `restore_failed: true` if the caller's timeline could
221
+ not be made current again.
222
+
223
+ A failed call (an `{"error": {...}}` body) also comes back with MCP
224
+ `isError: true`, so hooks and retry policies that follow the spec see the
225
+ failure without parsing the body.
226
+
227
+ **Unread params are reported.** When an action succeeds without reading some
228
+ key you passed, the result carries `ignored_params: [...]` and the envelope
229
+ lists them under `warnings`. Treat it as "that key did nothing": a misspelled
230
+ key, an option this action does not take, or an alternative another key
231
+ overrode (an `index` passed alongside `id`).
232
+
233
+ **Pinning a call to a timeline.** `timeline`, `timeline_item*`,
234
+ `timeline_markers`, `timeline_frame`, `timeline_ai`, `graph` and `render`
235
+ act on the current timeline unless you pass `timeline_id` (preferred) or
236
+ `timeline_name`. The server then makes that timeline current for the call
237
+ and restores yours afterwards, so a user clicking another timeline mid-session
238
+ cannot redirect your write. An unknown id, an ambiguous name or an id/name
239
+ pair that disagree is refused before anything runs. Exceptions, where these
240
+ keys keep their own meaning: `timeline.set_current` (selects by
241
+ `timeline_id`) and the imports (`timeline_name` names the new timeline).
218
242
 
219
243
  The envelope is namespaced under `_operation` rather than merged into the top
220
244
  level because `status`, `operation`, `warnings`, `result` and `changes` are all
@@ -1949,8 +1973,11 @@ Key actions: `create_subtitles(settings?)` (human-readable settings resolved to
1949
1973
 
1950
1974
  Timeline items are identified by `track_type`, `track_index`, and `item_index`
1951
1975
  (all default to `"video"`, `1`, `0` respectively — the first clip on the first
1952
- video track). Always retrieve item IDs via `timeline.get_items` before operating
1953
- on specific items.
1976
+ video track), or by `timeline_item_id`, the item's `GetUniqueId()`. Prefer the
1977
+ id: on Resolve 21.1 `item_index` also counts transitions, so it shifts whenever
1978
+ a transition is added or removed. Read ids with `timeline.get_items` or
1979
+ `timeline.probe_timeline_structure`. An unknown id is refused with
1980
+ `ITEM_NOT_FOUND`; it never falls back to item 0.
1954
1981
 
1955
1982
  **`timeline_item`** — Properties, transform, speed, audio, keyframes.
1956
1983
 
@@ -2467,6 +2494,8 @@ reports, deletes the project, and removes generated media. See
2467
2494
  | `"No project open"` | No project is currently loaded | Call `project_manager(action="load", params={"name": "..."})` |
2468
2495
  | `"No current timeline"` | Project has no timeline set as current | Call `timeline(action="set_current", params={"index": 1})` |
2469
2496
  | `"No item at index N"` | `item_index` out of range for the track | Call `timeline(action="get_items", ...)` first to find valid indices |
2497
+ | `ITEM_NOT_FOUND` | No item with that `timeline_item_id` on the target timeline | Re-read ids; pass `timeline_id` if the item is on another timeline |
2498
+ | `TIMELINE_NOT_FOUND` / `TIMELINE_NAME_AMBIGUOUS` | The pin names no timeline, or several share the name | `timeline(action="list")` and pin by `timeline_id` |
2470
2499
  | `"Clip not found"` | Stale or wrong `clip_id` | Re-fetch IDs via `media_pool(action="get_selected")` or `folder(action="get_clips")` |
2471
2500
  | `"Gallery not available"` | Not on Color page | `resolve_control(action="open_page", params={"page": "color"})` |
2472
2501
  | `"GrabStill failed"` | Not on Color page or no clip under playhead | Switch to Color page, move playhead over a clip |
@@ -62,6 +62,12 @@ Supported aliases include `aaf`, `drt`, `edl`, `edl_cdl`, `edl_sdl`,
62
62
  - Source range reporting grouped generated media paths with requested handles.
63
63
  - Guarded exports succeeded for FCPXML, DRT, EDL, AAF, and OTIO.
64
64
  - FCPXML directory-style exports are normalized with a `primary_file` path.
65
+ `import_timeline_checked` also accepts the bundle directory itself (the
66
+ export's `path`) and imports the `Info.fcpxml` inside it.
67
+ - Clip fades set with `SetFades` do not survive an FCPXML round trip (reported
68
+ on Studio 21.1: imported clips read FadeIn/FadeOut 0 while Cross Dissolves
69
+ survive). This is likely Resolve's FCPXML writer; it has not been measured
70
+ on other formats.
65
71
  - DRT export/import/compare round-trip succeeded and cleaned up the imported
66
72
  timeline.
67
73
  - Synthetic-only unlink, missing-media detection, relink candidate planning,
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.15.5"
40
+ VERSION = "4.16.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": "4.15.5",
3
+ "version": "4.16.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -250,6 +250,14 @@ const generateSchema = z.object({
250
250
  'units (wheels raw, saturation ~1.0 neutral) for lossless/programmatic callers.'
251
251
  ),
252
252
  metadata: z.object({}).passthrough().optional().describe('Optional { label, width, height } metadata'),
253
+ target: z
254
+ .enum(['clip', 'timeline'])
255
+ .optional()
256
+ .describe(
257
+ "'clip' (default) writes the grade as a clip grade. 'timeline' writes it as the timeline grade " +
258
+ "(<pTrackVer>), for graph.apply_grade_from_drx with source='timeline'; a clip-target file applied to " +
259
+ 'the timeline is refused (DRX_NO_TIMELINE_GRADE).'
260
+ ),
253
261
  outputPath: z.string().optional().describe('If set, write the .drx here'),
254
262
  });
255
263
 
@@ -684,7 +692,8 @@ export const drxTool = {
684
692
  if (action === 'generate') {
685
693
  const p = generateSchema.parse(args);
686
694
  const warnings = [];
687
- const out = await drxGenerator().generateDRX(normalizeGradeParams(p.gradeParams, warnings), p.metadata || {});
695
+ const metadata = { ...(p.metadata || {}), ...(p.target ? { target: p.target } : {}) };
696
+ const out = await drxGenerator().generateDRX(normalizeGradeParams(p.gradeParams, warnings), metadata);
688
697
  const content = typeof out === 'string' ? out : out?.content || out?.drxContent;
689
698
  const warn = warnings.length ? { warnings } : {};
690
699
  if (p.outputPath && content) {
@@ -1974,7 +1974,11 @@ async function generateDRX(gradeParams, metadata = {}) {
1974
1974
  sourceTimeline = 'Timeline 1',
1975
1975
  sourceTC = '00:00:00:00',
1976
1976
  recordTC = '01:00:00:00',
1977
+ target = 'clip',
1977
1978
  } = metadata;
1979
+ if (target !== 'clip' && target !== 'timeline') {
1980
+ throw new Error(`target must be 'clip' or 'timeline', got ${JSON.stringify(target)}`);
1981
+ }
1978
1982
 
1979
1983
  // Generate the body (pass label for single-node mode)
1980
1984
  const body = await generateDRXBody(gradeParams, { width, height, label });
@@ -1988,6 +1992,31 @@ async function generateDRX(gradeParams, metadata = {}) {
1988
1992
  // Current timestamp
1989
1993
  const now = new Date().toISOString().replace('Z', '');
1990
1994
 
1995
+ // A timeline ApplyGradeFromDRX loads <pTrackVer> (VerType 1), not the clip
1996
+ // grade in <pClipFullVer>. This path used to write no <pTrackVer> at all, so a
1997
+ // timeline apply loaded nothing and replaced the timeline graph with an empty
1998
+ // one (#318). target:'timeline' carries the grade in <pTrackVer> and marks the
1999
+ // clip part uncorrected: the shape the #318 reporter applied successfully on
2000
+ // Studio 21.1.1.10 (timeline graph 0 -> 1 node, no other graph changed).
2001
+ const forTimeline = target === 'timeline';
2002
+ const trackSection = forTimeline
2003
+ ? `
2004
+ <pTrackVer>
2005
+ <ListMgt::LmVersion DbId="${uuidv4()}">
2006
+ <FieldsBlob/>
2007
+ <Name/>
2008
+ <HasCorrection>true</HasCorrection>
2009
+ <VerType>1</VerType>
2010
+ <ImplVersion>1</ImplVersion>
2011
+ <IncludedInRecording>true</IncludedInRecording>
2012
+ <FlatPassEnabled>false</FlatPassEnabled>
2013
+ <RGBAOutputEnabled>false</RGBAOutputEnabled>
2014
+ <Body>${bodyHex}</Body>
2015
+ <UseVersionClipProcParams>true</UseVersionClipProcParams>
2016
+ </ListMgt::LmVersion>
2017
+ </pTrackVer>`
2018
+ : '';
2019
+
1991
2020
  // Build DRX XML
1992
2021
  const xml = `<?xml version="1.0" encoding="UTF-8"?>
1993
2022
  <!--DbAppVer="19.1.3.0007" DbPrjVer="14"-->
@@ -2010,7 +2039,7 @@ async function generateDRX(gradeParams, metadata = {}) {
2010
2039
  <ListMgt::LmVersion DbId="${versionId}">
2011
2040
  <FieldsBlob/>
2012
2041
  <Name/>
2013
- <HasCorrection>true</HasCorrection>
2042
+ <HasCorrection>${forTimeline ? 'false' : 'true'}</HasCorrection>
2014
2043
  <VerType>0</VerType>
2015
2044
  <ImplVersion>1</ImplVersion>
2016
2045
  <IncludedInRecording>true</IncludedInRecording>
@@ -2019,7 +2048,7 @@ async function generateDRX(gradeParams, metadata = {}) {
2019
2048
  <Body>${bodyHex}</Body>
2020
2049
  <UseVersionClipProcParams>true</UseVersionClipProcParams>
2021
2050
  </ListMgt::LmVersion>
2022
- </pClipFullVer>
2051
+ </pClipFullVer>${trackSection}
2023
2052
  <PrimaryCCMode>0</PrimaryCCMode>
2024
2053
  </Gallery::GyStill>
2025
2054
  `;
@@ -19,6 +19,7 @@ for path in (SRC_DIR, PROJECT_DIR):
19
19
 
20
20
  from mcp.server.fastmcp import FastMCP
21
21
  from mcp.types import ToolAnnotations
22
+ from src.utils.mcp_error_flag import ErrorFlaggingFastMCP
22
23
 
23
24
  from src.utils.app_control import (
24
25
  get_app_state,
@@ -91,14 +92,14 @@ if not logging.getLogger().handlers:
91
92
  handlers=[logging.StreamHandler()],
92
93
  )
93
94
 
94
- VERSION = "4.15.5"
95
+ VERSION = "4.16.0"
95
96
  logger = logging.getLogger("davinci-resolve-mcp")
96
97
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
97
98
  logger.info(f"Detected platform: {get_platform()}")
98
99
  logger.info(f"Using Resolve API path: {RESOLVE_API_PATH}")
99
100
  logger.info(f"Using Resolve library path: {RESOLVE_LIB_PATH}")
100
101
 
101
- mcp = FastMCP("DaVinciResolveMCP")
102
+ mcp = ErrorFlaggingFastMCP("DaVinciResolveMCP")
102
103
  if hasattr(mcp, "_mcp_server"):
103
104
  mcp._mcp_server.version = VERSION
104
105