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 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
- [![Version](https://img.shields.io/badge/version-4.13.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.13.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-37%20(390%20full)-blue.svg)](#server-modes)
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-4.13.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
5
+ [![Version](https://img.shields.io/badge/version-4.13.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-37%20(390%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
- > 本翻译对应 v4.13.0 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
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.0"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "davinci-resolve-mcp",
3
- "version": "4.13.0",
3
+ "version": "4.13.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -91,7 +91,7 @@ if not logging.getLogger().handlers:
91
91
  handlers=[logging.StreamHandler()],
92
92
  )
93
93
 
94
- VERSION = "4.13.0"
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.0"
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