davinci-resolve-mcp 2.69.0 → 2.69.1

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,49 @@
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.69.1
6
+
7
+ Bug fix. The Studio bridge differential no longer reports the Deliver page its
8
+ own render probes navigated to as a transport difference.
9
+
10
+ ### Fixed
11
+
12
+ - **A clean bridge produced a red result on any run that did not start on
13
+ Deliver.** Run live on Studio 19.1.3.7, the differential reported
14
+ `resolve.GetCurrentPage` as a `value_mismatch` — bridge `edit`, native
15
+ `deliver` — and it reproduced on command: start on Edit, one difference; start
16
+ on Deliver, none. The transport was never involved. The harness's own render
17
+ probes navigate Resolve to the Deliver page, and the bridge pass runs before
18
+ the native pass, so the native pass read a page the bridge pass had moved. Read
19
+ at the same instant the two transports always agreed.
20
+
21
+ Adding the method to `VOLATILE_METHODS` would have turned red green and thrown
22
+ away the signal — a bridge that genuinely reports the wrong page is exactly
23
+ what this harness exists to catch. Instead each pass now records the page on
24
+ the way in and on the way out, and the value is compared **only when both
25
+ passes prove it held still**. If both were stable and the values still
26
+ disagree, that is the transport, and it is reported as before. A page that
27
+ cannot be read counts as unknown rather than stable, so an unreadable page
28
+ disables the comparison instead of quietly restoring the false positive.
29
+
30
+ `page_compared_by_value` and both passes' before/after values now ride in the
31
+ report either way. "The page was not compared" is a fact about the run, and
32
+ burying it would recreate the silence this fixes.
33
+
34
+ ### Added
35
+
36
+ - `tests/test_bridge_differential.py` — the module's first tests, 13 of them,
37
+ including the one that matters: a stable page that genuinely disagrees must
38
+ still be reported, or the fix is a mute button rather than a fix. The absence
39
+ of any coverage here is why the false positive shipped.
40
+
41
+ ### Validation
42
+
43
+ - Static checks and the full unit suite: 2243 → 2256 tests, pyflakes clean.
44
+ - Live Resolve validation on Studio 19.1.3.7 after the fix: 142 probes, 0
45
+ differences, 110/112 read methods, started from the Edit page that previously
46
+ failed.
47
+
5
48
  ## What's New in v2.69.0
6
49
 
