davinci-resolve-mcp 2.33.7 → 2.33.8

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,20 @@
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.33.8
6
+
7
+ Bridge-call performance instrumentation.
8
+
9
+ - **Added** `src/utils/bridge_metrics.py` — a counting proxy that wraps a Resolve
10
+ handle and tallies attribute accesses and method calls (each a COM/socket
11
+ round-trip), so the real bridge cost of an operation is measured rather than
12
+ guessed.
13
+ - **Added** `scripts/measure_bridge_cost.py` — runs a representative media-pool
14
+ traversal through the proxy and reports round-trips per clip. A minimal
15
+ name+type walk measured ~6.7 round-trips per clip, confirming round-trips scale
16
+ linearly with traversal size. A property cache remains gated on profiling
17
+ *repeated*-read patterns in real workflows (don't cache blind).
18
+
5
19
  ## What's New in v2.33.7
6
20
 
7
21
  Read/write symmetry audit and a gap it surfaced.
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.33.7-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.33.8-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(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.33.7"
38
+ VERSION = "2.33.8"
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.33.7",
3
+ "version": "2.33.8",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """Measure DaVinci Resolve bridge round-trips for representative operations.
3
+
4
+ Wraps the live project in a counting proxy and runs a few common traversals,
5
+ reporting how many attribute accesses + method calls (each a bridge round-trip)
6
+ they cost. This is the measurement that gates whether a property cache is worth
7
+ building. Run against an OPEN project; it only reads.
8
+
9
+ PYTHONPATH=. venv/bin/python scripts/measure_bridge_cost.py
10
+ """
11
+ import sys
12
+
13
+ import src.server as s
14
+ from src.utils.bridge_metrics import measure
15
+
16
+
17
+ def _walk_media_pool(project_proxy):
18
+ """A typical media-pool traversal: every folder, every clip, name + a property."""
19
+ mp = project_proxy.GetMediaPool()
20
+ root = mp.GetRootFolder()
21
+ clips_seen = 0
22
+
23
+ def walk(folder):
24
+ nonlocal clips_seen
25
+ for clip in (folder.GetClipList() or []):
26
+ clip.GetName()
27
+ clip.GetClipProperty("Type")
28
+ clips_seen += 1
29
+ for sub in (folder.GetSubFolderList() or []):
30
+ walk(sub)
31
+
32
+ walk(root)
33
+ return clips_seen
34
+
35
+
36
+ def main():
37
+ r = s.get_resolve()
38
+ if not r:
39
+ print("Not connected to Resolve.")
40
+ return 1
41
+ proj = r.GetProjectManager().GetCurrentProject()
42
+ if not proj:
43
+ print("No project open.")
44
+ return 1
45
+
46
+ clips_holder = {}
47
+
48
+ def op(proj_proxy):
49
+ clips_holder["n"] = _walk_media_pool(proj_proxy)
50
+
51
+ counts = measure(op, proj)
52
+ n = clips_holder.get("n", 0)
53
+ total = counts["attr_access"] + counts["calls"]
54
+ print(f"project: {proj.GetName()!r}")
55
+ print(f"clips walked: {n}")
56
+ print(f"bridge attr-accesses: {counts['attr_access']}")
57
+ print(f"bridge method-calls: {counts['calls']}")
58
+ print(f"total round-trips: {total}")
59
+ if n:
60
+ print(f"round-trips per clip: {total / n:.1f}")
61
+ return 0
62
+
63
+
64
+ if __name__ == "__main__":
65
+ sys.exit(main())
@@ -80,7 +80,7 @@ if not logging.getLogger().handlers:
80
80
  handlers=[logging.StreamHandler()],
81
81
  )
82
82
 
83
- VERSION = "2.33.7"
83
+ VERSION = "2.33.8"
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.33.7"
14
+ VERSION = "2.33.8"
15
15
 
16
16
  import base64
17
17
  import os
@@ -0,0 +1,76 @@
1
+ """Instrumentation for counting DaVinci Resolve bridge round-trips.
2
+
3
+ Every attribute access and method call on a Resolve object is a COM/socket
4
+ round-trip — the dominant cost of most operations. Before optimizing (or adding
5
+ a cache), you have to *measure* where the round-trips concentrate. This module
6
+ provides an opt-in counting proxy that wraps a Resolve handle and tallies
7
+ attribute accesses and method calls, so a hot path's real bridge cost is
8
+ visible instead of guessed.
9
+
10
+ This is the measurement half of the bridge-perf work. A property cache is only
11
+ worth building once measurement shows a clear, repeated-read hot spot — don't
12
+ cache blind.
13
+
14
+ Example:
15
+
16
+ counts = {}
17
+ rp = CountingProxy(resolve, counts)
18
+ rp.GetProjectManager().GetCurrentProject().GetName()
19
+ print(counts) # {'attr_access': N, 'calls': M}
20
+ """
21
+ from typing import Any, Dict, Optional
22
+
23
+
24
+ class CountingProxy:
25
+ """Wrap a Resolve object, counting attribute accesses and method calls.
26
+
27
+ Method return values that are themselves objects are wrapped too, so a whole
28
+ call chain through the bridge is counted. Primitives are returned as-is.
29
+ """
30
+
31
+ __slots__ = ("_target", "_counter")
32
+
33
+ def __init__(self, target: Any, counter: Dict[str, int]):
34
+ object.__setattr__(self, "_target", target)
35
+ object.__setattr__(self, "_counter", counter)
36
+ counter.setdefault("attr_access", 0)
37
+ counter.setdefault("calls", 0)
38
+
39
+ def __getattr__(self, name: str) -> Any:
40
+ counter = object.__getattribute__(self, "_counter")
41
+ target = object.__getattribute__(self, "_target")
42
+ counter["attr_access"] += 1
43
+ attr = getattr(target, name)
44
+ if callable(attr):
45
+ def wrapped(*args: Any, **kwargs: Any) -> Any:
46
+ counter["calls"] += 1
47
+ result = attr(*args, **kwargs)
48
+ return _maybe_wrap(result, counter)
49
+ return wrapped
50
+ return _maybe_wrap(attr, counter)
51
+
52
+
53
+ _PRIMITIVE = (str, int, float, bool, bytes, type(None))
54
+
55
+
56
+ def _maybe_wrap(value: Any, counter: Dict[str, int]) -> Any:
57
+ if isinstance(value, _PRIMITIVE):
58
+ return value
59
+ if isinstance(value, (list, tuple)):
60
+ # Wrap object elements so traversals (e.g. clip lists) are counted.
61
+ return type(value)(_maybe_wrap(v, counter) for v in value)
62
+ if isinstance(value, dict):
63
+ return {k: _maybe_wrap(v, counter) for k, v in value.items()}
64
+ # Likely a Resolve API object — wrap so its further use is counted.
65
+ return CountingProxy(value, counter)
66
+
67
+
68
+ def measure(fn, target: Any) -> Dict[str, int]:
69
+ """Run ``fn(proxy)`` against a counting proxy of ``target``; return the counts.
70
+
71
+ ``fn`` receives the proxy and should exercise the operation under study.
72
+ """
73
+ counter: Dict[str, int] = {}
74
+ proxy = CountingProxy(target, counter)
75
+ fn(proxy)
76
+ return counter