davinci-resolve-mcp 2.64.0 → 2.66.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.
@@ -81,7 +81,7 @@ if not logging.getLogger().handlers:
81
81
  handlers=[logging.StreamHandler()],
82
82
  )
83
83
 
84
- VERSION = "2.64.0"
84
+ VERSION = "2.66.0"
85
85
  logger = logging.getLogger("davinci-resolve-mcp")
86
86
  logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
87
87
  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.64.0"
14
+ VERSION = "2.66.0"
15
15
 
16
16
  import base64
17
17
  import os
@@ -22,6 +22,9 @@ import subprocess
22
22
  import sys
23
23
  import threading
24
24
  import time
25
+ import urllib.error
26
+ import urllib.parse
27
+ import urllib.request
25
28
  from datetime import datetime, timezone
26
29
  from pathlib import Path
27
30
  from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple
@@ -354,6 +357,9 @@ _ensure_path_includes_standard_tool_dirs()
354
357
  ANALYSIS_DIR_NAME = "davinci-resolve-mcp-analysis"
355
358
  HIDDEN_ANALYSIS_DIR_NAME = ".davinci-resolve-mcp-analysis"
356
359
  ANALYSIS_VERSION = "0.2"
360
+
361
+ HTTP_TRANSCRIPTION_PROVIDERS_ENV = "DAVINCI_RESOLVE_MCP_TRANSCRIPTION_HTTP_PROVIDERS"
362
+ HTTP_TRANSCRIPTION_BACKEND_PREFIX = "http:"
357
363
  ANALYSIS_INDEX_FILENAME = "index.sqlite"
358
364
  ANALYSIS_REGISTRY_FILENAME = "analysis_registry.json"
359
365
  ANALYSIS_INDEX_SCHEMA_VERSION = 1
@@ -1606,6 +1612,115 @@ def install_plan_for(tool_name: str, platform_id: Optional[str] = None) -> Dict[
1606
1612
  }
1607
1613
 
1608
1614
 
