davinci-resolve-mcp 2.144.0 → 2.145.1

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,50 @@
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.145.1 — bridge config override, honored end to end
6
+
7
+ ### Fixed
8
+
9
+ - **The free-edition bridge installer now honors
10
+ `DAVINCI_RESOLVE_BRIDGE_CONFIG`** — adapted from
11
+ [PR #177](https://github.com/samuelgursky/davinci-resolve-mcp/pull/177) by
12
+ @altiss. The bridge client already resolved the override, but the installer
13
+ always wrote and embedded the fixed default path, so a client configured
14
+ with an override could look in one place while the in-app bridge was
15
+ installed with another — both sides "successfully" installed yet never
16
+ sharing a token. The installer resolves the effective path at call time
17
+ with the client's exact semantics (env wins, `~` expands, default
18
+ otherwise) across create, report, runtime copy, and launcher embed. One
19
+ correction on top of the PR: the step-4 probe guidance keeps pointing at
20
+ the FIXED default directory — the probe runs inside Resolve, which never
21
+ inherits the shell's environment, so it always writes there.
22
+
23
+ ## What's New in v2.145.0 — whole projects, assembled offline
24
+
25
+ ### Added
26
+
27
+ - **`drt.assemble_project`: multi-timeline .drp archives** — two or more
28
+ full assemble specs merge into one project file (reel-per-timeline conform
29
+ packages). Import as a project (`safe_project_import`); pull singles with
30
+ `extract_from_drp`. Live-proven: a two-reel .drp imported with both
31
+ timelines materialized and the second reel rendering its exact content.
32
+
33
+ ### The five merge laws (each found by a failed import)
34
+
35
+ 1. Template-fixed cluster identities collide — the pool clip, its version
36
+ table, container, sequence and track ids must remap; media pool element
37
+ ids are the deliberate exception (fixed by capture, so identical sources
38
+ dedup and MediaRefs keep pointing at the survivor).
39
+ 2. Keyed FieldsBlobs carry uuids as UTF-16 (`ActiveVer`, `SeqRef`) —
40
+ invisible to plaintext remaps; patched through the keyed codec.
41
+ 3. `project.xml`'s `<TimelineHandleVec>` is THE timeline registry: a pool
42
+ clip absent from it imports as pool furniture and never materializes.
43
+ 4. Folder children live INSIDE `<MediaVec>` — an element appended after its
44
+ close parses fine and is silently invisible.
45
+ 5. Media pool tags are `Sm2MpVideoClip`/`Sm2MpAudioClip`, not `Sm2MpMedia`
46
+ — an over-narrow tag set let media ids into the remap and every
47
+ `MediaRef` in the merged reel dangled (whole reel offline).
48
+
5
49
  ## What's New in v2.144.0 — turnover clip markers ride the items
6
50
 
7
51
  ### Added
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.144.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.145.1-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)
@@ -52,6 +52,11 @@ python scripts/install_resolve_bridge.py
52
52
  # restart Resolve, open a project, then: Workspace > Scripts > resolve_bridge
