davinci-resolve-mcp 2.213.3 → 2.214.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,55 @@
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.214.0 — color_trace reads exported .drp files, and the trace is live-validated
6
+
7
+ ### Added
8
+
9
+ - **`color_trace plan` takes `sourceDrp` / `targetDrp`.** The v2.213.0 matcher
10
+ read both timelines from `Project.db`, which a Postgres, network or cloud
11
+ library does not have. `ProjectManager.ExportProject` works on any project by
12
+ name without loading it, and the `.drp` it writes carries every field the
13
+ matcher keys on — name, record start, duration, in-point, media path, reel,
14
+ media ref, and the active grade version's body inline — so either side of a
15
+ plan can now be an exported `.drp`. The reader
16
+ (`resolve-advanced/server/drp-timeline-clips.mjs`) resolves a timeline name
17
+ through `MediaPool/**/MpFolder.xml` to its sequence id and reads the
18
+ `SeqContainer/<uuid>.xml` whose tracks reference it; it returns the same row
19
+ shape as the DB reader, active-version-first.
20
+ - **`apply_trace_plan` writes its full report to a file.** A real conform is
21
+ 800+ plan rows, and the first live run's per-clip tables (290k characters)
22
+ blew past what a tool response can carry. The full tables now go to
23
+ `report_path` (default `dry-run-report.json` / `apply-report.json` next to
24
+ the plan); the response keeps the summary, an `attention` list (ties,
25
+ partial overlaps, every skip that is not bulk `unmatched` /
26
+ `no-source-grade`) and the first `max_rows` rows (default 40, `verbose:
27
+ true` for everything). `failed` is never truncated.
28
+
29
+ ### Live-validated (Resolve Studio 19.1.3.7, Postgres library)
30
+
31
+ - Source: a 659-clip picture-lock turnover with 263 graded clips. Target: an
32
+ 878-clip conform of the next version, media on a different volume, reels
33
+ mostly empty, clip names carrying an extension the source names lack.
34
+ - Plan: 613 matched (583 by file name + source-range overlap, 3 by reel, 27 by
35
+ name only), 265 unmatched (reference masters, offline screeners, clips not
36
+ in the source), 266 with a grade to carry. The 12 name-only matches sat
37
+ below the default 0.8 gate and were skipped.
38
+ - Apply: 254 of 254 resolved clips graded, 0 failures, timeline archived as
39
+ `_archived_v01` first, a local version `traced V07 Sizing` added per clip.
40
+ Six sampled clips read back with exactly the node count their `.drx`
41
+ decodes to (9, 9, 8, 9, 5, 1) and the source's tools (Reduce Noise, a film
42
+ LUT, Halation, Glow, HDR wheels, hue curves).
43
+ - Reported for review, not hidden: 70 applied clips span more source range
44
+ than the graded section they matched (lowest overlap 14 percent), 2 applied
45
+ on a tie between identical candidates.
46
+
47
+ ### Notes
48
+
49
+ - Reading the Postgres library directly was not attempted: Resolve keeps the
50
+ connection password in plain text in `dblist.conf`, and the `.drp` route
51
+ needs no credential at all. `resolve-advanced/README.md` still says
52
+ `project_read` handles "SQLite or Postgres"; only SQLite is implemented.
53
+
5
54
  ## What's New in v2.213.3 — the capture docstring says what is restored and what is only reset
6
55
 
7
56
  ### Documentation
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [简体中文](README.zh-CN.md)
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.213.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.214.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-36%20(353%20full)-blue.svg)](#server-modes)
package/README.zh-CN.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](README.md) | 简体中文
4
4
 
