davinci-resolve-mcp 2.27.2 → 2.28.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 CHANGED
@@ -2,6 +2,59 @@
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 v2.28.0
6
+
7
+ This release adds a structural timeline-diff engine, a declarative project spec
8
+ you can `apply` like infrastructure-as-code, a project health `lint`, a clip
9
+ query DSL, and a machine-readable `state` field on error responses.
10
+
11
+ **Timeline version diff — see exactly what an edit changed.** Comparing two
12
+ archived timeline versions now reports clips that were **added, removed, moved,
13
+ and trimmed**, plus summary counts and before/after clip totals. A new reusable
14
+ diff engine aligns clips by a rename-stable identity (so a reordered or renamed
15
+ clip reads as a move/change, not a delete-and-re-add).
16
+
17
+ - `timeline_versioning(action="diff_versions", timeline_name, from_version, to_version)`
18
+ now returns `{added, removed, moved, trimmed, summary}` (the previous
19
+ `added`/`removed`/`moved` keys are unchanged).
20
+ - Dashboard endpoint `GET /api/timeline_versions/diff?timeline_name=&from_version=&to_version=`
21
+ exposes the same diff to the control panel.
22
+
23
+ **Declarative project spec + `apply` — reproducible project setup.** Describe a
24
+ project's desired settings, color preset, timelines, and markers in a
25
+ `project.dvr.yaml` (or `.json`), then reconcile the live project toward it. Runs
26
+ are **idempotent** — applying twice is a no-op — and a dry run previews every
27
+ change before anything is touched.
28
+
29
+ - New MCP actions on `project_manager`:
30
+ - `diff_to_spec(spec_path | spec)` — preview drift without mutating.
31
+ - `plan_spec(spec_path | spec)` — the ordered action list (dry run).
32
+ - `apply_spec(spec_path | spec, dry_run?, run_hooks?, continue_on_error?)` —
33
+ reconcile. Color/HDR settings apply in dependency order; markers are only
34
+ added when absent; an explicit `color_preset` can be overridden by explicit
35
+ `settings`. Failures can abort on first error or accumulate.
36
+ - New headless CLI commands: `davinci-resolve-mcp batch plan-spec SPEC` and
37
+ `davinci-resolve-mcp batch apply SPEC [--dry-run] [--run-hooks] [--continue-on-error]`.
38
+ Exit codes follow the existing convention (`0` ok, `2` partial, `3` fatal).
39
+ - Optional before/after shell **hooks** in the spec run only when `run_hooks` is
40
+ passed (opt-in).
41
+
42
+ **Project health `lint` — a pre-flight before editing.** `project_manager(action="lint")`
43
+ returns a graded issue list (errors / warnings / info) covering: no project, no
44
+ current timeline, mixed frame rates across timelines, empty timelines, unset
45
+ render format, unmanaged color science, offline media, and unanalyzed clips.
46
+
47
+ **Clip query DSL — find clips in one call.** `timeline(action="clip_where", ...)`
48
+ returns the clips on the current timeline matching named filters (AND), instead
49
+ of enumerating tracks by hand. Live filters: `track_type`, `track_index`,
50
+ `name_contains`, `duration_lt`, `duration_gt`. A typo'd filter name is rejected
51
+ rather than silently matching everything.
52
+
53
+ **Machine-readable error context.** Structured error responses can now carry an
54
+ optional `state` object — a snapshot of the relevant values at failure time
55
+ (e.g. which filter was unknown, which spec failed and where) — so an agent can
56
+ react without parsing prose. Existing error fields are unchanged.
57
+
5
58
  ## What's New in v2.27.2
6
59
 
