davinci-resolve-mcp 2.63.2 → 2.65.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.
@@ -44,6 +44,7 @@ from src.utils.layout_presets import (
44
44
  )
45
45
  from src.utils.object_inspection import inspect_object, print_object_help
46
46
  from src.utils.platform import get_platform, get_resolve_paths
47
+ from src.utils.resolve_connection import connect_resolve
47
48
  from src.utils.project_properties import (
48
49
  get_all_project_properties,
49
50
  get_color_settings,
@@ -80,7 +81,7 @@ if not logging.getLogger().handlers:
80
81
  handlers=[logging.StreamHandler()],
81
82
  )
82
83
 
83
- VERSION = "2.63.2"
84
+ VERSION = "2.65.0"
84
85
  logger = logging.getLogger("davinci-resolve-mcp")
85
86
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
86
87
  logger.info(f"Detected platform: {get_platform()}")
@@ -233,7 +234,7 @@ dvr_script = None
233
234
  try:
234
235
  import DaVinciResolveScript as dvr_script # type: ignore
235
236
 
236
- resolve = dvr_script.scriptapp("Resolve")
237
+ resolve = connect_resolve(dvr_script)
237
238
  if resolve:
238
239
  logger.info(
239
240
  f"Connected to DaVinci Resolve: {resolve.GetProductName()} {resolve.GetVersionString()}"
@@ -311,7 +312,7 @@ def _try_connect():
311
312
  """Attempt to connect to Resolve once. Returns resolve object or None."""
312
313
  global resolve
313
314
  try:
314
- candidate = dvr_script.scriptapp("Resolve")
315
+ candidate = connect_resolve(dvr_script)
315
316
  if candidate and _is_resolve_handle_live(candidate):
316
317
  resolve = candidate
317
318
  logger.info(f"Connected: {resolve.GetProductName()} {resolve.GetVersionString()}")
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.63.2"
14
+ VERSION = "2.65.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -97,6 +97,7 @@ from src.utils.media_analysis_jobs import (
97
97
  run_batch_job_slice as run_media_analysis_batch_job_slice,
98
98
  )
99
99
  from src.utils.platform import get_resolve_paths, get_resolve_plugin_paths
100
+ from src.utils.resolve_connection import connect_resolve
100
101
  from src.utils.lut_paths import master_lut_dir, ensure_lut_in_master
101
102
  from src.utils import fuse_templates, dctl_templates, script_templates