5
- [![Version](https://img.shields.io/badge/version-2.213.3-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.214.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-36%20(353%20full)-blue.svg)](#服务器模式)
@@ -12,7 +12,7 @@
12
12
  [![Python](https://img.shields.io/badge/python-3.10+-green.svg)](https://www.python.org/downloads/)
13
13
  [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
14
14
 
15
- > 本翻译对应 v2.213.3 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.214.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -33,7 +33,7 @@ All actions are exposed through `timeline_item_color`.
33
33
  | `safe_set_cdl` | Validate and normalize CDL payloads before calling `SetCDL`; supports dry run. |
34
34
  | `safe_copy_grade` | Resolve target timeline item IDs before calling `CopyGrades`; supports dry run. |
35
35
  | `safe_apply_drx` | Validate DRX file existence and temp-path guard before calling `ApplyGradeFromDRX`. |
36
- | `apply_trace_plan` | Apply the advanced server's `color_trace` plan to the current timeline: resolve each entry to a live clip by (name, record start, duration), dry-run resolution table, one confirm_token for the batch, timeline archived first, then `ApplyGradeFromDRX` per clip; `version_name` adds a local version per clip so the previous grade survives. Unresolved entries are reported, never guessed. |
36
+ | `apply_trace_plan` | Apply the advanced server's `color_trace` plan to the current timeline: resolve each entry to a live clip by (name, record start, duration), dry-run resolution table, one confirm_token for the batch, timeline archived first, then `ApplyGradeFromDRX` per clip; `version_name` adds a local version per clip so the previous grade survives. Unresolved entries are reported, never guessed; the full per-clip tables go to `report_path`, the response carries the summary, an `attention` list and the first `max_rows` rows. |
37
37
  | `safe_export_lut` | Resolve LUT export type aliases and require temp output paths by default. |
38
38
  | `grade_version_snapshot` | Read current, local, and remote grade version names. |
39
39
  | `grade_version_restore` | Safely load a named local/remote version after verifying it exists. |
@@ -139,7 +139,8 @@ files and the project DB.
139
139
  compared clear of its transition windows, and Resolve's own FCP7 export
140
140
  ingests with its `-1` edges resolved and its missing ticks tolerated).
141
141
  - **`color_trace`** — cross-project clip matching on media identity (pool id /
142
- file path / reel / file name + source-range overlap; names last) a trace
142
+ file path / reel / file name + source-range overlap; names last), from a
143
+ `Project.db` or an exported `.drp` (the Postgres/network-library route) → a trace
143
144
  plan with a lossless `.drx` per graded match and a `plan.json`. The live half
144
145
  is `timeline_item_color.apply_trace_plan` (dry-run resolution table → one
145
146
  confirm_token → ApplyGradeFromDRX per resolved clip, timeline archived first).
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.213.3"
40
+ VERSION = "2.214.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": "2.213.3",
3
+ "version": "2.214.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -86,9 +86,13 @@ Each dispatches on an `action`. Highlights:
86
86
  timeline (any two projects, from `Project.db`, read-only, no Resolve) on **media identity** —
87
87
  pool item id / file path / reel / file name plus source-range overlap, so a stringout cut into
88
88
  graded sections, a renamed clip, or relinked media still traces; clip names are the last resort.
89
- Emits one lossless `.drx` per graded match plus a `plan.json`; the live server's
89
+ Either side is a `Project.db` (`…ProjectDb` / `…ProjectName`) **or an exported `.drp`**
90
+ (`sourceDrp` / `targetDrp`) — the `.drp` route is how a Postgres / network / cloud library
91
+ is read, since `ProjectManager.ExportProject` works on any project by name without loading
92
+ it. Emits one lossless `.drx` per graded match plus a `plan.json`; the live server's
90
93
  `timeline_item_color.apply_trace_plan` applies it (dry-run resolution table, one
91
- confirm_token for the batch, timeline archived first).
94
+ confirm_token for the batch, timeline archived first, full per-clip report to a file).
95
+ Live-validated 2026-09-08: 878-clip conform, 254 grades carried, 0 failures.
92
96
  - **`project_read` / `project_db`** — read/patch the Resolve project DB (SQLite or Postgres).
93
97
  Includes `list_subtitle_styles` / `set_subtitle_style` — caption font family/size/weight/italic
94
98
  and normalised position, which the scripting API cannot touch at all. Whole-track (not
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Timeline clips from an exported .drp — the DB-agnostic twin of
3
+ * project_read.readTimelineClips. A Postgres / cloud / network library has no
4
+ * Project.db to open, but ProjectManager.ExportProject works on ANY project by
5
+ * name without loading it, and the .drp it writes carries every field the
6
+ * color_trace matcher keys on:
7
+ *
8
+ * MediaPool/**\/MpFolder.xml <Sm2Timeline><Name>…</Name><Sequence><Sm2Sequence DbId=SEQ>
9
+ * SeqContainer/<uuid>.xml <Sm2TiTrack><Type>0</Type><Sequence>SEQ</Sequence><Items>
10
+ * <Sm2TiVideoClip DbId=…> Name/Start/Duration/In/MediaRef/
11
+ * MediaFilePath/MediaReelNumber/pLmVerTable(pActive → LmVersion Body)
12
+ *
13
+ * Measured on Resolve 19.1.3.7 exports (DbPrjVer 14): one container per
14
+ * timeline, tracks reference their sequence by id, the grade body sits inline
15
+ * in the clip's version table. Row shape matches readTimelineClips so the
16
+ * matcher cannot tell the two sources apart.
17
+ */
18
+
19
+ import fs from 'node:fs';
20
+ import os from 'node:os';
21
+ import path from 'node:path';
22
+ import { spawnSync } from 'node:child_process';
23
+
24
+ const scalar = (xml, tag) => {
25
+ const m = xml.match(new RegExp(`<${tag}>([^<]*)</${tag}>`));
26
+ return m ? m[1].trim() : null;
27
+ };
28
+ const blank = (xml, tag) => new RegExp(`<${tag}/>`).test(xml);
29
+
30
+ /** Walk every `<Sm2Timeline>` in the media-pool folder XMLs → [{name, timelineId, sequenceId}]. */
31
+ export function listDrpTimelines(mpFolderXmls) {
32
+ const out = [];
33
+ for (const xml of mpFolderXmls) {
34
+ for (const m of xml.matchAll(/<Sm2Timeline DbId="([^"]+)">([\s\S]*?)<\/Sm2Timeline>/g)) {
35
+ const body = m[2];
36
+ const name = scalar(body, 'Name');
37
+ const seq = body.match(/<Sm2Sequence DbId="([^"]+)"/);
38
+ out.push({ name, timelineId: m[1], sequenceId: seq ? seq[1] : null });
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+
44
+ /** Decode `<In>` the way project_read does: plain decimal, or the FIRST field of a
45
+ * pipe-joined composite ('47|0000eaffffffef3f'). Anything else → null, never 0. */
46
+ function decodeIn(v) {
47
+ if (v === null || v === '') return null;
48
+ const head = String(v).split('|', 1)[0];
49
+ return /^-?\d+(\.\d+)?$/.test(head) ? Number(head) : null;
50
+ }
51
+
52
+ /** Active grade of one clip element: {gradeVersion, gradeBody} or nulls.
53
+ * pActive names the active version; fall back to the first corrected one. */
54
+ function activeGrade(clipXml) {
55
+ const vt = clipXml.match(/<pLmVerTable>([\s\S]*?)<\/pLmVerTable>/);
56
+ if (!vt) return { gradeVersion: null, gradeBody: null };
57
+ const active = scalar(vt[1], 'pActive');
58
+ const versions = [...vt[1].matchAll(/<ListMgt::LmVersion DbId="([^"]+)">([\s\S]*?)<\/ListMgt::LmVersion>/g)].map((m) => {
59
+ const body = m[2];
60
+ const bodyHex = (body.match(/<Body>([0-9a-fA-F\s]*)<\/Body>/) || [null, ''])[1];
61
+ return {
62
+ id: m[1],
63
+ name: scalar(body, 'Name'),
64
+ corrected: /<HasCorrection>true<\/HasCorrection>/.test(body),
65
+ bodyHex: bodyHex ? bodyHex.replace(/[^0-9a-fA-F]/g, '').toLowerCase() : '',
66
+ };
67
+ });
68
+ const pick = versions.find((v) => v.id === active && v.corrected && v.bodyHex) || versions.find((v) => v.corrected && v.bodyHex);
69
+ return pick ? { gradeVersion: pick.name, gradeBody: pick.bodyHex } : { gradeVersion: null, gradeBody: null };
70
+ }
71
+
72
+ /** Clips of the tracks that belong to `sequenceId` inside one SeqContainer XML.
73
+ * trackType 0 = video (Sm2TiVideoClip), 1 = audio (Sm2TiAudioClip). */
74
+ export function parseSeqContainerClips(containerXml, sequenceId, { includeGrade = false } = {}) {
75
+ const rows = [];
76
+ for (const t of containerXml.matchAll(/<Sm2TiTrack DbId="([^"]+)">([\s\S]*?)<\/Sm2TiTrack>/g)) {
77
+ const trackXml = t[2];
78
+ if (sequenceId && scalar(trackXml, 'Sequence') !== sequenceId) continue;
79
+ const trackType = Number(scalar(trackXml, 'Type') ?? 0);
80
+ const tag = trackType === 0 ? 'Sm2TiVideoClip' : 'Sm2TiAudioClip';
81
+ for (const c of trackXml.matchAll(new RegExp(`<${tag} DbId="([^"]+)">([\\s\\S]*?)</${tag}>`, 'g'))) {
82
+ const x = c[2];
83
+ const g = includeGrade ? activeGrade(x) : { gradeVersion: null, gradeBody: null };
84
+ rows.push({
85
+ itemId: c[1],
86
+ name: scalar(x, 'Name'),
87
+ trackType,
88
+ start: Number(scalar(x, 'Start')),
89
+ duration: Number(scalar(x, 'Duration')),
90
+ reel: blank(x, 'MediaReelNumber') ? '' : scalar(x, 'MediaReelNumber') || '',
91
+ mediaStart: scalar(x, 'MediaStartTime'),
92
+ sourceIn: decodeIn(scalar(x, 'In')),
93
+ mediaPath: blank(x, 'MediaFilePath') ? '' : scalar(x, 'MediaFilePath') || '',
94
+ poolId: scalar(x, 'MediaRef'), // per-project media ref; equal only inside one project
95
+ trackId: t[1],
96
+ gradeVersion: g.gradeVersion,
97
+ hasGrade: /<HasCorrection>true<\/HasCorrection>/.test(x),
98
+ ...(includeGrade ? { gradeBody: g.gradeBody } : {}),
99
+ });
100
+ }
101
+ }
102
+ rows.sort((a, b) => a.trackType - b.trackType || a.start - b.start);
103
+ return rows;
104
+ }
105
+
106
+ /** Unzip a .drp into a temp dir and return {dir, mpFolderXmls, containers:[{file, xml}]}. */
107
+ export function openDrp(drpPath) {
108
+ if (!fs.existsSync(drpPath)) throw new Error(`.drp not found: ${drpPath}`);
109
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'drp-clips-'));
110
+ const r = spawnSync('unzip', ['-q', '-o', drpPath, '-d', dir], { encoding: 'utf8' });
111
+ if (r.status !== 0) throw new Error(`unzip failed for ${drpPath}: ${(r.stderr || '').slice(-200)}`);
112
+ const walk = (d, out = []) => {
113
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
114
+ const p = path.join(d, e.name);
115
+ if (e.isDirectory()) walk(p, out);
116
+ else out.push(p);
117
+ }
118
+ return out;
119
+ };
120
+ const files = walk(dir);
121
+ const read = (p) => fs.readFileSync(p, 'utf8');
122
+ return {
123
+ dir,
124
+ mpFolderXmls: files.filter((p) => /MpFolder\.xml$/.test(p)).map(read),
125
+ containers: files.filter((p) => /(^|\/)SeqContainer\/[^/]+\.xml$/.test(p)).map((p) => ({ file: p, xml: read(p) })),
126
+ close: () => {
127
+ try {
128
+ fs.rmSync(dir, { recursive: true, force: true });
129
+ } catch {
130
+ /* ignore */
131
+ }
132
+ },
133
+ };
134
+ }
135
+
136
+ /** readTimelineClips twin for a .drp: clips of the named timeline, video by default. */
137
+ export function readTimelineClipsFromDrp(drpPath, timeline, trackType = 'video', includeGrade = false) {
138
+ const drp = openDrp(drpPath);
139
+ try {
140
+ const timelines = listDrpTimelines(drp.mpFolderXmls);
141
+ const tl = timelines.find((t) => t.name === timeline);
142
+ if (!tl) {
143
+ throw new Error(`timeline "${timeline}" not found in ${drpPath}. Timelines: ${timelines.map((t) => JSON.stringify(t.name)).join(', ') || '(none)'}`);
144
+ }
145
+ if (!tl.sequenceId) throw new Error(`timeline "${timeline}" has no <Sm2Sequence> in its media-pool entry`);
146
+ const holder = drp.containers.find((c) => c.xml.includes(`<Sequence>${tl.sequenceId}</Sequence>`));
147
+ if (!holder) throw new Error(`no SeqContainer references sequence ${tl.sequenceId} for timeline "${timeline}"`);
148
+ let rows = parseSeqContainerClips(holder.xml, tl.sequenceId, { includeGrade });
149
+ if (trackType === 'video') rows = rows.filter((r) => r.trackType === 0);
150
+ else if (trackType === 'audio') rows = rows.filter((r) => r.trackType !== 0);
151
+ return rows;
152
+ } finally {
153
+ drp.close();
154
+ }
155
+ }
@@ -32,12 +32,28 @@ import crypto from 'node:crypto';
32
32
  import { z } from 'zod';
