davinci-resolve-mcp 4.14.2 → 4.14.3

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,28 @@
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.14.3 — Secure footage indexing
6
+
7
+ ### Security
8
+
9
+ - Fix argument injection in `conform.relink_scalefix` (GHSA-x29h-6rgf-w233).
10
+ Replace the `find` subprocess with Node filesystem traversal so caller-supplied
11
+ footage roots cannot execute commands or inject filesystem operations.
12
+ - Validate all footage roots before indexing. Preserve recursive MOV/MP4 indexing,
13
+ proxy exclusions, relative paths, file symlinks, and duplicate filtering;
14
+ skip nested directory symlinks and handle filenames containing newlines safely.
15
+ - Thanks to HexMystic for reporting the vulnerability.
16
+
17
+ ### Validation
18
+
19
+ - Focused conform and vendor suite: 121 passed, 30 skipped, zero failed.
20
+ - Full advanced-server Node suite: 1,141 passed, 60 skipped, zero failed using
21
+ the Node runtime matching the installed native dependencies.
22
+ - Required release static checks pass. Control-panel documentation reviewed;
23
+ no panel behavior or navigation changed.
24
+ - No Resolve scripting or project authoring behavior changed; live Resolve
25
+ validation is not required for this filesystem indexing fix.
26
+
5
27
  ## What's New in v4.14.2 — Refresh advanced-server dependencies
6
28
 
7
29
  Lands #302. Fixes #301.
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.14.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.14.3-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.14.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.14.3-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.14.2 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v4.14.3 版 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
@@ -443,6 +443,12 @@ Operating rules an agent must know:
443
443
  SQLite DB and verify transactionally. See
444
444
  [subtitle-track editing](guides/subtitle-track-editing.md) for selectors,
445
445
  Word Highlight controls, dry-run examples and live-validation limits.
446
+ - **Offline conform footage indexing**: `conform.relink_scalefix` accepts existing
447
+ directories in `footageDir` / `footageDirs` (absolute or relative). Empty,
448
+ missing, non-directory, and `-`-prefixed roots are refused. Indexing uses Node
449
+ filesystem traversal, preserves MOV/MP4 filtering and proxy exclusions, and
450
+ does not follow nested directory symlinks. Explicit `media` lists bypass
451
+ directory indexing.
446
452
  - **Guards are load-bearing.** Advanced tools refuse rather than fabricate
447
453
  (silent-lie guards): a thrown "refused" error usually means wrong input space,
448
454
  log-encoded frames, or missing media — read the message before retrying.
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "4.14.2"
40
+ VERSION = "4.14.3"
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.14.2",
3
+ "version": "4.14.3",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import fs from 'node:fs/promises';
23
- import { spawnSync } from 'node:child_process';
23
+ import path from 'node:path';
24
24
  import { z } from 'zod';
25
25
  import { createRequire } from 'node:module';
26
26
  import { hasSharp } from '../capabilities.mjs';
