davinci-resolve-mcp 2.23.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.
Files changed (92) hide show
  1. package/AGENTS.md +85 -0
  2. package/CHANGELOG.md +802 -0
  3. package/CLAUDE.md +15 -0
  4. package/LICENSE +21 -0
  5. package/README.md +159 -0
  6. package/SECURITY.md +53 -0
  7. package/bin/davinci-resolve-mcp.mjs +376 -0
  8. package/docs/README.md +56 -0
  9. package/docs/SKILL.md +1145 -0
  10. package/docs/authoring/fuse-dctl-authoring.md +242 -0
  11. package/docs/authoring/script-plugin-authoring.md +195 -0
  12. package/docs/contributing.md +82 -0
  13. package/docs/guides/color-decision-guide.md +387 -0
  14. package/docs/guides/editorial-decision-guide.md +136 -0
  15. package/docs/guides/media-analysis-guide.md +615 -0
  16. package/docs/guides/multicam-setup-guide.md +138 -0
  17. package/docs/install.md +198 -0
  18. package/docs/integrations/workflow-integrations.md +120 -0
  19. package/docs/kernels/README.md +28 -0
  20. package/docs/kernels/audio-fairlight-kernel.md +86 -0
  21. package/docs/kernels/color-grade-kernel.md +103 -0
  22. package/docs/kernels/extension-authoring-kernel.md +101 -0
  23. package/docs/kernels/fusion-composition-kernel.md +91 -0
  24. package/docs/kernels/media-pool-ingest-kernel.md +147 -0
  25. package/docs/kernels/project-lifecycle-kernel.md +120 -0
  26. package/docs/kernels/render-deliver-kernel.md +92 -0
  27. package/docs/kernels/review-annotation-kernel.md +110 -0
  28. package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
  29. package/docs/kernels/timeline-edit-kernel.md +189 -0
  30. package/docs/notes/codec-plugin-notes.md +136 -0
  31. package/docs/notes/dctl-notes.md +234 -0
  32. package/docs/notes/fusion-template-notes.md +136 -0
  33. package/docs/notes/lut-notes.md +136 -0
  34. package/docs/notes/openfx-notes.md +120 -0
  35. package/docs/process/release-process.md +152 -0
  36. package/docs/reference/api-coverage.md +488 -0
  37. package/docs/reference/resolve_scripting_api.txt +1012 -0
  38. package/examples/README.md +53 -0
  39. package/examples/markers/README.md +81 -0
  40. package/examples/media/README.md +94 -0
  41. package/examples/timeline/README.md +98 -0
  42. package/install.py +1196 -0
  43. package/package.json +52 -0
  44. package/scripts/audit_api_parity.py +275 -0
  45. package/scripts/live_media_analysis_polish_probe.py +65 -0
  46. package/src/__init__.py +3 -0
  47. package/src/analysis_dashboard.py +4936 -0
  48. package/src/control_panel.py +13 -0
  49. package/src/granular/__init__.py +17 -0
  50. package/src/granular/common.py +727 -0
  51. package/src/granular/folder.py +287 -0
  52. package/src/granular/gallery.py +306 -0
  53. package/src/granular/graph.py +309 -0
  54. package/src/granular/media_pool.py +679 -0
  55. package/src/granular/media_pool_item.py +852 -0
  56. package/src/granular/media_storage.py +179 -0
  57. package/src/granular/project.py +1594 -0
  58. package/src/granular/resolve_control.py +521 -0
  59. package/src/granular/timeline.py +1074 -0
  60. package/src/granular/timeline_item.py +2251 -0
  61. package/src/resolve_mcp_server.py +43 -0
  62. package/src/server.py +15691 -0
  63. package/src/utils/__init__.py +3 -0
  64. package/src/utils/app_control.py +319 -0
  65. package/src/utils/audio_fairlight_live_probe.py +263 -0
  66. package/src/utils/cdl.py +20 -0
  67. package/src/utils/cloud_operations.py +192 -0
  68. package/src/utils/color_grade_live_probe.py +444 -0
  69. package/src/utils/dctl_templates.py +368 -0
  70. package/src/utils/extension_authoring_live_probe.py +292 -0
  71. package/src/utils/fuse_templates.py +1968 -0
  72. package/src/utils/fusion_composition_live_probe.py +284 -0
  73. package/src/utils/layout_presets.py +333 -0
  74. package/src/utils/mcp_stdio.py +32 -0
  75. package/src/utils/media_analysis.py +3618 -0
  76. package/src/utils/media_analysis_jobs.py +796 -0
  77. package/src/utils/media_pool_ingest_live_probe.py +592 -0
  78. package/src/utils/multicam.py +393 -0
  79. package/src/utils/object_inspection.py +287 -0
  80. package/src/utils/platform.py +157 -0
  81. package/src/utils/project_lifecycle_live_probe.py +376 -0
  82. package/src/utils/project_properties.py +601 -0
  83. package/src/utils/render_deliver_live_probe.py +384 -0
  84. package/src/utils/resolve_connection.py +77 -0
  85. package/src/utils/review_annotation_live_probe.py +352 -0
  86. package/src/utils/script_templates.py +1193 -0
  87. package/src/utils/sync_detection.py +887 -0
  88. package/src/utils/timeline_conform_live_probe.py +280 -0
  89. package/src/utils/timeline_kernel_live_probe.py +1091 -0
  90. package/src/utils/timeline_kernel_probe.py +185 -0
  91. package/src/utils/timeline_title_text.py +87 -0
  92. package/src/utils/update_check.py +610 -0
