davinci-resolve-mcp 2.49.0 → 2.50.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,28 @@
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.50.0
6
+
7
+ The last JSON-fed readers now source from the DB-canonical analysis store
8
+ (consistency/perf hygiene — the JSON export is lockstep with the DB, so this
9
+ is shape-preserving by construction).
10
+
11
+ - **Changed** `summarize_reports` aggregates from the DB (blob + human
12
+ overlay) when every report dir on disk is covered by an ingested clip row;
13
+ pre-v9 roots and MIXED roots (some clips not ingested) fall back WHOLESALE
14
+ to the JSON walk — a partial DB view would silently under-report. The
15
+ summary gains a `"source": "db"|"json"` key for observability; the F1
16
+ provenance map (source_reports / missing_reports) is unchanged.
17
+ - **Changed** `build_analysis_index` sources local reports from the DB
18
+ instead of re-parsing every `analysis.json`; job-linked EXTERNAL report
19
+ paths (their rows live under another project's DB) and pre-v9 dirs keep
20
+ the JSON read. The FTS schema and the query surface are identical; the
21
+ result gains `report_sources` counts.
22
+ - **Added** DB-vs-JSON parity tests (semantic equality with normalized
23
+ ordering) plus wholesale-fallback regressions for mixed and pre-v9 roots;
24
+ live-validated on the sample analysis root (summary, index counts, and
25
+ FTS query results identical on both paths).
26
+
5
27
  ## What's New in v2.49.0
6
28
 
7
29
  Cross-shot relationships (spec §4 — pattern recognition only): the shot
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.49.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.50.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-34%20(341%20full)-blue.svg)](#server-modes)
package/install.py CHANGED
@@ -35,7 +35,7 @@ from src.utils.update_check import (
35
35
 
36
36
  # ─── Version ──────────────────────────────────────────────────────────────────
37
37
 
38
- VERSION = "2.49.0"
38
+ VERSION = "2.50.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.49.0",
3
+ "version": "2.50.0",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.49.0"
83
+ VERSION = "2.50.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.49.0"
14
+ VERSION = "2.50.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -5994,20 +5994,68 @@ def load_report(project_root: str, report_path: Optional[str] = None, clip_dir:
5994
5994
  return {"success": True, "path": path, "report": payload}
5995
5995
 
5996
5996
 
5997
- def summarize_reports(project_root: str) -> Dict[str, Any]:
5998
- root = normalize_path(project_root)
5997
+ def _collect_reports_for_summary(root: str) -> Tuple[List[Dict[str, Any]], List[str], str]:
5998
+ """(reports, report_paths, source) for summarize_reports.
5999
+
6000
+ DB-first: when every report dir on disk is covered by an ingested clip
6001
+ row, reports come from the DB-canonical store (blob + human overlay —
6002
+ identical content to the lockstep JSON export). Pre-v9 roots and MIXED
6003
+ roots (some clips not ingested) fall back WHOLESALE to the JSON walk —
6004
+ a partial DB view would silently under-report.
6005
+ """
5999
6006
  clips_root = os.path.join(root, "clips")
6000
- reports: List[Dict[str, Any]] = []
6001
- report_paths: List[str] = []
6007
+ disk_paths: List[str] = []
6002
6008
  if os.path.isdir(clips_root):
6003
6009
  for dirpath, _, filenames in os.walk(clips_root):
6004
6010
  if "analysis.json" in filenames:
6005
- report_path = os.path.join(dirpath, "analysis.json")
6011
+ disk_paths.append(os.path.join(dirpath, "analysis.json"))
6012
+ disk_paths.sort()
6013
+
6014
+ try:
6015
+ from src.utils import analysis_store, timeline_brain_db
6016
+
6017
+ conn = timeline_brain_db.connect(root)
6018
+ db_dirs = {
6019
+ str(r["clip_dir"]): str(r["clip_uuid"])
6020
+ for r in conn.execute(
6021
+ "SELECT clip_dir, clip_uuid FROM clips WHERE clip_dir IS NOT NULL"
6022
+ ).fetchall()
6023
+ }
6024
+ except Exception: # noqa: BLE001 — no DB (pre-v9) → JSON
6025
+ db_dirs = {}
6026
+ if disk_paths and db_dirs:
6027
+ dir_names = [os.path.basename(os.path.dirname(p)) for p in disk_paths]
6028
+ if all(name in db_dirs for name in dir_names):
6029
+ from src.utils import analysis_store
6030
+
6031
+ reports: List[Dict[str, Any]] = []
6032
+ complete = True
6033
+ for path, name in zip(disk_paths, dir_names):
6006
6034
  try:
6007
- reports.append(_read_json(report_path))
6008
- report_paths.append(report_path)
6009
- except (OSError, json.JSONDecodeError):
6010
- continue
6035
+ report = analysis_store.export_report(root, db_dirs[name])
6036
+ except Exception: # noqa: BLE001
6037
+ report = None
6038
+ if not isinstance(report, dict):
6039
+ complete = False
6040
+ break
6041
+ reports.append(report)
6042
+ if complete:
6043
+ return reports, disk_paths, "db"
6044
+
6045
+ reports = []
6046
+ report_paths: List[str] = []
6047
+ for path in disk_paths:
6048
+ try:
6049
+ reports.append(_read_json(path))
6050
+ report_paths.append(path)
6051
+ except (OSError, json.JSONDecodeError):
6052
+ continue
6053
+ return reports, report_paths, "json"
6054
+
6055
+
6056
+ def summarize_reports(project_root: str) -> Dict[str, Any]:
6057
+ root = normalize_path(project_root)
6058
+ reports, report_paths, reports_source = _collect_reports_for_summary(root)
6011
6059
  warnings = []
6012
6060
  motion_counts: Dict[str, int] = {}
6013
6061
  tags: Dict[str, int] = {}
@@ -6047,6 +6095,7 @@ def summarize_reports(project_root: str) -> Dict[str, Any]:
6047
6095
  summary = {
6048
6096
  "success": True,
6049
6097
  "project_root": root,
6098
+ "source": reports_source, # "db" (canonical store) | "json" (walk fallback)
6050
6099
  "clip_reports": len(reports),
6051
6100
  "motion_distribution": motion_counts,
6052
6101
  "technical_warning_count": len(warnings),
@@ -7202,9 +7251,42 @@ def build_analysis_index(project_root: str, *, index_path: Optional[Any] = None)
7202
7251
  conn.execute("INSERT INTO index_metadata (key, value) VALUES (?, ?)", ("fts_enabled", "1" if fts_enabled else "0"))
7203
7252
  conn.execute("INSERT INTO index_metadata (key, value) VALUES (?, ?)", ("image_blob_policy", "excluded"))
7204
7253
 
7254
+ # DB-first sourcing: local reports whose clip dir is ingested come from
7255
+ # the DB-canonical store (blob + human overlay — identical content to
7256
+ # the lockstep JSON export) instead of re-parsing every analysis.json.
7257
+ # Pre-v9 dirs and job-linked EXTERNAL report paths (their rows live
7258
+ # under another project's DB) keep the JSON read. The index schema and
7259
+ # the query surface are unchanged either way.
7260
+ clips_root_prefix = os.path.realpath(os.path.join(root, "clips")) + os.sep
7261
+ try:
7262
+ from src.utils import timeline_brain_db as _brain_db
7263
+
7264
+ _db_dirs = {
7265
+ str(r["clip_dir"]): str(r["clip_uuid"])
7266
+ for r in _brain_db.connect(root).execute(
7267
+ "SELECT clip_dir, clip_uuid FROM clips WHERE clip_dir IS NOT NULL"
7268
+ ).fetchall()
7269
+ }
7270
+ except Exception: # noqa: BLE001 — no DB (pre-v9) → JSON for everything
7271
+ _db_dirs = {}
7272
+ report_sources = {"db": 0, "json": 0}
7205
7273
  for report_path in sorted(_iter_analysis_report_files(root)):
7206
7274
  try:
7207
- report = _read_json(report_path)
7275
+ report = None
7276
+ if _db_dirs and os.path.realpath(report_path).startswith(clips_root_prefix):
7277
+ clip_uuid = _db_dirs.get(os.path.basename(os.path.dirname(report_path)))
7278
+ if clip_uuid:
7279
+ try:
7280
+ from src.utils import analysis_store as _analysis_store
7281
+
7282
+ report = _analysis_store.export_report(root, clip_uuid)
7283
+ except Exception: # noqa: BLE001 — fall back per-report
7284
+ report = None
7285
+ if isinstance(report, dict):
7286
+ report_sources["db"] += 1
7287
+ else:
7288
+ report = _read_json(report_path)
7289
+ report_sources["json"] += 1
7208
7290
  row_counts = _insert_analysis_report_into_index(conn, report_path, report, fts_enabled=fts_enabled)
7209
7291
  counts["clips"] += 1
7210
7292
  for key, value in row_counts.items():
@@ -7244,6 +7326,7 @@ def build_analysis_index(project_root: str, *, index_path: Optional[Any] = None)
7244
7326
  "image_blob_policy": "excluded",
7245
7327
  "fts_enabled": bool(counts["clips"]) and _sqlite_table_exists(db_path, "clips_fts"),
7246
7328
  "counts": counts,
7329
+ "report_sources": report_sources, # how many reports came from the DB vs JSON
7247
7330
  "failed_report_count": len(failed_reports),
7248
7331
  "failed_reports": failed_reports[:50],
7249
7332
  "size_bytes": os.path.getsize(db_path) if os.path.isfile(db_path) else 0,