davinci-resolve-mcp 4.13.0 → 4.13.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 +45 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +5 -2
- package/src/utils/mcp_transport.py +7 -0
- package/src/utils/proc.py +60 -1
- package/src/utils/resolve_busy.py +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,51 @@
|
|
|
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.1 — Windows: liveness checks no longer kill the process they check, and the offline suite runs clean
|
|
6
|
+
|
|
7
|
+
Closes #278, reported by @AllastorV with the failures grouped by cause. Nobody
|
|
8
|
+
here runs Windows, so everything below was worked out from the code and
|
|
9
|
+
simulated on macOS: the Windows-only API calls are covered by tests that run
|
|
10
|
+
only on Windows.
|
|
11
|
+
|
|
12
|
+
### Fixes
|
|
13
|
+
|
|
14
|
+
- **On Windows, three pid liveness checks terminated the process they asked
|
|
15
|
+
about.** Python's `os.kill(pid, 0)` is a harmless existence check on POSIX,
|
|
16
|
+
but on Windows any signal other than `CTRL_C_EVENT`/`CTRL_BREAK_EVENT` goes
|
|
17
|
+
through `TerminateProcess`. `resolve_busy._pid_alive` (the busy sidecar),
|
|
18
|
+
`mcp_transport.read_transport_state` (the networked-transport state file)
|
|
19
|
+
and `server._control_panel_pid_alive` (the control-panel pidfile) used it,
|
|
20
|
+
and all three are routinely asked about the server's own pid. On Windows they
|
|
21
|
+
now use `proc.windows_pid_alive` (`OpenProcess` + `WaitForSingleObject`, the
|
|
22
|
+
approach the bridge already used); POSIX is unchanged. A new AST guard fails
|
|
23
|
+
the suite if a signal-0 probe lands in `src/` without a Windows branch.
|
|
24
|
+
- **`.gitattributes` pins `eol=lf` on the 32 generated agent-rule files**, so
|
|
25
|
+
a default `core.autocrlf=true` checkout no longer fails
|
|
26
|
+
`generate.mjs --check`. Existing Windows clones need
|
|
27
|
+
`git add --renormalize .` or a fresh checkout.
|
|
28
|
+
|
|
29
|
+
### Tests (Windows portability, no behaviour change)
|
|
30
|
+
|
|
31
|
+
- **SQLite handles closed before temp dirs are removed** (56 tests across
|
|
32
|
+
`test_resolve_ai_ledger`, `test_resolve_ai_governance`,
|
|
33
|
+
`test_resolve21_actions`, `test_provenance`, `MediaAnalysisPlanningTests` and
|
|
34
|
+
others). Each class follows the existing pattern, closing with
|
|
35
|
+
`timeline_brain_db.close_all` registered so it runs before the removal. No
|
|
36
|
+
central hook was added, so a test that leaks a handle still shows. The list
|
|
37
|
+
came from a macOS run that flags any removal under a live connection: 56
|
|
38
|
+
before, 0 after.
|
|
39
|
+
- Path expectations are built the way the code builds them, not as POSIX
|
|
40
|
+
strings (`test_cdl_and_install_config` and others).
|
|
41
|
+
- Symlink-dependent cases skip, with the `OSError` as the reason, when the
|
|
42
|
+
process cannot create symlinks.
|
|
43
|
+
- One-offs: `sys.executable` instead of `python3`/`cat`; `tempfile.gettempdir()`
|
|
44
|
+
instead of a literal `/tmp`; the home directory stays resolvable
|
|
45
|
+
(`USERPROFILE` as well as `HOME`); sandbox-container installer tests pinned
|
|
46
|
+
to macOS; the unwritable-target installer tests reworked so they still test
|
|
47
|
+
Windows; a stale `taskkill` expectation updated; `proc.kill()` instead of
|
|
48
|
+
`SIGKILL`.
|
|
49
|
+
|
|
5
50
|
## What's New in v4.13.0 — Resolve 21.1 render-setting parity, `SetHighPriority(bool)`, and a project-destroying preset order
|
|
6
51
|
|
|
7
52
|
Lands the three items queued for @legionsound in #207. The 21.1 behaviour
|
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.1 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
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.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
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.1"
|
|
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.1"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -75,7 +75,7 @@ from src.utils.page_lock import (
|
|
|
75
75
|
restoring_page as _restoring_page,
|
|
76
76
|
page_lock as _page_lock,
|
|
77
77
|
)
|
|
78
|
-
from src.utils.proc import safe_run
|
|
78
|
+
from src.utils.proc import safe_run, windows_pid_alive
|
|
79
79
|
from src.utils.readback import verify_by_readback, verification_stats as _verification_stats
|
|
80
80
|
from src.utils import operation_result as _operation_result
|
|
81
81
|
from src.utils import operation_log as _operation_log
|
|
@@ -18015,6 +18015,9 @@ def _control_panel_pid_alive(pid: int) -> bool:
|
|
|
18015
18015
|
"""Check whether a PID is alive on this OS without killing it."""
|
|
18016
18016
|
if not pid or pid <= 0:
|
|
18017
18017
|
return False
|
|
18018
|
+
if os.name == "nt":
|
|
18019
|
+
# os.kill(pid, 0) is TerminateProcess on Windows, not a probe.
|
|
18020
|
+
return windows_pid_alive(pid) is True
|
|
18018
18021
|
try:
|
|
18019
18022
|
os.kill(pid, 0) # signal 0 = existence check
|
|
18020
18023
|
except (ProcessLookupError, PermissionError):
|
|
@@ -28,6 +28,7 @@ import sys
|
|
|
28
28
|
import time
|
|
29
29
|
|
|
30
30
|
from src.utils.private_state import private_state_dir, write_private_json
|
|
31
|
+
from src.utils.proc import windows_pid_alive
|
|
31
32
|
|
|
32
33
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
33
34
|
|
|
@@ -172,6 +173,12 @@ def read_transport_state():
|
|
|
172
173
|
return None
|
|
173
174
|
pid = state.get("pid")
|
|
174
175
|
if isinstance(pid, int):
|
|
176
|
+
if os.name == "nt":
|
|
177
|
+
# os.kill(pid, 0) is TerminateProcess on Windows, not a probe: the
|
|
178
|
+
# state file normally names the running server itself.
|
|
179
|
+
if windows_pid_alive(pid) is not True:
|
|
180
|
+
return None
|
|
181
|
+
return state
|
|
175
182
|
try:
|
|
176
183
|
os.kill(pid, 0)
|
|
177
184
|
except (OSError, ProcessLookupError):
|
package/src/utils/proc.py
CHANGED
|
@@ -5,9 +5,12 @@ process that inherits stdin can race-read bytes off the protocol stream and
|
|
|
5
5
|
corrupt it; ``capture_output`` only redirects stdout/stderr. These wrappers
|
|
6
6
|
default ``stdin`` to ``DEVNULL`` so subprocess hygiene is centralized rather
|
|
7
7
|
than re-applied at every call site.
|
|
8
|
+
|
|
9
|
+
``windows_pid_alive`` lives here too: the Windows replacement for the
|
|
10
|
+
``os.kill(pid, 0)`` liveness probe, which on Windows terminates the process.
|
|
8
11
|
"""
|
|
9
12
|
import subprocess
|
|
10
|
-
from typing import Any
|
|
13
|
+
from typing import Any, Optional
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
def safe_run(*args: Any, **kwargs: Any) -> "subprocess.CompletedProcess":
|
|
@@ -24,3 +27,59 @@ def safe_popen(*args: Any, **kwargs: Any) -> "subprocess.Popen":
|
|
|
24
27
|
"""subprocess.Popen with stdin defaulted to DEVNULL (override by passing stdin)."""
|
|
25
28
|
kwargs.setdefault("stdin", subprocess.DEVNULL)
|
|
26
29
|
return subprocess.Popen(*args, **kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
#: OpenProcess access rights and status codes used by `windows_pid_alive`.
|
|
33
|
+
_WIN_SYNCHRONIZE = 0x00100000
|
|
34
|
+
_WIN_PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
|
35
|
+
_WIN_WAIT_OBJECT_0 = 0x0
|
|
36
|
+
_WIN_WAIT_TIMEOUT = 0x102
|
|
37
|
+
_WIN_ERROR_ACCESS_DENIED = 5
|
|
38
|
+
_WIN_ERROR_INVALID_PARAMETER = 87
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def windows_pid_alive(pid: int) -> Optional[bool]:
|
|
42
|
+
"""Whether `pid` is a running process on Windows, without touching it.
|
|
43
|
+
|
|
44
|
+
The POSIX liveness probe, ``os.kill(pid, 0)``, must never run on Windows:
|
|
45
|
+
there any signal other than CTRL_C_EVENT/CTRL_BREAK_EVENT goes through
|
|
46
|
+
``TerminateProcess``, so the "existence check" kills the process it asks
|
|
47
|
+
about — including this one, when a record names our own pid.
|
|
48
|
+
|
|
49
|
+
True: running (access-denied counts, since only a live process can refuse).
|
|
50
|
+
False: no such process, or it has exited. None: could not be determined.
|
|
51
|
+
"""
|
|
52
|
+
if not isinstance(pid, int) or pid <= 0:
|
|
53
|
+
return False
|
|
54
|
+
try:
|
|
55
|
+
import ctypes
|
|
56
|
+
from ctypes import wintypes
|
|
57
|
+
|
|
58
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
59
|
+
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
60
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
61
|
+
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
62
|
+
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
|
63
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
64
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
65
|
+
|
|
66
|
+
access = _WIN_SYNCHRONIZE | _WIN_PROCESS_QUERY_LIMITED_INFORMATION
|
|
67
|
+
handle = kernel32.OpenProcess(access, False, pid)
|
|
68
|
+
if not handle:
|
|
69
|
+
error = ctypes.get_last_error()
|
|
70
|
+
if error == _WIN_ERROR_INVALID_PARAMETER:
|
|
71
|
+
return False
|
|
72
|
+
if error == _WIN_ERROR_ACCESS_DENIED:
|
|
73
|
+
return True
|
|
74
|
+
return None
|
|
75
|
+
try:
|
|
76
|
+
status = kernel32.WaitForSingleObject(handle, 0)
|
|
77
|
+
finally:
|
|
78
|
+
kernel32.CloseHandle(handle)
|
|
79
|
+
if status == _WIN_WAIT_OBJECT_0:
|
|
80
|
+
return False # signalled == exited
|
|
81
|
+
if status == _WIN_WAIT_TIMEOUT:
|
|
82
|
+
return True
|
|
83
|
+
return None
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
@@ -34,6 +34,8 @@ import time
|
|
|
34
34
|
from contextlib import contextmanager
|
|
35
35
|
from typing import Any, Dict, Optional
|
|
36
36
|
|
|
37
|
+
from src.utils.proc import windows_pid_alive
|
|
38
|
+
|
|
37
39
|
_SIDECAR = os.path.join(tempfile.gettempdir(), "davinci_resolve_mcp_busy.json")
|
|
38
40
|
# A long op that has run this long is presumed crashed/leaked and ignored.
|
|
39
41
|
MAX_OP_AGE_SECONDS = 2 * 60 * 60
|
|
@@ -47,6 +49,10 @@ _local_owner: Optional[int] = None # thread ident of the in-process registrant
|
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def _pid_alive(pid: int) -> bool:
|
|
52
|
+
if os.name == "nt":
|
|
53
|
+
# os.kill(pid, 0) is TerminateProcess on Windows, not a probe: a record
|
|
54
|
+
# naming this process would kill it.
|
|
55
|
+
return windows_pid_alive(pid) is True
|
|
50
56
|
try:
|
|
51
57
|
os.kill(pid, 0)
|
|
52
58
|
return True
|