1615
+ def _http_provider_endpoint(provider: Dict[str, Any], path_key: str) -> str:
1616
+ path = str(provider[path_key]).strip()
1617
+ if path.startswith(("http://", "https://")):
1618
+ return path
1619
+ return f"{provider['base_url']}/{path.lstrip('/')}"
1620
+
1621
+
1622
+ def _load_http_transcription_providers(env: Dict[str, str]) -> Tuple[List[Dict[str, Any]], Optional[str]]:
1623
+ raw = (env.get(HTTP_TRANSCRIPTION_PROVIDERS_ENV) or "").strip()
1624
+ if not raw:
1625
+ return [], None
1626
+ try:
1627
+ entries = json.loads(raw)
1628
+ except json.JSONDecodeError as exc:
1629
+ return [], f"{HTTP_TRANSCRIPTION_PROVIDERS_ENV} is not valid JSON: {exc}"
1630
+ if not isinstance(entries, list):
1631
+ return [], f"{HTTP_TRANSCRIPTION_PROVIDERS_ENV} must be a JSON array"
1632
+
1633
+ providers: List[Dict[str, Any]] = []
1634
+ seen_ids = set()
1635
+ for index, entry in enumerate(entries):
1636
+ if not isinstance(entry, dict):
1637
+ return [], f"HTTP transcription provider at index {index} must be an object"
1638
+ provider_id = str(entry.get("id") or "").strip()
1639
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}", provider_id):
1640
+ return [], f"HTTP transcription provider at index {index} has an invalid id"
1641
+ if provider_id in seen_ids:
1642
+ return [], f"Duplicate HTTP transcription provider id: {provider_id}"
1643
+ seen_ids.add(provider_id)
1644
+
1645
+ base_url = str(entry.get("base_url") or "").strip().rstrip("/")
1646
+ parsed_url = urllib.parse.urlparse(base_url)
1647
+ if parsed_url.scheme not in {"http", "https"} or not parsed_url.netloc:
1648
+ return [], f"HTTP transcription provider '{provider_id}' base_url must use http:// or https://"
1649
+ headers = entry.get("headers") or {}
1650
+ request_body = entry.get("request_body") or {}
1651
+ field_map = entry.get("field_map") or {}
1652
+ if not isinstance(headers, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in headers.items()):
1653
+ return [], f"HTTP transcription provider '{provider_id}' headers must be a string map"
1654
+ if not isinstance(request_body, dict):
1655
+ return [], f"HTTP transcription provider '{provider_id}' request_body must be an object"
1656
+ if not isinstance(field_map, dict) or not all(isinstance(k, str) and isinstance(v, str) for k, v in field_map.items()):
1657
+ return [], f"HTTP transcription provider '{provider_id}' field_map must be a string map"
1658
+
1659
+ label = str(entry.get("label") or provider_id).strip() or provider_id
1660
+ providers.append({
1661
+ "id": provider_id,
1662
+ "label": label,
1663
+ "base_url": base_url,
1664
+ "model": str(entry.get("model") or "").strip() or None,
1665
+ "health_path": str(entry.get("health_path") or "/health").strip(),
1666
+ "transcribe_path": str(entry.get("transcribe_path") or "/stt").strip(),
1667
+ "health_field": str(entry.get("health_field") or "status").strip(),
1668
+ "health_value": entry.get("health_value", "ok"),
1669
+ "response_field": str(entry.get("response_field", "transcript")).strip(),
1670
+ "headers": dict(headers),
1671
+ "request_body": dict(request_body),
1672
+ "field_map": dict(field_map),
1673
+ })
1674
+ return providers, None
1675
+
1676
+
1677
+ def _detect_http_transcription_providers(env: Dict[str, str]) -> Dict[str, Any]:
1678
+ providers, config_error = _load_http_transcription_providers(env)
1679
+ if config_error:
1680
+ return {
1681
+ "available": False,
1682
+ "configured": True,
1683
+ "config_env": HTTP_TRANSCRIPTION_PROVIDERS_ENV,
1684
+ "error": config_error,
1685
+ "providers": [],
1686
+ }
1687
+
1688
+ detected = []
1689
+ for provider in providers:
1690
+ public = {
1691
+ "id": provider["id"],
1692
+ "label": provider["label"],
1693
+ "url": provider["base_url"],
1694
+ "model": provider["model"],
1695
+ "backend": f"{HTTP_TRANSCRIPTION_BACKEND_PREFIX}{provider['id']}",
1696
+ }
1697
+ try:
1698
+ request = urllib.request.Request(
1699
+ _http_provider_endpoint(provider, "health_path"),
1700
+ headers=provider["headers"],
1701
+ method="GET",
1702
+ )
1703
+ with urllib.request.urlopen(request, timeout=1.0) as response:
1704
+ payload = json.loads(response.read().decode("utf-8"))
1705
+ if not isinstance(payload, dict):
1706
+ raise ValueError("health response must be a JSON object")
1707
+ if payload.get(provider["health_field"]) != provider["health_value"]:
1708
+ raise ValueError(
1709
+ f"health field '{provider['health_field']}' did not equal the configured ready value"
1710
+ )
1711
+ public.update({"available": True, "api_version": payload.get("api_version")})
1712
+ except (OSError, ValueError) as exc:
1713
+ public.update({"available": False, "error": str(exc)})
1714
+ detected.append(public)
1715
+
1716
+ return {
1717
+ "available": any(provider["available"] for provider in detected),
1718
+ "configured": bool(providers),
1719
+ "config_env": HTTP_TRANSCRIPTION_PROVIDERS_ENV,
1720
+ "providers": detected,
1721
+ }
1722
+
1723
+
1609
1724
  def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1610
1725
  """Detect available analysis helpers without installing or downloading."""
1611
1726
  env = env if env is not None else os.environ
@@ -1618,6 +1733,7 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1618
1733
  mlx_whisper = importlib.util.find_spec("mlx_whisper") is not None
1619
1734
  cv2 = importlib.util.find_spec("cv2") is not None
1620
1735
  provider = env.get("DAVINCI_RESOLVE_MCP_VISION_PROVIDER")
1736
+ http_transcription = _detect_http_transcription_providers(env)
1621
1737
 
1622
1738
  sync_events = detect_sync_event_capabilities()
1623
1739
 
@@ -1653,6 +1769,7 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1653
1769
  "whisper_cli": _tool_entry("whisper_cli", bool(whisper_cli), {"path": whisper_cli}),
1654
1770
  "whisper_cpp": _tool_entry("whisper_cpp", bool(whisper_cpp), {"path": whisper_cpp}),
1655
1771
  "mlx_whisper": _tool_entry("mlx_whisper", bool(mlx_whisper), {"python_module": "mlx_whisper"}),