@@ -87,10 +87,48 @@ const deriveSchema = z.object({
87
87
  const manifestSchema = z.object({ conformed: z.array(z.object({}).passthrough()), opts: z.object({}).passthrough().optional() });
88
88
  const reportSchema = z.object({ meta: z.object({}).passthrough().optional(), cuts: z.array(z.object({}).passthrough()).optional() });
89
89
 
90
+ async function indexFootageRoots(roots, excludeMarkers) {
91
+ // Treat every root as a filesystem path, never as an external command argument.
92
+ // Validate the complete list before traversal so malformed roots fail closed.
93
+ const directories = [];
94
+ for (const root of roots) {
95
+ if (typeof root !== 'string' || !root.length || root.startsWith('-')) {
96
+ throw new Error(`conform.relink_scalefix: invalid footage root: ${root}`);
97
+ }
98
+ const directory = path.resolve(root);
99
+ const stat = await fs.stat(directory).catch(() => null);
100
+ if (!stat?.isDirectory()) throw new Error(`conform.relink_scalefix: invalid footage root (expected directory): ${root}`);
101
+ directories.push(directory);
102
+ }
103
+ const media = [];
104
+ const seen = new Set();
105
+ const pending = [...directories].reverse();
106
+ while (pending.length) {
107
+ const directory = pending.pop();
108
+ if (seen.has(directory)) continue;
109
+ seen.add(directory);
110
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
111
+ const filePath = path.join(directory, entry.name);
112
+ if (filePath.includes(`${path.sep}._`) || excludeMarkers.some((marker) => filePath.includes(marker))) continue;
113
+ // Do not follow nested directory symlinks (including cycles).
114
+ if (entry.isDirectory()) pending.push(filePath);
115
+ else if (
116
+ /\.(mov|mp4)$/i.test(entry.name) &&
117
+ !seen.has(filePath) &&
118
+ (entry.isFile() || (entry.isSymbolicLink() && (await fs.stat(filePath).catch(() => null))?.isFile()))
119
+ ) {
120
+ seen.add(filePath);
121
+ media.push({ path: filePath, basename: entry.name });
122
+ }
123
+ }
124
+ }
125
+ return media;
126
+ }
127
+
90
128
  export const conformTool = {
91
129
  name: 'conform',
92
130
  description:
93
- 'Conform/relink QC engine (offline core). Actions: parse_geometry, oracle_derive, list_targets, repair_ladder, build_manifest, analyze_media (ffprobe target media → res/aspect/codec/bitdepth/fps + quality rank), relink_scalefix (surgical: repoint proxy→highres pathurls + fix the scale double-count per clip, preserving retimes/tracks/audio byte-for-byte; index MULTIPLE footage roots via footageDirs, PROXY excluded; returns warnings.leftOnProxyOrTest + unresolvedSources so a source stuck on its proxy cannot slip through), report; verify (gated on optional sharp); fix_reverse_clip (LIVE DB patch — restore a reversed clip dropped on import: copy a generic reverse MediaTimemapBA blob + set the mirrored "In"; SQLite or PostgreSQL; project must be CLOSED; locate→probe→calibrate K→apply); snapshot (sequence LINEAGE store — ingest_xml/ingest_live an editorial-or-conform XML or a live timeline into one content-hashed schema, then list/diff/show/rollback_plan; the diff is the incremental frame-QC worklist; ingest_xml reads Resolve\'s OWN FCP7 export: `-1` clip edges resolve to the adjacent transition\'s junction in record order, each cut records the transition windows its edges sit in, and a missing pproTicksIn (Resolve writes none — measured) falls back to `<in>` for the oracle frame instead of losing it, reported as resolvedEdges/unresolvedEdges/ticksAbsent); qc (comprehensive per-cut frame diff vs the reference render → MATCH/OFFSET/WRONG/REF_OFFLINE/UNREADABLE cached per snapshot; each cut is compared at the first frame CLEAR of its transition windows — inside a window the reference is a blend, or black for a fade — with the source frame advanced at the cut\'s speed, and the result says where it sampled (sample_note); a flat/untextured frame is UNREADABLE (review), never a fake MATCH; a cut that is a FLATTENED COMPOUND (Resolve\'s XML writer leaves a media-less clipitem named after the compound — ingest tags is_compound) is never sampled: review with a note pointing at the OTIO export, never a false WRONG; conform frame is sampled at the oracle source frame AND scale-corrected to the edit framing before diffing, so reversed/scaled clips compare apples-to-apples; REF_OFFLINE = the reference is black/flat at that record position because the shot was offline upstream in editorial — inconclusive, NOT a conform error; op run [ffmpeg+sharp] / markers [red conform / yellow turnover / blue ref-offline / cyan review marker plan] / propagate [carry unchanged verdicts to a new snapshot]). Frame-math is the truth, not filename matches.',
131
+ "Conform/relink QC engine (offline core). Actions: parse_geometry, oracle_derive, list_targets, repair_ladder, build_manifest, analyze_media (ffprobe target media → res/aspect/codec/bitdepth/fps + quality rank), relink_scalefix (surgical: repoint proxy→highres pathurls + fix the scale double-count per clip, preserving retimes/tracks/audio byte-for-byte; index MULTIPLE footage roots via footageDirs, PROXY excluded; returns warnings.leftOnProxyOrTest + unresolvedSources so a source stuck on its proxy cannot slip through), report; verify (gated on optional sharp); fix_reverse_clip (LIVE DB patch — restore a reversed clip dropped on import: copy a generic reverse MediaTimemapBA blob + set the mirrored \"In\"; SQLite or PostgreSQL; project must be CLOSED; locate→probe→calibrate K→apply); snapshot (sequence LINEAGE store — ingest_xml/ingest_live an editorial-or-conform XML or a live timeline into one content-hashed schema, then list/diff/show/rollback_plan; the diff is the incremental frame-QC worklist; ingest_xml reads Resolve's OWN FCP7 export: `-1` clip edges resolve to the adjacent transition's junction in record order, each cut records the transition windows its edges sit in, and a missing pproTicksIn (Resolve writes none — measured) falls back to `<in>` for the oracle frame instead of losing it, reported as resolvedEdges/unresolvedEdges/ticksAbsent); qc (comprehensive per-cut frame diff vs the reference render → MATCH/OFFSET/WRONG/REF_OFFLINE/UNREADABLE cached per snapshot; each cut is compared at the first frame CLEAR of its transition windows — inside a window the reference is a blend, or black for a fade — with the source frame advanced at the cut's speed, and the result says where it sampled (sample_note); a flat/untextured frame is UNREADABLE (review), never a fake MATCH; a cut that is a FLATTENED COMPOUND (Resolve's XML writer leaves a media-less clipitem named after the compound — ingest tags is_compound) is never sampled: review with a note pointing at the OTIO export, never a false WRONG; conform frame is sampled at the oracle source frame AND scale-corrected to the edit framing before diffing, so reversed/scaled clips compare apples-to-apples; REF_OFFLINE = the reference is black/flat at that record position because the shot was offline upstream in editorial — inconclusive, NOT a conform error; op run [ffmpeg+sharp] / markers [red conform / yellow turnover / blue ref-offline / cyan review marker plan] / propagate [carry unchanged verdicts to a new snapshot]). Frame-math is the truth, not filename matches.",
94
132
  async handler({ action, args }) {
95
133
  if (action === 'parse_geometry') {
96
134
  const p = parseSchema.parse(args);
@@ -152,7 +190,7 @@ export const conformTool = {
152
190
  .optional()
153
191
  .describe('Case-insensitive path fragments that mark a proxy/test path (default ["PROXY","TEST"]) — flagged as left-on-proxy after relink'),
154
192
  })
155
- .refine((a) => a.footageDir || a.footageDirs || a.media, { message: 'provide footageDir, footageDirs, or media' })
193
+ .refine((a) => a.footageDir || a.footageDirs?.length || a.media, { message: 'provide footageDir, footageDirs, or media' })
156
194
  .parse(args);
157
195
  const xml = await fs.readFile(p.filePath, 'utf8');
158
196
  const excludeMarkers = p.indexExcludeMarkers || ['/PROXY/'];
@@ -160,14 +198,7 @@ export const conformTool = {
160
198
  let media = p.media;
161
199
  if (!media) {
162
200
  const roots = p.footageDirs && p.footageDirs.length ? p.footageDirs : [p.footageDir];
163
- const r = spawnSync('find', [...roots, '-iname', '*.mov', '-o', '-iname', '*.mp4'], { encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });
164
- if (r.status !== 0) throw new Error(`conform.relink_scalefix: find failed in [${roots.join(', ')}]: ${(r.stderr || '').slice(-200)}`);
165
- const seen = new Set();
166
- media = r.stdout
167
- .split('\n')
168
- .filter((l) => l && !l.includes('/._') && !excludeMarkers.some((m) => l.includes(m)))
169
- .filter((path) => (seen.has(path) ? false : (seen.add(path), true)))
170
- .map((path) => ({ path, basename: path.split('/').pop() }));
201
+ media = await indexFootageRoots(roots, excludeMarkers);
171
202
  } else {
172
203
  media = media.map((m) => ({ ...m, basename: m.basename || m.path.split('/').pop() }));
173
204
  }
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
91
91
  handlers=[logging.StreamHandler()],
92
92
  )
93
93
 
94
- VERSION = "4.14.2"
94
+ VERSION = "4.14.3"
95
95
  logger = logging.getLogger("davinci-resolve-mcp")
96
96
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
97
97
  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 = "4.14.2"
14
+ VERSION = "4.14.3"
15
15
 
16
16
  import base64
17
17
  import os