davinci-resolve-mcp 4.13.6 → 4.13.7
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 +39 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +5 -1
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +73 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
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.13.7 — Missing-media relink works for paths written on another OS
|
|
6
|
+
|
|
7
|
+
Lands #294, the follow-up to #293 for the Windows/macOS path work tracked in
|
|
8
|
+
#278. A project moved between macOS and Windows keeps the paths the other OS
|
|
9
|
+
wrote, so the missing-media diagnosis and relink plan now go by the path's own
|
|
10
|
+
form.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Wanted basename (`src/server.py`, `_missing_media_diagnosis` and
|
|
15
|
+
`_build_relink_plan`)**: both used `os.path.basename`, which splits on the
|
|
16
|
+
running OS's separator only. On macOS the basename of `E:\DCIM\P1047043.MOV`
|
|
17
|
+
was the whole string, so a bounded search could never match. Both now split
|
|
18
|
+
on `/` and `\`.
|
|
19
|
+
- **Volumes from another OS**: after #293, `E:\` on macOS (or `/Volumes/X` on
|
|
20
|
+
Windows) counted as an unmounted volume. That skipped the relink search and
|
|
21
|
+
advised mounting a volume this OS can never mount. Such roots are now flagged
|
|
22
|
+
`foreign_os_path` on volume, folder, item and candidate rows, and are never
|
|
23
|
+
probed with `isdir`. They report the new
|
|
24
|
+
`primary_cause="foreign_os_path"`, which advises searching the approved roots,
|
|
25
|
+
and they no longer trigger `skip_search_when_volume_missing`. A same-OS
|
|
26
|
+
unmounted volume behaves as before. When both kinds are present, the advice
|
|
27
|
+
names both.
|
|
28
|
+
- **Sanitized relink plans**: with `sanitized=true`, `file_path` is dropped, so
|
|
29
|
+
the plan searched by the Resolve clip name. It now takes the basename from
|
|
30
|
+
`file_path_sanitized`, which keeps the filename.
|
|
31
|
+
|
|
32
|
+
### Documentation
|
|
33
|
+
|
|
34
|
+
- `docs/SKILL.md` documents `foreign_os_path`.
|
|
35
|
+
|
|
36
|
+
### Validation
|
|
37
|
+
|
|
38
|
+
- New `ForeignOsMediaPathTests` and basename/form tables pin the host OS, so
|
|
39
|
+
Linux, macOS and Windows CI all run both directions. Linux CI green on the PR.
|
|
40
|
+
- Full offline suite and release static checks pass on macOS.
|
|
41
|
+
- Diagnosis only, with no Resolve API call changed, so a live test is not
|
|
42
|
+
required.
|
|
43
|
+
|
|
5
44
|
## What's New in v4.13.6 — `drt` downgrade stamps the real Resolve build
|
|
6
45
|
|
|
7
46
|
### 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)
|
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.13.
|
|
15
|
+
> 本翻译对应 v4.13.7 版 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
|
@@ -1806,7 +1806,11 @@ missing volume roots, sample basenames, and a recommended next step. If a source
|
|
|
1806
1806
|
volume such as a camera card is not mounted, `build_relink_plan` skips broad
|
|
1807
1807
|
search by default and reports `skip_reason="missing_source_volume_not_mounted"`;
|
|
1808
1808
|
mount the volume or pass `skip_search_when_volume_missing=false` only when a
|
|
1809
|
-
bounded scan of approved roots is intentional.
|
|
1809
|
+
bounded scan of approved roots is intentional. Paths written on another OS
|
|
1810
|
+
(`E:\...` on macOS, `/Volumes/...` on Windows, after a project moves between
|
|
1811
|
+
machines) are flagged `foreign_os_path`: their volume can never be mounted
|
|
1812
|
+
here, so they report `primary_cause="foreign_os_path"`, do not hold back the
|
|
1813
|
+
search, and are matched by their own basename under the approved roots.
|
|
1810
1814
|
|
|
1811
1815
|
Audio / Fairlight kernel actions (v2.14.0+) add live-tested audio state,
|
|
1812
1816
|
mapping, voice-isolation, sync, transcription, subtitle, and Fairlight boundary
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.13.
|
|
40
|
+
VERSION = "4.13.7"
|
|
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
|
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
|
|
|
91
91
|
handlers=[logging.StreamHandler()],
|
|
92
92
|
)
|
|
93
93
|
|
|
94
|
-
VERSION = "4.13.
|
|
94
|
+
VERSION = "4.13.7"
|
|
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.13.
|
|
14
|
+
VERSION = "4.13.7"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -21,6 +21,7 @@ import inspect
|
|
|
21
21
|
import json
|
|
22
22
|
import logging
|
|
23
23
|
import math
|
|
24
|
+
import ntpath
|
|
24
25
|
import platform
|
|
25
26
|
import re
|
|
26
27
|
import shutil
|
|
@@ -8465,6 +8466,43 @@ def _media_path_parts(text: str) -> List[str]:
|
|
|
8465
8466
|
return [part for part in re.split(r"[\\/]", text) if part]
|
|
8466
8467
|
|
|
8467
8468
|
|
|
8469
|
+
def _media_path_basename(path: Any) -> str:
|
|
8470
|
+
# `os.path.basename` splits on the running OS's separator only, so on macOS
|
|
8471
|
+
# the basename of `E:\DCIM\P1047043.MOV` is the whole string and a search
|
|
8472
|
+
# for it can never match. Go by the path's own form instead.
|
|
8473
|
+
parts = _media_path_parts(str(path or ""))
|
|
8474
|
+
return parts[-1] if parts else ""
|
|
8475
|
+
|
|
8476
|
+
|
|
8477
|
+
def _media_path_form(path: Any) -> Optional[str]:
|
|
8478
|
+
# "windows" for a drive letter or UNC share, "posix" for a /-rooted path,
|
|
8479
|
+
# None for a relative path, which says nothing about the OS that wrote it.
|
|
8480
|
+
text = str(path or "")
|
|
8481
|
+
if text.startswith("\\\\") or _WINDOWS_DRIVE_PATH.match(text):
|
|
8482
|
+
return "windows"
|
|
8483
|
+
if text.startswith("/"):
|
|
8484
|
+
return "posix"
|
|
8485
|
+
return None
|
|
8486
|
+
|
|
8487
|
+
|
|
8488
|
+
def _host_media_path_form() -> str:
|
|
8489
|
+
return "windows" if os.name == "nt" else "posix"
|
|
8490
|
+
|
|
8491
|
+
|
|
8492
|
+
def _media_path_is_foreign(path: Any) -> bool:
|
|
8493
|
+
# A path another OS wrote: `E:\` on macOS, `/Volumes/X` on Windows. Its
|
|
8494
|
+
# volume can never be mounted on this machine, so "mount it" is the wrong
|
|
8495
|
+
# advice; the media has to be found under a root this OS can see.
|
|
8496
|
+
form = _media_path_form(path)
|
|
8497
|
+
return form is not None and form != _host_media_path_form()
|
|
8498
|
+
|
|
8499
|
+
|
|
8500
|
+
def _media_path_dirname(text: str) -> str:
|
|
8501
|
+
# ntpath understands drive letters, UNC shares and both separators on any
|
|
8502
|
+
# OS; posix-form paths keep the running OS's own dirname.
|
|
8503
|
+
return ntpath.dirname(text) if _media_path_form(text) == "windows" else os.path.dirname(text)
|
|
8504
|
+
|
|
8505
|
+
|
|
8468
8506
|
def _sanitize_media_path(path: Any, *, keep_filename: bool = True) -> Optional[str]:
|
|
8469
8507
|
if not path:
|
|
8470
8508
|
return None
|
|
@@ -8508,7 +8546,8 @@ def _missing_media_diagnosis(missing_rows: List[Dict[str, Any]]) -> Dict[str, An
|
|
|
8508
8546
|
for row in missing_rows:
|
|
8509
8547
|
file_path = row.get("file_path")
|
|
8510
8548
|
media_pool_item_id = row.get("media_pool_item_id") or file_path or row.get("name")
|
|
8511
|
-
basename =
|
|
8549
|
+
basename = _media_path_basename(file_path or row.get("media_pool_item_name") or row.get("name"))
|
|
8550
|
+
foreign = _media_path_is_foreign(file_path)
|
|
8512
8551
|
item = by_media_pool_item.setdefault(
|
|
8513
8552
|
str(media_pool_item_id),
|
|
8514
8553
|
{
|
|
@@ -8517,6 +8556,7 @@ def _missing_media_diagnosis(missing_rows: List[Dict[str, Any]]) -> Dict[str, An
|
|
|
8517
8556
|
"wanted_basename": basename,
|
|
8518
8557
|
"file_path_sanitized": _sanitize_media_path(file_path),
|
|
8519
8558
|
"volume_root_sanitized": _sanitize_media_path(_media_path_volume_root(file_path), keep_filename=False),
|
|
8559
|
+
"foreign_os_path": foreign,
|
|
8520
8560
|
"timeline_occurrence_count": 0,
|
|
8521
8561
|
"track_refs": [],
|
|
8522
8562
|
},
|
|
@@ -8536,7 +8576,11 @@ def _missing_media_diagnosis(missing_rows: List[Dict[str, Any]]) -> Dict[str, An
|
|
|
8536
8576
|
volume_root,
|
|
8537
8577
|
{
|
|
8538
8578
|
"volume_root_sanitized": _sanitize_media_path(volume_root, keep_filename=False),
|
|
8539
|
-
|
|
8579
|
+
# Never probed: this OS reads the name as something else
|
|
8580
|
+
# (`/Volumes/X` is a folder on the current Windows drive,
|
|
8581
|
+
# `E:\` a relative name on macOS).
|
|
8582
|
+
"mounted": False if foreign else os.path.isdir(volume_root),
|
|
8583
|
+
"foreign_os_path": foreign,
|
|
8540
8584
|
"clip_count": 0,
|
|
8541
8585
|
"sample_basenames": [],
|
|
8542
8586
|
},
|
|
@@ -8545,13 +8589,14 @@ def _missing_media_diagnosis(missing_rows: List[Dict[str, Any]]) -> Dict[str, An
|
|
|
8545
8589
|
if basename and basename not in volume_row["sample_basenames"] and len(volume_row["sample_basenames"]) < 8:
|
|
8546
8590
|
volume_row["sample_basenames"].append(basename)
|
|
8547
8591
|
|
|
8548
|
-
folder =
|
|
8592
|
+
folder = _media_path_dirname(str(file_path)) if file_path else ""
|
|
8549
8593
|
if folder:
|
|
8550
8594
|
folder_row = missing_folders.setdefault(
|
|
8551
8595
|
folder,
|
|
8552
8596
|
{
|
|
8553
8597
|
"folder_sanitized": _sanitize_media_path(folder, keep_filename=False),
|
|
8554
|
-
"exists": os.path.isdir(folder),
|
|
8598
|
+
"exists": False if foreign else os.path.isdir(folder),
|
|
8599
|
+
"foreign_os_path": foreign,
|
|
8555
8600
|
"clip_count": 0,
|
|
8556
8601
|
"sample_basenames": [],
|
|
8557
8602
|
},
|
|
@@ -8563,14 +8608,28 @@ def _missing_media_diagnosis(missing_rows: List[Dict[str, Any]]) -> Dict[str, An
|
|
|
8563
8608
|
unique_items = list(by_media_pool_item.values())
|
|
8564
8609
|
missing_volume_rows = list(missing_volumes.values())
|
|
8565
8610
|
missing_folder_rows = list(missing_folders.values())
|
|
8566
|
-
|
|
8611
|
+
foreign_volumes = [row for row in missing_volume_rows if row.get("foreign_os_path")]
|
|
8612
|
+
absent_volumes = [row for row in missing_volume_rows if not row.get("mounted") and not row.get("foreign_os_path")]
|
|
8567
8613
|
absent_folders = [row for row in missing_folder_rows if not row.get("exists")]
|
|
8614
|
+
foreign_labels = ", ".join(row["volume_root_sanitized"] for row in foreign_volumes[:3])
|
|
8568
8615
|
primary_cause = "none"
|
|
8569
8616
|
recommended_next_step = "No offline media detected."
|
|
8570
8617
|
if absent_volumes:
|
|
8571
8618
|
primary_cause = "volume_not_mounted"
|
|
8572
8619
|
labels = ", ".join(row["volume_root_sanitized"] for row in absent_volumes[:3])
|
|
8573
8620
|
recommended_next_step = f"Mount the missing volume(s): {labels}."
|
|
8621
|
+
if foreign_volumes:
|
|
8622
|
+
recommended_next_step += (
|
|
8623
|
+
f" Paths written on another operating system ({foreign_labels}) cannot be mounted here;"
|
|
8624
|
+
" relink those from approved media roots by basename."
|
|
8625
|
+
)
|
|
8626
|
+
elif foreign_volumes:
|
|
8627
|
+
primary_cause = "foreign_os_path"
|
|
8628
|
+
recommended_next_step = (
|
|
8629
|
+
f"These media paths were written on another operating system, so their volume(s)"
|
|
8630
|
+
f" ({foreign_labels}) cannot be mounted here. Search approved media roots for the wanted basenames"
|
|
8631
|
+
" (build_relink_plan), then relink with media_pool.safe_relink."
|
|
8632
|
+
)
|
|
8574
8633
|
elif absent_folders:
|
|
8575
8634
|
primary_cause = "folder_not_found"
|
|
8576
8635
|
labels = ", ".join(row["folder_sanitized"] for row in absent_folders[:3])
|
|
@@ -8731,8 +8790,10 @@ def _build_relink_plan(tl, p: Dict[str, Any]):
|
|
|
8731
8790
|
return _err(f"search_roots must be existing directories: {invalid}")
|
|
8732
8791
|
missing_report = _detect_missing_media(tl, p)
|
|
8733
8792
|
diagnosis = missing_report.get("diagnosis") or _missing_media_diagnosis(missing_report.get("missing", []))
|
|
8793
|
+
# A volume another OS wrote can never be mounted here, so it never holds the
|
|
8794
|
+
# search back: approved roots are the only place its media can turn up.
|
|
8734
8795
|
if _coerce_bool(p.get("skip_search_when_volume_missing"), True) and any(
|
|
8735
|
-
not row.get("mounted") for row in diagnosis.get("missing_volumes", [])
|
|
8796
|
+
not row.get("mounted") and not row.get("foreign_os_path") for row in diagnosis.get("missing_volumes", [])
|
|
8736
8797
|
):
|
|
8737
8798
|
return {
|
|
8738
8799
|
"success": True,
|
|
@@ -8749,7 +8810,10 @@ def _build_relink_plan(tl, p: Dict[str, Any]):
|
|
|
8749
8810
|
candidates = []
|
|
8750
8811
|
rows_by_basename: Dict[str, List[Dict[str, Any]]] = {}
|
|
8751
8812
|
for row in missing_report.get("missing", []):
|
|
8752
|
-
|
|
8813
|
+
# A sanitized report drops `file_path`; its sanitized form keeps the
|
|
8814
|
+
# filename and the root's form, which is all the search needs.
|
|
8815
|
+
path = row.get("file_path") or row.get("file_path_sanitized")
|
|
8816
|
+
wanted = _media_path_basename(path or row.get("media_pool_item_name") or row.get("name"))
|
|
8753
8817
|
if wanted:
|
|
8754
8818
|
rows_by_basename.setdefault(wanted, []).append(row)
|
|
8755
8819
|
for wanted, rows in rows_by_basename.items():
|
|
@@ -8763,6 +8827,7 @@ def _build_relink_plan(tl, p: Dict[str, Any]):
|
|
|
8763
8827
|
{
|
|
8764
8828
|
**row,
|
|
8765
8829
|
"wanted_basename": wanted,
|
|
8830
|
+
"foreign_os_path": _media_path_is_foreign(row.get("file_path") or row.get("file_path_sanitized")),
|
|
8766
8831
|
"timeline_occurrence_count": len(rows),
|
|
8767
8832
|
"candidate_paths": [] if _coerce_bool(p.get("sanitize_paths")) or _coerce_bool(p.get("sanitized")) else matches,
|
|
8768
8833
|
"candidate_paths_sanitized": sanitized_matches,
|