davinci-resolve-mcp 4.7.11 → 4.8.0
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 +36 -0
- package/README.md +1 -1
- package/README.zh-CN.md +2 -2
- package/docs/SKILL.md +13 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +138 -2
- package/src/utils/execution_lifecycle.py +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
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.0 — one read before planning: `project_manager snapshot`
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`project_manager(action="snapshot")`** — a read-only readout of the state an agent
|
|
10
|
+
inspects before it plans an edit. ([#251](https://github.com/samuelgursky/davinci-resolve-mcp/pull/251), @tpellet)
|
|
11
|
+
Returns `project`, `timeline` (per-track items), `gaps_overlaps`, `render`
|
|
12
|
+
(`is_rendering` plus each job's status) and `media_pool` counts in one call, instead
|
|
13
|
+
of `get_current` + `timeline.get_current` + `probe_timeline_structure` +
|
|
14
|
+
`detect_gaps_overlaps` + `render.is_rendering` one LLM turn at a time. The
|
|
15
|
+
motivation is measured: in 8,407 mined agent tool turns from one real project,
|
|
16
|
+
2,222 were state inspection. It composes the existing helpers, so the values match
|
|
17
|
+
what those actions already return, and it never switches page, timeline or folder.
|
|
18
|
+
- `include` picks sections; an unknown name or an empty list is refused rather than
|
|
19
|
+
widened to everything. `item_limit` (default 200) caps the items returned across
|
|
20
|
+
tracks and sets `timeline.items_truncated`, while `item_count` and
|
|
21
|
+
`gaps_overlaps` still cover the whole timeline. A failing section reports
|
|
22
|
+
`{error}` in its own place; the others still return.
|
|
23
|
+
- It saves turns and response size, not read time: the timeline sections cost what
|
|
24
|
+
`probe_timeline_structure` costs and `media_pool` walks every pool clip, so on a
|
|
25
|
+
large project pass `include` with only the sections you need. `docs/SKILL.md` says
|
|
26
|
+
so.
|
|
27
|
+
- Tested against stubs only (16 tests, including parity with the actions it replaces
|
|
28
|
+
and a sixty-item readout kept under 16 KB); not yet run against a live Resolve.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **The risk classifier now recognises `project_manager.snapshot` as a LOW read.** Its
|
|
33
|
+
name carries no read verb, so on the contributed branch it fell to the name-based
|
|
34
|
+
MEDIUM default with a "risk unestablished" reason on every call — noise on the one
|
|
35
|
+
read an agent makes before planning. Safe mode would not have blocked it (only an
|
|
36
|
+
established HIGH/CRITICAL is), but a pure read should not carry that. Explicit
|
|
37
|
+
verbless reads now live in `RiskClassificationHook._READ_ONLY_PAIRS` beside
|
|
38
|
+
`dctl.validate_native`; guard test `tests/test_snapshot_read_rule.py` also pins that
|
|
39
|
+
the table does not widen into a wildcard.
|
|
40
|
+
|
|
5
41
|
## What's New in v4.7.11 — `allow_partial_item_delete="false"` no longer lets a range delete take whole clips
|
|
6
42
|
|
|
7
43
|
### 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.
|
|
15
|
+
> 本翻译对应 v4.8.0 版 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
|
@@ -821,6 +821,19 @@ Key actions: `list`, `list_attributes`, `get_current`,
|
|
|
821
821
|
`notes`, and `liveCollaborationMode` per project in the current folder without
|
|
822
822
|
loading any of them.
|
|
823
823
|
|
|
824
|
+
`snapshot(include?, track_types?, item_limit?)` is the one read to make before
|
|
825
|
+
planning: `project`, `timeline` (per-track items), `gaps_overlaps`, `render`
|
|
826
|
+
(`is_rendering` plus each job's status) and `media_pool` counts in a single
|
|
827
|
+
read-only call, instead of `get_current` + `timeline.get_current` +
|
|
828
|
+
`probe_timeline_structure` + `detect_gaps_overlaps` + `render.is_rendering` one
|
|
829
|
+
turn at a time. `include` picks sections, `item_limit` (default 200) caps the
|
|
830
|
+
items returned and sets `timeline.items_truncated`, and a section that fails
|
|
831
|
+
reports `{error}` in its own place. It saves turns and response size, not read
|
|
832
|
+
time: the timeline sections cost what `probe_timeline_structure` costs and
|
|
833
|
+
`media_pool` walks every pool clip, so on a large project pass `include` with
|
|
834
|
+
only the sections you need. Frame fields are `probe_timeline_structure`'s,
|
|
835
|
+
unchanged.
|
|
836
|
+
|
|
824
837
|
Project / Database / Archive kernel actions (v2.15.0+) add guarded project
|
|
825
838
|
lifecycle, settings, database, preset, and archive boundary helpers:
|
|
826
839
|
|
package/install.py
CHANGED
|
@@ -37,7 +37,7 @@ from src.utils.update_check import (
|
|
|
37
37
|
|
|
38
38
|
# ─── Version ──────────────────────────────────────────────────────────────────
|
|
39
39
|
|
|
40
|
-
VERSION = "4.
|
|
40
|
+
VERSION = "4.8.0"
|
|
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.
|
|
96
|
+
VERSION = "4.8.0"
|
|
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.
|
|
14
|
+
VERSION = "4.8.0"
|
|
15
15
|
|
|
16
16
|
import base64
|
|
17
17
|
import os
|
|
@@ -19077,6 +19077,124 @@ def _project_lint_live(r, pm) -> Dict[str, Any]:
|
|
|
19077
19077
|
return _ok(**_project_lint.lint_report(state))
|
|
19078
19078
|
|
|
19079
19079
|
|
|
19080
|
+
_SNAPSHOT_SECTIONS = ["project", "timeline", "gaps_overlaps", "render", "media_pool"]
|
|
19081
|
+
|
|
19082
|
+
# The per-item fields a planning read needs. probe_timeline_structure carries
|
|
19083
|
+
# the rest (file path, media status, seconds, clip properties) for a caller
|
|
19084
|
+
# that wants them; repeating them here is what makes a 60-item readout too big
|
|
19085
|
+
# to be the first call of every turn.
|
|
19086
|
+
_SNAPSHOT_ITEM_FIELDS = (
|
|
19087
|
+
"item_index", "timeline_item_id", "name", "start", "end",
|
|
19088
|
+
"source_start", "source_end", "source_fps", "media_pool_item_id",
|
|
19089
|
+
)
|
|
19090
|
+
|
|
19091
|
+
|
|
19092
|
+
def _project_state_snapshot(r, proj, p: Dict[str, Any]) -> Dict[str, Any]:
|
|
19093
|
+
"""One read-only readout of the state an agent inspects before planning.
|
|
19094
|
+
|
|
19095
|
+
Composes the helpers behind project_summary, probe_timeline_structure,
|
|
19096
|
+
detect_gaps_overlaps and the render status actions, so the numbers match
|
|
19097
|
+
what those actions return. Each section fails on its own: an exception is
|
|
19098
|
+
reported as that section's {"error"}, never as a whole-call failure.
|
|
19099
|
+
"""
|
|
19100
|
+
include = p.get("include")
|
|
19101
|
+
if include is None:
|
|
19102
|
+
include = list(_SNAPSHOT_SECTIONS)
|
|
19103
|
+
# An empty include is refused: falling back to every section would hand a
|
|
19104
|
+
# caller that filtered down to nothing the most expensive read instead.
|
|
19105
|
+
if not isinstance(include, list) or not include:
|
|
19106
|
+
return _err("include must be a non-empty list", category="invalid_input")
|
|
19107
|
+
unknown = [name for name in include if name not in _SNAPSHOT_SECTIONS]
|
|
19108
|
+
if unknown:
|
|
19109
|
+
return _err(
|
|
19110
|
+
f"Unknown snapshot section(s): {unknown}",
|
|
19111
|
+
code="UNKNOWN_SECTION", category="invalid_input",
|
|
19112
|
+
state={"unknown": unknown, "supported": list(_SNAPSHOT_SECTIONS)},
|
|
19113
|
+
)
|
|
19114
|
+
track_types = p.get("track_types")
|
|
19115
|
+
if track_types is not None and not isinstance(track_types, list):
|
|
19116
|
+
return _err("track_types must be a list", category="invalid_input")
|
|
19117
|
+
try:
|
|
19118
|
+
item_limit = int(p.get("item_limit", 200))
|
|
19119
|
+
except (TypeError, ValueError):
|
|
19120
|
+
return _err("item_limit must be an integer", category="invalid_input")
|
|
19121
|
+
if item_limit < 0:
|
|
19122
|
+
return _err("item_limit must be 0 or greater", category="invalid_input")
|
|
19123
|
+
|
|
19124
|
+
out: Dict[str, Any] = {}
|
|
19125
|
+
|
|
19126
|
+
if "project" in include:
|
|
19127
|
+
try:
|
|
19128
|
+
section = _project_object_summary(proj) or {}
|
|
19129
|
+
section["current_page"] = r.GetCurrentPage()
|
|
19130
|
+
section["timeline_count"] = proj.GetTimelineCount()
|
|
19131
|
+
section["settings"] = {
|
|
19132
|
+
key: _ser(proj.GetSetting(key))
|
|
19133
|
+
for key in ("timelineFrameRate", "timelineResolutionWidth", "timelineResolutionHeight")
|
|
19134
|
+
}
|
|
19135
|
+
out["project"] = section
|
|
19136
|
+
except Exception as exc:
|
|
19137
|
+
out["project"] = {"error": str(exc)}
|
|
19138
|
+
|
|
19139
|
+
if "timeline" in include or "gaps_overlaps" in include:
|
|
19140
|
+
wanted = [name for name in ("timeline", "gaps_overlaps") if name in include]
|
|
19141
|
+
try:
|
|
19142
|
+
tl = proj.GetCurrentTimeline()
|
|
19143
|
+
if not tl:
|
|
19144
|
+
for name in wanted:
|
|
19145
|
+
out[name] = {"available": False, "error": "No current timeline"}
|
|
19146
|
+
else:
|
|
19147
|
+
snapshot = _timeline_conform_snapshot(
|
|
19148
|
+
tl, {"track_types": track_types, "include_markers": False})
|
|
19149
|
+
# Gaps are measured on the whole snapshot, before item_limit
|
|
19150
|
+
# trims what is returned.
|
|
19151
|
+
gaps_overlaps = _detect_gaps_overlaps_from_snapshot(
|
|
19152
|
+
snapshot, {"track_types": track_types})
|
|
19153
|
+
if "timeline" in include:
|
|
19154
|
+
snapshot.pop("markers", None)
|
|
19155
|
+
try:
|
|
19156
|
+
snapshot["fps"] = float(tl.GetSetting("timelineFrameRate"))
|
|
19157
|
+
except Exception:
|
|
19158
|
+
snapshot["fps"] = None
|
|
19159
|
+
remaining = item_limit
|
|
19160
|
+
for type_payload in snapshot["tracks"].values():
|
|
19161
|
+
for track in type_payload["tracks"]:
|
|
19162
|
+
kept = track["items"][:remaining]
|
|
19163
|
+
remaining -= len(kept)
|
|
19164
|
+
track["items"] = [
|
|
19165
|
+
{field: item.get(field) for field in _SNAPSHOT_ITEM_FIELDS}
|
|
19166
|
+
for item in kept
|
|
19167
|
+
]
|
|
19168
|
+
snapshot["items_returned"] = item_limit - remaining
|
|
19169
|
+
snapshot["items_truncated"] = snapshot["items_returned"] < snapshot["item_count"]
|
|
19170
|
+
out["timeline"] = snapshot
|
|
19171
|
+
if "gaps_overlaps" in include:
|
|
19172
|
+
out["gaps_overlaps"] = gaps_overlaps
|
|
19173
|
+
except Exception as exc:
|
|
19174
|
+
for name in wanted:
|
|
19175
|
+
out.setdefault(name, {"error": str(exc)})
|
|
19176
|
+
|
|
19177
|
+
if "render" in include:
|
|
19178
|
+
try:
|
|
19179
|
+
jobs = []
|
|
19180
|
+
for job in (proj.GetRenderJobList() or []):
|
|
19181
|
+
row = _ser(job)
|
|
19182
|
+
if isinstance(row, dict) and row.get("JobId"):
|
|
19183
|
+
row["status"] = _ser(proj.GetRenderJobStatus(row["JobId"]))
|
|
19184
|
+
jobs.append(row)
|
|
19185
|
+
out["render"] = {"is_rendering": bool(proj.IsRenderingInProgress()), "jobs": jobs}
|
|
19186
|
+
except Exception as exc:
|
|
19187
|
+
out["render"] = {"error": str(exc)}
|
|
19188
|
+
|
|
19189
|
+
if "media_pool" in include:
|
|
19190
|
+
try:
|
|
19191
|
+
out["media_pool"] = _project_summary(proj)["media_pool"]
|
|
19192
|
+
except Exception as exc:
|
|
19193
|
+
out["media_pool"] = {"error": str(exc)}
|
|
19194
|
+
|
|
19195
|
+
return out
|
|
19196
|
+
|
|
19197
|
+
|
|
19080
19198
|
@mcp.tool()
|
|
19081
19199
|
@_guard_missing_params
|
|
19082
19200
|
@_destructive_op("project_manager")
|
|
@@ -19089,6 +19207,19 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19089
19207
|
— Resolve 21.0.4+. Per-project attributes for the current folder without
|
|
19090
19208
|
loading any project.
|
|
19091
19209
|
get_current() -> {name, id}
|
|
19210
|
+
snapshot(include?, track_types?, item_limit?) -> {project, timeline, gaps_overlaps, render, media_pool}
|
|
19211
|
+
One read-only readout of what an agent inspects before planning; it never
|
|
19212
|
+
switches page, timeline or folder. include picks sections (default all);
|
|
19213
|
+
item_limit caps the items returned across tracks (default 200) and sets
|
|
19214
|
+
timeline.items_truncated, while item_count and gaps_overlaps still cover
|
|
19215
|
+
the whole timeline. item_limit bounds the response, not the read: the
|
|
19216
|
+
timeline sections cost what probe_timeline_structure costs (they scale
|
|
19217
|
+
with item_count) and media_pool walks every pool clip, so on a large
|
|
19218
|
+
project leave out the sections you do not need. A section that fails
|
|
19219
|
+
reports {error} in its own place; an empty include is refused. Frame
|
|
19220
|
+
fields are probe_timeline_structure's, unchanged: start/end
|
|
19221
|
+
are TimelineItem GetStart/GetEnd record frames; source_start/source_end
|
|
19222
|
+
are file-relative frames at source_fps, source_end exclusive.
|
|
19092
19223
|
create(name, media_location_path?) -> {success, name}
|
|
19093
19224
|
load(name) -> {success}
|
|
19094
19225
|
save() -> {success}
|
|
@@ -19181,6 +19312,11 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19181
19312
|
elif action == "get_current":
|
|
19182
19313
|
proj = pm.GetCurrentProject()
|
|
19183
19314
|
return {"name": proj.GetName(), "id": proj.GetUniqueId()} if proj else _err("No project open")
|
|
19315
|
+
elif action == "snapshot":
|
|
19316
|
+
proj = pm.GetCurrentProject()
|
|
19317
|
+
if not proj:
|
|
19318
|
+
return _err("No project open")
|
|
19319
|
+
return _project_state_snapshot(r, proj, p)
|
|
19184
19320
|
elif action == "create":
|
|
19185
19321
|
if not p.get("name"):
|
|
19186
19322
|
return _err("create requires name")
|
|
@@ -19280,7 +19416,7 @@ def project_manager(action: str, params: Optional[Dict[str, Any]] = None) -> Dic
|
|
|
19280
19416
|
if not p.get("path"):
|
|
19281
19417
|
return _err("restore requires path")
|
|
19282
19418
|
return {"success": bool(pm.RestoreProject(p["path"], p.get("name")))}
|
|
19283
|
-
return _unknown(action, ["list","list_attributes","get_current","create","load","save","close","delete","import_project","export_project","archive","restore","lint","diff_to_spec","plan_spec","apply_spec", *_PROJECT_KERNEL_ACTIONS])
|
|
19419
|
+
return _unknown(action, ["list","list_attributes","get_current","snapshot","create","load","save","close","delete","import_project","export_project","archive","restore","lint","diff_to_spec","plan_spec","apply_spec", *_PROJECT_KERNEL_ACTIONS])
|
|
19284
19420
|
|
|
19285
19421
|
|
|
19286
19422
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -378,6 +378,14 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
378
378
|
return BlastRadius.TIMELINE, "the timeline node graph (every clip on the timeline)"
|
|
379
379
|
|
|
380
380
|
_READ_ONLY_PREFIXES = ("get_", "list_", "query_", "probe_", "inspect_", "export_", "check_")
|
|
381
|
+
#: Reads whose action name carries no read verb. Without an entry here a
|
|
382
|
+
#: pure read falls to the name-based MEDIUM default and every call reports
|
|
383
|
+
#: "risk unestablished" — noise on the one call an agent makes before
|
|
384
|
+
#: planning (project_manager.snapshot, #251).
|
|
385
|
+
_READ_ONLY_PAIRS = frozenset({
|
|
386
|
+
("dctl", "validate_native"),
|
|
387
|
+
("project_manager", "snapshot"),
|
|
388
|
+
})
|
|
381
389
|
|
|
382
390
|
@classmethod
|
|
383
391
|
def classify(cls, tool_name: str, action: str, params: Dict[str, Any]) -> RiskAssessment:
|
|
@@ -448,7 +456,7 @@ class RiskClassificationHook(LifecycleHook):
|
|
|
448
456
|
else BlastRadius.ITEM
|
|
449
457
|
)
|
|
450
458
|
reasons.append(f"Recoverable edit to existing state: {action}")
|
|
451
|
-
elif any(action.startswith(p) for p in cls._READ_ONLY_PREFIXES) or action in {"read", "status", "info"} or pair
|
|
459
|
+
elif any(action.startswith(p) for p in cls._READ_ONLY_PREFIXES) or action in {"read", "status", "info"} or pair in cls._READ_ONLY_PAIRS:
|
|
452
460
|
level = RiskLevel.LOW
|
|
453
461
|
destructive = False
|
|
454
462
|
radius = BlastRadius.ITEM
|