1772
+ "http_transcription": http_transcription,
1656
1773
  "opencv": _tool_entry("opencv", bool(cv2), {"python_module": "cv2"}),
1657
1774
  "ollama_embeddings": _tool_entry(
1658
1775
  "ollama_embeddings",
@@ -1672,8 +1789,12 @@ def detect_capabilities(env: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
1672
1789
  },
1673
1790
  "embeddings": embedding_caps,
1674
1791
  "transcription": {
1675
- "available": bool(whisper_cli or whisper_cpp or mlx_whisper),
1792
+ "available": bool(http_transcription["available"] or whisper_cli or whisper_cpp or mlx_whisper),
1676
1793
  "backends": [
1794
+ provider["backend"]
1795
+ for provider in http_transcription["providers"]
1796
+ if provider["available"]
1797
+ ] + [
1677
1798
  name for name, available in (
1678
1799
  ("whisper_cli", bool(whisper_cli)),
1679
1800
  ("whisper_cpp", bool(whisper_cpp)),
@@ -1730,12 +1851,13 @@ def install_guidance(capabilities: Optional[Dict[str, Any]] = None) -> Dict[str,
1730
1851
  missing["transcription"] = {
1731
1852
  "required_for": ["transcription analysis", "default Resolve media analysis"],
1732
1853
  "options": [
1854
+ f"Configure one or more HTTP providers with {HTTP_TRANSCRIPTION_PROVIDERS_ENV}",
1733
1855
  "Install/configure whisper CLI",
1734
1856
  "Install/configure whisper-cpp",
1735
1857
  "Install mlx-whisper on supported Apple Silicon systems",
1736
1858
  ],
1737
1859
  "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.",
1860
+ "note": "The MCP server must not install backends or download model files automatically.",
1739
1861
  }
1740
1862
  if not tools.get("opencv", {}).get("available"):
1741
1863
  missing["opencv"] = {
@@ -3934,6 +4056,92 @@ def _transcribe_with_mlx_whisper(path: str, artifacts: Dict[str, Any], transcrip
3934
4056
  return payload
3935
4057
 
3936
4058
 
4059
+ def _transcribe_with_http_provider(
4060
+ path: str,
4061
+ artifacts: Dict[str, Any],
4062
+ transcription: Dict[str, Any],
4063
+ provider: Dict[str, Any],
4064
+ ) -> Dict[str, Any]:
4065
+ backend = f"{HTTP_TRANSCRIPTION_BACKEND_PREFIX}{provider['id']}"
4066
+ model = str(transcription.get("model") or provider.get("model") or "").strip()
4067
+ transcript_json = artifacts.get("transcript_json") or artifacts["analysis_json"]
4068
+ output_base = os.path.splitext(transcript_json)[0]
4069
+ canonical_payload = {
4070
+ "audio": path,
4071
+ "output_path": output_base,
4072
+ "format": "json",
4073
+ "verbose": False,
4074
+ "allow_download": _coerce_bool(transcription.get("allow_model_download"), default=False),
4075
+ }
4076
+ if model:
4077
+ canonical_payload["model"] = model
4078
+ if transcription.get("language"):
4079
+ canonical_payload["language"] = transcription["language"]
4080
+ request_payload = dict(provider.get("request_body") or {})
4081
+ field_map = provider.get("field_map") or {}
4082
+ for key, value in canonical_payload.items():
4083
+ request_payload[field_map.get(key, key)] = value
4084
+ headers = {"Content-Type": "application/json"}
4085
+ headers.update(provider.get("headers") or {})
4086
+ request = urllib.request.Request(
4087
+ _http_provider_endpoint(provider, "transcribe_path"),
4088
+ data=json.dumps(request_payload).encode("utf-8"),
4089
+ headers=headers,
4090
+ method="POST",
4091
+ )
4092
+ try:
4093
+ with urllib.request.urlopen(request, timeout=int(transcription.get("timeout", 1800))) as response:
4094
+ response_payload = json.loads(response.read().decode("utf-8"))
4095
+ except urllib.error.HTTPError as exc:
4096
+ detail = exc.read().decode("utf-8", errors="replace").strip()
4097
+ return {
4098
+ "success": False,
4099
+ "backend": backend,
4100
+ "provider": provider["label"],
4101
+ "error": detail or f"HTTP transcription provider returned HTTP {exc.code}",
4102
+ }
4103
+ except (OSError, ValueError) as exc:
4104
+ return {"success": False, "backend": backend, "provider": provider["label"], "error": str(exc)}
4105
+
4106
+ if not isinstance(response_payload, dict):
4107
+ return {
4108
+ "success": False,
4109
+ "backend": backend,
4110
+ "provider": provider["label"],
4111
+ "error": "HTTP transcription response must be a JSON object.",
4112
+ }
4113
+ response_field = provider.get("response_field")
4114
+ if response_field and response_field not in response_payload:
4115
+ return {
4116
+ "success": False,
4117
+ "backend": backend,
4118
+ "provider": provider["label"],
4119
+ "error": f"HTTP transcription response did not include '{response_field}'.",
4120
+ }
4121
+ raw_transcript = response_payload[response_field] if response_field else response_payload
4122
+ if isinstance(raw_transcript, str):
4123
+ try:
4124
+ raw = json.loads(raw_transcript)
4125
+ except json.JSONDecodeError:
4126
+ raw = {"text": raw_transcript, "segments": []}
4127
+ else:
4128
+ raw = raw_transcript
4129
+ if not isinstance(raw, dict):
4130
+ return {
4131
+ "success": False,
4132
+ "backend": backend,
4133
+ "provider": provider["label"],
4134
+ "error": "HTTP transcription payload must resolve to a JSON object or text string.",
4135
+ }
4136
+
4137
+ payload = _normalize_transcript_payload(raw, backend, transcription.get("language"))
4138
+ payload["provider"] = provider["label"]
4139
+ if response_payload.get("model") or model:
4140
+ payload["model"] = response_payload.get("model") or model
4141
+ _write_transcript_artifacts(payload, artifacts)
4142
+ return payload
4143
+
4144
+
3937
4145
  def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], capabilities: Dict[str, Any]) -> Dict[str, Any]:
3938
4146
  transcription = options.get("transcription") or {}
3939
4147
  if not _coerce_bool(transcription.get("enabled"), default=DEFAULT_TRANSCRIPTION_ENABLED):
@@ -3975,6 +4183,20 @@ def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], c
3975
4183
  payload = {"success": True, "backend": backend, "language": transcription.get("language", "unknown"), "segments": segments, "text": " ".join(s.get("text", "") for s in segments)}
3976
4184
  _write_transcript_artifacts(payload, artifacts)
3977
4185
  return payload
4186
+ if isinstance(backend, str) and backend.startswith(HTTP_TRANSCRIPTION_BACKEND_PREFIX):
4187
+ providers, config_error = _load_http_transcription_providers(os.environ)
4188
+ if config_error:
4189
+ return {"success": False, "backend": backend, "error": config_error}
4190
+ provider_id = backend[len(HTTP_TRANSCRIPTION_BACKEND_PREFIX):]
4191
+ provider = next((item for item in providers if item["id"] == provider_id), None)
4192
+ if provider is None:
4193
+ return {
4194
+ "success": False,
4195
+ "status": "skipped",
4196
+ "backend": backend,
4197
+ "reason": f"HTTP transcription provider '{provider_id}' is not configured.",
4198
+ }
4199
+ return _transcribe_with_http_provider(path, artifacts, transcription, provider)
3978
4200
  if backend in {"whisper_cli", "mlx_whisper"}:
3979
4201
  if not _coerce_bool(transcription.get("allow_model_download"), default=False):
3980
4202
  return {
@@ -4016,7 +4238,13 @@ def _transcribe(path: str, artifacts: Dict[str, Any], options: Dict[str, Any], c
4016
4238
  # original branches below so behaviour stays identical for those.
4017
4239
  if result is not None and result.get("status") != "fallthrough":
4018
4240
  return result
4019
- if backend in {"mock", "local_mock", "whisper_cli", "mlx_whisper"}:
4241
+ if (
4242
+ backend in {"mock", "local_mock", "whisper_cli", "mlx_whisper"}
4243
+ or (
4244
+ isinstance(backend, str)
4245
+ and backend.startswith(HTTP_TRANSCRIPTION_BACKEND_PREFIX)
4246
+ )
4247
+ ):
4020
4248
  return result if result is not None else {"success": False, "backend": backend}
4021
4249
  elif backend == "whisper_cpp":
4022
4250
  if not transcription.get("model_path"):