davinci-resolve-mcp 2.40.0 → 2.41.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,46 @@
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.41.0
6
+
7
+ DB-canonical clip analysis (C1) — Phase A of the analysis + edit-engine
8
+ program. The per-project SQLite DB is now the source of truth for clip
9
+ analysis; `analysis.json` becomes a derived export written in lockstep.
10
+
11
+ - **Added** schema v9 to the per-project timeline-brain DB: `clips`,
12
+ `clip_aliases`, `analysis_reports` (canonical full payload), `shots`,
13
+ `subjective_fields` + `field_changelog` (per-field provenance),
14
+ `transcript_segments`, `frames`, and `qc_observations`.
15
+ - **Added** `src/utils/analysis_store.py`: transactional ingest, export with
16
+ human-correction overlay (human rows always win and survive re-analysis),
17
+ alias-based clip lookup, shot ids stable under one-second boundary jitter,
18
+ and a round-trip guard verified against a real sample analysis root.
19
+ - **Added** `media_analysis` actions `db_status` (schema version + row
20
+ counts) and `db_ingest` (one-shot migration of existing JSON reports and
21
+ `corrections.json` sidecars into the DB).
22
+ - **Changed** the analysis write path: `execute_plan` and `commit_vision`
23
+ write DB rows first, then export the JSON. Panel clip/shot endpoints read
24
+ DB-first with JSON fallback for pre-v9 reports and job-linked report dirs.
25
+ - **Changed** `update_clip_field` / `update_shot_field` / `revert_field` to
26
+ mirror corrections into the DB as row-level provenance (the
27
+ `corrections.json` sidecar remains for compatibility).
28
+ - **Fixed** eight V2 actions that were unreachable from MCP dispatch since
29
+ v2.24.0 (`get_panel_state`, `set_panel_state`, `session_start_context`,
30
+ `update_shot_field`, `update_clip_field`, `get_field_history`,
31
+ `revert_field`, `list_corrections`): they were checked inside the
32
+ project-root dispatch block but missing from its membership set. The
33
+ control panel proxied to the helpers directly, which masked it.
34
+ - **Fixed** the action-list drift guard to inspect async tool functions
35
+ (media_analysis had drifted unchecked) and to fail on actions that are
36
+ unreachable inside membership blocks — the exact class above. Four
37
+ reachable-but-unadvertised actions (`coverage_report`,
38
+ `get_resolve_ai_usage`, `get_ai_governance`, `set_ai_governance`) are now
39
+ listed in the unknown-action error.
40
+ - **Validation**: full offline suite (1066 tests; 12 new), round-trip guard
41
+ on the real 2026-05-17 sample root, and a live headless pipeline run on
42
+ synthetic media verifying rows-then-export parity, row-level corrections,
43
+ and DB-first panel reads.
44
+
5
45
  ## What's New in v2.40.0
6
46
 
7
47
  Control panel UX overhaul + docs refresh, from a full live audit of every
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.40.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.41.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-33%20(341%20full)-blue.svg)](#server-modes)
package/docs/SKILL.md CHANGED
@@ -491,8 +491,24 @@ Key actions: `capabilities`, `install_guidance`, `resolve_output_root`, `plan`,
491
491
  `publish_clip_metadata`, `commit_vision`, `summarize`, `get_report`,
492
492
  `build_index`, `index_status`, `query_index`, `start_batch_job`,
493
493
  `run_batch_job_slice`, `batch_job_status`, `list_batch_jobs`,
494
- `cancel_batch_job`, `resume_batch_job`, `review_timeline_markers`, and
495
- `cleanup_artifacts`.
494
+ `cancel_batch_job`, `resume_batch_job`, `review_timeline_markers`,
495
+ `cleanup_artifacts`, `db_status`, `db_ingest`, `get_panel_state`,
496
+ `set_panel_state`, `session_start_context`, `update_clip_field`,
497
+ `update_shot_field`, `get_field_history`, `revert_field`, and
498
+ `list_corrections`.
499
+
500
+ **DB-canonical analysis store (v2.41.0+).** The per-project SQLite DB
501
+ (`_soul/timeline_brain.sqlite`, schema v9+) is the source of truth for clip
502
+ analysis; `analysis.json` is a derived export written in lockstep. Analysis
503
+ runs write rows first (clips, shots, per-field subjective provenance,
504
+ transcript segments, sampled frames, QC observations) and then export the
505
+ JSON. Human corrections recorded via `update_clip_field` / `update_shot_field`
506
+ live as row-level provenance and always survive re-analysis. Readers
507
+ (panel API, exports) load DB-first and fall back to `analysis.json` for
508
+ reports that predate v9. `db_status` reports schema version + row counts;
509
+ `db_ingest` migrates an existing project's JSON reports (and
510
+ `corrections.json` sidecars) into the DB — run it once on older analysis
511
+ roots.
496
512
  The tool never installs
497
513
  dependencies and validates that outputs stay under
498
514
  `davinci-resolve-mcp-analysis` project roots rather than beside source media.
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.40.0"
38
+ VERSION = "2.41.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.40.0",
3
+ "version": "2.41.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -12450,6 +12450,25 @@ def _v2_load_analysis(clip_dir: str) -> Optional[Dict[str, Any]]:
12450
12450
  return None
