davinci-resolve-mcp 4.7.3 → 4.7.4
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 +29 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/SECURITY.md +5 -1
- package/docs/install.md +12 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +1 -1
- package/src/utils/mcp_transport.py +90 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,35 @@
|
|
|
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.7.4 — the networked transport can serve a client on another machine
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`--transport streamable-http` / `sse` bound to a LAN address answered every
|
|
10
|
+
request with HTTP 421.** ([#241](https://github.com/samuelgursky/davinci-resolve-mcp/issues/241), reported with the diagnosis by @TeamCLP)
|
|
11
|
+
`src/server.py` builds `FastMCP(...)` without a host, so the SDK (1.30.0)
|
|
12
|
+
auto-enables DNS-rebinding protection pinned to loopback
|
|
13
|
+
(`allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"]`). `run_networked` then
|
|
14
|
+
set `settings.host` to `DAVINCI_MCP_HOST` but never touched
|
|
15
|
+
`settings.transport_security`, and the app handed that loopback-only allowlist to
|
|
16
|
+
the transport middleware. The 421 came after the bearer check, so a wrong token
|
|
17
|
+
still got 401 and the bind looked healthy — a non-loopback bind could never serve
|
|
18
|
+
anyone, including an instance the control panel's Start button launched.
|
|
19
|
+
Reproduced on v4.7.3 with the real SDK app before the fix (LAN Host → 421, wrong
|
|
20
|
+
token → 401, loopback → 200).
|
|
21
|
+
- New `transport_security_for(host, extra_hosts)` in `src/utils/mcp_transport.py`,
|
|
22
|
+
applied **before** the app is built (the app reads the setting once). Loopback
|
|
23
|
+
binds are untouched. A specific non-loopback bind keeps protection ON with an
|
|
24
|
+
allowlist of the bind host, loopback, and any names in the new
|
|
25
|
+
**`DAVINCI_MCP_ALLOWED_HOSTS`** (comma-separated, for clients that reach the box
|
|
26
|
+
by a DNS name); IPv6 literals are bracketed. A wildcard bind (`0.0.0.0` / `::`)
|
|
27
|
+
with no names listed turns the Host check off with a warning, since a client
|
|
28
|
+
never sends the wildcard as its Host — the bearer token remains on every request.
|
|
29
|
+
- `tests/test_mcp_transport_host_allowlist.py`: the policy table plus the real
|
|
30
|
+
streamable-http app through `run_networked` — LAN Host 200, wrong token 401,
|
|
31
|
+
loopback 200, foreign Host still 421. `SECURITY.md` and `docs/install.md`
|
|
32
|
+
describe the allowlist and the new variable.
|
|
33
|
+
|
|
5
34
|
## What's New in v4.7.3 — a false spelling no longer grants permission on six opt-in flags
|
|
6
35
|
|
|
7
36
|
### 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.7.
|
|
15
|
+
> 本翻译对应 v4.7.4 版 README。如与英文原版有出入,以 [英文原版](README.md) 为准。
|
|
16
16
|
|
|
17
17
|
一个 Model Context Protocol (MCP) 服务器,让 AI 助手通过官方脚本 API 控制 DaVinci Resolve Studio(达芬奇)。它提供完整的 API 覆盖,外加带护栏的工作流助手,涵盖剪辑、媒体池整理、渲染设置、审阅标记、调色、Fusion、Fairlight、项目生命周期任务、扩展开发,以及不碰源媒体的媒体分析。
|
|
18
18
|
|
package/SECURITY.md
CHANGED
|
@@ -24,7 +24,11 @@ Their posture:
|
|
|
24
24
|
- **Loopback only.** The panel refuses any bind host other than
|
|
25
25
|
`127.0.0.1` / `localhost` / `::1` — the bind address is not a tool parameter
|
|
26
26
|
an AI can widen. The transport defaults to loopback and logs a loud warning
|
|
27
|
-
if `DAVINCI_MCP_HOST` points elsewhere.
|
|
27
|
+
if `DAVINCI_MCP_HOST` points elsewhere. On a non-loopback bind its
|
|
28
|
+
DNS-rebinding allowlist is the bind host plus loopback, extended by
|
|
29
|
+
`DAVINCI_MCP_ALLOWED_HOSTS` (comma-separated names clients will use); on a
|
|
30
|
+
wildcard bind (`0.0.0.0` / `::`) with that variable unset the Host check is
|
|
31
|
+
off and the bearer token is the only gate, and the log says so.
|
|
28
32
|
- **Bearer token on every request.** Each panel launch generates a fresh
|
|
29
33
|
`secrets.token_urlsafe(32)` token, passed to the child via environment (not
|
|
30
34
|
argv) and delivered to the browser in the URL fragment (`#token=…`), which
|
package/docs/install.md
CHANGED
|
@@ -237,6 +237,18 @@ Network scripting permits remote control of Resolve. Prefer Local mode when
|
|
|
237
237
|
remote access is unnecessary; otherwise restrict access with host firewall and
|
|
238
238
|
network controls.
|
|
239
239
|
|
|
240
|
+
The MCP server's own networked transport (`--transport streamable-http` or
|
|
241
|
+
`sse`) binds `127.0.0.1:8000` by default and requires `Authorization: Bearer
|
|
242
|
+
<token>` on every request (`DAVINCI_MCP_TOKEN`, or a generated one). To serve a
|
|
243
|
+
client on another machine, set `DAVINCI_MCP_HOST` to the address to bind and,
|
|
244
|
+
if clients reach the box by a DNS name rather than that address, list the names
|
|
245
|
+
in `DAVINCI_MCP_ALLOWED_HOSTS` (comma-separated). The transport keeps
|
|
246
|
+
DNS-rebinding protection on, pinned to the bind host, loopback, and those
|
|
247
|
+
names; a request whose `Host` header is none of them gets 421. A wildcard bind
|
|
248
|
+
(`0.0.0.0` / `::`) with no names listed turns the Host check off, since a
|
|
249
|
+
client never sends the wildcard as its Host, and the bearer token is then the
|
|
250
|
+
only gate. Restrict a non-loopback bind with a host firewall.
|
|
251
|
+
|
|
240
252
|
Run the read-only doctor against Network mode explicitly:
|
|
241
253
|
|
|
242
254
|
```bash
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.7.
|
|
40
|
+
VERSION = "4.7.4"
|
|
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.7.
|
|
96
|
+
VERSION = "4.7.4"
|
|
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
|
@@ -13,6 +13,9 @@ it, so a logged token would outlive the session in a file the state file's
|
|
|
13
13
|
|
|
14
14
|
Security posture:
|
|
15
15
|
- Default host is loopback; a non-loopback bind logs a loud warning.
|
|
16
|
+
- DNS-rebinding protection follows the bind host (see
|
|
17
|
+
``transport_security_for``); a loopback-only allowlist on a LAN bind
|
|
18
|
+
answered every request with 421 until v4.7.4 (issue #241).
|
|
16
19
|
- Every HTTP request must carry ``Authorization: Bearer <token>`` (constant-time
|
|
17
20
|
compared); otherwise 401.
|
|
18
21
|
- stdio (the default transport) is unaffected by anything here.
|
|
@@ -36,6 +39,75 @@ def _state_path() -> str:
|
|
|
36
39
|
# Resolved lazily so DAVINCI_RESOLVE_MCP_STATE_DIR set by a test harness is honored.
|
|
37
40
|
TRANSPORT_STATE_PATH = _state_path()
|
|
38
41
|
LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"}
|
|
42
|
+
#: Binds that listen on every interface. A client never sends one of these as
|
|
43
|
+
#: its Host header, so an allowlist cannot be derived from the bind address.
|
|
44
|
+
WILDCARD_HOSTS = {"0.0.0.0", "::", ""}
|
|
45
|
+
ALLOWED_HOSTS_ENV = "DAVINCI_MCP_ALLOWED_HOSTS"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _host_pattern(name: str) -> str:
|
|
49
|
+
"""`Host`-header pattern for one name: any port, IPv6 literals bracketed."""
|
|
50
|
+
name = name.strip()
|
|
51
|
+
if ":" in name and not name.startswith("["):
|
|
52
|
+
name = f"[{name}]"
|
|
53
|
+
return f"{name}:*"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def extra_allowed_hosts(env=None):
|
|
57
|
+
"""Names from $DAVINCI_MCP_ALLOWED_HOSTS (comma-separated), stripped, deduped."""
|
|
58
|
+
raw = (env if env is not None else os.environ).get(ALLOWED_HOSTS_ENV, "")
|
|
59
|
+
seen, out = set(), []
|
|
60
|
+
for name in raw.split(","):
|
|
61
|
+
name = name.strip()
|
|
62
|
+
if name and name not in seen:
|
|
63
|
+
seen.add(name)
|
|
64
|
+
out.append(name)
|
|
65
|
+
return out
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def transport_security_for(host, extra_hosts=()):
|
|
69
|
+
"""The DNS-rebinding allowlist the transport should run with for `host`.
|
|
70
|
+
|
|
71
|
+
Returns None for a loopback bind: the SDK already pins the allowlist to
|
|
72
|
+
loopback when FastMCP is built without a host, and that is correct there.
|
|
73
|
+
|
|
74
|
+
Everything else exists because of issue #241. `src/server.py` builds
|
|
75
|
+
`FastMCP(...)` without a host, so the SDK (1.30.0,
|
|
76
|
+
`mcp/server/fastmcp/server.py`) auto-enables DNS-rebinding protection with
|
|
77
|
+
`allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"]`. `run_networked`
|
|
78
|
+
then set `settings.host` to the LAN address but never touched
|
|
79
|
+
`settings.transport_security`, and `streamable_http_app()` / `sse_app()`
|
|
80
|
+
hand that loopback-only allowlist to the transport middleware. Every request
|
|
81
|
+
to the LAN address therefore answered **421 Misdirected Request** — after the
|
|
82
|
+
bearer check, so a wrong token still got 401 and the bind looked healthy.
|
|
83
|
+
A non-loopback bind could never serve anyone.
|
|
84
|
+
|
|
85
|
+
- Specific non-loopback host: protection stays ON; the allowlist is the bind
|
|
86
|
+
host, the loopback names, and any extra names from
|
|
87
|
+
`$DAVINCI_MCP_ALLOWED_HOSTS` (for clients that reach the box by a DNS name
|
|
88
|
+
rather than the bound address).
|
|
89
|
+
- Wildcard bind (`0.0.0.0` / `::`): a client never sends the wildcard as its
|
|
90
|
+
Host, so with no extra names there is nothing to allow. Protection is then
|
|
91
|
+
turned OFF with a warning — the bearer token remains on every request, and
|
|
92
|
+
a rebinding page does not hold it. Set `$DAVINCI_MCP_ALLOWED_HOSTS` to keep
|
|
93
|
+
the protection on for a wildcard bind.
|
|
94
|
+
"""
|
|
95
|
+
from mcp.server.transport_security import TransportSecuritySettings
|
|
96
|
+
|
|
97
|
+
if host in LOOPBACK_HOSTS:
|
|
98
|
+
return None
|
|
99
|
+
names = []
|
|
100
|
+
if host not in WILDCARD_HOSTS:
|
|
101
|
+
names.append(host)
|
|
102
|
+
names.extend(n for n in extra_hosts if n not in names)
|
|
103
|
+
if not names:
|
|
104
|
+
return TransportSecuritySettings(enable_dns_rebinding_protection=False)
|
|
105
|
+
names.extend(sorted(LOOPBACK_HOSTS))
|
|
106
|
+
return TransportSecuritySettings(
|
|
107
|
+
enable_dns_rebinding_protection=True,
|
|
108
|
+
allowed_hosts=[_host_pattern(n) for n in names],
|
|
109
|
+
allowed_origins=[f"http://{_host_pattern(n)}" for n in names],
|
|
110
|
+
)
|
|
39
111
|
|
|
40
112
|
|
|
41
113
|
def resolve_token():
|
|
@@ -125,6 +197,24 @@ def run_networked(mcp, transport):
|
|
|
125
197
|
mcp.settings.port = port
|
|
126
198
|
token, generated = resolve_token()
|
|
127
199
|
|
|
200
|
+
# Must happen BEFORE the app is built: sse_app()/streamable_http_app() read
|
|
201
|
+
# settings.transport_security once, when they construct the middleware.
|
|
202
|
+
extra = extra_allowed_hosts()
|
|
203
|
+
security = transport_security_for(host, extra)
|
|
204
|
+
if security is not None:
|
|
205
|
+
mcp.settings.transport_security = security
|
|
206
|
+
if security.enable_dns_rebinding_protection:
|
|
207
|
+
logger.info("MCP transport Host allowlist: %s",
|
|
208
|
+
", ".join(security.allowed_hosts))
|
|
209
|
+
else:
|
|
210
|
+
logger.warning(
|
|
211
|
+
"SECURITY: MCP transport bound to %r with no %s set — a client "
|
|
212
|
+
"never sends the wildcard as its Host header, so DNS-rebinding "
|
|
213
|
+
"protection is OFF for this bind (the bearer token still gates "
|
|
214
|
+
"every request). Set %s to the names clients will use to keep "
|
|
215
|
+
"it on.", host, ALLOWED_HOSTS_ENV, ALLOWED_HOSTS_ENV,
|
|
216
|
+
)
|
|
217
|
+
|
|
128
218
|
app = mcp.sse_app() if transport == "sse" else mcp.streamable_http_app()
|
|
129
219
|
app.add_middleware(_auth_middleware_cls(token))
|
|
130
220
|
|