53
53
  ```
54
54
 
55
+ The installer and MCP client use `~/.config/davinci-resolve-mcp/bridge.json`
56
+ by default. To keep the authenticated bridge config elsewhere, set
57
+ `DAVINCI_RESOLVE_BRIDGE_CONFIG` while running the installer and in the MCP
58
+ client environment; both sides will use that path.
59
+
55
60
  Once that listener is running it is used **automatically** whenever external
56
61
  scripting is unavailable — no environment variable required. Setting
57
62
  `DAVINCI_RESOLVE_BRIDGE=1` *forces* the bridge instead: it becomes the only
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.144.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-2.145.1-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.144.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
15
+ > 本翻译对应 v2.145.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
16
16
 
17
17
  一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
18
18
 
@@ -61,6 +61,7 @@ window. `render.verify_output` covers the container-level checks.
61
61
  | Compound authoring | `spec.compounds` (multiple compose; nested edits render) | v2.131–2.132 |
62
62
  | Nested compounds | `compounds[].compounds` (depth-2 through depth-4 playback render-verified) | v2.134–2.141 |
63
63
  | Fusion titles | `elements: [{type:'title', text}]` — **21-gen hosts only** | v2.108 |
64
+ | Multi-timeline projects | `drt.assemble_project` (reel-per-timeline .drp; import as a project) | v2.145 |
64
65
 
65
66
  `assemble_from_interchange` drives the same engine from an EDL / OTIO /
66
67
  FCP7-XML / AAF / **.prproj** (Premiere, read offline — no Premiere needed)
package/install.py CHANGED
@@ -37,7 +37,7 @@ from src.utils.update_check import (
37
37
 
38
38
  # ─── Version ──────────────────────────────────────────────────────────────────
39
39
 
40
- VERSION = "2.144.0"
40
+ VERSION = "2.145.1"
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.144.0",
3
+ "version": "2.145.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -291,6 +291,167 @@ export const drtTool = {
291
291
  'Import with timeline.import_timeline_checked (timeline is named after the FILE). Dissolves/cross-fades, forward+reverse retimes, multi-track video, audio events and markers are AUTHORED when geometry allows; everything else drops WITH a reason — see `conform` for the ledger.',
292
292
  };
293
293
  }
294
+ if (action === 'assemble_project') {
295
+ // MULTI-TIMELINE archive (E85): assemble each timeline spec with the
296
+ // full single-timeline engine, then merge the later archives into the
297
+ // first — SeqContainers copied wholesale, MpFolder elements merged with
298
+ // DbId dedup (captured media templates carry FIXED pool DbIds, so the
299
+ // same source assembled twice IS the same element and every clip's
300
+ // MediaRef already points at it), and LocableBlobSet marker blobs
301
+ // carried over. Import the result as a PROJECT
302
+ // (project_manager.import_project / safe_project_import) — the
303
+ // single-timeline .drt import path only takes one timeline per file.
304
+ const p = z.object({
305
+ timelines: z.array(z.object({}).passthrough()).min(2)
306
+ .describe('Two or more assembleTimeline specs (same shape as `assemble` spec); timelineName required and unique per entry'),
307
+ outputPath: z.string().describe('Where the multi-timeline .drp is written'),
308
+ targetAppVersion: z.union([z.string(), z.number()]).optional(),
309
+ }).parse(args);
310
+ const names = p.timelines.map((t, i) => t.timelineName || `Timeline ${i + 1}`);
311
+ if (new Set(names).size !== names.length) throw new Error(`assemble_project: timelineName must be unique per timeline (got: ${names.join(', ')})`);
312
+ const { assembleTimeline } = drp();
313
+ const buffers = [];
314
+ for (const [i, spec] of p.timelines.entries()) {
315
+ const s = { ...spec, timelineName: names[i] };
316
+ if (s.templateVersion === undefined && p.targetAppVersion !== undefined) {
317
+ s.templateVersion = parseFloat(p.targetAppVersion) >= 21 ? 21 : 19;
318
+ }
319
+ buffers.push((await assembleTimeline(s)).buffer);
320
+ }
321
+ // Every assembly reuses the TEMPLATE's fixed identities across the
322
+ // whole TIMELINE CLUSTER — pool clip element (which nests a version
323
+ // table with back-references: MpTimelineClip, pActive, UniqueSequenceId,
324
+ // UniqueMediaPoolItemId, numeric IDs), the parent container, its shared
325
+ // sequence and track ids — so archives 2..n silently collide with
326
+ // archive 1 (measured twice: the merge imported as ONE timeline, first
327
+ // via container overwrite, then via UniqueMediaPoolItemId dedup).
328
+ // Rather than freshening named fields one by one, remap EVERY uuid the
329
+ // later archive's cluster shares with the first archive's cluster —
330
+ // except media-element ids, which are shared BY DESIGN (captured
331
+ // templates carry fixed pool DbIds so identical sources dedup and
332
+ // MediaRefs keep pointing at the survivor). The keyed SeqRef inside the
333
+ // pool clip's embedded sequence blob is patched through the codec.
334
+ const { createRequire } = await import('node:module');
335
+ const requireCjs = createRequire(import.meta.url);
336
+ const { decodeKeyedDict, encodeKeyedDict } = requireCjs('../../vendor/drp-format/keyed-dict.js');
337
+ const { randomUUID } = await import('node:crypto');
338
+ const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
339
+ const TL_EL_RE = /<Element>\s*<Sm2MpTimelineClip DbId="[^"]+">[\s\S]*?<\/Sm2MpTimelineClip>\s*<\/Element>/g;
340
+ // Media pool elements: Sm2MpVideoClip/Sm2MpAudioClip (captured
341
+ // template media — the tag is NOT Sm2MpMedia; measured the hard way
342
+ // when an over-narrow pattern left media ids in the remap set and
343
+ // every item MediaRef in the merged reel went dangling → offline)
344
+ // plus Sm2MpCompoundClip clusters.
345
+ const MEDIA_EL_RE = /<Element>\s*<Sm2Mp(?:VideoClip|AudioClip|Media|CompoundClip) DbId="[^"]+">[\s\S]*?<\/Sm2Mp(?:VideoClip|AudioClip|Media|CompoundClip)>\s*<\/Element>/g;
346
+ const base = await JSZip.loadAsync(buffers[0]);
347
+ const mpP = 'MediaPool/Master/MpFolder.xml';
348
+ let mpXml = await base.file(mpP).async('string');
349
+ let pjXml = await base.file('project.xml').async('string');
350
+ const baseIds = new Set((mpXml + pjXml).match(UUID_RE) || []);
351
+ for (const n of Object.keys(base.files)) {
352
+ if (!base.files[n].dir && /SeqContainer\/.+\.xml$/.test(n)) {
353
+ for (const id of (await base.file(n).async('string')).match(UUID_RE) || []) baseIds.add(id);
354
+ }
355
+ }
356
+ let nextPoolId = 101; // template pool clips carry <ID>100</ID>
357
+ for (const buf of buffers.slice(1)) {
358
+ const zip = await JSZip.loadAsync(buf);
359
+ let mp2 = await zip.file(mpP).async('string');
360
+ const mediaEls = mp2.match(MEDIA_EL_RE) || [];
361
+ const mediaIds = new Set(mediaEls.flatMap((el) => el.match(UUID_RE) || []));
362
+ const tlEl = (mp2.match(TL_EL_RE) || [])[0];
363
+ if (!tlEl) throw new Error('assemble_project: merged archive has no timeline pool clip');
364
+ // cluster text: the pool clip element + every SeqContainer entry
365
+ const containers = {};
366
+ for (const n of Object.keys(zip.files)) {
367
+ if (!zip.files[n].dir && /SeqContainer\/.+\.xml$/.test(n)) containers[n] = await zip.file(n).async('string');
368
+ }
369
+ const clusterText = tlEl + Object.values(containers).join('');
370
+ const remap = new Map();
371
+ for (const id of new Set(clusterText.match(UUID_RE) || [])) {
372
+ if (baseIds.has(id) && !mediaIds.has(id)) remap.set(id, randomUUID());
373
+ }
374
+ const apply = (text) => { let t = text; for (const [a, b] of remap) t = t.split(a).join(b); return t; };
375
+ // Keyed FieldsBlobs store uuid VALUES as UTF-16 — invisible to the
376
+ // plaintext remap. (Measured the hard way: the pool clip's top
377
+ // FieldsBlob is keyed {ActiveVer→uuid}; left unpatched it pointed
378
+ // REEL_02's active version at REEL_01's cluster and the timeline
379
+ // silently never materialized.) Decode every keyed blob, remap any
380
+ // string entry matching a remapped uuid, re-encode. Non-keyed blobs
381
+ // (zstd bodies etc.) decode-fail and pass through untouched.
382
+ const applyBlobs = (text) => text.replace(/<FieldsBlob>([0-9a-fA-F]+)<\/FieldsBlob>/g, (whole, hex) => {
383
+ try {
384
+ const dict = decodeKeyedDict(Buffer.from(hex, 'hex'));
385
+ let changed = false;
386
+ for (const e of dict.entries) {
387
+ if (typeof e.value === 'string' && remap.has(e.value)) { e.value = remap.get(e.value); changed = true; }
388
+ }
389
+ if (!changed) return whole;
390
+ return `<FieldsBlob>${encodeKeyedDict({ hdr: dict.hdr, entries: dict.entries }).toString('hex')}</FieldsBlob>`;
391
+ } catch { return whole; }
392
+ });
393
+ // pool clip: uuid remap + keyed SeqRef patch + fresh numeric IDs
394
+ let tlNew = applyBlobs(apply(tlEl)).replace(/<ID>\d+<\/ID>/g, `<ID>${nextPoolId}<\/ID>`.replace('<\/ID>', '</ID>'));
395
+ nextPoolId += 1;
396
+ const fbM = tlNew.match(/(<Sm2Sequence DbId="[^"]+">\s*<FieldsBlob>)([0-9a-fA-F]+)(<\/FieldsBlob>)/);
397
+ if (fbM) {
398
+ const dict = decodeKeyedDict(Buffer.from(fbM[2], 'hex'));
399
+ const seqRef = dict.entries.find((e) => e.key === 'SeqRef');
400
+ if (seqRef && remap.has(String(seqRef.value))) {
401
+ seqRef.value = remap.get(String(seqRef.value));
402
+ tlNew = tlNew.replace(fbM[0], `${fbM[1]}${encodeKeyedDict({ hdr: dict.hdr, entries: dict.entries }).toString('hex')}${fbM[3]}`);
403
+ }
404
+ }
405
+ for (const [n, xml] of Object.entries(containers)) {
406
+ const remapped = applyBlobs(apply(xml));
407
+ const cid = (remapped.match(/<Sm2SequenceContainer DbId="([^"]+)"/) || [])[1];
408
+ base.file(cid ? `SeqContainer/${cid}.xml` : n, remapped);
409
+ }
410
+ // Children live INSIDE <MediaVec> — an element appended after its
411
+ // close parses fine and is silently invisible (measured: the whole
412
+ // REEL_02 subtree vanished while every id chain checked out).
413
+ mpXml = mpXml.replace('</MediaVec>', `${tlNew}\n</MediaVec>`);
414
+ for (const el of mediaEls) {
415
+ const id = el.match(/DbId="([^"]+)"/)[1];
416
+ if (!mpXml.includes(`DbId="${id}"`)) mpXml = mpXml.replace('</MediaVec>', `${el}\n</MediaVec>`);
417
+ }
418
+ const pj2 = await zip.file('project.xml').async('string');
419
+ for (const blob of pj2.match(/<Element>\s*<Sm2(?:Sequence|TiItem)LockableBlob DbId="[^"]+">[\s\S]*?<\/Sm2(?:Sequence|TiItem)LockableBlob>\s*<\/Element>/g) || []) {
420
+ pjXml = pjXml.replace('</LocableBlobSet>', `${apply(blob)}</LocableBlobSet>`);
421
+ }
422
+ // THE timeline registry (found the hard way — two invisible merges):
423
+ // project.xml's <TimelineHandleVec> lists each pool clip's embedded
424
+ // Sm2Timeline DbId; a pool clip absent from the vec imports as pool
425
+ // furniture but never materializes as a timeline. Carry the later
426
+ // archive's handles over, remapped with its cluster.
427
+ const vec2 = pj2.match(/<TimelineHandleVec>([\s\S]*?)<\/TimelineHandleVec>/);
428
+ if (vec2) {
429
+ for (const h of vec2[1].match(/<Element>[^<]*<\/Element>/g) || []) {
430
+ pjXml = pjXml.replace('</TimelineHandleVec>', ` ${apply(h)}\n </TimelineHandleVec>`);
431
+ }
432
+ }
433
+ // future merges must not collide with THIS timeline's fresh ids either
434
+ for (const id of remap.values()) baseIds.add(id);
435
+ for (const id of clusterText.match(UUID_RE) || []) if (!remap.has(id)) baseIds.add(id);
436
+ }
437
+ base.file(mpP, mpXml);
438
+ base.file('project.xml', pjXml);
439
+ let outBuf = await base.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
440
+ let stamped = null;
441
+ if (p.targetAppVersion !== undefined) {
442
+ const targetPV = resolveTargetProjectVersion({ targetAppVersion: p.targetAppVersion });
443
+ const appVer = `${p.targetAppVersion}${'.0'.repeat(Math.max(0, 4 - String(p.targetAppVersion).split('.').length))}`;
444
+ const zip = await JSZip.loadAsync(outBuf);
445
+ const { out, elementPatches, stampPatches } = await applyVersionStamps(zip, targetPV, appVer);
446
+ outBuf = await out.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
447
+ stamped = { targetProjectVersion: targetPV, elementPatches, stampPatches };
448
+ }
449
+ await fs.writeFile(p.outputPath, outBuf);
450
+ return {
451
+ outputPath: p.outputPath, bytes: outBuf.length, timelines: names, stamped,
452
+ note: 'Import as a PROJECT (project_manager.safe_project_import); timeline.import_timeline_checked takes one timeline per file — use drt.extract_from_drp to pull singles.',
453
+ };
454
+ }
294
455
  if (action === 'assemble') {
295
456
  const p = assembleSchema.parse(args);
296
457
  // Native-schema authoring: template-spliced real Resolve structures
@@ -24,10 +24,17 @@ import sys
24
24
  from pathlib import Path
25
25
 
26
26
  REPO = Path(__file__).resolve().parent.parent
27
- CONFIG_DIR = Path.home() / ".config/davinci-resolve-mcp"
28
- CONFIG_PATH = CONFIG_DIR / "bridge.json"
27
+ DEFAULT_CONFIG_PATH = Path.home() / ".config/davinci-resolve-mcp/bridge.json"
28
+ ENV_CONFIG_PATH = "DAVINCI_RESOLVE_BRIDGE_CONFIG"
29
29
  DEFAULT_PORT = 49632
30
30
 
31
+
32
+ def config_path() -> Path:
33
+ """Bridge config written by this installer and read by the MCP client."""
34
+ override = os.environ.get(ENV_CONFIG_PATH)
35
+ return Path(override).expanduser() if override else DEFAULT_CONFIG_PATH
36
+
37
+
31
38
  _SCRIPTS_SUFFIX = "Library/Application Support/Blackmagic Design/DaVinci Resolve/Fusion/Scripts/Utility"
32
39
 
33
40
  #: A sandboxed (App Store) build sees its container as HOME, so Resolve's
@@ -448,10 +455,11 @@ def python_preflight() -> dict:
448
455
 
449
456
 
450
457
  def ensure_config(port: int, rotate: bool) -> dict:
458
+ path = config_path()
451
459
  existing: dict = {}
452
- if CONFIG_PATH.exists():
460
+ if path.exists():
453
461
  try:
454
- existing = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
462
+ existing = json.loads(path.read_text(encoding="utf-8"))
455
463
  except (OSError, ValueError):
456
464
  existing = {}
457
465
  token = existing.get("token")
@@ -469,12 +477,12 @@ def ensure_config(port: int, rotate: bool) -> dict:
469
477
  "allowed_media_roots": existing_media or [str(Path.home())],
470
478
  "allowed_output_roots": existing_output or [str(Path.home() / "Movies")],
471
479
  }
472
- CONFIG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
473
- tmp = CONFIG_PATH.with_suffix(".tmp")
480
+ path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
481
+ tmp = path.with_suffix(".tmp")
474
482
  tmp.write_text(json.dumps(config, indent=2, sort_keys=True), encoding="utf-8")
475
483
  os.chmod(tmp, 0o600)
476
- tmp.replace(CONFIG_PATH)
477
- os.chmod(CONFIG_PATH, 0o600)
484
+ tmp.replace(path)
485
+ os.chmod(path, 0o600)
478
486
  return config
479
487
 
480
488
 
@@ -525,8 +533,9 @@ def install(*, probe_only: bool, port: int, rotate: bool) -> dict:
525
533
  )
526
534
  result["skipped"] = [path for path, _ in skipped]
527
535
  if not probe_only:
528
- loaded = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
529
- result["config"] = {"path": str(CONFIG_PATH),
536
+ path = config_path()
537
+ loaded = json.loads(path.read_text(encoding="utf-8"))
538
+ result["config"] = {"path": str(path),
530
539
  **{k: v for k, v in loaded.items() if k != "token"}}
531
540
  return result
532
541
 
@@ -558,9 +567,10 @@ def _install_to(target: Path, *, probe_only: bool, installed: list[str]) -> None
558
567
  # unreachable by construction (measured: "Operation not permitted").
559
568
  # The runtime dir is already inside the container, so the config goes
560
569
  # beside the modules. Same token, so the out-of-sandbox client still
561
- # authenticates against ~/.config.
570
+ # authenticates against the selected config path.
562
571
  runtime_config = runtime / "bridge.json"
563
- shutil.copy2(CONFIG_PATH, runtime_config)
572
+ path = config_path()
573
+ shutil.copy2(path, runtime_config)
564
574
  os.chmod(runtime_config, 0o600)
565
575
  installed.append(str(runtime_config))
566
576
  # The launcher IS the menu entry. Without it the modules sit in the
@@ -571,7 +581,7 @@ def _install_to(target: Path, *, probe_only: bool, installed: list[str]) -> None
571
581
  base64.urlsafe_b64encode(str(runtime).encode("utf-8")).decode("ascii"),
572
582
  ).replace(
573
583
  "@@CONFIG_PATH_B64@@",
574
- base64.urlsafe_b64encode(str(CONFIG_PATH).encode("utf-8")).decode("ascii"),
584
+ base64.urlsafe_b64encode(str(path).encode("utf-8")).decode("ascii"),
575
585
  )
576
586
  launcher_path = target / "resolve_bridge.py"
577
587
  launcher_path.write_text(launcher, encoding="utf-8")
@@ -605,6 +615,9 @@ def main() -> int:
605
615
  print(" 1. Restart DaVinci Resolve so it re-scans the Scripts folders.")
606
616
  print(" 2. Open a saved project (the Scripts menu is empty in Project Manager).")
607
617
  print(" 3. Workspace > Scripts > resolve_bridge_probe — run it TWICE.")
618
+ # The probe runs INSIDE Resolve, which never sees the shell's
619
+ # DAVINCI_RESOLVE_BRIDGE_CONFIG — it always writes to the fixed default
620
+ # directory, so the guidance must not follow the override.
608
621
  print(" 4. Read ~/.config/davinci-resolve-mcp/host-model-probe.json")
609
622
  return 0
610
623
 
@@ -87,7 +87,7 @@ if not logging.getLogger().handlers:
87
87
  handlers=[logging.StreamHandler()],
88
88
  )
89
89
 
90
- VERSION = "2.144.0"
90
+ VERSION = "2.145.1"
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.144.0"
14
+ VERSION = "2.145.1"
15
15
 
16
16
  import base64
17
17
  import os