33
33
  import { resolveDbPath } from '../db-patch.mjs';
34
34
  import { readTimelineClips } from './project_read.mjs';
35
+ import { readTimelineClipsFromDrp } from '../drp-timeline-clips.mjs';
35
36
 
36
37
  const side = (name) => ({
37
38
  [`${name}ProjectDb`]: z.string().optional(),
38
39
  [`${name}ProjectName`]: z.string().optional(),
40
+ [`${name}Drp`]: z
41
+ .string()
42
+ .optional()
43
+ .describe(
44
+ `An exported .drp of the ${name} project (ProjectManager.ExportProject / project_manager safe_project_export). The DB-agnostic route: use it when the project lives in a Postgres / network / cloud library that has no Project.db to open.`,
45
+ ),
39
46
  [`${name}Timeline`]: z.string(),
40
47
  });
48
+
49
+ /** One side of the plan: a .drp when given, else a Project.db (path or name). */
50
+ function sideClips(p, name, includeGrade) {
51
+ const drp = p[`${name}Drp`];
52
+ const timeline = p[`${name}Timeline`];
53
+ if (drp) return { origin: { drp, timeline }, clips: readTimelineClipsFromDrp(drp, timeline, 'video', includeGrade) };
54
+ const db = resolveDbPath({ projectDb: p[`${name}ProjectDb`], projectName: p[`${name}ProjectName`] });
55
+ return { origin: { projectDb: db, timeline }, clips: readTimelineClips(db, timeline, 'video', includeGrade) };
56
+ }
41
57
  const planSchema = z.object({
42
58
  ...side('source'),
43
59
  ...side('target'),
@@ -274,14 +290,14 @@ const clipRef = (c) => ({
274
290
  export const colorTraceTool = {
275
291
  name: 'color_trace',
276
292
  description:
277
- 'Better ColorTrace — match clips between a SOURCE and TARGET timeline (cross-project, from Project.db, read-only, no Resolve) on media identity (pool id / file path / reel / file name + source-range overlap), names last → a trace plan with a lossless .drx per graded match and a plan.json for timeline_item_color.apply_trace_plan on the live server. Action: plan.',
293
+ 'Better ColorTrace — match clips between a SOURCE and TARGET timeline (cross-project, from Project.db OR an exported .drp — the .drp route covers Postgres/network/cloud libraries — read-only, no Resolve) on media identity (pool id / file path / reel / file name + source-range overlap), names last → a trace plan with a lossless .drx per graded match and a plan.json for timeline_item_color.apply_trace_plan on the live server. Action: plan.',
278
294
  async handler({ action, args }) {
279
295
  if (action === 'plan') {
280
296
  const p = planSchema.parse(args);
281
- const srcDb = resolveDbPath({ projectDb: p.sourceProjectDb, projectName: p.sourceProjectName });
282
- const tgtDb = resolveDbPath({ projectDb: p.targetProjectDb, projectName: p.targetProjectName });
283
- const srcClips = readTimelineClips(srcDb, p.sourceTimeline, 'video', true); // includeGrade
284
- const tgtClips = readTimelineClips(tgtDb, p.targetTimeline, 'video');
297
+ const src = sideClips(p, 'source', true); // includeGrade
298
+ const tgt = sideClips(p, 'target', false);
299
+ const srcClips = src.clips;
300
+ const tgtClips = tgt.clips;
285
301
  if (!srcClips.length) throw new Error(`source timeline "${p.sourceTimeline}" has no video clips`);
286
302
  if (!tgtClips.length) throw new Error(`target timeline "${p.targetTimeline}" has no video clips`);
287
303
  if (p.emitDir) fs.mkdirSync(p.emitDir, { recursive: true });
@@ -330,8 +346,8 @@ export const colorTraceTool = {
330
346
  });
331
347
  const matched = matches.filter((m) => m.source).length;
332
348
  const result = {
333
- source: { projectDb: srcDb, timeline: p.sourceTimeline, clips: srcClips.length, graded: srcClips.filter((c) => c.gradeBody).length },
334
- target: { projectDb: tgtDb, timeline: p.targetTimeline, clips: tgtClips.length },
349
+ source: { ...src.origin, clips: srcClips.length, graded: srcClips.filter((c) => c.gradeBody).length },
350
+ target: { ...tgt.origin, clips: tgtClips.length },
335
351
  summary: { matched, unmatched: tgtClips.length - matched, gradesReady, ambiguous, belowThreshold, byMethod },
336
352
  emitDir: p.emitDir || null,
337
353
  planPath: null,
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.213.3"
90
+ VERSION = "2.214.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 353-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.213.3"
14
+ VERSION = "2.214.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -27093,6 +27093,33 @@ def _resolve_trace_plan(tl, plan: Dict[str, Any], p: Dict[str, Any]) -> List[Dic
27093
27093
  return rows
27094
27094
 
27095
27095
 
27096
+ _TRACE_MAX_ROWS_DEFAULT = 40
27097
+
27098
+
27099
+ def _trace_report(p: Dict[str, Any], plan: Dict[str, Any], kind: str, payload: Dict[str, Any]) -> Optional[str]:
27100
+ """Write the full per-clip tables next to the plan (or to report_path) and
27101
+ return the path. A real cut is 800+ rows — far past what a tool response
27102
+ should carry — so the file is the durable artifact and the response stays
27103
+ compact (see max_rows)."""
27104
+ report_path = p.get("report_path")
27105
+ if not report_path:
27106
+ plan_path = p.get("plan_path")
27107
+ base = os.path.dirname(os.path.abspath(plan_path)) if plan_path else tempfile.gettempdir()
27108
+ report_path = os.path.join(base, f"{kind}-report.json")
27109
+ try:
27110
+ with open(report_path, "w", encoding="utf-8") as fh:
27111
+ json.dump({"kind": f"color_trace.{kind}", "written_at": _time.time(),
27112
+ "plan_source": plan.get("source"), **payload}, fh, indent=1, default=str)
27113
+ return report_path
27114
+ except Exception as exc:
27115
+ logger.warning("apply_trace_plan: could not write %s report: %s", kind, exc)
27116
+ return None
27117
+
27118
+
27119
+ def _trace_compact(rows: List[Dict[str, Any]], max_rows: int) -> Dict[str, Any]:
27120
+ return {"count": len(rows), "truncated": len(rows) > max_rows, "rows": rows[:max_rows]}
27121
+
27122
+
27096
27123
  def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
27097
27124
  plan, err = _load_trace_plan(p)
27098
27125
  if err:
@@ -27126,12 +27153,25 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
27126
27153
  public = [{k: v for k, v in r.items() if not k.startswith("_")} for r in rows]
27127
27154
  grade_mode = p.get("grade_mode", 0)
27128
27155
  version_name = p.get("version_name")
27156
+ max_rows = p.get("max_rows", _TRACE_MAX_ROWS_DEFAULT)
27157
+ max_rows = len(public) if p.get("verbose") else max(0, int(max_rows))
27158
+ # Entries that deserve a human look: skipped for a reason other than the
27159
+ # two bulk ones, or applied on a tie / a partial source-range overlap.
27160
+ bulk = {"unmatched", "no-source-grade"}
27161
+ attention = [r for r in public if (r["status"] != "apply" and r["reason"] not in bulk)
27162
+ or (r["status"] == "apply" and r["ambiguous"])]
27129
27163
 
27130
27164
  if p.get("dry_run"):
27165
+ report_path = _trace_report(p, plan, "dry-run", {"summary": summary, "resolution": public})
27131
27166
  return _ok(dry_run=True, timeline=live_name, plan_source=plan.get("source"),
27132
- summary=summary, resolution=public, grade_mode=grade_mode, version_name=version_name)
27167
+ summary=summary, report_path=report_path,
27168
+ attention=_trace_compact(attention, max_rows),
27169
+ resolution=_trace_compact(public, max_rows),
27170
+ grade_mode=grade_mode, version_name=version_name)
27133
27171
  if not to_apply:
27134
- return _ok(timeline=live_name, applied=[], failed=[], summary=summary, resolution=public,
27172
+ report_path = _trace_report(p, plan, "apply", {"summary": summary, "resolution": public, "applied": [], "failed": []})
27173
+ return _ok(timeline=live_name, applied=[], failed=[], summary=summary, report_path=report_path,
27174
+ attention=_trace_compact(attention, max_rows), resolution=_trace_compact(public, max_rows),
27135
27175
  note="nothing to apply — every plan entry was skipped (see skipped_by_reason)")
27136
27176
  if "confirm_token" not in p and "confirmToken" not in p and _confirm_token_required():
27137
27177
  preview = {
@@ -27177,13 +27217,17 @@ def _apply_trace_plan(p: Dict[str, Any]) -> Dict[str, Any]:
27177
27217
  rec["status"] = "applied" if ok else "failed"
27178
27218
  (applied if ok else failed).append(rec)
27179
27219
  summary.update({"applied": len(applied), "failed": len(failed)})
27220
+ skipped = [r for r in public if r["status"] != "apply"]
27221
+ report_path = _trace_report(p, plan, "apply", {"summary": summary, "applied": applied, "failed": failed, "skipped": skipped})
27180
27222
  return {
27181
27223
  "success": not failed,
27182
27224
  "timeline": live_name,
27183
27225
  "plan_source": plan.get("source"),
27184
- "applied": applied,
27185
- "failed": failed,
27186
- "skipped": [r for r in public if r["status"] != "apply"],
27226
+ "report_path": report_path,
27227
+ "applied": _trace_compact(applied, max_rows),
27228
+ "failed": failed, # never truncated: every failure is actionable
27229
+ "skipped": _trace_compact(skipped, max_rows),
27230
+ "attention": _trace_compact(attention, max_rows),
27187
27231
  "summary": summary,
27188
27232
  "grade_mode": grade_mode,
27189
27233
  "version_name": version_name,
@@ -27311,8 +27355,8 @@ _ACTION_HELP: Dict[str, Dict[str, Dict[str, Any]]] = {
27311
27355
  },
27312
27356
  "apply_trace_plan": {
27313
27357
  "summary": "Apply a color_trace plan (advanced server) to the CURRENT timeline: one ApplyGradeFromDRX per resolved clip. REPLACES those graphs; gated by confirm_token; timeline archived first.",
27314
- "params": "plan_path: str (planPath from color_trace plan with emitDir) | plan: dict, dry_run?, min_confidence? (default 0.8), start_tolerance? (frames, default 0), grade_mode? (0=no keyframes, 1=source TC aligned, 2=start aligned), version_name? (AddVersion per clip before applying), require_temp_path? (default True), allow_timeline_mismatch?, confirm_token?",
27315
- "returns": "{success, timeline, applied: [...], failed: [...], skipped: [...], summary: {plan_matches, would_apply, applied, failed, skipped_by_reason, ambiguous_in_plan}}; dry_run → {resolution: [{target, source, method, confidence, status, reason, live}]}",
27358
+ "params": "plan_path: str (planPath from color_trace plan with emitDir) | plan: dict, dry_run?, min_confidence? (default 0.8), start_tolerance? (frames, default 0), grade_mode? (0=no keyframes, 1=source TC aligned, 2=start aligned), version_name? (AddVersion per clip before applying), require_temp_path? (default True), allow_timeline_mismatch?, max_rows? (default 40; verbose=True returns every row), report_path? (default <plan dir>/dry-run-report.json | apply-report.json), confirm_token?",
27359
+ "returns": "{success, timeline, report_path, summary: {plan_matches, would_apply, applied, failed, skipped_by_reason, ambiguous_in_plan}, attention: {count, truncated, rows}, applied: {count, truncated, rows}, failed: [...] (never truncated), skipped: {count, truncated, rows}}; dry_run → {resolution: {count, truncated, rows: [{target, source, method, confidence, status, reason, live}]}}",
27316
27360
  "example": (
27317
27361
  '# 1. advanced server (no Resolve needed):\n'
27318
27362
  '# color_trace(action="plan", {sourceProjectName: "SHOW_v07", sourceTimeline: "REEL_01 v07",\n'
@@ -28147,13 +28191,15 @@ def timeline_item_color(action: str, params: Optional[Dict[str, Any]] = None) ->
28147
28191
  safe_apply_drx(path, source?, grade_mode?, require_temp_path?) -> {success}
28148
28192
  REPLACES the target graph. Captures a version snapshot first; require_temp_path defaults True.
28149
28193
  # example: action_help(name='<action_name>')
28150
- apply_trace_plan(plan_path|plan, dry_run?, min_confidence?=0.8, start_tolerance?=0, grade_mode?=0, version_name?, require_temp_path?=True, allow_timeline_mismatch?, confirm_token?) -> {success, applied, failed, skipped, summary}
28194
+ apply_trace_plan(plan_path|plan, dry_run?, min_confidence?=0.8, start_tolerance?=0, grade_mode?=0, version_name?, require_temp_path?=True, allow_timeline_mismatch?, max_rows?=40, verbose?, report_path?, confirm_token?) -> {success, applied, failed, skipped, attention, summary, report_path}
28151
28195
  The live half of the advanced server's color_trace (a ColorTrace that matches on media
28152
28196
  identity, cross-project, from the project DB). Resolves each plan entry to a clip on the
28153
28197
  CURRENT timeline by (name, record start, duration), then ApplyGradeFromDRX per clip —
28154
28198
  one confirm_token for the batch, timeline archived first. dry_run returns the resolution
28155
28199
  table without a token. version_name adds a new local version per clip before applying so
28156
28200
  the previous grade stays intact. Never guesses: unresolved entries are reported, not applied.
28201
+ The full per-clip tables go to report_path (default: next to the plan); the response carries
28202
+ the summary, `attention` (ties, partial overlaps, non-bulk skips) and the first max_rows rows.
28157
28203
  # example: action_help(name='<action_name>')
28158
28204
  safe_export_lut(type?, path, require_temp_path?) -> {success, path, size}
28159
28205
  Sandboxed LUT export.