7
50
  The free edition is documented as reachable, silence ripple stops clipping
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.69.0-blue.svg)](https://github.com/samuelgursky/davinci-resolve-mcp/releases)
3
+ [![Version](https://img.shields.io/badge/version-2.69.1-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
@@ -36,7 +36,7 @@ from src.utils.update_check import (
36
36
 
37
37
  # ─── Version ──────────────────────────────────────────────────────────────────
38
38
 
39
- VERSION = "2.69.0"
39
+ VERSION = "2.69.1"
40
40
  # Only hard floor: mcp[cli] requires Python 3.10+. There is no upper bound —
41
41
  # Resolve's scripting bridge loads into newer interpreters on recent builds
42
42
  # (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.69.0",
3
+ "version": "2.69.1",
4
4
  "description": "NPM bootstrapper for the DaVinci Resolve MCP Server.",
5
5
  "license": "MIT",
6
6
  "author": "Samuel Gursky <samgursky@gmail.com>",
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
85
85
  handlers=[logging.StreamHandler()],
86
86
  )
87
87
 
88
- VERSION = "2.69.0"
88
+ VERSION = "2.69.1"
89
89
  logger = logging.getLogger("davinci-resolve-mcp")
90
90
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
91
91
  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.69.0"
14
+ VERSION = "2.69.1"
15
15
 
16
16
  import base64
17
17
  import os
@@ -61,6 +61,28 @@ VOLATILE_METHODS = frozenset({
61
61
  "IsPlayheadOnVideoFrame",
62
62
  })
63
63
 
64
+ #: App-global state that **the harness's own probes move**, which is a different
65
+ #: problem from `VOLATILE_METHODS` drifting on its own.
66
+ #:
67
+ #: Measured on Studio 19.1.3.7: the render probes navigate Resolve to the Deliver
68
+ #: page, and the bridge pass runs before the native pass — so the native pass
69
+ #: reads a page the bridge pass changed. That produced a `value_mismatch` on
70
+ #: `GetCurrentPage` (bridge `edit`, native `deliver`) on every run that did not
71
+ #: happen to start on Deliver, and none on the runs that did. Read at the same
72
+ #: instant the two transports always agreed.
73
+ #:
74
+ #: Excusing it unconditionally would throw away a real signal — a bridge that
75
+ #: genuinely reported the wrong page is exactly the bug this harness is for. So
76
+ #: each pass records the page on the way in and on the way out, and the value is
77
+ #: only excused when a pass proves it did not hold still. If both passes were
78
+ #: stable and the values still disagree, that is the transport and it is reported.
79
+ ORDER_SENSITIVE_METHODS = frozenset({"GetCurrentPage"})
80
+
81
+ #: Key `run_probes` adds for `compare`'s own use rather than as a probe result.
82
+ #: Underscore-prefixed so the diff skips it instead of reading it as a method
83
+ #: that one transport has and the other does not.
84
+ PASS_META_KEY = "_pass_meta"
85
+
64
86
  #: Never invoked by the harness, whatever the enumeration says. These either
65
87
  #: change the project, touch the filesystem, or block on UI. The write paths are
66
88
  #: exercised by named scenarios instead, where the setup and teardown are
@@ -326,6 +348,19 @@ def probe_one(target: Any, method: str) -> Dict[str, Any]:
326
348
  return {"outcome": "returned", "value": normalise(value), "type": describe(value)}
327
349
 
328
350
 
351
+ def _current_page(resolve: Any) -> Optional[str]:
352
+ """The page, or None if it cannot be read.
353
+
354
+ Never raises: this is bookkeeping around the probes, and a failure here must
355
+ not take down a pass that is otherwise fine.
356
+ """
357
+ try:
358
+ page = resolve.GetCurrentPage()
359
+ except Exception: # noqa: BLE001 - an unreadable page is simply unknown
360
+ return None
361
+ return page if isinstance(page, str) else None
362
+
363
+
329
364
  def run_probes(resolve: Any, methods: Sequence[str]) -> Dict[str, Dict[str, Any]]:
330
365
  """Every enumerated safe method, against every object that has it.
331
366
 
@@ -333,6 +368,7 @@ def run_probes(resolve: Any, methods: Sequence[str]) -> Dict[str, Dict[str, Any]
333
368
  not merely that they did.
334
369
  """
335
370
  results: Dict[str, Dict[str, Any]] = {}
371
+ page_before = _current_page(resolve)
336
372
  for label, chain in OBJECT_GRAPH:
337
373
  target = walk(resolve, chain)
338
374
  if target is None:
@@ -342,13 +378,28 @@ def run_probes(resolve: Any, methods: Sequence[str]) -> Dict[str, Dict[str, Any]
342
378
  if getattr(target, method, None) is None:
343
379
  continue # not on this object; absence is reported by the diff
344
380
  results[f"{label}.{method}"] = probe_one(target, method)
381
+ page_after = _current_page(resolve)
382
+ results[PASS_META_KEY] = {
383
+ "page_before": page_before,
384
+ "page_after": page_after,
385
+ # None on either side means we could not tell, which must not read as
386
+ # "stable" — an unknown has to disable the value comparison, not enable it.
387
+ "page_stable": page_before is not None and page_before == page_after,
388
+ }
345
389
  return results
346
390
 
347
391
 
348
392
  def compare(native: Dict[str, Dict[str, Any]], bridged: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
349
393
  """Diff two probe runs. Empty `differences` is the pass condition."""
350
394
  differences: List[Dict[str, Any]] = []
395
+ # A pass that moved the page cannot have its page compared by value against
396
+ # another pass taken at a different moment. Both must have held still.
397
+ native_meta = native.get(PASS_META_KEY) or {}
398
+ bridge_meta = bridged.get(PASS_META_KEY) or {}
399
+ page_held_still = bool(native_meta.get("page_stable")) and bool(bridge_meta.get("page_stable"))
351
400
  for key in sorted(set(native) | set(bridged)):
401
+ if key.startswith("_"):
402
+ continue # harness bookkeeping, not a probe result
352
403
  left, right = native.get(key), bridged.get(key)
353
404
  if left is None or right is None:
354
405
  differences.append({
@@ -364,17 +415,32 @@ def compare(native: Dict[str, Dict[str, Any]], bridged: Dict[str, Dict[str, Any]
364
415
  differences.append({"key": key, "kind": "volatile_type_mismatch",
365
416
  "native": left.get("type"), "bridge": right.get("type")})
366
417
  continue
418
+ if method in ORDER_SENSITIVE_METHODS and not page_held_still:
419
+ # The harness moved it mid-run, so the two readings are of different
420
+ # moments. Still hold the transport to returning the same kind of
421
+ # thing, and say why the value was not compared.
422
+ if left.get("type") != right.get("type") or left["outcome"] != right["outcome"]:
423
+ differences.append({"key": key, "kind": "order_sensitive_type_mismatch",
424
+ "native": left.get("type"), "bridge": right.get("type")})
425
+ continue
367
426
  if left != right:
368
427
  differences.append({"key": key, "kind": "value_mismatch",
369
428
  "native": left, "bridge": right})
370
- agreed = len(set(native) & set(bridged)) - len(
429
+ probe_keys = {k for k in set(native) | set(bridged) if not k.startswith("_")}
430
+ agreed = len({k for k in set(native) & set(bridged) if not k.startswith("_")}) - len(
371
431
  [d for d in differences if d["kind"] != "only_one_transport_reached_it"]
372
432
  )
373
433
  return {
374
- "compared": len(set(native) | set(bridged)),
434
+ "compared": len(probe_keys),
375
435
  "agreed": agreed,
376
436
  "differences": differences,
377
437
  "identical": not differences,
438
+ # Reported either way. "The page was not compared" is a fact about the
439
+ # run that a reader needs, and burying it would recreate the silence
440
+ # this fix exists to end.
441
+ "page_compared_by_value": page_held_still,
442
+ "page_native": {k: native_meta.get(k) for k in ("page_before", "page_after")},
443
+ "page_bridge": {k: bridge_meta.get(k) for k in ("page_before", "page_after")},
378
444
  }
379
445
 
380
446