12451
12451
 
12452
12452
 
12453
+ def _v2_load_analysis_db_first(project_root: str, clip_dir: str) -> Optional[Dict[str, Any]]:
12454
+ """C1 — DB-canonical reader with JSON fallback.
12455
+
12456
+ Falls back to analysis.json for reports that predate schema v9 and for
12457
+ job-linked report dirs whose rows live under another project's DB.
12458
+ """
12459
+ try:
12460
+ from src.utils import analysis_store
12461
+
12462
+ report = analysis_store.load_db_report(
12463
+ project_root, clip_dir=os.path.basename(clip_dir.rstrip("/\\"))
12464
+ )
12465
+ if report is not None:
12466
+ return report
12467
+ except Exception:
12468
+ pass
12469
+ return _v2_load_analysis(clip_dir)
12470
+
12471
+
12453
12472
  def _v2_clip_duration(report: Dict[str, Any]) -> Optional[float]:
12454
12473
  marker_plan = report.get("clip_analysis_markers") if isinstance(report.get("clip_analysis_markers"), dict) else {}
12455
12474
  duration = marker_plan.get("duration_seconds")
@@ -12530,7 +12549,7 @@ def list_analyzed_clips(project_root: str) -> Dict[str, Any]:
12530
12549
  """List analyzed clips for the bin grid. One row per analysis.json found."""
12531
12550
  rows: List[Dict[str, Any]] = []
12532
12551
  for slug, clip_dir in _v2_iter_analysis_dirs(project_root):
12533
- report = _v2_load_analysis(clip_dir)
12552
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12534
12553
  if report is None:
12535
12554
  continue
12536
12555
  rows.append(_v2_clip_summary_card(slug, clip_dir, report))
@@ -12561,7 +12580,7 @@ def get_analyzed_clip(project_root: str, clip_id: str) -> Dict[str, Any]:
12561
12580
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
12562
12581
  if not clip_dir:
12563
12582
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
12564
- report = _v2_load_analysis(clip_dir)
12583
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12565
12584
  if report is None:
12566
12585
  return {"success": False, "error": "analysis.json unreadable"}
12567
12586
  visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
@@ -12598,7 +12617,7 @@ def get_analyzed_clip_shots(project_root: str, clip_id: str) -> Dict[str, Any]:
12598
12617
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
12599
12618
  if not clip_dir:
12600
12619
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
12601
- report = _v2_load_analysis(clip_dir)
12620
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12602
12621
  if report is None:
12603
12622
  return {"success": False, "error": "analysis.json unreadable"}
12604
12623
  visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
@@ -12615,7 +12634,7 @@ def get_analyzed_clip_shot(project_root: str, clip_id: str, shot_index: int) ->
12615
12634
  clip_dir = _v2_find_clip_dir(project_root, clip_id)
12616
12635
  if not clip_dir:
12617
12636
  return {"success": False, "error": f"No analyzed clip found for id={clip_id}"}
12618
- report = _v2_load_analysis(clip_dir)
12637
+ report = _v2_load_analysis_db_first(project_root, clip_dir)
12619
12638
  if report is None:
12620
12639
  return {"success": False, "error": "analysis.json unreadable"}
12621
12640
  visual = report.get("visual") if isinstance(report.get("visual"), dict) else {}
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.40.0"
83
+ VERSION = "2.41.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()}")
package/src/server.py CHANGED
@@ -11,7 +11,7 @@ Usage:
11
11
  python src/server.py --full # Start the 341-tool granular server instead