package/install.py ADDED
@@ -0,0 +1,1196 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ DaVinci Resolve MCP Server — Universal Installer
4
+
5
+ Supports: macOS, Windows, Linux
6
+ Configures: Claude Desktop, Claude Code, Cursor, VS Code (Copilot),
7
+ Windsurf, Cline, Roo Code, Zed, Continue, and manual setup.
8
+
9
+ Usage:
10
+ python install.py # Interactive mode
11
+ python install.py --clients all # Install all clients non-interactively
12
+ python install.py --clients cursor,claude-desktop --no-venv
13
+ python install.py --help
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import os
19
+ import platform
20
+ import shutil
21
+ import subprocess
22
+ import sys
23
+ import textwrap
24
+ from pathlib import Path
25
+
26
+ from src.utils.update_check import (
27
+ clear_update_prompt_preferences,
28
+ check_for_updates,
29
+ ignore_update_version,
30
+ set_update_mode,
31
+ snooze_update_prompt,
32
+ update_prompt_decision,
33
+ )
34
+
35
+ # ─── Version ──────────────────────────────────────────────────────────────────
36
+
37
+ VERSION = "2.23.0"
38
+ SUPPORTED_PYTHON_MIN = (3, 10)
39
+ SUPPORTED_PYTHON_MAX = (3, 12)
40
+
41
+ # ─── Colors (disabled on Windows cmd without ANSI support) ────────────────────
42
+
43
+ def _supports_color():
44
+ if os.environ.get("NO_COLOR"):
45
+ return False
46
+ if platform.system() == "Windows":
47
+ return os.environ.get("WT_SESSION") or os.environ.get("TERM_PROGRAM")
48
+ return hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
49
+
50
+ USE_COLOR = _supports_color()
51
+
52
+ def _c(code, text):
53
+ return f"\033[{code}m{text}\033[0m" if USE_COLOR else text
54
+
55
+ def green(t): return _c("32", t)
56
+ def yellow(t): return _c("33", t)
57
+ def red(t): return _c("31", t)
58
+ def bold(t): return _c("1", t)
59
+ def dim(t): return _c("2", t)
60
+ def cyan(t): return _c("36", t)
61
+
62
+ # ─── Platform Detection ──────────────────────────────────────────────────────
63
+
64
+ SYSTEM = platform.system() # Darwin, Windows, Linux
65
+
66
+ def is_mac(): return SYSTEM == "Darwin"
67
+ def is_windows(): return SYSTEM == "Windows"
68
+ def is_linux(): return SYSTEM == "Linux"
69
+
70
+ def platform_name():
71
+ if is_mac(): return "macOS"
72
+ if is_windows(): return "Windows"
73
+ if is_linux(): return "Linux"
74
+ return SYSTEM
75
+
76
+
77
+ def is_supported_python_version(version):
78
+ major, minor = version[:2]
79
+ return major == 3 and SUPPORTED_PYTHON_MIN[1] <= minor <= SUPPORTED_PYTHON_MAX[1]
80
+
81
+
82
+ def format_python_version(version):
83
+ return ".".join(str(part) for part in version[:3])
84
+
85
+
86
+ def python_requirement_text():
87
+ return (
88
+ f"Python {SUPPORTED_PYTHON_MIN[0]}.{SUPPORTED_PYTHON_MIN[1]}-"
89
+ f"{SUPPORTED_PYTHON_MAX[0]}.{SUPPORTED_PYTHON_MAX[1]}"
90
+ )
91
+
92
+
93
+ def _version_for_python(python_path):
94
+ script = (
95
+ "import sys; "
96
+ "print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"
97
+ )
98
+ result = subprocess.run(
99
+ [str(python_path), "-c", script],
100
+ capture_output=True,
101
+ text=True,
102
+ timeout=10,
103
+ )
104
+ if result.returncode != 0:
105
+ output = (result.stderr or result.stdout or "").strip()
106
+ raise RuntimeError(output or f"{python_path} exited with code {result.returncode}")
107
+ parts = result.stdout.strip().split(".")
108
+ if len(parts) < 2:
109
+ raise RuntimeError(f"could not parse Python version from {python_path!s}")
110
+ return tuple(int(part) for part in parts[:3])
111
+
112
+
113
+ def require_supported_python(python_path, label="Python"):
114
+ try:
115
+ version = _version_for_python(python_path)
116
+ except Exception as exc:
117
+ print(f" {red(label + ':')} Could not inspect {python_path}: {exc}")
118
+ sys.exit(1)
119
+ if not is_supported_python_version(version):
120
+ print(
121
+ f" {red(label + ':')} {python_requirement_text()} is required "
122
+ f"for Resolve scripting compatibility; found {format_python_version(version)} "
123
+ f"at {python_path}"
124
+ )
125
+ sys.exit(1)
126
+ return version
127
+
128
+
129
+ def require_current_python(label="Python"):
130
+ version = (sys.version_info.major, sys.version_info.minor, sys.version_info.micro)
131
+ if not is_supported_python_version(version):
132
+ print(
133
+ f" {red(label + ':')} {python_requirement_text()} is required "
134
+ f"for Resolve scripting compatibility; current interpreter is "
135
+ f"{format_python_version(version)} at {sys.executable}"
136
+ )
137
+ sys.exit(1)
138
+ return version
139
+
140
+ # ─── Resolve Path Detection ──────────────────────────────────────────────────
141
+
142
+ RESOLVE_PATHS = {
143
+ "Darwin": {
144
+ "api": [
145
+ "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting",
146
+ ],
147
+ "lib": [
148
+ "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so",
149
+ ],
150
+ "app": [
151
+ "/Applications/DaVinci Resolve/DaVinci Resolve.app",
152
+ ],
153
+ },
154
+ "Windows": {
155
+ "api": [
156
+ r"C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting",
157
+ r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Developer\Scripting",
158
+ ],
159
+ "lib": [
160
+ r"C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll",
161
+ ],
162
+ "app": [
163
+ r"C:\Program Files\Blackmagic Design\DaVinci Resolve\Resolve.exe",
164
+ ],
165
+ },
166
+ "Linux": {
167
+ "api": [
168
+ "/opt/resolve/Developer/Scripting",
169
+ "/opt/resolve/libs/Fusion/Developer/Scripting",
170
+ "/home/{user}/.local/share/DaVinciResolve/Developer/Scripting",
171
+ ],
172
+ "lib": [
173
+ "/opt/resolve/libs/Fusion/fusionscript.so",
174
+ "/opt/resolve/bin/fusionscript.so",
175
+ ],
176
+ "app": [
177
+ "/opt/resolve/bin/resolve",
178
+ ],
179
+ },
180
+ }
181
+
182
+
183
+ def find_resolve_paths():
184
+ """Auto-detect DaVinci Resolve installation paths."""
185
+ candidates = RESOLVE_PATHS.get(SYSTEM, RESOLVE_PATHS["Linux"])
186
+ username = os.environ.get("USER", os.environ.get("USERNAME", ""))
187
+
188
+ api_path = None
189
+ lib_path = None
190
+
191
+ for p in candidates["api"]:
192
+ expanded = p.replace("{user}", username)
193
+ if os.path.isdir(expanded):
194
+ api_path = expanded
195
+ break
196
+
197
+ for p in candidates["lib"]:
198
+ expanded = p.replace("{user}", username)
199
+ if os.path.isfile(expanded):
200
+ lib_path = expanded
201
+ break
202
+
203
+ return api_path, lib_path
204
+
205
+
206
+ def check_resolve_running():
207
+ """Check if DaVinci Resolve is currently running."""
208
+ try:
209
+ if is_mac():
210
+ result = subprocess.run(
211
+ ["pgrep", "-f", "DaVinci Resolve"],
212
+ capture_output=True, text=True
213
+ )
214
+ return result.returncode == 0
215
+ elif is_windows():
216
+ result = subprocess.run(
217
+ ["tasklist", "/FI", "IMAGENAME eq Resolve.exe"],
218
+ capture_output=True, text=True
219
+ )
220
+ return "Resolve.exe" in result.stdout
221
+ else: # Linux
222
+ result = subprocess.run(
223
+ ["pgrep", "-f", "resolve"],
224
+ capture_output=True, text=True
225
+ )
226
+ return result.returncode == 0
227
+ except Exception:
228
+ return False
229
+
230
+ # ─── MCP Client Definitions ──────────────────────────────────────────────────
231
+
232
+ def home():
233
+ return Path.home()
234
+
235
+ def appdata():
236
+ """Windows %APPDATA% equivalent."""
237
+ return Path(os.environ.get("APPDATA", home() / "AppData" / "Roaming"))
238
+
239
+ def xdg_config():
240
+ """Linux XDG_CONFIG_HOME or default."""
241
+ return Path(os.environ.get("XDG_CONFIG_HOME", home() / ".config"))
242
+
243
+ def vscode_global_storage():
244
+ """VS Code global storage path per platform."""
245
+ if is_mac():
246
+ return home() / "Library" / "Application Support" / "Code" / "User" / "globalStorage"
247
+ elif is_windows():
248
+ return appdata() / "Code" / "User" / "globalStorage"
249
+ else:
250
+ return xdg_config() / "Code" / "User" / "globalStorage"
251
+
252
+
253
+ # Each client entry:
254
+ # id, name, config_path_fn, config_key, merge_strategy, notes
255
+ # config_path_fn returns the path; config_key is the JSON key wrapping the server entry
256
+ # merge_strategy: "merge" = add to existing JSON; "create" = create if not exists
257
+
258
+ MCP_CLIENTS = [
259
+ {
260
+ "id": "antigravity",
261
+ "name": "Antigravity",
262
+ "get_path": lambda: home() / ".gemini" / "antigravity" / "mcp_config.json",
263
+ "config_key": "mcpServers",
264
+ "notes": "Google's agentic AI coding assistant (VS Code fork)",
265
+ },
266
+ {
267
+ "id": "claude-desktop",
268
+ "name": "Claude Desktop",
269
+ "get_path": lambda: {
270
+ "Darwin": home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json",
271
+ "Windows": appdata() / "Claude" / "claude_desktop_config.json",
272
+ "Linux": xdg_config() / "Claude" / "claude_desktop_config.json",
273
+ }.get(SYSTEM),
274
+ "config_key": "mcpServers",
275
+ "notes": "Anthropic's desktop app for Claude",
276
+ },
277
+ {
278
+ "id": "claude-code",
279
+ "name": "Claude Code",
280
+ "get_path": lambda: Path.cwd() / ".mcp.json",
281
+ "config_key": "mcpServers",
282
+ "notes": "Project-scoped config (committed to repo)",
283
+ },
284
+ {
285
+ "id": "cursor",
286
+ "name": "Cursor",
287
+ "get_path": lambda: {
288
+ "Darwin": home() / ".cursor" / "mcp.json",
289
+ "Windows": home() / ".cursor" / "mcp.json",
290
+ "Linux": home() / ".cursor" / "mcp.json",
291
+ }.get(SYSTEM),
292
+ "config_key": "mcpServers",
293
+ "notes": "AI-native code editor (VS Code fork)",
294
+ },
295
+ {
296
+ "id": "vscode",
297
+ "name": "VS Code (GitHub Copilot)",
298
+ "get_path": lambda: Path.cwd() / ".vscode" / "mcp.json",
299
+ "config_key": "servers",
300
+ "notes": "Workspace-scoped config for Copilot agent mode",
301
+ },
302
+ {
303
+ "id": "windsurf",
304
+ "name": "Windsurf",
305
+ "get_path": lambda: {
306
+ "Darwin": home() / ".codeium" / "windsurf" / "mcp_config.json",
307
+ "Windows": appdata() / "windsurf" / "mcp_settings.json",
308
+ "Linux": home() / ".codeium" / "windsurf" / "mcp_config.json",
309
+ }.get(SYSTEM),
310
+ "config_key": "mcpServers",
311
+ "notes": "Codeium's AI code editor",
312
+ },
313
+ {
314
+ "id": "cline",
315
+ "name": "Cline",
316
+ "get_path": lambda: vscode_global_storage() / "saoudrizwan.claude-dev" / "settings" / "cline_mcp_settings.json",
317
+ "config_key": "mcpServers",
318
+ "notes": "AI coding assistant (VS Code extension)",
319
+ },
320
+ {
321
+ "id": "roo-code",
322
+ "name": "Roo Code",
323
+ "get_path": lambda: vscode_global_storage() / "rooveterinaryinc.roo-cline" / "settings" / "mcp_settings.json",
324
+ "config_key": "mcpServers",
325
+ "notes": "Autonomous AI coding assistant (VS Code extension)",
326
+ },
327
+ {
328
+ "id": "zed",
329
+ "name": "Zed",
330
+ "get_path": lambda: {
331
+ "Darwin": home() / ".config" / "zed" / "settings.json",
332
+ "Windows": None, # Zed doesn't support Windows yet
333
+ "Linux": home() / ".config" / "zed" / "settings.json",
334
+ }.get(SYSTEM),
335
+ "config_key": "context_servers",
336
+ "notes": "High-performance code editor (macOS/Linux only)",
337
+ },
338
+ {
339
+ "id": "continue",
340
+ "name": "Continue",
341
+ "get_path": lambda: {
342
+ "Darwin": home() / ".continue" / "config.json",
343
+ "Windows": home() / ".continue" / "config.json",
344
+ "Linux": home() / ".continue" / "config.json",
345
+ }.get(SYSTEM),
346
+ "config_key": "mcpServers",
347
+ "notes": "Open-source AI code assistant",
348
+ },
349
+ ]
350
+
351
+ CLIENT_IDS = [c["id"] for c in MCP_CLIENTS]
352
+
353
+ # ─── Server Entry Builder ────────────────────────────────────────────────────
354
+
355
+ def get_python_base_install(python_path):
356
+ """Resolve the base Python install used by the selected interpreter."""
357
+ script = "import sys; print(sys.base_prefix or sys.prefix)"
358
+ try:
359
+ result = subprocess.run(
360
+ [str(python_path), "-c", script],
361
+ capture_output=True,
362
+ text=True,
363
+ timeout=5,
364
+ )
365
+ base_prefix = result.stdout.strip()
366
+ if result.returncode == 0 and base_prefix:
367
+ return base_prefix
368
+ except Exception as exc:
369
+ print(f"Warning: Could not query Python base prefix: {exc}")
370
+
371
+ resolved = Path(python_path).resolve()
372
+ if resolved.parent.name.lower() in {"scripts", "bin"}:
373
+ return str(resolved.parent.parent)
374
+ return str(resolved.parent)
375
+
376
+
377
+ def build_server_env(python_path, api_path, lib_path, system=SYSTEM, python_home=None):
378
+ """Build the env block used by all generated stdio MCP configs."""
379
+ api_value = str(api_path or "")
380
+ lib_value = str(lib_path or "")
381
+ env = {
382
+ "RESOLVE_SCRIPT_API": api_value,
383
+ "RESOLVE_SCRIPT_LIB": lib_value,
384
+ "PYTHONPATH": str(Path(api_value) / "Modules") if api_value else "",
385
+ }
386
+
387
+ if system == "Windows":
388
+ env["PYTHONHOME"] = str(python_home or get_python_base_install(python_path))
389
+
390
+ return env
391
+
392
+
393
+ def build_server_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
394
+ """Build the standard MCP server config entry."""
395
+ return {
396
+ "command": str(python_path),
397
+ "args": [str(server_path)],
398
+ "env": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
399
+ }
400
+
401
+
402
+ def build_zed_entry(python_path, server_path, api_path, lib_path, system=SYSTEM, python_home=None):
403
+ """Build Zed-specific server entry (different format)."""
404
+ return {
405
+ "command": {
406
+ "path": str(python_path),
407
+ "args": [str(server_path)],
408
+ },
409
+ "env": build_server_env(python_path, api_path, lib_path, system=system, python_home=python_home),
410
+ "settings": {},
411
+ }
412
+
413
+ # ─── Config File Operations ──────────────────────────────────────────────────
414
+
415
+ def read_json(path):
416
+ """Read a JSON file, return empty dict if missing or invalid."""
417
+ try:
418
+ with open(path, "r") as f:
419
+ return json.load(f)
420
+ except (FileNotFoundError, json.JSONDecodeError):
421
+ return {}
422
+
423
+
424
+ def write_json(path, data):
425
+ """Write JSON to file, creating parent directories."""
426
+ path = Path(path)
427
+ path.parent.mkdir(parents=True, exist_ok=True)
428
+
429
+ # Backup existing file
430
+ if path.exists():
431
+ backup = path.with_suffix(path.suffix + ".backup")
432
+ shutil.copy2(path, backup)
433
+
434
+ with open(path, "w") as f:
435
+ json.dump(data, f, indent=2)
436
+ f.write("\n")
437
+
438
+
439
+ def write_client_config(client, python_path, server_path, api_path, lib_path, dry_run=False):
440
+ """Write or merge MCP config for a specific client. Returns (success, message)."""
441
+ config_path = client["get_path"]()
442
+ if config_path is None:
443
+ return False, f"{client['name']} is not available on {platform_name()}"
444
+
445
+ config_key = client["config_key"]
446
+ is_zed = client["id"] == "zed"
447
+
448
+ # Build the server entry
449
+ if is_zed:
450
+ server_entry = build_zed_entry(python_path, server_path, api_path, lib_path)
451
+ else:
452
+ server_entry = build_server_entry(python_path, server_path, api_path, lib_path)
453
+
454
+ if dry_run:
455
+ preview = {config_key: {"davinci-resolve": server_entry}}
456
+ return True, f"Would write to {config_path}:\n{json.dumps(preview, indent=2)}"
457
+
458
+ # Read existing config and merge
459
+ existing = read_json(config_path)
460
+
461
+ if config_key not in existing:
462
+ existing[config_key] = {}
463
+
464
+ existing[config_key]["davinci-resolve"] = server_entry
465
+
466
+ write_json(config_path, existing)
467
+ return True, str(config_path)
468
+
469
+
470
+ def generate_manual_config(python_path, server_path, api_path, lib_path):
471
+ """Generate config snippets for manual setup."""
472
+ entry = build_server_entry(python_path, server_path, api_path, lib_path)
473
+ zed_entry = build_zed_entry(python_path, server_path, api_path, lib_path)
474
+
475
+ standard = json.dumps({"mcpServers": {"davinci-resolve": entry}}, indent=2)
476
+ vscode_fmt = json.dumps({"servers": {"davinci-resolve": entry}}, indent=2)
477
+ zed_fmt = json.dumps({"context_servers": {"davinci-resolve": zed_entry}}, indent=2)
478
+
479
+ return standard, vscode_fmt, zed_fmt
480
+
481
+ # ─── Virtual Environment ─────────────────────────────────────────────────────
482
+
483
+ def find_python():
484
+ """Find the best Python 3 executable."""
485
+ candidates = ["python3", "python"]
486
+ for cmd in candidates:
487
+ try:
488
+ result = subprocess.run(
489
+ [cmd, "--version"], capture_output=True, text=True
490
+ )
491
+ if result.returncode == 0 and "Python 3" in result.stdout:
492
+ return cmd
493
+ except FileNotFoundError:
494
+ continue
495
+ return None
496
+
497
+
498
+ def create_venv(venv_path):
499
+ """Create a Python virtual environment."""
500
+ print(f"\n Creating virtual environment at {dim(str(venv_path))}...")
501
+ subprocess.run(
502
+ [sys.executable, "-m", "venv", str(venv_path)],
503
+ check=True
504
+ )
505
+
506
+
507
+ def get_venv_python(venv_path):
508
+ """Get the Python executable inside a venv."""
509
+ if is_windows():
510
+ return venv_path / "Scripts" / "python.exe"
511
+ return venv_path / "bin" / "python"
512
+
513
+
514
+ def get_venv_pip(venv_path):
515
+ """Get the pip executable inside a venv."""
516
+ if is_windows():
517
+ return venv_path / "Scripts" / "pip.exe"
518
+ return venv_path / "bin" / "pip"
519
+
520
+
521
+ def install_dependencies(venv_path, project_dir):
522
+ """Install Python dependencies into the venv."""
523
+ pip = get_venv_pip(venv_path)
524
+ req_file = project_dir / "requirements.txt"
525
+
526
+ print(f" Installing dependencies...")
527
+
528
+ # Install MCP SDK
529
+ subprocess.run(
530
+ [str(pip), "install", "-q", "mcp[cli]"],
531
+ check=True, capture_output=True
532
+ )
533
+
534
+ # Install from requirements.txt if it exists
535
+ if req_file.exists():
536
+ subprocess.run(
537
+ [str(pip), "install", "-q", "-r", str(req_file)],
538
+ check=True, capture_output=True
539
+ )
540
+
541
+ # ─── Connection Verification ─────────────────────────────────────────────────
542
+
543
+ def verify_resolve_connection(python_path, api_path, lib_path):
544
+ """Try to import DaVinciResolveScript and connect."""
545
+ if not api_path:
546
+ return False, "Resolve API path not found"
547
+
548
+ env = {**os.environ, **build_server_env(python_path, api_path, lib_path)}
549
+ modules_path = env["PYTHONPATH"]
550
+ test_script = textwrap.dedent(f"""\
551
+ import sys
552
+ sys.path.insert(0, {modules_path!r})
553
+ try:
554
+ import DaVinciResolveScript as dvr
555
+ resolve = dvr.scriptapp('Resolve')
556
+ if resolve:
557
+ name = resolve.GetProductName()
558
+ ver = resolve.GetVersionString()
559
+ print(f"CONNECTED: {{name}} {{ver}}")
560
+ else:
561
+ print("IMPORTED_OK: Module loads but Resolve not running or not responding")
562
+ except ImportError as e:
563
+ print(f"IMPORT_ERROR: {{e}}")
564
+ except Exception as e:
565
+ print(f"ERROR: {{e}}")
566
+ """)
567
+
568
+ try:
569
+ result = subprocess.run(
570
+ [str(python_path), "-c", test_script],
571
+ capture_output=True,
572
+ text=True,
573
+ timeout=10,
574
+ env=env,
575
+ )
576
+ output = result.stdout.strip() or result.stderr.strip()
577
+ if output.startswith("CONNECTED:"):
578
+ return True, output.replace("CONNECTED: ", "")
579
+ elif output.startswith("IMPORTED_OK:"):
580
+ return True, "API module loaded (Resolve not running)"
581
+ else:
582
+ if output:
583
+ return False, output
584
+ return False, f"Process exited with code {result.returncode}"
585
+ except subprocess.TimeoutExpired:
586
+ return False, "Connection timed out"
587
+ except Exception as e:
588
+ return False, str(e)
589
+
590
+ # ─── Interactive UI ───────────────────────────────────────────────────────────
591
+
592
+ def print_banner():
593
+ title = f"DaVinci Resolve MCP Server — Installer v{VERSION}"
594
+ subtitle = "32 compound · 329 full · 3 platforms"
595
+ print()
596
+ print(bold(" ╔══════════════════════════════════════════════════════╗"))
597
+ print(bold(f" ║{title:^54}║"))
598
+ print(bold(" ╠══════════════════════════════════════════════════════╣"))
599
+ print(bold(f" ║{subtitle:^54}║"))
600
+ print(bold(" ╚══════════════════════════════════════════════════════╝"))
601
+ print()
602
+
603
+
604
+ def print_step(num, total, text):
605
+ print(f"\n {cyan(f'[{num}/{total}]')} {bold(text)}")
606
+ print(f" {'─' * 50}")
607
+
608
+
609
+ def print_update_status(project_dir, *, force=False):
610
+ """Best-effort installer update notice."""
611
+ result = check_for_updates(VERSION, project_dir, timeout=2.0, force=force)
612
+ status = result.get("status")
613
+ if status == "update_available":
614
+ version_note = dim(
615
+ f"v{result.get('current_version')} -> v{result.get('latest_version')}"
616
+ )
617
+ release_note = dim(f"Latest release: {result.get('release_url')}")
618
+ print(
619
+ f" MCP Update: {yellow('Available')} "
620
+ f"{version_note}"
621
+ )
622
+ print(f" {release_note}")
623
+ elif status == "up_to_date":
624
+ print(f" MCP Update: {green('Up to date')} {dim(f'v{VERSION}')}")
625
+ elif status == "current_ahead":
626
+ print(f" MCP Update: {green('Local build ahead of latest release')} {dim(f'v{VERSION}')}")
627
+ elif status == "disabled":
628
+ print(f" MCP Update: {dim('Check disabled')}")
629
+ elif status == "error":
630
+ print(f" MCP Update: {yellow('Could not check')} {dim(str(result.get('error', '')))}")
631
+ return result
632
+
633
+
634
+ def _run_git(project_dir, args, timeout=45):
635
+ try:
636
+ return subprocess.run(
637
+ ["git", "-C", str(project_dir), *args],
638
+ capture_output=True,
639
+ text=True,
640
+ timeout=timeout,
641
+ )
642
+ except FileNotFoundError:
643
+ return None
644
+ except subprocess.TimeoutExpired as exc:
645
+ return exc
646
+
647
+
648
+ def _git_failure_message(result, fallback):
649
+ if result is None:
650
+ return "git is not available on PATH"
651
+ if isinstance(result, subprocess.TimeoutExpired):
652
+ return "git command timed out"
653
+ output = (result.stderr or result.stdout or "").strip()
654
+ return output or fallback
655
+
656
+
657
+ def apply_safe_self_update(project_dir, dry_run=False):
658
+ """Apply a guarded git fast-forward update for clean checkouts only."""
659
+ inside = _run_git(project_dir, ["rev-parse", "--is-inside-work-tree"])
660
+ if inside is None or isinstance(inside, subprocess.TimeoutExpired) or inside.returncode != 0:
661
+ return {"success": False, "reason": "not_git", "message": _git_failure_message(inside, "not a git checkout")}
662
+
663
+ status = _run_git(project_dir, ["status", "--porcelain"])
664
+ if status is None or isinstance(status, subprocess.TimeoutExpired) or status.returncode != 0:
665
+ return {"success": False, "reason": "status_failed", "message": _git_failure_message(status, "could not inspect git status")}
666
+ if status.stdout.strip():
667
+ return {
668
+ "success": False,
669
+ "reason": "local_changes",
670
+ "message": "local changes are present; continuing with the current build",
671
+ }
672
+
673
+ upstream = _run_git(project_dir, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"])
674
+ if upstream is None or isinstance(upstream, subprocess.TimeoutExpired) or upstream.returncode != 0:
675
+ return {
676
+ "success": False,
677
+ "reason": "no_upstream",
678
+ "message": _git_failure_message(upstream, "current branch has no configured upstream"),
679
+ }
680
+
681
+ if dry_run:
682
+ return {
683
+ "success": True,
684
+ "changed": False,
685
+ "dry_run": True,
686
+ "message": f"would fast-forward from {upstream.stdout.strip()}",
687
+ }
688
+
689
+ fetch = _run_git(project_dir, ["fetch", "--tags", "--prune"], timeout=90)
690
+ if fetch is None or isinstance(fetch, subprocess.TimeoutExpired) or fetch.returncode != 0:
691
+ return {"success": False, "reason": "fetch_failed", "message": _git_failure_message(fetch, "git fetch failed")}
692
+
693
+ pull = _run_git(project_dir, ["pull", "--ff-only"], timeout=120)
694
+ if pull is None or isinstance(pull, subprocess.TimeoutExpired) or pull.returncode != 0:
695
+ return {"success": False, "reason": "pull_failed", "message": _git_failure_message(pull, "git pull --ff-only failed")}
696
+
697
+ output = "\n".join(part.strip() for part in (pull.stdout, pull.stderr) if part and part.strip())
698
+ changed = "Already up to date." not in output
699
+ return {"success": True, "changed": changed, "message": output or "update complete"}
700
+
701
+
702
+ def _restart_installer():
703
+ print(f" {green('Restarting installer with the updated build...')}")
704
+ os.execv(sys.executable, [sys.executable, *sys.argv])
705
+
706
+
707
+ def _print_update_apply_result(result, *, restart_on_change=True):
708
+ if result.get("success"):
709
+ if result.get("dry_run"):
710
+ print(f" MCP Update: {yellow('Dry run')} {dim(result.get('message', ''))}")
711
+ return
712
+ print(f" MCP Update: {green('Update command completed')}")
713
+ message = str(result.get("message") or "").strip()
714
+ if message:
715
+ for line in message.splitlines()[:6]:
716
+ print(f" {dim(line)}")
717
+ if result.get("changed") and restart_on_change:
718
+ _restart_installer()
719
+ return
720
+
721
+ print(f" MCP Update: {yellow('Not applied')} {dim(result.get('message', ''))}")
722
+
723
+
724
+ def maybe_prompt_for_update(project_dir, result, *, interactive, force_update=False, dry_run=False):
725
+ """Prompt or auto-apply updates only in this human-facing installer."""
726
+ if not result or result.get("status") != "update_available":
727
+ return
728
+
729
+ decision = update_prompt_decision(result)
730
+ if force_update or decision.get("action") == "auto":
731
+ print(f" MCP Update: {yellow('Applying safe fast-forward update...')}")
732
+ _print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
733
+ return
734
+
735
+ if decision.get("action") == "notify":
736
+ print(f" {dim('Update policy is notify-only; continuing with the current build.')}")
737
+ return
738
+ if decision.get("reason") == "ignored":
739
+ print(f" {dim('This release was ignored; continuing with the current build.')}")
740
+ return
741
+ if decision.get("reason") == "snoozed":
742
+ snooze_note = f"Update reminder snoozed until {decision.get('snooze_until_iso')}."
743
+ print(f" {dim(snooze_note)}")
744
+ return
745
+ if not interactive:
746
+ return
747
+
748
+ latest = result.get("latest_version") or result.get("latest_tag") or "latest"
749
+ print()
750
+ print(f" {yellow('A newer DaVinci Resolve MCP is available.')} {dim(f'v{VERSION} -> v{latest}')}")
751
+ print(f" {dim('Safe auto-update only runs for clean git checkouts with a configured upstream.')}")
752
+ print(f" {cyan('1')}. Update now")
753
+ print(f" {cyan('2')}. Continue current build")
754
+ print(f" {cyan('3')}. Remind me tomorrow")
755
+ print(f" {cyan('4')}. Ignore this version")
756
+ print(f" {cyan('5')}. Auto-update this checkout when safe")
757
+ print(f" {cyan('6')}. Never check automatically")
758
+ try:
759
+ choice = input(f" Select {dim('[2]')} ").strip().lower()
760
+ except (EOFError, KeyboardInterrupt):
761
+ print()
762
+ return
763
+
764
+ if choice in ("1", "u", "update", "update now"):
765
+ _print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
766
+ elif choice in ("3", "r", "remind", "later", "snooze"):
767
+ snooze_update_prompt(project_dir)
768
+ print(f" MCP Update: {dim('Reminder snoozed for 24 hours.')}")
769
+ elif choice in ("4", "i", "ignore"):
770
+ ignore_update_version(project_dir, result)
771
+ print(f" MCP Update: {dim(f'Ignored v{latest}. Newer releases will still prompt.')}")
772
+ elif choice in ("5", "a", "auto", "auto-update", "autoupdate"):
773
+ set_update_mode(project_dir, "auto")
774
+ print(f" MCP Update: {green('Safe auto-update enabled for this checkout.')}")
775
+ _print_update_apply_result(apply_safe_self_update(project_dir, dry_run=dry_run))
776
+ elif choice in ("6", "n", "never", "disable", "disabled"):
777
+ set_update_mode(project_dir, "never")
778
+ print(f" MCP Update: {dim('Automatic update checks disabled for this checkout.')}")
779
+
780
+
781
+ def prompt_yes_no(question, default=True):
782
+ """Prompt for yes/no with a default."""
783
+ suffix = "[Y/n]" if default else "[y/N]"
784
+ try:
785
+ answer = input(f" {question} {dim(suffix)} ").strip().lower()
786
+ except (EOFError, KeyboardInterrupt):
787
+ print()
788
+ return default
789
+ if not answer:
790
+ return default
791
+ return answer in ("y", "yes")
792
+
793
+
794
+ def prompt_clients():
795
+ """Interactive client selection menu."""
796
+ print(f"\n Which MCP client(s) do you want to configure?\n")
797
+
798
+ for i, client in enumerate(MCP_CLIENTS, 1):
799
+ path = client["get_path"]()
800
+ available = path is not None
801
+ status = ""
802
+ if not available:
803
+ status = dim(f" (not available on {platform_name()})")
804
+ elif path and path.exists():
805
+ status = green(" (config exists)")
806
+
807
+ num = f"{i:>2}"
808
+ print(f" {cyan(num)}. {client['name']:<28} {dim(client['notes'])}{status}")
809
+
810
+ all_num = str(len(MCP_CLIENTS) + 1).rjust(2)
811
+ manual_num = str(len(MCP_CLIENTS) + 2).rjust(2)
812
+ print(f"\n {cyan(all_num)}. {bold('All of the above')}")
813
+ print(f" {cyan(manual_num)}. {bold('Manual setup')} {dim('(print config, I will set it up myself)')}")
814
+ print(f" {cyan(' 0')}. {dim('Skip client configuration')}")
815
+
816
+ print()
817
+ try:
818
+ choice = input(f" Select (comma-separated, e.g. 1,3,5): ").strip()
819
+ except (EOFError, KeyboardInterrupt):
820
+ print()
821
+ return []
822
+
823
+ if not choice or choice == "0":
824
+ return []
825
+
826
+ selections = []
827
+ for part in choice.replace(" ", "").split(","):
828
+ try:
829
+ idx = int(part)
830
+ if idx == len(MCP_CLIENTS) + 1: # "All"
831
+ return CLIENT_IDS + ["manual"]
832
+ elif idx == len(MCP_CLIENTS) + 2: # "Manual"
833
+ selections.append("manual")
834
+ elif 1 <= idx <= len(MCP_CLIENTS):
835
+ selections.append(MCP_CLIENTS[idx - 1]["id"])
836
+ except ValueError:
837
+ # Try matching by name/id
838
+ part_lower = part.lower()
839
+ for client in MCP_CLIENTS:
840
+ if part_lower in client["id"] or part_lower in client["name"].lower():
841
+ selections.append(client["id"])
842
+ break
843
+
844
+ return list(dict.fromkeys(selections)) # deduplicate, preserve order
845
+
846
+ # ─── Main Install Flow ───────────────────────────────────────────────────────
847
+
848
+ def main():
849
+ parser = argparse.ArgumentParser(
850
+ description="DaVinci Resolve MCP Server — Universal Installer",
851
+ formatter_class=argparse.RawDescriptionHelpFormatter,
852
+ epilog=textwrap.dedent("""\
853
+ Examples:
854
+ python install.py Interactive mode
855
+ python install.py --clients all Configure all clients
856
+ python install.py --clients cursor,claude-desktop
857
+ python install.py --clients manual Just print the config
858
+ python install.py --no-venv --clients cursor Skip venv, configure Cursor
859
+ python install.py --dry-run --clients all Preview without writing
860
+ python install.py --update-policy auto Enable guarded auto-updates
861
+ python install.py --update-policy never Disable update checks
862
+ """)
863
+ )
864
+ parser.add_argument(
865
+ "--clients", type=str, default=None,
866
+ help="Comma-separated client IDs, or 'all' / 'manual' (skip interactive prompt)"
867
+ )
868
+ parser.add_argument(
869
+ "--no-venv", action="store_true",
870
+ help="Skip virtual environment creation (use system Python)"
871
+ )
872
+ parser.add_argument(
873
+ "--dry-run", action="store_true",
874
+ help="Preview config changes without writing files"
875
+ )
876
+ parser.add_argument(
877
+ "--python", type=str, default=None,
878
+ help="Path to Python executable to use in MCP configs"
879
+ )
880
+ parser.add_argument(
881
+ "--server", type=str, default=None,
882
+ help="Path to the MCP server script"
883
+ )
884
+ parser.add_argument(
885
+ "--update-policy",
886
+ choices=["prompt", "auto", "notify", "never"],
887
+ default=None,
888
+ help="Set local update policy: prompt, auto, notify, or never"
889
+ )
890
+ parser.add_argument(
891
+ "--update-now", action="store_true",
892
+ help="Apply a safe git fast-forward update if a newer release is available"
893
+ )
894
+ parser.add_argument(
895
+ "--clear-update-preferences", action="store_true",
896
+ help="Clear ignored-version and snooze update preferences"
897
+ )
898
+
899
+ args = parser.parse_args()
900
+ interactive = args.clients is None
901
+
902
+ # ── Banner ──
903
+ if interactive:
904
+ print_banner()
905
+
906
+ project_dir = Path(__file__).resolve().parent
907
+ total_steps = 5
908
+
909
+ if args.clear_update_preferences:
910
+ clear_update_prompt_preferences(project_dir)
911
+ print(f" MCP Update: {green('Cleared ignored-version and snooze preferences')}")
912
+ if args.update_policy:
913
+ set_update_mode(project_dir, args.update_policy)
914
+ print(f" MCP Update: {green('Policy set')} {dim(args.update_policy)}")
915
+
916
+ # ══════════════════════════════════════════════════════════════════════
917
+ # STEP 1: Platform & Resolve Detection
918
+ # ══════════════════════════════════════════════════════════════════════
919
+
920
+ if interactive:
921
+ print_step(1, total_steps, "Detecting Platform & DaVinci Resolve")
922
+
923
+ print(f" Platform: {bold(platform_name())} ({platform.machine()})")
924
+ update_result = print_update_status(project_dir, force=args.update_now)
925
+ maybe_prompt_for_update(
926
+ project_dir,
927
+ update_result,
928
+ interactive=interactive,
929
+ force_update=args.update_now,
930
+ dry_run=args.dry_run,
931
+ )
932
+
933
+ api_path, lib_path = find_resolve_paths()
934
+
935
+ if api_path:
936
+ print(f" API Path: {green(api_path)}")
937
+ else:
938
+ print(f" API Path: {red('Not found')}")
939
+ if is_linux():
940
+ print(f" {dim('Tip: DaVinci Resolve typically installs to /opt/resolve/')}")
941
+ print(f" {dim(' Set RESOLVE_SCRIPT_API environment variable if installed elsewhere')}")
942
+
943
+ # Check environment variable fallback
944
+ env_api = os.environ.get("RESOLVE_SCRIPT_API")
945
+ if env_api and os.path.isdir(env_api):
946
+ api_path = env_api
947
+ print(f" {green('Found via $RESOLVE_SCRIPT_API:')} {api_path}")
948
+
949
+ if lib_path:
950
+ print(f" Library: {green(lib_path)}")
951
+ else:
952
+ print(f" Library: {yellow('Not found')} {dim('(optional — API path is sufficient)')}")
953
+
954
+ resolve_running = check_resolve_running()
955
+ if resolve_running:
956
+ print(f" Resolve: {green('Running')}")
957
+ else:
958
+ print(f" Resolve: {yellow('Not running')} {dim('(start Resolve to verify connection)')}")
959
+
960
+ if not api_path:
961
+ print(f"\n {yellow('Warning:')} Could not auto-detect DaVinci Resolve installation.")
962
+ print(f" The installer will continue, but you may need to set RESOLVE_SCRIPT_API manually.")
963
+ if interactive and not prompt_yes_no("Continue anyway?"):
964
+ print(f"\n {dim('Aborted.')}")
965
+ sys.exit(1)
966
+
967
+ # ══════════════════════════════════════════════════════════════════════
968
+ # STEP 2: Python Virtual Environment
969
+ # ══════════════════════════════════════════════════════════════════════
970
+
971
+ venv_path = project_dir / "venv"
972
+ venv_python = get_venv_python(venv_path)
973
+ skip_venv = args.no_venv
974
+
975
+ if interactive:
976
+ print_step(2, total_steps, "Python Environment")
977
+
978
+ if skip_venv:
979
+ print(f" Skipping venv (--no-venv)")
980
+ python_path = Path(args.python) if args.python else Path(sys.executable)
981
+ require_supported_python(python_path, "Selected Python")
982
+ print(f" Using: {python_path}")
983
+ elif venv_path.exists() and venv_python.exists():
984
+ print(f" Venv: {green('Already exists')} at {dim(str(venv_path))}")
985
+ python_path = venv_python
986
+ require_supported_python(python_path, "Existing venv")
987
+
988
+ # Check if deps are installed
989
+ try:
990
+ result = subprocess.run(
991
+ [str(python_path), "-c", "import mcp; print('ok')"],
992
+ capture_output=True, text=True
993
+ )
994
+ if result.stdout.strip() == "ok":
995
+ print(f" MCP SDK: {green('Installed')}")
996
+ else:
997
+ print(f" MCP SDK: {yellow('Missing')} — installing...")
998
+ install_dependencies(venv_path, project_dir)
999
+ print(f" MCP SDK: {green('Installed')}")
1000
+ except Exception:
1001
+ install_dependencies(venv_path, project_dir)
1002
+ print(f" MCP SDK: {green('Installed')}")
1003
+ else:
1004
+ if interactive:
1005
+ create_venv_ok = prompt_yes_no("Create virtual environment?")
1006
+ else:
1007
+ create_venv_ok = True
1008
+
1009
+ if create_venv_ok:
1010
+ require_current_python("Virtual environment")
1011
+ create_venv(venv_path)
1012
+ python_path = venv_python
1013
+ require_supported_python(python_path, "Created venv")
1014
+ install_dependencies(venv_path, project_dir)
1015
+ print(f" Venv: {green('Created')}")
1016
+ print(f" MCP SDK: {green('Installed')}")
1017
+ else:
1018
+ python_path = Path(args.python) if args.python else Path(sys.executable)
1019
+ require_supported_python(python_path, "Selected Python")
1020
+ print(f" Using system Python: {python_path}")
1021
+
1022
+ # Override python path if explicitly provided
1023
+ if args.python:
1024
+ python_path = Path(args.python)
1025
+ require_supported_python(python_path, "Selected --python")
1026
+
1027
+ # ══════════════════════════════════════════════════════════════════════
1028
+ # STEP 3: Locate Server Script
1029
+ # ══════════════════════════════════════════════════════════════════════
1030
+
1031
+ if interactive:
1032
+ print_step(3, total_steps, "Server Script")
1033
+
1034
+ if args.server:
1035
+ server_path = Path(args.server).resolve()
1036
+ else:
1037
+ # Try to find the compound server first.
1038
+ candidates = [
1039
+ project_dir / "src" / "server.py",
1040
+ project_dir / "src" / "resolve_mcp_server.py",
1041
+ project_dir / "src" / "main.py",
1042
+ project_dir / "resolve_mcp_server.py",
1043
+ ]
1044
+ server_path = None
1045
+ for c in candidates:
1046
+ if c.exists():
1047
+ server_path = c
1048
+ break
1049
+ if server_path is None:
1050
+ server_path = candidates[0] # default even if not found yet
1051
+
1052
+ if server_path.exists():
1053
+ print(f" Server: {green(str(server_path))}")
1054
+ else:
1055
+ print(f" Server: {yellow(str(server_path))} {dim('(file not found — check path)')}")
1056
+
1057
+ # ══════════════════════════════════════════════════════════════════════
1058
+ # STEP 4: Configure MCP Clients
1059
+ # ══════════════════════════════════════════════════════════════════════
1060
+
1061
+ if interactive:
1062
+ print_step(4, total_steps, "MCP Client Configuration")
1063
+
1064
+ # Determine which clients to configure
1065
+ if args.clients:
1066
+ if args.clients.lower() == "all":
1067
+ selected_ids = CLIENT_IDS
1068
+ elif args.clients.lower() == "manual":
1069
+ selected_ids = ["manual"]
1070
+ else:
1071
+ selected_ids = [s.strip() for s in args.clients.split(",")]
1072
+ elif interactive:
1073
+ selected_ids = prompt_clients()
1074
+ else:
1075
+ selected_ids = []
1076
+
1077
+ # Show manual config if requested
1078
+ show_manual = "manual" in selected_ids
1079
+ client_ids = [c for c in selected_ids if c != "manual"]
1080
+
1081
+ configured = []
1082
+ skipped = []
1083
+
1084
+ for client_id in client_ids:
1085
+ client = next((c for c in MCP_CLIENTS if c["id"] == client_id), None)
1086
+ if not client:
1087
+ print(f" {yellow('Unknown client:')} {client_id}")
1088
+ skipped.append(client_id)
1089
+ continue
1090
+
1091
+ success, message = write_client_config(
1092
+ client, python_path, server_path, api_path, lib_path, dry_run=args.dry_run
1093
+ )
1094
+
1095
+ if success:
1096
+ if args.dry_run:
1097
+ print(f"\n {cyan(client['name'])} {dim('(dry run)')}")
1098
+ for line in message.split("\n"):
1099
+ print(f" {line}")
1100
+ configured.append(client["name"])
1101
+ else:
1102
+ print(f" {green('✓')} {client['name']:<28} → {dim(message)}")
1103
+ configured.append(client["name"])
1104
+ else:
1105
+ print(f" {yellow('⊘')} {client['name']:<28} {dim(message)}")
1106
+ skipped.append(client["name"])
1107
+
1108
+ # Show manual config
1109
+ if show_manual:
1110
+ standard, vscode_fmt, zed_fmt = generate_manual_config(
1111
+ python_path, server_path, api_path, lib_path
1112
+ )
1113
+ env_preview = build_server_env(python_path, api_path, lib_path)
1114
+ print(f"\n {bold('Manual Configuration')}")
1115
+ print(f" {'─' * 50}")
1116
+ print(f"\n {cyan('Standard format')} (Claude Desktop, Cursor, Windsurf, Cline, Roo Code, Continue):")
1117
+ print()
1118
+ for line in standard.split("\n"):
1119
+ print(f" {line}")
1120
+ print(f"\n {cyan('VS Code format')} (GitHub Copilot agent mode — save as .vscode/mcp.json):")
1121
+ print()
1122
+ for line in vscode_fmt.split("\n"):
1123
+ print(f" {line}")
1124
+ print(f"\n {cyan('Zed format')} (add to ~/.config/zed/settings.json):")
1125
+ print()
1126
+ for line in zed_fmt.split("\n"):
1127
+ print(f" {line}")
1128
+ print(f"\n {cyan('JetBrains IDEs')} (IntelliJ, WebStorm, PyCharm, etc.):")
1129
+ print(f" Settings → Tools → AI Assistant → Model Context Protocol (MCP)")
1130
+ print(f" Add server with command: {python_path} {server_path}")
1131
+ for key, value in env_preview.items():
1132
+ print(f" Set env: {key}={value}")
1133
+ print()
1134
+
1135
+ if not selected_ids:
1136
+ print(f" {dim('No clients selected — skipping configuration')}")
1137
+
1138
+ # ══════════════════════════════════════════════════════════════════════
1139
+ # STEP 5: Verify Connection
1140
+ # ══════════════════════════════════════════════════════════════════════
1141
+
1142
+ if interactive:
1143
+ print_step(5, total_steps, "Verification")
1144
+
1145
+ if api_path:
1146
+ success, message = verify_resolve_connection(python_path, api_path, lib_path)
1147
+ if success:
1148
+ if "not running" in message.lower():
1149
+ print(f" API: {green('Module loads OK')}")
1150
+ print(f" Resolve: {yellow('Not running')} — start Resolve to use MCP tools")
1151
+ else:
1152
+ print(f" Connected: {green(message)}")
1153
+ else:
1154
+ print(f" Verify: {yellow(message)}")
1155
+ else:
1156
+ print(f" {yellow('Skipped')} — Resolve API path not detected")
1157
+
1158
+ # ══════════════════════════════════════════════════════════════════════
1159
+ # Summary
1160
+ # ══════════════════════════════════════════════════════════════════════
1161
+
1162
+ print(f"\n {'═' * 50}")
1163
+ if configured or show_manual:
1164
+ print(f" {green(bold('Setup complete!'))}")
1165
+ if configured:
1166
+ print(f" Configured: {', '.join(configured)}")
1167
+ print()
1168
+ print(f" {bold('Next steps:')}")
1169
+ if not resolve_running:
1170
+ print(f" 1. Start DaVinci Resolve")
1171
+ print(f" 2. Open your MCP client")
1172
+ print(f" 3. Start using natural language to control Resolve!")
1173
+ else:
1174
+ print(f" 1. Open your MCP client")
1175
+ print(f" 2. Start using natural language to control Resolve!")
1176
+ print()
1177
+ print(f" {dim(f'Server: {server_path}')}")
1178
+ print(f" {dim(f'Python: {python_path}')}")
1179
+ if api_path:
1180
+ print(f" {dim(f'API: {api_path}')}")
1181
+ elif not selected_ids:
1182
+ print(f" {green(bold('Environment ready!'))}")
1183
+ print(f" Run {cyan('python install.py --clients all')} to configure MCP clients later.")
1184
+ else:
1185
+ print(f" {yellow('No clients configured.')}")
1186
+ print(f" Run {cyan('python install.py')} again to retry.")
1187
+
1188
+ print()
1189
+
1190
+
1191
+ if __name__ == "__main__":
1192
+ try:
1193
+ main()
1194
+ except KeyboardInterrupt:
1195
+ print(f"\n\n {dim('Interrupted.')}\n")
1196
+ sys.exit(1)