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.
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +802 -0
- package/CLAUDE.md +15 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +53 -0
- package/bin/davinci-resolve-mcp.mjs +376 -0
- package/docs/README.md +56 -0
- package/docs/SKILL.md +1145 -0
- package/docs/authoring/fuse-dctl-authoring.md +242 -0
- package/docs/authoring/script-plugin-authoring.md +195 -0
- package/docs/contributing.md +82 -0
- package/docs/guides/color-decision-guide.md +387 -0
- package/docs/guides/editorial-decision-guide.md +136 -0
- package/docs/guides/media-analysis-guide.md +615 -0
- package/docs/guides/multicam-setup-guide.md +138 -0
- package/docs/install.md +198 -0
- package/docs/integrations/workflow-integrations.md +120 -0
- package/docs/kernels/README.md +28 -0
- package/docs/kernels/audio-fairlight-kernel.md +86 -0
- package/docs/kernels/color-grade-kernel.md +103 -0
- package/docs/kernels/extension-authoring-kernel.md +101 -0
- package/docs/kernels/fusion-composition-kernel.md +91 -0
- package/docs/kernels/media-pool-ingest-kernel.md +147 -0
- package/docs/kernels/project-lifecycle-kernel.md +120 -0
- package/docs/kernels/render-deliver-kernel.md +92 -0
- package/docs/kernels/review-annotation-kernel.md +110 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
- package/docs/kernels/timeline-edit-kernel.md +189 -0
- package/docs/notes/codec-plugin-notes.md +136 -0
- package/docs/notes/dctl-notes.md +234 -0
- package/docs/notes/fusion-template-notes.md +136 -0
- package/docs/notes/lut-notes.md +136 -0
- package/docs/notes/openfx-notes.md +120 -0
- package/docs/process/release-process.md +152 -0
- package/docs/reference/api-coverage.md +488 -0
- package/docs/reference/resolve_scripting_api.txt +1012 -0
- package/examples/README.md +53 -0
- package/examples/markers/README.md +81 -0
- package/examples/media/README.md +94 -0
- package/examples/timeline/README.md +98 -0
- package/install.py +1196 -0
- package/package.json +52 -0
- package/scripts/audit_api_parity.py +275 -0
- package/scripts/live_media_analysis_polish_probe.py +65 -0
- package/src/__init__.py +3 -0
- package/src/analysis_dashboard.py +4936 -0
- package/src/control_panel.py +13 -0
- package/src/granular/__init__.py +17 -0
- package/src/granular/common.py +727 -0
- package/src/granular/folder.py +287 -0
- package/src/granular/gallery.py +306 -0
- package/src/granular/graph.py +309 -0
- package/src/granular/media_pool.py +679 -0
- package/src/granular/media_pool_item.py +852 -0
- package/src/granular/media_storage.py +179 -0
- package/src/granular/project.py +1594 -0
- package/src/granular/resolve_control.py +521 -0
- package/src/granular/timeline.py +1074 -0
- package/src/granular/timeline_item.py +2251 -0
- package/src/resolve_mcp_server.py +43 -0
- package/src/server.py +15691 -0
- package/src/utils/__init__.py +3 -0
- package/src/utils/app_control.py +319 -0
- package/src/utils/audio_fairlight_live_probe.py +263 -0
- package/src/utils/cdl.py +20 -0
- package/src/utils/cloud_operations.py +192 -0
- package/src/utils/color_grade_live_probe.py +444 -0
- package/src/utils/dctl_templates.py +368 -0
- package/src/utils/extension_authoring_live_probe.py +292 -0
- package/src/utils/fuse_templates.py +1968 -0
- package/src/utils/fusion_composition_live_probe.py +284 -0
- package/src/utils/layout_presets.py +333 -0
- package/src/utils/mcp_stdio.py +32 -0
- package/src/utils/media_analysis.py +3618 -0
- package/src/utils/media_analysis_jobs.py +796 -0
- package/src/utils/media_pool_ingest_live_probe.py +592 -0
- package/src/utils/multicam.py +393 -0
- package/src/utils/object_inspection.py +287 -0
- package/src/utils/platform.py +157 -0
- package/src/utils/project_lifecycle_live_probe.py +376 -0
- package/src/utils/project_properties.py +601 -0
- package/src/utils/render_deliver_live_probe.py +384 -0
- package/src/utils/resolve_connection.py +77 -0
- package/src/utils/review_annotation_live_probe.py +352 -0
- package/src/utils/script_templates.py +1193 -0
- package/src/utils/sync_detection.py +887 -0
- package/src/utils/timeline_conform_live_probe.py +280 -0
- package/src/utils/timeline_kernel_live_probe.py +1091 -0
- package/src/utils/timeline_kernel_probe.py +185 -0
- package/src/utils/timeline_title_text.py +87 -0
- 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)
|