12
12
  """
13
13
 
14
- VERSION = "2.40.0"
14
+ VERSION = "2.41.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -11215,6 +11215,25 @@ def _v2_update_field(project_root: str, p: Dict[str, Any], *, entity_type: str)
11215
11215
  write_result = _v2_write_corrections(path, data)
11216
11216
  if not write_result.get("success"):
11217
11217
  return write_result
11218
+ # C1 — mirror the correction into the DB-canonical store (row-level
11219
+ # provenance). Best-effort: the sidecar remains authoritative for projects
11220
+ # whose DB predates v9 or lacks ingested rows.
11221
+ db_result: Dict[str, Any]
11222
+ try:
11223
+ from src.utils import analysis_store
11224
+ db_result = analysis_store.record_human_correction(
11225
+ project_root,
11226
+ clip_ref=clip_id or os.path.basename(os.path.dirname(path)),
11227
+ entity_type=entity_type,
11228
+ entity_uuid=entity_uuid,
11229
+ field_path=field_path,
11230
+ value=new_value,
11231
+ author=author,
11232
+ reason=reason,
11233
+ confidence=str(confidence) if confidence else None,
11234
+ )
11235
+ except Exception as exc: # noqa: BLE001 — DB mirror must not break corrections
11236
+ db_result = {"success": False, "error": f"{type(exc).__name__}: {exc}"}
11218
11237
  return {
11219
11238
  "success": True,
11220
11239
  "entity_type": entity_type,
@@ -11225,6 +11244,7 @@ def _v2_update_field(project_root: str, p: Dict[str, Any], *, entity_type: str)
11225
11244
  "author": author,
11226
11245
  "timestamp": now,
11227
11246
  "corrections_path": path,
11247
+ "db": db_result,
11228
11248
  }
11229
11249
 
11230
11250
 
@@ -11307,6 +11327,35 @@ def _v2_revert_field(project_root: str, p: Dict[str, Any]) -> Dict[str, Any]:
11307
11327
  write_result = _v2_write_corrections(path, data)
11308
11328
  if not write_result.get("success"):
11309
11329
  return write_result
11330
+ # C1 — mirror the revert into the DB-canonical store. A revert back to a
11331
+ # human value re-records it; a revert to machine-derived clears the human
11332
+ # row so the export overlay falls back to the blob value.
11333
+ db_result: Dict[str, Any]
11334
+ try:
11335
+ from src.utils import analysis_store
11336
+ clip_ref = clip_id or os.path.basename(os.path.dirname(path))
11337
+ if action_taken == "removed (back to machine-derived)" or revert_source != "human":
11338
+ db_result = analysis_store.clear_human_field(
11339
+ project_root,
11340
+ clip_ref=clip_ref,
11341
+ entity_type=entity_type,
11342
+ entity_uuid=entity_uuid,
11343
+ field_path=field_path,
11344
+ author=author,
11345
+ )
11346
+ else:
11347
+ db_result = analysis_store.record_human_correction(
11348
+ project_root,
11349
+ clip_ref=clip_ref,
11350
+ entity_type=entity_type,
11351
+ entity_uuid=entity_uuid,
11352
+ field_path=field_path,
11353
+ value=revert_to,
11354
+ author=author,
11355
+ reason=f"revert by {author}",
11356
+ )
11357
+ except Exception as exc: # noqa: BLE001 — DB mirror must not break corrections
11358
+ db_result = {"success": False, "error": f"{type(exc).__name__}: {exc}"}
11310
11359
  return {
11311
11360
  "success": True,
11312
11361
  "action": action_taken,
@@ -11314,6 +11363,7 @@ def _v2_revert_field(project_root: str, p: Dict[str, Any]) -> Dict[str, Any]:
11314
11363
  "reverted_value": revert_to,
11315
11364
  "timestamp": now,
11316
11365
  "corrections_path": path,
11366
+ "db": db_result,
11317
11367
  }
11318
11368
 
11319
11369
 
@@ -14955,6 +15005,21 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
14955
15005
  "cancel_batch_job",
14956
15006
  "resume_batch_job",
14957
15007
  "cleanup_artifacts",
15008
+ # V2 session/panel state + C4 corrections. These checks lived inside
15009
+ # this block since v2.24.0 but were missing from the membership set,
15010
+ # making them unreachable from MCP dispatch (the panel proxied to the
15011
+ # helpers directly, which masked it). Fixed as part of C1 (Phase A).
15012
+ "get_panel_state",
15013
+ "set_panel_state",
15014
+ "session_start_context",
15015
+ "update_shot_field",
15016
+ "update_clip_field",
15017
+ "get_field_history",
15018
+ "revert_field",
15019
+ "list_corrections",
15020
+ # C1 — DB-canonical analysis store.
15021
+ "db_status",
15022
+ "db_ingest",
14958
15023
  }:
14959
15024
  root = resolve_media_analysis_output_root(
14960
15025
  project_name=project_name,
@@ -15007,6 +15072,13 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15007
15072
  return _v2_revert_field(project_root, p)
15008
15073
  if action == "list_corrections":
15009
15074
  return _v2_list_corrections(project_root, p)
15075
+ # C1 — DB-canonical analysis store (Phase A).
15076
+ if action == "db_status":
15077
+ from src.utils import analysis_store
15078
+ return analysis_store.db_status(project_root)
15079
+ if action == "db_ingest":
15080
+ from src.utils import analysis_store
15081
+ return analysis_store.ingest_project(project_root)
15010
15082
  if action in {"build_index", "rebuild_index"}:
15011
15083
  return build_analysis_index(project_root, index_path=p.get("index_path") or p.get("indexPath"))
15012
15084
  if action == "index_status":
@@ -15408,6 +15480,23 @@ async def media_analysis(action: str, params: Optional[Dict[str, Any]] = None, c
15408
15480
  "cancel_batch_job",
15409
15481
  "resume_batch_job",
15410
15482
  "cleanup_artifacts",
15483
+ "get_panel_state",
15484
+ "set_panel_state",
15485
+ "session_start_context",
15486
+ "update_shot_field",
15487
+ "update_clip_field",
15488
+ "get_field_history",
15489
+ "revert_field",
15490
+ "list_corrections",
15491
+ "db_status",
15492
+ "db_ingest",
15493
+ "get_caps",
15494
+ "set_caps_preset",
15495
+ "get_usage",
15496
+ "coverage_report",
15497
+ "get_resolve_ai_usage",
15498
+ "get_ai_governance",
15499
+ "set_ai_governance",
15411
15500
  ])
15412
15501
 
15413
15502