102
103
  from src.utils.timeline_title_text import (
@@ -804,7 +805,7 @@ def _try_connect():
804
805
  if dvr_script is None:
805
806
  return None
806
807
  try:
807
- candidate = dvr_script.scriptapp("Resolve")
808
+ candidate = connect_resolve(dvr_script)
808
809
  if candidate and _is_resolve_handle_live(candidate):
809
810
  resolve = candidate
810
811
  logger.info(f"Connected: {resolve.GetProductName()} {resolve.GetVersionString()}")
@@ -22,6 +22,8 @@ import subprocess
22
22
  import sys
23
23
  import threading
24
24
  import time
25
+ import urllib.error
26
+ import urllib.request
25
27
  from datetime import datetime, timezone
26
28
  from pathlib import Path
27
29
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
@@ -354,6 +356,9 @@ _ensure_path_includes_standard_tool_dirs()
354
356
  ANALYSIS_DIR_NAME = "davinci-resolve-mcp-analysis"
355
357
  HIDDEN_ANALYSIS_DIR_NAME = ".davinci-resolve-mcp-analysis"
356
358
  ANALYSIS_VERSION = "0.2"
359
+
360
+ MLX_AUDIO_ROUTER_URL_ENV = "DAVINCI_RESOLVE_MCP_MLX_AUDIO_URL"
361
+ MLX_AUDIO_ROUTER_MODEL_ENV = "DAVINCI_RESOLVE_MCP_MLX_AUDIO_MODEL"
357
362
  ANALYSIS_INDEX_FILENAME = "index.sqlite"
358
363
  ANALYSIS_REGISTRY_FILENAME = "analysis_registry.json"
359
364
  ANALYSIS_INDEX_SCHEMA_VERSION = 1
@@ -1606,6 +1611,49 @@ def install_plan_for(tool_name: str, platform_id: Optional[str] = None) -> Dict[
1606
1611
  }
1607
1612
 
1608
1613
 
1614
+ def _detect_mlx_audio_router(env: Dict[str, str]) -> Dict[str, Any]:
1615
+ router_url = (env.get(MLX_AUDIO_ROUTER_URL_ENV) or "").strip().rstrip("/")
1616
+ model = (env.get(MLX_AUDIO_ROUTER_MODEL_ENV) or "").strip() or None
1617
+ if not router_url:
1618
+ return {
1619
+ "available": False,
1620
+ "configured": False,
1621
+ "url_env": MLX_AUDIO_ROUTER_URL_ENV,
1622
+ "model_env": MLX_AUDIO_ROUTER_MODEL_ENV,
1623
+ }
1624
+ if not router_url.startswith(("http://", "https://")):
1625
+ return {
1626
+ "available": False,
1627
+ "configured": True,
1628
+ "url": router_url,
1629
+ "model": model,
1630
+ "error": f"{MLX_AUDIO_ROUTER_URL_ENV} must use http:// or https://",
1631
+ }
1632
+
1633
+ try:
1634
+ request = urllib.request.Request(f"{router_url}/health", method="GET")
1635
+ with urllib.request.urlopen(request, timeout=1.0) as response:
1636
+ payload = json.loads(response.read().decode("utf-8"))
1637
+ if payload.get("status") != "ok":
1638
+ raise ValueError("health response status is not 'ok'")
1639
+ except (OSError, ValueError) as exc:
1640
+ return {
1641
+ "available": False,
1642
+ "configured": True,
1643
+ "url": router_url,
1644
+ "model": model,
1645
+ "error": str(exc),
1646
+ }
1647
+
1648
+ return {
1649
+ "available": True,
1650
+ "configured": True,
1651
+ "url": router_url,
1652
+ "model": model,
1653
+ "api_version": payload.get("api_version"),
1654
+ }
1655
+
1656
+
1609
1657
  def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1610
1658
  """Detect available analysis helpers without installing or downloading."""
1611
1659
  env = env if env is not None else os.environ
@@ -1618,6 +1666,7 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1618
1666
  mlx_whisper = importlib.util.find_spec("mlx_whisper") is not None
1619
1667
  cv2 = importlib.util.find_spec("cv2") is not None
1620
1668
  provider = env.get("DAVINCI_RESOLVE_MCP_VISION_PROVIDER")
1669
+ mlx_audio_router = _detect_mlx_audio_router(env)
1621
1670
 
1622
1671
  sync_events = detect_sync_event_capabilities()
1623
1672
 
@@ -1653,6 +1702,7 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1653
1702
  "whisper_cli": _tool_entry("whisper_cli", bool(whisper_cli), {"path": whisper_cli}),
1654
1703
  "whisper_cpp": _tool_entry("whisper_cpp", bool(whisper_cpp), {"path": whisper_cpp}),
1655
1704
  "mlx_whisper": _tool_entry("mlx_whisper", bool(mlx_whisper), {"python_module": "mlx_whisper"}),
1705
+ "mlx_audio_router": mlx_audio_router,
1656
1706
  "opencv": _tool_entry("opencv", bool(cv2), {"python_module": "cv2"}),
1657
1707
  "ollama_embeddings": _tool_entry(
1658
1708
  "ollama_embeddings",
@@ -1672,9 +1722,10 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1672
1722
  },
1673
1723
  "embeddings": embedding_caps,
1674
1724
  "transcription": {
1675
- "available": bool(whisper_cli or whisper_cpp or mlx_whisper),
1725
+ "available": bool(mlx_audio_router["available"] or whisper_cli or whisper_cpp or mlx_whisper),
1676
1726
  "backends": [
1677
1727
  name for name, available in (
1728
+ ("mlx_audio_router", mlx_audio_router["available"]),
1678
1729
  ("whisper_cli", bool(whisper_cli)),
1679
1730
  ("whisper_cpp", bool(whisper_cpp)),
1680
1731
  ("mlx_whisper", bool(mlx_whisper)),
@@ -1730,12 +1781,13 @@ def install_guidance(capabilities: Optional[Dict[str, Any]] = None) -> Dict[str,
1730
1781
  missing["transcription"] = {
1731
1782
  "required_for": ["transcription analysis", "default Resolve media analysis"],
1732
1783
  "options": [
1784
+ f"Configure an MLX Audio Router with {MLX_AUDIO_ROUTER_URL_ENV}",
1733
1785
  "Install/configure whisper CLI",
1734
1786
  "Install/configure whisper-cpp",
1735
1787
  "Install mlx-whisper on supported Apple Silicon systems",
1736
1788
  ],
1737
1789
  "macos": "Ask the user before running: brew install whisper-cpp, or configure another supported local Whisper backend.",
1738
- "note": "The MCP server must not install these automatically.",
1790
+ "note": "The MCP server must not install backends or download model files automatically.",
1739
1791
  }
1740
1792
  if not tools.get("opencv", {}).get("available"):
1741
1793
  missing["opencv"] = {
@@ -3934,6 +3986,95 @@ def _transcribe_with_mlx_whisper(path: str, artifacts: Dict[str, Any], transcrip
3934
3986
  return payload
3935
3987
 
3936
3988
 
3989
+ def _transcribe_with_mlx_audio_router(
3990
+ path: str,
3991
+ artifacts: Dict[str, Any],
3992
+ transcription: Dict[str, Any],
3993
+ ) -> Dict[str, Any]:
3994
+ router_url = str(
3995
+ transcription.get("router_url")
3996
+ or os.environ.get(MLX_AUDIO_ROUTER_URL_ENV)
3997
+ or ""
3998
+ ).strip().rstrip("/")
3999
+ if not router_url:
4000
+ return {
4001
+ "success": False,
4002
+ "status": "skipped",
4003
+ "backend": "mlx_audio_router",
4004
+ "reason": f"Set {MLX_AUDIO_ROUTER_URL_ENV} to the MLX Audio Router base URL.",
4005
+ }
4006
+
4007
+ model = str(
4008
+ transcription.get("model")
4009
+ or os.environ.get(MLX_AUDIO_ROUTER_MODEL_ENV)
4010
+ or ""
4011
+ ).strip()
4012
+ transcript_json = artifacts.get("transcript_json") or artifacts["analysis_json"]
4013
+ output_base = os.path.splitext(transcript_json)[0]
4014
+ request_payload = {
4015
+ "provider": "mlx",
4016
+ "audio": path,
4017
+ "output_path": output_base,
4018
+ "format": "json",
4019
+ "verbose": False,
4020
+ "allow_download": _coerce_bool(transcription.get("allow_model_download"), default=False),
4021
+ }
4022
+ if model:
4023
+ request_payload["model"] = model
4024
+ request = urllib.request.Request(
4025
+ f"{router_url}/stt",
4026
+ data=json.dumps(request_payload).encode("utf-8"),
4027
+ headers={"Content-Type": "application/json"},
4028
+ method="POST",
4029
+ )
4030
+ try:
4031
+ with urllib.request.urlopen(request, timeout=int(transcription.get("timeout", 1800))) as response:
4032
+ response_payload = json.loads(response.read().decode("utf-8"))
4033
+ except urllib.error.HTTPError as exc:
4034
+ detail = exc.read().decode("utf-8", errors="replace").strip()
4035
+ return {
4036
+ "success": False,
4037
+ "backend": "mlx_audio_router",
4038
+ "error": detail or f"MLX Audio Router returned HTTP {exc.code}",
4039
+ }
4040
+ except (OSError, ValueError) as exc:
4041
+ return {"success": False, "backend": "mlx_audio_router", "error": str(exc)}
4042
+
4043
+ if not isinstance(response_payload, dict):
4044
+ return {
4045
+ "success": False,
4046
+ "backend": "mlx_audio_router",
4047
+ "error": "MLX Audio Router response must be a JSON object.",
4048
+ }
4049
+ raw_transcript = response_payload.get("transcript")
4050
+ if not isinstance(raw_transcript, str) or not raw_transcript.strip():
4051
+ return {
4052
+ "success": False,
4053
+ "backend": "mlx_audio_router",
4054
+ "error": "MLX Audio Router response did not include a JSON transcript.",
4055
+ }
4056
+ try:
4057
+ raw = json.loads(raw_transcript)
4058
+ except json.JSONDecodeError as exc:
4059
+ return {
4060
+ "success": False,
4061
+ "backend": "mlx_audio_router",
4062
+ "error": f"MLX Audio Router transcript is not valid JSON: {exc}",
4063
+ }
4064
+ if not isinstance(raw, dict):
4065
+ return {
4066
+ "success": False,
4067
+ "backend": "mlx_audio_router",
4068
+ "error": "MLX Audio Router transcript JSON must be an object.",
4069
+ }
4070
+
4071
+ payload = _normalize_transcript_payload(raw, "mlx_audio_router", transcription.get("language"))
4072
+ if response_payload.get("model") or model:
4073
+ payload["model"] = response_payload.get("model") or model
4074
+ _write_transcript_artifacts(payload, artifacts)
4075
+ return payload
4076
+
4077
+
3937
4078
  def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], capabilities: Dict[str, Any]) -> Dict[str, Any]:
3938
4079
  transcription = options.get("transcription") or {}
3939
4080
  if not _coerce_bool(transcription.get("enabled"), default=DEFAULT_TRANSCRIPTION_ENABLED):
@@ -3975,6 +4116,8 @@ def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], c
3975
4116
  payload = {"success": True, "backend": backend, "language": transcription.get("language", "unknown"), "segments": segments, "text": " ".join(s.get("text", "") for s in segments)}
3976
4117
  _write_transcript_artifacts(payload, artifacts)
3977
4118
  return payload
4119
+ if backend == "mlx_audio_router":
4120
+ return _transcribe_with_mlx_audio_router(path, artifacts, transcription)
3978
4121
  if backend in {"whisper_cli", "mlx_whisper"}:
3979
4122
  if not _coerce_bool(transcription.get("allow_model_download"), default=False):
3980
4123
  return {
@@ -4016,7 +4159,7 @@ def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], c
4016
4159
  # original branches below so behaviour stays identical for those.
4017
4160
  if result is not None and result.get("status") != "fallthrough":
4018
4161
  return result
4019
- if backend in {"mock", "local_mock", "whisper_cli", "mlx_whisper"}:
4162
+ if backend in {"mock", "local_mock", "mlx_audio_router", "whisper_cli", "mlx_whisper"}:
4020
4163
  return result if result is not None else {"success": False, "backend": backend}
4021
4164
  elif backend == "whisper_cpp":
4022
4165
  if not transcription.get("model_path"):
@@ -3,35 +3,66 @@
3
3
  DaVinci Resolve Connection Utilities
4
4
  """
5
5
 
6
- import os
7
6
  import logging
7
+ import math
8
+ import os
9
+
8
10
  from .platform import get_platform, get_resolve_paths, setup_environment
9
11
 
10
12
  logger = logging.getLogger("davinci-resolve-mcp.connection")
11
13
 
14
+ DEFAULT_RESOLVE_SCRIPT_TIMEOUT = 5.0
15
+
16
+
17
+ def _network_timeout():
18
+ value = os.environ.get(
19
+ "RESOLVE_SCRIPT_TIMEOUT",
20
+ str(DEFAULT_RESOLVE_SCRIPT_TIMEOUT),
21
+ )
22
+ try:
23
+ timeout = float(value)
24
+ except ValueError as exc:
25
+ raise ValueError(
26
+ "RESOLVE_SCRIPT_TIMEOUT must be a positive finite number"
27
+ ) from exc
28
+ if not math.isfinite(timeout) or timeout <= 0:
29
+ raise ValueError(
30
+ "RESOLVE_SCRIPT_TIMEOUT must be a positive finite number"
31
+ )
32
+ return timeout
33
+
34
+
35
+ def connect_resolve(dvr_script):
36
+ """Create a Resolve handle, optionally targeting an explicit network host."""
37
+ host = os.environ.get("RESOLVE_SCRIPT_HOST")
38
+ if host:
39
+ return dvr_script.scriptapp("Resolve", host, _network_timeout())
40
+ return dvr_script.scriptapp("Resolve")
41
+
42
+
12
43
  def initialize_resolve():
13
44
  """Initialize connection to DaVinci Resolve application."""
14
45
  try:
15
46
  # Import the DaVinci Resolve scripting module
16
47
  import DaVinciResolveScript as dvr_script
17
-
48
+
18
49
  # Get the resolve object
19
- resolve = dvr_script.scriptapp("Resolve")
20
-
50
+ resolve = connect_resolve(dvr_script)
51
+
21
52
  if resolve is None:
22
53
  logger.error("Failed to get Resolve object. Is DaVinci Resolve running?")
23
54
  return None
24
-
55
+
25
56
  logger.info(f"Connected to DaVinci Resolve: {resolve.GetProductName()} {resolve.GetVersionString()}")
26
57
  return resolve
27
-
58
+
28
59
  except ImportError:
29
60
  platform_name = get_platform()
30
61
  paths = get_resolve_paths()
31
-
62
+
32
63
  logger.error("Failed to import DaVinciResolveScript. Check environment variables.")
33
64
  logger.error("RESOLVE_SCRIPT_API, RESOLVE_SCRIPT_LIB, and PYTHONPATH must be set correctly.")
34
-
65
+
35
66
  if platform_name == 'darwin':
36
67
  logger.error("On macOS, typically:")
37
68
  logger.error(f'export RESOLVE_SCRIPT_API="{paths["api_path"]}"')
@@ -47,24 +78,25 @@ def initialize_resolve():
47
78
  logger.error(f'export RESOLVE_SCRIPT_API="{paths["api_path"]}"')
48
79
  logger.error(f'export RESOLVE_SCRIPT_LIB="{paths["lib_path"]}"')
49
80
  logger.error(f'export PYTHONPATH="$PYTHONPATH:{paths["modules_path"]}"')
50
-
81
+
51
82
  return None
52
-
83
+
53
84
  except Exception as e:
54
85
  logger.error(f"Unexpected error initializing Resolve: {str(e)}")
55
86
  return None
56
87
 
88
+
57
89
  def check_environment_variables():
58
90
  """Check if the required environment variables are set."""
59
91
  resolve_script_api = os.environ.get("RESOLVE_SCRIPT_API")
60
92
  resolve_script_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
61
-
93
+
62
94
  missing_vars = []
63
95
  if not resolve_script_api:
64
96
  missing_vars.append("RESOLVE_SCRIPT_API")
65
97
  if not resolve_script_lib:
66
98
  missing_vars.append("RESOLVE_SCRIPT_LIB")
67
-
99
+
68
100
  return {
69
101
  "all_set": len(missing_vars) == 0,
70
102
  "missing": missing_vars,
@@ -72,6 +104,7 @@ def check_environment_variables():
72
104
  "resolve_script_lib": resolve_script_lib
73
105
  }
74
106
 
107
+
75
108
  def set_default_environment_variables():
76
109
  """Set the default environment variables based on platform."""
77
- return setup_environment()
110
+ return setup_environment()