davinci-resolve-mcp 2.214.2 → 2.214.4
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 +54 -0
- package/README.md +4 -2
- package/README.zh-CN.md +2 -2
- package/bin/davinci-resolve-advanced-mcp.mjs +73 -14
- package/bin/node-floor.mjs +116 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +32 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
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.4 — the offline test suite no longer writes into the operator's server.log
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **The unit suite was appending to `logs/server.log`.** Importing
|
|
10
|
+
`src/server.py` attaches the root logger's FileHandler to that file, and the
|
|
11
|
+
suite imports the server, so every MagicMock "connection" the bridge tests
|
|
12
|
+
provoke and every lifecycle warning about a MagicMock timeline landed in the
|
|
13
|
+
operator's real log — the file a live debugging session reads. On the
|
|
14
|
+
maintainer's machine that log had grown to 128 MB with 240 such lines in it.
|
|
15
|
+
The log target is now `RESOLVE_MCP_LOG_FILE`: unset means `logs/server.log`
|
|
16
|
+
as before, a path means that file, empty means no file. `tests/__init__.py`
|
|
17
|
+
sets it to a temporary file before any test module imports the server, on
|
|
18
|
+
both the `unittest` and `pytest` paths, and `tests/test_log_isolation.py`
|
|
19
|
+
fails the suite if a handler on the root or `resolve-mcp` logger ever targets
|
|
20
|
+
the real log during a run. The live server's behaviour is unchanged, and the
|
|
21
|
+
transport-token redaction test, which builds its own root FileHandler, still
|
|
22
|
+
passes.
|
|
23
|
+
- **The existing log is untouched.** The 128 MB file is the operator's; this
|
|
24
|
+
change only stops adding to it. Rotate or trim it by hand if wanted.
|
|
25
|
+
|
|
26
|
+
### Validation
|
|
27
|
+
|
|
28
|
+
- `tests/test_log_isolation.py` (four tests, including the unset-means-real-log
|
|
29
|
+
case that pins the live behaviour). Proven with an audited full run — a
|
|
30
|
+
`sys.addaudithook` on every `open` of the operator's log — that recorded zero
|
|
31
|
+
opens across 3371 tests. Full offline suite, drift guards and the advanced
|
|
32
|
+
Node suite green.
|
|
33
|
+
|
|
34
|
+
## What's New in v2.214.3 — the advanced launcher heals a wrong-Node registration
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
|
|
38
|
+
- **`davinci-resolve-advanced-mcp` re-executes itself under a Node ≥ 20.9 when
|
|
39
|
+
started by an older one.** The floor is unchanged and deliberate (sharp's own
|
|
40
|
+
engine floor is 20.9; better-sqlite3 is a native module built for one ABI;
|
|
41
|
+
Node 18 is end-of-life). What kept recurring was the MCP registration's
|
|
42
|
+
`command` landing on an nvm v18 binary after a client app rewrote its config
|
|
43
|
+
(twice on the reference machine), which left the server "disconnected" with
|
|
44
|
+
the fix buried in a log. The launcher now looks for a suitable Node before
|
|
45
|
+
refusing — `DAVINCI_RESOLVE_NODE` first, then nvm's versions directory
|
|
46
|
+
newest-first, then Homebrew/system paths (Windows: Program Files and the
|
|
47
|
+
per-user install) — probes each with `-p process.versions.node`, re-execs
|
|
48
|
+
with the same stdio under the first that passes, and says so on stderr.
|
|
49
|
+
A re-exec marks itself so a bad replacement cannot loop; with no usable
|
|
50
|
+
candidate the refusal now lists what was probed. Measured live: started by
|
|
51
|
+
v18.20.8, it came up under v22.22.3 and served the MCP handshake.
|
|
52
|
+
- **`--node-check`** prints `{node, execPath, reexec}` after the floor check —
|
|
53
|
+
the answer to "which Node is this registration actually running?".
|
|
54
|
+
`--version` and `--help` still answer before the floor, whatever started them.
|
|
55
|
+
- `DAVINCI_RESOLVE_ADVANCED_ASSUME_NODE` fakes the running version and
|
|
56
|
+
`DAVINCI_RESOLVE_ADVANCED_NO_NODE_SEARCH=1` limits the search to the explicit
|
|
57
|
+
override; both exist for the tests and are documented in the launcher.
|
|
58
|
+
|
|
5
59
|
## What's New in v2.214.2 — a running Resolve is counted by its executable path, not only by its argument vector
|
|
6
60
|
|
|
7
61
|
### Fixed
|
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)
|
|
@@ -298,7 +298,9 @@ and an audit document is the last place to let a reader read it as an all-clear.
|
|
|
298
298
|
|
|
299
299
|
Traces live in a 100-entry in-memory ring and are appended to
|
|
300
300
|
`logs/execution-traces.jsonl` beside `server.log` — `RESOLVE_MCP_TRACE_FILE`
|
|
301
|
-
moves it
|
|
301
|
+
moves it, and `RESOLVE_MCP_LOG_FILE` moves `server.log` itself (a path, or empty
|
|
302
|
+
for no file; the offline test suite points it at a temporary file so it never
|
|
303
|
+
writes into the operator's log). `list_recent_executions` reports that path and whether it is
|
|
302
304
|
writable, so "the log is empty" and "nothing is being written" are
|
|
303
305
|
distinguishable without reading the source. What is recorded is tool name,
|
|
304
306
|
action, timing, status, semantic deltas and verification — no parameters and no
|
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
|
-
> 本翻译对应 v2.214.
|
|
15
|
+
> 本翻译对应 v2.214.4 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
|
@@ -26,6 +26,7 @@ Usage:
|
|
|
26
26
|
davinci-resolve-advanced-mcp
|
|
27
27
|
davinci-resolve-advanced-mcp --version
|
|
28
28
|
davinci-resolve-advanced-mcp --help
|
|
29
|
+
davinci-resolve-advanced-mcp --node-check (which Node the server would run under)
|
|
29
30
|
|
|
30
31
|
Starts the offline DaVinci Resolve advanced MCP server over stdio.
|
|
31
32
|
`;
|
|
@@ -49,19 +50,77 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
49
50
|
// command in an MCP registration resolves to on a machine whose shell
|
|
50
51
|
// default lags. Silent degradation is this repo's least favorite failure
|
|
51
52
|
// mode; refuse loudly with the fix instead.
|
|
52
|
-
|
|
53
|
+
//
|
|
54
|
+
// Self-heal (2026-09-09, after the registration landed on v18 for the second
|
|
55
|
+
// time): before refusing, look for a Node >=20.9 on this machine (an explicit
|
|
56
|
+
// DAVINCI_RESOLVE_NODE, nvm's versions dir newest-first, Homebrew/system
|
|
57
|
+
// paths), re-exec this launcher under it with the same stdio, and say so on
|
|
58
|
+
// stderr. The registration can then point at any node and still come up.
|
|
59
|
+
// DAVINCI_RESOLVE_ADVANCED_ASSUME_NODE fakes the running version (tests);
|
|
60
|
+
// DAVINCI_RESOLVE_ADVANCED_NO_NODE_SEARCH=1 limits the search to the explicit
|
|
61
|
+
// override; the re-exec marks itself so a bad replacement cannot loop.
|
|
62
|
+
const runningNode = process.env.DAVINCI_RESOLVE_ADVANCED_ASSUME_NODE || process.versions.node;
|
|
63
|
+
const [major, minor] = runningNode.split('.').map(Number);
|
|
53
64
|
if (major < 20 || (major === 20 && minor < 9)) {
|
|
54
|
-
|
|
55
|
-
`[davinci-resolve-advanced-mcp] Node ${
|
|
65
|
+
const refusal = (extra = '') =>
|
|
66
|
+
`[davinci-resolve-advanced-mcp] Node ${runningNode} is below the ` +
|
|
56
67
|
`supported floor (>=20.9). This process was started by: ${process.execPath}\n` +
|
|
57
68
|
`Fix: point the MCP registration's command at a Node >=20.9 binary ` +
|
|
58
69
|
`(e.g. the absolute path from \`nvm which 20\`), or update the default ` +
|
|
59
70
|
`node on PATH. Re-running install.py also rewrites client configs with ` +
|
|
60
|
-
`an absolute, version-checked node path.\n
|
|
61
|
-
|
|
71
|
+
`an absolute, version-checked node path.\n${extra}`;
|
|
72
|
+
let picked = null;
|
|
73
|
+
const probed = [];
|
|
74
|
+
if (!process.env.DAVINCI_RESOLVE_ADVANCED_REEXEC) {
|
|
75
|
+
try {
|
|
76
|
+
// Sibling module; a managed install that only received this file falls
|
|
77
|
+
// through to the plain refusal instead of dying on the import.
|
|
78
|
+
const floor = await import(pathToFileURL(path.join(__dirname, 'node-floor.mjs')).href);
|
|
79
|
+
const candidates = process.env.DAVINCI_RESOLVE_ADVANCED_NO_NODE_SEARCH
|
|
80
|
+
? [process.env.DAVINCI_RESOLVE_NODE].filter(Boolean)
|
|
81
|
+
: floor.candidateNodeBinaries();
|
|
82
|
+
picked = floor.findReplacementNode({ candidates, probed });
|
|
83
|
+
} catch (err) {
|
|
84
|
+
probed.push({
|
|
85
|
+
path: '(search unavailable)',
|
|
86
|
+
version: null,
|
|
87
|
+
error: String(err && err.message ? err.message : err),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (picked) {
|
|
92
|
+
process.stderr.write(
|
|
93
|
+
`[davinci-resolve-advanced-mcp] Node ${runningNode} is below the supported floor (>=20.9); ` +
|
|
94
|
+
`re-executing under ${picked.path} (Node ${picked.version}). ` +
|
|
95
|
+
`Point the MCP registration's command at that path to skip this step.\n`,
|
|
96
|
+
);
|
|
97
|
+
const { spawnSync } = await import('node:child_process');
|
|
98
|
+
const child = spawnSync(picked.path, [fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
99
|
+
stdio: 'inherit',
|
|
100
|
+
env: {
|
|
101
|
+
...process.env,
|
|
102
|
+
DAVINCI_RESOLVE_ADVANCED_REEXEC: '1',
|
|
103
|
+
DAVINCI_RESOLVE_ADVANCED_ASSUME_NODE: '',
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
process.exit(child.status === null ? 1 : child.status);
|
|
107
|
+
}
|
|
108
|
+
const probedLine = probed.length
|
|
109
|
+
? `No usable replacement found; probed: ${probed.map((p) => `${p.path} (${p.version || (p.error ? p.error : 'not node')})`).join(', ')}\n`
|
|
110
|
+
: '';
|
|
111
|
+
process.stderr.write(refusal(probedLine));
|
|
62
112
|
process.exit(1);
|
|
63
113
|
}
|
|
64
114
|
|
|
115
|
+
// After the floor: reports the Node the server actually runs under, including
|
|
116
|
+
// a self-heal re-exec — the answer to "which node is the registration using?"
|
|
117
|
+
if (command === '--node-check') {
|
|
118
|
+
process.stdout.write(
|
|
119
|
+
`${JSON.stringify({ node: process.versions.node, execPath: process.execPath, reexec: process.env.DAVINCI_RESOLVE_ADVANCED_REEXEC === '1' })}\n`,
|
|
120
|
+
);
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
65
124
|
const advancedRoot = path.resolve(packageRoot, 'resolve-advanced');
|
|
66
125
|
const serverEntry = path.join(advancedRoot, 'server', 'index.mjs');
|
|
67
126
|
|
|
@@ -79,12 +138,12 @@ function missingRuntimePieces() {
|
|
|
79
138
|
const manifest = JSON.parse(fs.readFileSync(path.join(advancedRoot, 'package.json'), 'utf8'));
|
|
80
139
|
required = Object.keys(manifest.dependencies || {});
|
|
81
140
|
} catch {
|
|
82
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
what: `resolve-advanced/package.json (cannot tell which deps are required)`,
|
|
143
|
+
};
|
|
83
144
|
}
|
|
84
145
|
const modulesDir = path.join(advancedRoot, 'node_modules');
|
|
85
|
-
const missing = required.filter(
|
|
86
|
-
(dep) => !fs.existsSync(path.join(modulesDir, ...dep.split('/'))),
|
|
87
|
-
);
|
|
146
|
+
const missing = required.filter((dep) => !fs.existsSync(path.join(modulesDir, ...dep.split('/'))));
|
|
88
147
|
// Deps may also be hoisted above the package (an npm/npx install puts them in
|
|
89
148
|
// a parent node_modules), so an empty local node_modules is not conclusive —
|
|
90
149
|
// only report deps the resolver genuinely cannot see.
|
|
@@ -103,11 +162,11 @@ const gap = missingRuntimePieces();
|
|
|
103
162
|
if (gap) {
|
|
104
163
|
process.stderr.write(
|
|
105
164
|
`[davinci-resolve-advanced-mcp] cannot start: ${gap.what}.\n` +
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
165
|
+
`This install is at ${packageRoot}.\n` +
|
|
166
|
+
`Fix: run \`npx davinci-resolve-mcp setup\` to repair the managed install ` +
|
|
167
|
+
`(it syncs resolve-advanced/ and installs its Node dependencies), or run the ` +
|
|
168
|
+
`server straight from the package with ` +
|
|
169
|
+
`\`npx -y --package davinci-resolve-mcp davinci-resolve-advanced-mcp\`.\n`,
|
|
111
170
|
);
|
|
112
171
|
process.exit(1);
|
|
113
172
|
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-floor self-heal for the advanced launcher.
|
|
3
|
+
*
|
|
4
|
+
* The advanced server needs Node >=20.9 (sharp's own floor; better-sqlite3 is
|
|
5
|
+
* a native module built for one ABI). The failure that keeps recurring is not
|
|
6
|
+
* the floor — it is the MCP registration's `command` landing on an old nvm
|
|
7
|
+
* binary (v18.20.8 on this machine, twice) because a GUI app rewrote the
|
|
8
|
+
* config or a shell whose default lags ran the installer. Refusing with the
|
|
9
|
+
* fix printed is right, but a launcher that can SEE a suitable Node on the
|
|
10
|
+
* same machine should use it: find one, re-exec under it, and say so.
|
|
11
|
+
*
|
|
12
|
+
* Pure functions here so the search and the version rule are unit-testable
|
|
13
|
+
* without spawning anything; the launcher wires them to real probes.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
|
|
20
|
+
export const NODE_FLOOR = { major: 20, minor: 9 };
|
|
21
|
+
|
|
22
|
+
/** '20.19.0' → true, 'v18.20.8' → false, garbage → false. */
|
|
23
|
+
export function meetsFloor(version, floor = NODE_FLOOR) {
|
|
24
|
+
const m = String(version || '').match(/^v?(\d+)\.(\d+)/);
|
|
25
|
+
if (!m) return false;
|
|
26
|
+
const [major, minor] = [Number(m[1]), Number(m[2])];
|
|
27
|
+
return major > floor.major || (major === floor.major && minor >= floor.minor);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Numeric sort key for a version string; unparsable → [0,0,0]. */
|
|
31
|
+
function versionKey(v) {
|
|
32
|
+
const m = String(v || '').match(/^v?(\d+)\.(\d+)\.(\d+)/);
|
|
33
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : [0, 0, 0];
|
|
34
|
+
}
|
|
35
|
+
const byVersionDesc = (a, b) => {
|
|
36
|
+
const ka = versionKey(a);
|
|
37
|
+
const kb = versionKey(b);
|
|
38
|
+
for (let i = 0; i < 3; i += 1) if (ka[i] !== kb[i]) return kb[i] - ka[i];
|
|
39
|
+
return 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where a Node >=20.9 might live on this machine, best first:
|
|
44
|
+
* 1. DAVINCI_RESOLVE_NODE (an explicit operator choice)
|
|
45
|
+
* 2. nvm's versions dir ($NVM_DIR or ~/.nvm), newest version first
|
|
46
|
+
* 3. Homebrew / system / Windows default install paths
|
|
47
|
+
* The current executable is excluded — it is the one that failed.
|
|
48
|
+
*/
|
|
49
|
+
export function candidateNodeBinaries({
|
|
50
|
+
env = process.env,
|
|
51
|
+
home = env.HOME || env.USERPROFILE || '',
|
|
52
|
+
platform = process.platform,
|
|
53
|
+
execPath = process.execPath,
|
|
54
|
+
exists = fs.existsSync,
|
|
55
|
+
readdir = (d) => fs.readdirSync(d),
|
|
56
|
+
} = {}) {
|
|
57
|
+
const out = [];
|
|
58
|
+
const push = (p) => {
|
|
59
|
+
if (p && p !== execPath && !out.includes(p)) out.push(p);
|
|
60
|
+
};
|
|
61
|
+
// The explicit override is honored as given — even when it names the very
|
|
62
|
+
// binary that started us (a faked-version test, or an operator re-pointing
|
|
63
|
+
// the registration and forgetting to restart).
|
|
64
|
+
if (env.DAVINCI_RESOLVE_NODE) out.push(env.DAVINCI_RESOLVE_NODE);
|
|
65
|
+
const nvmDir = env.NVM_DIR || (home ? path.join(home, '.nvm') : '');
|
|
66
|
+
const versionsDir = nvmDir ? path.join(nvmDir, 'versions', 'node') : '';
|
|
67
|
+
if (versionsDir && exists(versionsDir)) {
|
|
68
|
+
let entries = [];
|
|
69
|
+
try {
|
|
70
|
+
entries = readdir(versionsDir);
|
|
71
|
+
} catch {
|
|
72
|
+
entries = [];
|
|
73
|
+
}
|
|
74
|
+
for (const v of [...entries].sort(byVersionDesc)) {
|
|
75
|
+
push(platform === 'win32' ? path.join(versionsDir, v, 'node.exe') : path.join(versionsDir, v, 'bin', 'node'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (platform === 'win32') {
|
|
79
|
+
for (const p of [
|
|
80
|
+
env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs', 'node.exe') : '',
|
|
81
|
+
env.LOCALAPPDATA ? path.join(env.LOCALAPPDATA, 'Programs', 'nodejs', 'node.exe') : '',
|
|
82
|
+
])
|
|
83
|
+
push(p);
|
|
84
|
+
} else {
|
|
85
|
+
for (const p of ['/opt/homebrew/bin/node', '/usr/local/bin/node', '/usr/bin/node', '/snap/bin/node']) push(p);
|
|
86
|
+
}
|
|
87
|
+
return out.filter((p) => exists(p));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Ask a binary for its Node version; null when it does not run or is not Node. */
|
|
91
|
+
export function probeNodeVersion(binary, run = spawnSync) {
|
|
92
|
+
try {
|
|
93
|
+
const r = run(binary, ['-p', 'process.versions.node'], {
|
|
94
|
+
encoding: 'utf8',
|
|
95
|
+
timeout: 5000,
|
|
96
|
+
});
|
|
97
|
+
if (r.status !== 0) return null;
|
|
98
|
+
const v = String(r.stdout || '').trim();
|
|
99
|
+
return /^\d+\.\d+\.\d+/.test(v) ? v : null;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* First candidate whose probed version meets the floor → {path, version}, or
|
|
107
|
+
* null. `probed` collects every attempt for the refusal message.
|
|
108
|
+
*/
|
|
109
|
+
export function findReplacementNode({ candidates, probe = probeNodeVersion, floor = NODE_FLOOR, probed = [] } = {}) {
|
|
110
|
+
for (const p of candidates || []) {
|
|
111
|
+
const version = probe(p);
|
|
112
|
+
probed.push({ path: p, version });
|
|
113
|
+
if (version && meetsFloor(version, floor)) return { path: p, version };
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "2.214.
|
|
40
|
+
VERSION = "2.214.4"
|
|
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
package/src/granular/common.py
CHANGED
|
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
|
|
|
87
87
|
handlers=[logging.StreamHandler()],
|
|
88
88
|
)
|
|
89
89
|
|
|
90
|
-
VERSION = "2.214.
|
|
90
|
+
VERSION = "2.214.4"
|
|
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.214.
|
|
14
|
+
VERSION = "2.214.4"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -185,10 +185,40 @@ if RESOLVE_MODULES_PATH not in sys.path:
|
|
|
185
185
|
|
|
186
186
|
log_dir = os.path.join(project_dir, "logs")
|
|
187
187
|
os.makedirs(log_dir, exist_ok=True)
|
|
188
|
+
|
|
189
|
+
#: Where this process logs. Unset: `logs/server.log` under the repository, as
|
|
190
|
+
#: always. A path: that file (its directory is created). Empty: no file at all.
|
|
191
|
+
#:
|
|
192
|
+
#: The variable exists because importing this module attaches the file handler,
|
|
193
|
+
#: and the offline unit suite imports this module. Without it the suite's
|
|
194
|
+
#: MagicMock "connections" and lifecycle warnings landed in the operator's real
|
|
195
|
+
#: server.log — 240 such lines in a 128 MB log on the maintainer's machine —
|
|
196
|
+
#: which is the file every live debugging session reads. `tests/__init__.py`
|
|
197
|
+
#: points it at a temporary file before any test module runs.
|
|
198
|
+
ENV_LOG_FILE = "RESOLVE_MCP_LOG_FILE"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _log_file_from_env(env=None) -> str:
|
|
202
|
+
"""The log path this process should use, or "" for none."""
|
|
203
|
+
values = os.environ if env is None else env
|
|
204
|
+
value = values.get(ENV_LOG_FILE)
|
|
205
|
+
if value is None:
|
|
206
|
+
return os.path.join(log_dir, "server.log")
|
|
207
|
+
return os.path.expanduser(value.strip())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _log_handlers():
|
|
211
|
+
target = _log_file_from_env()
|
|
212
|
+
if not target:
|
|
213
|
+
return [logging.NullHandler()]
|
|
214
|
+
os.makedirs(os.path.dirname(target) or ".", exist_ok=True)
|
|
215
|
+
return [logging.FileHandler(target)]
|
|
216
|
+
|
|
217
|
+
|
|
188
218
|
logging.basicConfig(
|
|
189
219
|
level=logging.INFO,
|
|
190
220
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
191
|
-
handlers=
|
|
221
|
+
handlers=_log_handlers(),
|
|
192
222
|
)
|
|
193
223
|
logger = logging.getLogger("resolve-mcp")
|
|
194
224
|
|