7
60
  **Control panel under-counted analyzed clips after a Media Pool rename (issue
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DaVinci Resolve MCP Server
2
2
 
3
- [![Version](https://img.shields.io/badge/version-2.27.2-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.28.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
4
4
  [![npm](https://img.shields.io/npm/v/davinci-resolve-mcp.svg?label=npm&color=CB3837)](https://www.npmjs.com/package/davinci-resolve-mcp)
5
5
  [![API Coverage](https://img.shields.io/badge/API%20Coverage-100%25-brightgreen.svg)](docs/reference/api-coverage.md)
6
6
  [![Tools](https://img.shields.io/badge/MCP%20Tools-32%20(329%20full)-blue.svg)](#server-modes)
package/docs/SKILL.md CHANGED
@@ -355,6 +355,26 @@ lifecycle, settings, database, preset, and archive boundary helpers:
355
355
  - `preset_lifecycle_probe`
356
356
  - `project_boundary_report`
357
357
 
358
+ Health check and declarative spec (v2.28.0+):
359
+
360
+ - `lint` — graded project health pre-flight returning `{ok, counts, issues}`.
361
+ Issues (error / warning / info) cover: no project, no current timeline, mixed
362
+ frame rates across timelines, empty timeline, render format unset, color
363
+ science unmanaged, offline media, and unanalyzed clips. Composed from existing
364
+ probes; safe read-only.
365
+ - `diff_to_spec(spec_path | spec)` — preview drift between a declarative spec and
366
+ the live project WITHOUT mutating. Returns `{actions, diff, change_count}`.
367
+ - `plan_spec(spec_path | spec)` — the ordered action list as a dry run.
368
+ - `apply_spec(spec_path | spec, dry_run?, run_hooks?, continue_on_error?)` —
369
+ reconcile the project toward the spec. Idempotent (re-runs are no-ops); color/
370
+ HDR settings apply in dependency order; markers added only when absent; explicit
371
+ `settings` override a named `color_preset`; before/after shell hooks run only
372
+ with `run_hooks=true`. The spec is YAML or JSON:
373
+ `{project, color_preset?, settings?, timelines:[{name, fps?, settings?, markers?}], hooks?}`.
374
+ Note: `apply_spec` reconciles the **currently open or already-existing** project;
375
+ creating a brand-new project from a spec depends on Resolve's `CreateProject`
376
+ succeeding (it can return None when an unsaved project blocks the switch).
377
+
358
378
  Safe project actions require `_mcp_` names and temp paths by default. Database
359
379
  switching dry-runs by default because Resolve closes open projects when
360
380
  switching databases. Archive source media/cache/proxy flags are rejected unless
@@ -584,8 +604,10 @@ Key actions:
584
604
  `initiator`, `thumbnail_path`, and `drt_export_path` (set when the version
585
605
  was retention-collapsed to disk).
586
606
  - `diff_versions(timeline_name, from_version, to_version)` — structural diff
587
- between two snapshots: `{added, removed, moved}` lists of clips by
588
- media_pool_item_id and timeline position.
607
+ between two snapshots: `{added, removed, moved, trimmed, summary}`. `trimmed`
608
+ lists clips kept in place but re-trimmed (carries `out_frame_before`); `summary`
609
+ has per-bucket counts plus `before_clip_count`/`after_clip_count`. Clips are
610
+ keyed by media_pool_item_id and timeline position.
589
611
  - `get_history(timeline_name?, analysis_run_id?, limit?)` — brain-edit rows
590
612
  with `edit_type`, `target_metric`, `before_value`, `after_value`, `delta`,
591
613
  `rationale`, and `initiator`. Filter by timeline or run; defaults to 50.
@@ -674,6 +696,11 @@ Key actions:
674
696
  - `get_track_count(track_type)` — track_type: `"video"`, `"audio"`, `"subtitle"`
675
697
  - `add_track(track_type, sub_type?)` / `delete_track(track_type, index)`
676
698
  - `get_items(track_type, index)` — items on a track
699
+ - `clip_where(track_type?, track_index?, name_contains?, duration_lt?, duration_gt?)` —
700
+ (v2.28.0+) return clips on the current timeline matching named filters (AND),
701
+ instead of walking tracks by hand. Filters may be passed inline or as a
702
+ `filters` dict; a mistyped filter name is rejected rather than silently
703
+ matching everything. Returns `{clips, match_count, total_clips}`.
677
704
  - `delete_clips(clip_ids, ripple?)` — IDs are unique IDs from `get_items`
678
705
  - `duplicate_clips(clip_ids?, selected?, target_track_index?, track_offset?, placement?, record_frame?, record_frame_offset?, copy_properties?, include_linked?)` —
679
706
  duplicate existing video timeline items by re-appending the same Media Pool
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.27.2"
38
+ VERSION = "2.28.0"
39
39
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
40
40
  # Resolve's scripting bridge loads into newer interpreters on recent builds
41
41
  # (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": "2.27.2",
3
+ "version": "2.28.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -13481,6 +13481,25 @@ class Handler(BaseHTTPRequestHandler):
13481
13481
  if path == "/api/timeline_versions":
13482
13482
  self._json(list_timelines_with_versions(self.state.project_root))
13483
13483
  return
13484
+ if path == "/api/timeline_versions/diff":
13485
+ timeline_name = (query.get("timeline_name") or [""])[0]
13486
+ try:
13487
+ from_version = int((query.get("from_version") or [""])[0])
13488
+ to_version = int((query.get("to_version") or [""])[0])
13489
+ except (ValueError, TypeError):
13490
+ self._json({"success": False, "error": "from_version and to_version (ints) required"},
13491
+ HTTPStatus.BAD_REQUEST)
13492
+ return
13493
+ if not timeline_name:
13494
+ self._json({"success": False, "error": "timeline_name required"}, HTTPStatus.BAD_REQUEST)
13495
+ return
13496
+ self._json({"success": True, **_timeline_versioning.diff_versions(
13497
+ project_root=self.state.project_root,
13498
+ timeline_name=timeline_name,
13499
+ from_version=from_version,
13500
+ to_version=to_version,
13501
+ )})
13502
+ return
13484
13503
  if path.startswith("/api/timeline_versions/"):
13485
13504
  timeline_name = unquote(path[len("/api/timeline_versions/"):])
13486
13505
  if not timeline_name:
package/src/batch_cli.py CHANGED
@@ -318,6 +318,65 @@ def _cmd_cancel(args: argparse.Namespace) -> int:
318
318
  return EXIT_OK if payload.get("success") else EXIT_FATAL
319
319
 
320
320
 
321
+ def _run_spec_action(action: str, params: Dict[str, Any]):
322
+ """Connect to Resolve (auto-launch) and run a project_manager spec action.
323
+
324
+ Imported lazily so the analysis commands stay free of the MCP/Resolve import.
325
+ """
326
+ from src.server import get_resolve, _spec_action # lazy: pulls mcp + resolve
327
+
328
+ r = get_resolve()
329
+ if r is None:
330
+ return None
331
+ return _spec_action(r, r.GetProjectManager(), action, params)
332
+
333
+
334
+ def _emit_spec_result(result, *, json_mode: bool) -> int:
335
+ if result is None:
336
+ _emit("Could not connect to DaVinci Resolve.",
337
+ json_mode=json_mode,
338
+ payload={"success": False, "error": "not_connected"})
339
+ return EXIT_FATAL
340
+ if json_mode:
341
+ _emit("", json_mode=True, payload=result)
342
+ err = result.get("error")
343
+ if err:
344
+ _emit(f"Spec error: {err.get('message')}", json_mode=False)
345
+ return EXIT_FATAL
346
+ if "actions" in result: # plan / diff
347
+ changed = result.get("change_count", 0)
348
+ _emit(f"Project : {result.get('project')}", json_mode=False)
349
+ _emit(f"Changes : {changed}", json_mode=False)
350
+ for a in result.get("actions", []):
351
+ if a.get("op") != "noop":
352
+ _emit(f" [{a['op']:>6}] {a['target']} {a.get('detail', '')}", json_mode=False)
353
+ return EXIT_OK
354
+ # apply
355
+ failures = result.get("failures") or []
356
+ _emit(f"Applied : {result.get('applied_count', 0)}", json_mode=False)
357
+ if failures:
358
+ for f in failures:
359
+ _emit(f" [failed] {f.get('target')}", json_mode=False)
360
+ return EXIT_PARTIAL
361
+ _emit("Done: spec applied", json_mode=False)
362
+ return EXIT_OK
363
+
364
+
365
+ def _cmd_plan_spec(args: argparse.Namespace) -> int:
366
+ result = _run_spec_action("diff_to_spec", {"spec_path": args.spec})
367
+ return _emit_spec_result(result, json_mode=args.json)
368
+
369
+
370
+ def _cmd_apply(args: argparse.Namespace) -> int:
371
+ result = _run_spec_action("apply_spec", {
372
+ "spec_path": args.spec,
373
+ "dry_run": args.dry_run,
374
+ "run_hooks": args.run_hooks,
375
+ "continue_on_error": args.continue_on_error,
376
+ })
377
+ return _emit_spec_result(result, json_mode=args.json)
378
+
379
+
321
380
  def _add_run_args(parser: argparse.ArgumentParser) -> None:
322
381
  parser.add_argument("paths", nargs="+", help="Files or directories to analyze")
323
382
  parser.add_argument(
@@ -431,6 +490,26 @@ def _build_parser() -> argparse.ArgumentParser:
431
490
  p_cancel.add_argument("job_id")
432
491
  p_cancel.add_argument("--project-root", required=True)
433
492
 
493
+ p_plan_spec = sub.add_parser(
494
+ "plan-spec",
495
+ help="Preview drift between a declarative project spec and live Resolve",
496
+ parents=[common],
497
+ )
498
+ p_plan_spec.add_argument("spec", help="Path to project.dvr.yaml (or .json)")
499
+
500
+ p_apply = sub.add_parser(
501
+ "apply",
502
+ help="Reconcile the Resolve project toward a declarative spec (idempotent)",
503
+ parents=[common],
504
+ )
505
+ p_apply.add_argument("spec", help="Path to project.dvr.yaml (or .json)")
506
+ p_apply.add_argument("--dry-run", action="store_true",
507
+ help="Compute the plan without mutating")
508
+ p_apply.add_argument("--run-hooks", action="store_true",
509
+ help="Execute the spec's before/after shell hooks (opt-in)")
510
+ p_apply.add_argument("--continue-on-error", action="store_true",
511
+ help="Accumulate failures instead of aborting on the first")
512
+
434
513
  return parser
435
514
 
436
515
 
@@ -441,6 +520,8 @@ _HANDLERS = {
441
520
  "list": _cmd_list,
442
521
  "resume": _cmd_resume,
443
522
  "cancel": _cmd_cancel,
523
+ "plan-spec": _cmd_plan_spec,
524
+ "apply": _cmd_apply,
444
525
  }
445
526
 
446
527
 
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.27.2"
83
+ VERSION = "2.28.0"
84
84
  logger = logging.getLogger("davinci-resolve-mcp")
85
85
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
86
  logger.info(f"Detected platform: {get_platform()}")