davinci-resolve-mcp 4.8.21 → 4.8.22
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 +25 -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 +41 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
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.8.22 — the control panel port check cannot hang on a wedged lsof
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`open_control_panel` could block forever behind an unkillable `lsof`.** The
|
|
10
|
+
port-owner check ran `lsof -iTCP:<port> -sTCP:LISTEN -t` through
|
|
11
|
+
`subprocess.run(timeout=3)`. On macOS, lsof wedges in uninterruptible kernel wait
|
|
12
|
+
(state `U` in `ps`) when a network mount is stale, and a process in that state
|
|
13
|
+
ignores SIGKILL. `subprocess.run`'s timeout path kills the child and then waits
|
|
14
|
+
for it, so the 3-second timeout never returned: the caller hung with the child.
|
|
15
|
+
Measured on 2026-09-26 on the release machine, where 489 lsof processes had been
|
|
16
|
+
stuck for 12 hours and the offline suite sat in this function for 13 minutes.
|
|
17
|
+
`_port_owner_pid` now starts lsof in its own session, polls to the deadline, and
|
|
18
|
+
on expiry sends SIGKILL and abandons the child instead of joining it. stdout is
|
|
19
|
+
read only once `poll()` reports an exit, and the pipe is closed on every path.
|
|
20
|
+
A missing lsof is still `None`, not an exception.
|
|
21
|
+
|
|
22
|
+
### Tests
|
|
23
|
+
|
|
24
|
+
- `tests/test_port_owner_pid.py`: a fake child whose `poll()` never returns and
|
|
25
|
+
whose `kill()` is a no-op yields `None` within the deadline with `wait()` and
|
|
26
|
+
`communicate()` never called and the pipe closed; an exited child still yields
|
|
27
|
+
its PID; a missing binary yields `None`; and a real subprocess that ignores
|
|
28
|
+
SIGTERM, with `kill` patched out, is left running rather than joined.
|
|
29
|
+
|
|
5
30
|
## What's New in v4.8.21 — timeline duration no longer overcounts by one frame
|
|
6
31
|
|
|
7
32
|
### 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.8.
|
|
15
|
+
> 本翻译对应 v4.8.22 版 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.8.
|
|
40
|
+
VERSION = "4.8.22"
|
|
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
|
@@ -93,7 +93,7 @@ if not logging.getLogger().handlers:
|
|
|
93
93
|
handlers=[logging.StreamHandler()],
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
VERSION = "4.8.
|
|
96
|
+
VERSION = "4.8.22"
|
|
97
97
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
98
98
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
99
99
|
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.8.
|
|
14
|
+
VERSION = "4.8.22"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -17750,24 +17750,57 @@ def _control_panel_remote_version(host: str, port: int, timeout: float = 1.5) ->
|
|
|
17750
17750
|
return _control_panel_probe(host, port, timeout).get("version")
|
|
17751
17751
|
|
|
17752
17752
|
|
|
17753
|
-
def _port_owner_pid(host: str, port: int) -> Optional[int]:
|
|
17753
|
+
def _port_owner_pid(host: str, port: int, timeout: float = 3.0) -> Optional[int]:
|
|
17754
17754
|
"""Return PID of the process LISTENing on `port`, or None if free/unknown.
|
|
17755
17755
|
|
|
17756
17756
|
Uses lsof with `-iTCP:<port> -sTCP:LISTEN -t`: one PID per line, no header.
|
|
17757
17757
|
Host is informational only — lsof matches any local LISTEN socket on that
|
|
17758
17758
|
port (which is what we care about for port-collision detection).
|
|
17759
|
+
|
|
17760
|
+
The deadline is enforced by polling, not by ``subprocess.run(timeout=)``.
|
|
17761
|
+
On macOS, lsof can wedge in uninterruptible kernel wait (state ``U`` in
|
|
17762
|
+
``ps``) when a network mount is stale, and a child in that state ignores
|
|
17763
|
+
SIGKILL. ``subprocess.run``'s timeout path kills the child and then WAITS
|
|
17764
|
+
for it, so the caller hung with it — measured 2026-09-26 on the release
|
|
17765
|
+
machine: 489 lsof processes stuck in ``U`` for 12 h, and the offline suite
|
|
17766
|
+
blocked here for 13 min. Now the child is killed on expiry and abandoned
|
|
17767
|
+
rather than joined; stdout is read only once ``poll()`` says it exited.
|
|
17759
17768
|
"""
|
|
17760
17769
|
import subprocess
|
|
17770
|
+
import time as _t
|
|
17761
17771
|
try:
|
|
17762
|
-
|
|
17772
|
+
proc = subprocess.Popen(
|
|
17763
17773
|
["lsof", "-nP", "-iTCP:" + str(port), "-sTCP:LISTEN", "-t"],
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
stdin=subprocess.DEVNULL,
|
|
17774
|
+
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
17775
|
+
stdin=subprocess.DEVNULL, start_new_session=True,
|
|
17767
17776
|
)
|
|
17768
|
-
except
|
|
17777
|
+
except OSError:
|
|
17769
17778
|
return None
|
|
17770
|
-
|
|
17779
|
+
deadline = _t.monotonic() + max(0.0, float(timeout))
|
|
17780
|
+
out = b""
|
|
17781
|
+
try:
|
|
17782
|
+
while proc.poll() is None:
|
|
17783
|
+
if _t.monotonic() >= deadline:
|
|
17784
|
+
try:
|
|
17785
|
+
proc.kill()
|
|
17786
|
+
except OSError:
|
|
17787
|
+
pass
|
|
17788
|
+
# Deliberately no wait()/communicate(): a wedged lsof never
|
|
17789
|
+
# exits, and joining it is exactly the hang this guards against.
|
|
17790
|
+
return None
|
|
17791
|
+
_t.sleep(0.05)
|
|
17792
|
+
if proc.stdout is not None:
|
|
17793
|
+
try:
|
|
17794
|
+
out = proc.stdout.read() or b""
|
|
17795
|
+
except OSError:
|
|
17796
|
+
out = b""
|
|
17797
|
+
finally:
|
|
17798
|
+
if proc.stdout is not None:
|
|
17799
|
+
try:
|
|
17800
|
+
proc.stdout.close()
|
|
17801
|
+
except OSError:
|
|
17802
|
+
pass
|
|
17803
|
+
for line in out.decode("utf-8", "replace").splitlines():
|
|
17771
17804
|
line = line.strip()
|
|
17772
17805
|
if line.isdigit():
|
|
17773
17806
|
return int(line)
|