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 +22 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +6 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/resolve-advanced/server/tools/conform.mjs +42 -11
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
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
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#server-modes)
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
[](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
|
|
6
6
|
[](https://www.npmjs.com/package/davinci-resolve-mcp)
|
|
7
7
|
[](docs/reference/api-coverage.md)
|
|
8
8
|
[-blue.svg)](#服务器模式)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
[](https://www.python.org/downloads/)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
> 本翻译对应 v4.14.
|
|
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.
|
|
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
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import fs from 'node:fs/promises';
|
|
23
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/granular/common.py
CHANGED
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.14.
|
|
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()}")
|