This is page 2 of 5. Use http://codebase.md/samuelgursky/davinci-resolve-mcp?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .cursorrules
├── .gitignore
├── CHANGELOG.md
├── CHANGES.md
├── config
│ ├── cursor-mcp-example.json
│ ├── macos
│ │ ├── claude-desktop-config.template.json
│ │ └── cursor-mcp-config.template.json
│ ├── mcp-project-template.json
│ ├── README.md
│ ├── sample_config.json
│ └── windows
│ ├── claude-desktop-config.template.json
│ └── cursor-mcp-config.template.json
├── docs
│ ├── CHANGELOG.md
│ ├── COMMIT_MESSAGE.txt
│ ├── FEATURES.md
│ ├── PROJECT_MCP_SETUP.md
│ ├── TOOLS_README.md
│ └── VERSION.md
├── examples
│ ├── getting_started.py
│ ├── markers
│ │ ├── add_spaced_markers.py
│ │ ├── add_timecode_marker.py
│ │ ├── alternating_markers.py
│ │ ├── clear_add_markers.py
│ │ ├── README.md
│ │ └── test_marker_frames.py
│ ├── media
│ │ ├── import_folder.py
│ │ └── README.md
│ ├── README.md
│ └── timeline
│ ├── README.md
│ ├── timeline_check.py
│ └── timeline_info.py
├── INSTALL.md
├── LICENSE
├── logs
│ └── .gitkeep
├── README.md
├── requirements.txt
├── resolve_mcp_server.py
├── run-now.bat
├── run-now.sh
├── scripts
│ ├── batch_automation.py
│ ├── check-resolve-ready.bat
│ ├── check-resolve-ready.ps1
│ ├── check-resolve-ready.sh
│ ├── create_app_shortcut.sh
│ ├── create-release-zip.bat
│ ├── create-release-zip.sh
│ ├── launch.sh
│ ├── mcp_resolve_launcher.sh
│ ├── mcp_resolve-claude_start
│ ├── mcp_resolve-cursor_start
│ ├── README.md
│ ├── resolve_mcp_server.py
│ ├── restart-server.bat
│ ├── restart-server.sh
│ ├── run-now.bat
│ ├── run-now.sh
│ ├── run-server.sh
│ ├── server.sh
│ ├── setup
│ │ ├── install.bat
│ │ └── install.sh
│ ├── setup.sh
│ ├── utils.sh
│ ├── verify-installation.bat
│ └── verify-installation.sh
├── src
│ ├── __init__.py
│ ├── api
│ │ ├── __init__.py
│ │ ├── color_operations.py
│ │ ├── delivery_operations.py
│ │ ├── media_operations.py
│ │ ├── project_operations.py
│ │ └── timeline_operations.py
│ ├── bin
│ │ └── __init__.py
│ ├── main.py
│ ├── resolve_mcp_server.py
│ └── utils
│ ├── __init__.py
│ ├── app_control.py
│ ├── cloud_operations.py
│ ├── layout_presets.py
│ ├── object_inspection.py
│ ├── platform.py
│ ├── project_properties.py
│ └── resolve_connection.py
└── tests
├── benchmark_server.py
├── create_test_timeline.py
├── test_custom_timeline.py
├── test_improvements.py
├── test-after-restart.bat
└── test-after-restart.sh
```
# Files
--------------------------------------------------------------------------------
/examples/markers/alternating_markers.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Add alternating color markers every 10 seconds for 60 seconds in the current timeline
4 | """
5 |
6 | import os
7 | import sys
8 |
9 | # Set environment variables for DaVinci Resolve scripting
10 | RESOLVE_API_PATH = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
11 | RESOLVE_LIB_PATH = "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
12 | RESOLVE_MODULES_PATH = os.path.join(RESOLVE_API_PATH, "Modules")
13 |
14 | os.environ["RESOLVE_SCRIPT_API"] = RESOLVE_API_PATH
15 | os.environ["RESOLVE_SCRIPT_LIB"] = RESOLVE_LIB_PATH
16 | sys.path.append(RESOLVE_MODULES_PATH)
17 |
18 | # Import DaVinci Resolve scripting
19 | import DaVinciResolveScript as dvr_script
20 |
21 | def main():
22 | print("\n===== Adding Alternating Color Markers =====\n")
23 |
24 | # Connect to Resolve
25 | resolve = dvr_script.scriptapp("Resolve")
26 | if not resolve:
27 | print("Error: Failed to connect to DaVinci Resolve")
28 | return
29 |
30 | print(f"Connected to: {resolve.GetProductName()} {resolve.GetVersionString()}")
31 |
32 | # Get project manager
33 | project_manager = resolve.GetProjectManager()
34 | if not project_manager:
35 | print("Error: Failed to get Project Manager")
36 | return
37 |
38 | # Get current project
39 | current_project = project_manager.GetCurrentProject()
40 | if not current_project:
41 | print("Error: No project currently open")
42 | return
43 |
44 | print(f"Current project: {current_project.GetName()}")
45 |
46 | # Get current timeline
47 | current_timeline = current_project.GetCurrentTimeline()
48 | if not current_timeline:
49 | print("Error: No timeline currently active")
50 | return
51 |
52 | timeline_name = current_timeline.GetName()
53 | print(f"Current timeline: {timeline_name}")
54 |
55 | # Get timeline frame rate
56 | try:
57 | frame_rate = float(current_timeline.GetSetting("timelineFrameRate"))
58 | print(f"Timeline frame rate: {frame_rate} fps")
59 | except Exception as e:
60 | print(f"Error getting frame rate: {str(e)}")
61 | frame_rate = 24.0 # Default to 24 fps
62 | print(f"Using default frame rate: {frame_rate} fps")
63 |
64 | # Get timeline frame range
65 | start_frame = current_timeline.GetStartFrame()
66 | end_frame = current_timeline.GetEndFrame()
67 | print(f"Timeline frame range: {start_frame} to {end_frame}")
68 |
69 | # Get existing markers to avoid conflicts
70 | existing_markers = current_timeline.GetMarkers() or {}
71 | print(f"Found {len(existing_markers)} existing markers")
72 |
73 | # Calculate frame positions for markers (every 10 seconds for 60 seconds)
74 | markers_to_add = []
75 |
76 | # Get clips to ensure we're adding markers on actual clips
77 | clips = []
78 | for track_idx in range(1, 5): # Check first 4 video tracks
79 | try:
80 | track_clips = current_timeline.GetItemListInTrack("video", track_idx)
81 | if track_clips and len(track_clips) > 0:
82 | clips.extend(track_clips)
83 | except:
84 | continue
85 |
86 | if not clips:
87 | print("Error: No clips found in timeline")
88 | return
89 |
90 | # Find a reference clip to use as starting point
91 | reference_clip = clips[0]
92 | reference_start = reference_clip.GetStart()
93 | print(f"Reference clip start: {reference_start}")
94 |
95 | # Calculate one hour in frames
96 | one_hour_in_frames = int(frame_rate * 60 * 60)
97 |
98 | # Calculate start frame at 01:00:00:00 (subtract one hour from current 02:00:00:00)
99 | start_frame_position = reference_start - one_hour_in_frames
100 | print(f"New start position (01:00:00:00): {start_frame_position}")
101 |
102 | # Calculate frame positions (every 10 seconds)
103 | frames_per_10_sec = int(frame_rate * 10)
104 | colors = ["Blue", "Red", "Green", "Yellow", "Purple", "Cyan"]
105 |
106 | # Prepare markers at 0, 10, 20, 30, 40, 50, 60 seconds (7 markers total)
107 | for i in range(7):
108 | offset_frames = i * frames_per_10_sec
109 | frame_position = start_frame_position + offset_frames
110 | color_index = i % len(colors)
111 | markers_to_add.append({
112 | "frame": frame_position,
113 | "color": colors[color_index],
114 | "note": f"{i*10} seconds marker (01:00:00:00 + {i*10}s)"
115 | })
116 |
117 | # Add markers
118 | print("\n--- Adding Markers ---")
119 | markers_added = 0
120 |
121 | for marker in markers_to_add:
122 | frame = marker["frame"]
123 | color = marker["color"]
124 | note = marker["note"]
125 |
126 | # Skip if marker already exists at this frame
127 | if frame in existing_markers:
128 | print(f"Skipping frame {frame}: Marker already exists")
129 | continue
130 |
131 | # Verify the frame is within a clip
132 | frame_in_clip = False
133 | for clip in clips:
134 | if clip.GetStart() <= frame <= clip.GetEnd():
135 | frame_in_clip = True
136 | break
137 |
138 | if not frame_in_clip:
139 | print(f"Skipping frame {frame}: Not within a clip")
140 | continue
141 |
142 | # Add the marker
143 | print(f"Adding {color} marker at frame {frame} ({note})")
144 | result = current_timeline.AddMarker(
145 | frame,
146 | color,
147 | note,
148 | note,
149 | 1,
150 | ""
151 | )
152 |
153 | if result:
154 | print(f"✓ Successfully added marker")
155 | markers_added += 1
156 | else:
157 | print(f"✗ Failed to add marker")
158 |
159 | # Get final count of markers
160 | final_markers = current_timeline.GetMarkers() or {}
161 |
162 | print(f"\nAdded {markers_added} new markers")
163 | print(f"Timeline now has {len(final_markers)} total markers")
164 | print("\n===== Completed =====")
165 |
166 | if __name__ == "__main__":
167 | main()
```
--------------------------------------------------------------------------------
/examples/markers/clear_add_markers.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Clear existing markers and add new alternating color markers at visible timeline positions
4 | """
5 |
6 | import os
7 | import sys
8 |
9 | # Set environment variables for DaVinci Resolve scripting
10 | RESOLVE_API_PATH = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
11 | RESOLVE_LIB_PATH = "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
12 | RESOLVE_MODULES_PATH = os.path.join(RESOLVE_API_PATH, "Modules")
13 |
14 | os.environ["RESOLVE_SCRIPT_API"] = RESOLVE_API_PATH
15 | os.environ["RESOLVE_SCRIPT_LIB"] = RESOLVE_LIB_PATH
16 | sys.path.append(RESOLVE_MODULES_PATH)
17 |
18 | # Import DaVinci Resolve scripting
19 | import DaVinciResolveScript as dvr_script
20 |
21 | def main():
22 | print("\n===== Clearing and Adding New Markers =====\n")
23 |
24 | # Connect to Resolve
25 | resolve = dvr_script.scriptapp("Resolve")
26 | if not resolve:
27 | print("Error: Failed to connect to DaVinci Resolve")
28 | return
29 |
30 | print(f"Connected to: {resolve.GetProductName()} {resolve.GetVersionString()}")
31 |
32 | # Get project manager
33 | project_manager = resolve.GetProjectManager()
34 | if not project_manager:
35 | print("Error: Failed to get Project Manager")
36 | return
37 |
38 | # Get current project
39 | current_project = project_manager.GetCurrentProject()
40 | if not current_project:
41 | print("Error: No project currently open")
42 | return
43 |
44 | print(f"Current project: {current_project.GetName()}")
45 |
46 | # Get current timeline
47 | current_timeline = current_project.GetCurrentTimeline()
48 | if not current_timeline:
49 | print("Error: No timeline currently active")
50 | return
51 |
52 | timeline_name = current_timeline.GetName()
53 | print(f"Current timeline: {timeline_name}")
54 |
55 | # Get timeline frame rate
56 | try:
57 | frame_rate = float(current_timeline.GetSetting("timelineFrameRate"))
58 | print(f"Timeline frame rate: {frame_rate} fps")
59 | except Exception as e:
60 | print(f"Error getting frame rate: {str(e)}")
61 | frame_rate = 24.0 # Default to 24 fps
62 | print(f"Using default frame rate: {frame_rate} fps")
63 |
64 | # Get timeline frame range
65 | start_frame = current_timeline.GetStartFrame()
66 | end_frame = current_timeline.GetEndFrame()
67 | print(f"Timeline frame range: {start_frame} to {end_frame}")
68 |
69 | # Clear existing markers
70 | existing_markers = current_timeline.GetMarkers() or {}
71 | print(f"Found {len(existing_markers)} existing markers to clear")
72 |
73 | if existing_markers:
74 | for frame in existing_markers:
75 | current_timeline.DeleteMarkerAtFrame(frame)
76 | print("All existing markers cleared")
77 |
78 | # Get clips to ensure we're adding markers on actual clips
79 | clips = []
80 | for track_idx in range(1, 5): # Check first 4 video tracks
81 | try:
82 | track_clips = current_timeline.GetItemListInTrack("video", track_idx)
83 | if track_clips and len(track_clips) > 0:
84 | clips.extend(track_clips)
85 | except:
86 | continue
87 |
88 | if not clips:
89 | print("Error: No clips found in timeline")
90 | return
91 |
92 | # Define exact positions visible in the timeline for markers
93 | # Based on the screenshot where the playhead is at 01:00:00:00
94 | # We'll add markers at consistent intervals within the visible clips
95 |
96 | print("\n--- Adding Markers at Specific Positions ---")
97 |
98 | # Define the marker positions based on the visible clips
99 | colors = ["Blue", "Red", "Green", "Yellow", "Purple", "Cyan"]
100 | marker_positions = []
101 |
102 | # Add positions for the first clip (approximately first half of timeline)
103 | first_clip_start = 86400 # 01:00:00:00
104 |
105 | # Add markers at specific positions in 10-second intervals
106 | for i in range(6): # Add 6 markers in the 60-second span
107 | frame = first_clip_start + (i * int(frame_rate * 10)) # Every 10 seconds
108 | color_index = i % len(colors)
109 | marker_positions.append({
110 | "frame": frame,
111 | "color": colors[color_index],
112 | "note": f"Marker {i+1}: {i*10} seconds"
113 | })
114 |
115 | # Add a few markers in the other clips visible in the timeline
116 | second_clip_start = 87351 # Start of DaVinciResolveMCP-01_v04.mov
117 | third_clip_start = 88446 # Start of DaVinciResolveMCP-01_v02.mov
118 | fourth_clip_start = 89469 # Start of DaVinciResolveMCP-01_v03.mov
119 |
120 | # Add one marker in each clip
121 | additional_markers = [
122 | {"frame": second_clip_start + 240, "color": "Red", "note": "Clip 2 marker"},
123 | {"frame": third_clip_start + 240, "color": "Green", "note": "Clip 3 marker"},
124 | {"frame": fourth_clip_start + 240, "color": "Purple", "note": "Clip 4 marker"}
125 | ]
126 |
127 | marker_positions.extend(additional_markers)
128 |
129 | # Add markers
130 | markers_added = 0
131 |
132 | for marker in marker_positions:
133 | frame = marker["frame"]
134 | color = marker["color"]
135 | note = marker["note"]
136 |
137 | # Verify the frame is within a clip
138 | frame_in_clip = False
139 | for clip in clips:
140 | if clip.GetStart() <= frame <= clip.GetEnd():
141 | frame_in_clip = True
142 | break
143 |
144 | if not frame_in_clip:
145 | print(f"Skipping frame {frame}: Not within a clip")
146 | continue
147 |
148 | # Add the marker
149 | print(f"Adding {color} marker at frame {frame} ({note})")
150 | result = current_timeline.AddMarker(
151 | frame,
152 | color,
153 | note,
154 | note,
155 | 1,
156 | ""
157 | )
158 |
159 | if result:
160 | print(f"✓ Successfully added marker")
161 | markers_added += 1
162 | else:
163 | print(f"✗ Failed to add marker")
164 |
165 | # Get final count of markers
166 | final_markers = current_timeline.GetMarkers() or {}
167 |
168 | print(f"\nAdded {markers_added} new markers")
169 | print(f"Timeline now has {len(final_markers)} total markers")
170 | print("\n===== Completed =====")
171 |
172 | if __name__ == "__main__":
173 | main()
```
--------------------------------------------------------------------------------
/tests/create_test_timeline.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve Test Timeline Generator
4 | ---------------------------------------
5 | This script creates a test timeline with sample media for testing the MCP server.
6 | It generates colored test frames as clips if no media is available.
7 |
8 | Usage:
9 | python create_test_timeline.py
10 |
11 | Requirements:
12 | - DaVinci Resolve must be running
13 | - requests module (pip install requests)
14 | """
15 |
16 | import os
17 | import sys
18 | import time
19 | import requests
20 | import logging
21 | import tempfile
22 | import subprocess
23 | from typing import Dict, Any, List, Optional
24 |
25 | # Configure logging
26 | logging.basicConfig(
27 | level=logging.INFO,
28 | format='%(asctime)s - %(levelname)s - %(message)s',
29 | handlers=[
30 | logging.StreamHandler()
31 | ]
32 | )
33 | logger = logging.getLogger(__name__)
34 |
35 | # Server configuration
36 | SERVER_URL = "http://localhost:8000/api"
37 |
38 | def send_request(tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
39 | """Send a request to the MCP server."""
40 | try:
41 | payload = {
42 | "tool": tool_name,
43 | "params": params
44 | }
45 | response = requests.post(SERVER_URL, json=payload)
46 | response.raise_for_status()
47 | return response.json()
48 | except requests.exceptions.RequestException as e:
49 | logger.error(f"Request error: {e}")
50 | return {"success": False, "error": str(e)}
51 |
52 | def create_test_media() -> List[str]:
53 | """Create test media files for import."""
54 | logger.info("Creating test media files...")
55 |
56 | media_files = []
57 | temp_dir = tempfile.gettempdir()
58 |
59 | try:
60 | # Create three colored test frames using ffmpeg if available
61 | colors = ["red", "green", "blue"]
62 |
63 | for color in colors:
64 | output_file = os.path.join(temp_dir, f"test_{color}.mp4")
65 |
66 | # Check if ffmpeg is available
67 | try:
68 | # Create a 5-second test video with the specified color
69 | cmd = [
70 | "ffmpeg", "-y", "-f", "lavfi", "-i", f"color=c={color}:s=1280x720:r=30:d=5",
71 | "-c:v", "libx264", "-pix_fmt", "yuv420p", output_file
72 | ]
73 | subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
74 | media_files.append(output_file)
75 | logger.info(f"Created {color} test media: {output_file}")
76 | except (subprocess.SubprocessError, FileNotFoundError) as e:
77 | logger.error(f"Failed to create test media: {e}")
78 | # Try an alternative method if ffmpeg fails
79 | break
80 | except Exception as e:
81 | logger.error(f"Error creating test media: {e}")
82 |
83 | if not media_files:
84 | logger.warning("Could not create test media. The timeline will be empty.")
85 |
86 | return media_files
87 |
88 | def setup_test_project() -> bool:
89 | """Create a test project."""
90 | logger.info("Setting up test project...")
91 |
92 | # Create a new project
93 | result = send_request("mcp_davinci_resolve_create_project", {"name": "MCP_Test_Project"})
94 |
95 | if "error" in result and result["error"]:
96 | logger.error(f"Failed to create project: {result['error']}")
97 |
98 | # Try opening the project if it already exists
99 | open_result = send_request("mcp_davinci_resolve_open_project", {"name": "MCP_Test_Project"})
100 | if "error" in open_result and open_result["error"]:
101 | logger.error(f"Failed to open existing project: {open_result['error']}")
102 | return False
103 | else:
104 | logger.info("Opened existing test project")
105 | else:
106 | logger.info("Created new test project")
107 |
108 | # Set project settings
109 | send_request("mcp_davinci_resolve_set_project_setting",
110 | {"setting_name": "timelineFrameRate", "setting_value": 30})
111 |
112 | return True
113 |
114 | def create_test_timeline() -> bool:
115 | """Create a test timeline with imported media."""
116 | logger.info("Creating test timeline...")
117 |
118 | # Create timeline
119 | result = send_request("mcp_davinci_resolve_create_timeline", {"name": "MCP_Test_Timeline"})
120 |
121 | if "error" in result and result["error"]:
122 | logger.error(f"Failed to create timeline: {result['error']}")
123 | return False
124 |
125 | logger.info("Created test timeline")
126 |
127 | # Set as current timeline
128 | send_request("mcp_davinci_resolve_set_current_timeline", {"name": "MCP_Test_Timeline"})
129 |
130 | # Create and import test media
131 | media_files = create_test_media()
132 |
133 | # Import media files
134 | for media_file in media_files:
135 | import_result = send_request("mcp_davinci_resolve_import_media",
136 | {"file_path": media_file})
137 |
138 | if "error" not in import_result or not import_result["error"]:
139 | logger.info(f"Imported media: {media_file}")
140 |
141 | # Add to timeline (after short delay to ensure media is processed)
142 | time.sleep(1)
143 | clip_name = os.path.basename(media_file)
144 | add_result = send_request("mcp_davinci_resolve_add_clip_to_timeline",
145 | {"clip_name": clip_name, "timeline_name": "MCP_Test_Timeline"})
146 |
147 | if "error" not in add_result or not add_result["error"]:
148 | logger.info(f"Added clip to timeline: {clip_name}")
149 | else:
150 | logger.warning(f"Failed to add clip to timeline: {add_result.get('error', 'Unknown error')}")
151 | else:
152 | logger.warning(f"Failed to import media: {import_result.get('error', 'Unknown error')}")
153 |
154 | return True
155 |
156 | def main() -> None:
157 | """Run the test timeline creation process."""
158 | logger.info("Starting DaVinci Resolve test timeline setup")
159 | logger.info("=" * 50)
160 |
161 | # Set up test project
162 | if not setup_test_project():
163 | logger.error("Failed to set up test project. Exiting.")
164 | sys.exit(1)
165 |
166 | # Create test timeline with media
167 | if not create_test_timeline():
168 | logger.error("Failed to create test timeline. Exiting.")
169 | sys.exit(1)
170 |
171 | logger.info("=" * 50)
172 | logger.info("Test timeline setup complete!")
173 | logger.info("You can now use this timeline to test the MCP server features.")
174 | logger.info("=" * 50)
175 |
176 | if __name__ == "__main__":
177 | main()
```
--------------------------------------------------------------------------------
/examples/markers/add_timecode_marker.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | CLI utility to add a marker at a specific timecode position
4 | Usage: ./add_timecode_marker.py <timecode> [color] [note]
5 | Example: ./add_timecode_marker.py 01:00:15:00 Red "My marker note"
6 | """
7 |
8 | import os
9 | import sys
10 | import argparse
11 |
12 | # Set environment variables for DaVinci Resolve scripting
13 | RESOLVE_API_PATH = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
14 | RESOLVE_LIB_PATH = "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
15 | RESOLVE_MODULES_PATH = os.path.join(RESOLVE_API_PATH, "Modules")
16 |
17 | os.environ["RESOLVE_SCRIPT_API"] = RESOLVE_API_PATH
18 | os.environ["RESOLVE_SCRIPT_LIB"] = RESOLVE_LIB_PATH
19 | sys.path.append(RESOLVE_MODULES_PATH)
20 |
21 | # Import DaVinci Resolve scripting
22 | import DaVinciResolveScript as dvr_script
23 |
24 | def tc_to_frame(tc_str, fps):
25 | """Convert timecode string to frame number"""
26 | if not tc_str:
27 | return 0
28 |
29 | # Handle timecode format "HH:MM:SS:FF"
30 | parts = tc_str.split(":")
31 | if len(parts) != 4:
32 | return 0
33 |
34 | hours = int(parts[0])
35 | minutes = int(parts[1])
36 | seconds = int(parts[2])
37 | frames = int(parts[3])
38 |
39 | total_frames = int(round(
40 | (hours * 3600 + minutes * 60 + seconds) * fps + frames
41 | ))
42 |
43 | return total_frames
44 |
45 | def frame_to_tc(frame, fps):
46 | """Convert frame number to timecode string"""
47 | total_seconds = frame / fps
48 | hours = int(total_seconds // 3600)
49 | minutes = int((total_seconds % 3600) // 60)
50 | seconds = int(total_seconds % 60)
51 | frames = int((total_seconds - int(total_seconds)) * fps)
52 |
53 | return f"{hours:02d}:{minutes:02d}:{seconds:02d}:{frames:02d}"
54 |
55 | def add_marker(timecode, color="Blue", note=""):
56 | """Add a marker at the specified timecode"""
57 | print(f"Attempting to add {color} marker at {timecode} with note: {note}")
58 |
59 | # Connect to Resolve
60 | resolve = dvr_script.scriptapp("Resolve")
61 | if not resolve:
62 | print("Error: Failed to connect to DaVinci Resolve")
63 | return False
64 |
65 | print(f"Connected to: {resolve.GetProductName()} {resolve.GetVersionString()}")
66 |
67 | # Get project manager
68 | project_manager = resolve.GetProjectManager()
69 | current_project = project_manager.GetCurrentProject()
70 |
71 | if not current_project:
72 | print("Error: No project currently open")
73 | return False
74 |
75 | # Get current timeline
76 | current_timeline = current_project.GetCurrentTimeline()
77 | if not current_timeline:
78 | print("Error: No timeline currently active")
79 | return False
80 |
81 | timeline_name = current_timeline.GetName()
82 | print(f"Timeline: {timeline_name}")
83 |
84 | # Get frame rate
85 | fps = float(current_timeline.GetSetting("timelineFrameRate"))
86 | print(f"Frame rate: {fps} fps")
87 |
88 | # Get timeline start timecode
89 | start_tc = current_timeline.GetStartTimecode()
90 | if not start_tc:
91 | start_tc = "01:00:00:00" # Default
92 |
93 | print(f"Timeline start timecode: {start_tc}")
94 |
95 | # Convert input timecode to frame number
96 | frame = tc_to_frame(timecode, fps)
97 | print(f"Converted {timecode} to frame: {frame}")
98 |
99 | # Validate color
100 | valid_colors = [
101 | "Blue", "Cyan", "Green", "Yellow", "Red", "Pink", "Purple", "Fuchsia",
102 | "Rose", "Lavender", "Sky", "Mint", "Lemon", "Sand", "Cocoa", "Cream"
103 | ]
104 |
105 | if color not in valid_colors:
106 | print(f"Warning: Invalid color '{color}'. Using Blue instead.")
107 | color = "Blue"
108 |
109 | # Get clips to check if frame is valid
110 | clips = []
111 | for track_idx in range(1, 5): # Check first 4 video tracks
112 | try:
113 | track_clips = current_timeline.GetItemListInTrack("video", track_idx)
114 | if track_clips and len(track_clips) > 0:
115 | clips.extend(track_clips)
116 | except:
117 | continue
118 |
119 | # Check if frame is within a clip
120 | frame_in_clip = False
121 | for clip in clips:
122 | if clip.GetStart() <= frame <= clip.GetEnd():
123 | frame_in_clip = True
124 | clip_name = clip.GetName()
125 | print(f"Frame {frame} is within clip: {clip_name}")
126 | break
127 |
128 | if not frame_in_clip:
129 | print(f"Warning: Frame {frame} is not within any clip. Marker may not appear correctly.")
130 |
131 | # Add the marker
132 | print(f"Adding marker: Frame={frame}, Color={color}, Note='{note}'")
133 | result = current_timeline.AddMarker(
134 | frame,
135 | color,
136 | note or "Marker",
137 | note,
138 | 1,
139 | ""
140 | )
141 |
142 | if result:
143 | print(f"✓ Successfully added {color} marker at {timecode} (frame {frame})")
144 | return True
145 | else:
146 | print(f"✗ Failed to add marker at {timecode} (frame {frame})")
147 |
148 | # Check if a marker already exists at this frame
149 | markers = current_timeline.GetMarkers() or {}
150 | if frame in markers:
151 | print(f"A marker already exists at frame {frame}.")
152 | # Try alternate position
153 | alt_frame = frame + 1
154 | print(f"Trying alternate position: frame {alt_frame} ({frame_to_tc(alt_frame, fps)})")
155 |
156 | alt_result = current_timeline.AddMarker(
157 | alt_frame,
158 | color,
159 | note or "Marker",
160 | note,
161 | 1,
162 | ""
163 | )
164 |
165 | if alt_result:
166 | print(f"✓ Successfully added {color} marker at alternate position: {frame_to_tc(alt_frame, fps)} (frame {alt_frame})")
167 | return True
168 |
169 | return False
170 |
171 | def main():
172 | # Parse command line arguments
173 | parser = argparse.ArgumentParser(description="Add a marker at a specific timecode position")
174 | parser.add_argument("timecode", help="Timecode position (HH:MM:SS:FF)")
175 | parser.add_argument("color", nargs="?", default="Blue", help="Marker color")
176 | parser.add_argument("note", nargs="?", default="", help="Marker note")
177 |
178 | args = parser.parse_args()
179 |
180 | # Validate timecode format
181 | if not args.timecode or len(args.timecode.split(":")) != 4:
182 | print("Error: Invalid timecode format. Use HH:MM:SS:FF format.")
183 | return
184 |
185 | # Add the marker
186 | success = add_marker(args.timecode, args.color, args.note)
187 |
188 | print(f"\nMarker {'added successfully' if success else 'addition failed'}")
189 |
190 | if __name__ == "__main__":
191 | if len(sys.argv) < 2:
192 | print(f"Usage: {sys.argv[0]} <timecode> [color] [note]")
193 | print(f"Example: {sys.argv[0]} 01:00:15:00 Red \"My marker note\"")
194 | sys.exit(1)
195 |
196 | main()
```
--------------------------------------------------------------------------------
/INSTALL.md:
--------------------------------------------------------------------------------
```markdown
1 | # DaVinci Resolve MCP Integration - Installation Guide
2 |
3 | This guide provides step-by-step instructions for installing and configuring the DaVinci Resolve MCP integration for use with Cursor AI. The integration allows Cursor AI to control DaVinci Resolve through its API.
4 |
5 | ## Prerequisites
6 |
7 | - DaVinci Resolve installed (Free or Studio version)
8 | - Python 3.9+ installed
9 | - Cursor AI installed
10 |
11 | ## Installation Steps
12 |
13 | ### 1. New One-Step Installation (Recommended)
14 |
15 | We now provide a unified installation script that handles everything automatically, with robust error detection and configuration:
16 |
17 | **macOS/Linux:**
18 | ```bash
19 | # Make sure DaVinci Resolve is running, then:
20 | ./install.sh
21 | ```
22 |
23 | **Windows:**
24 | ```bash
25 | # Make sure DaVinci Resolve is running, then:
26 | install.bat
27 | ```
28 |
29 | This new installation script will:
30 | - Detect the correct installation path automatically
31 | - Create the Python virtual environment
32 | - Install all required dependencies
33 | - Set up environment variables
34 | - Generate the correct Cursor MCP configuration
35 | - Verify the installation
36 | - Optionally start the server if everything is correct
37 |
38 | ### 2. Quick Start (Alternative)
39 |
40 | The earlier quick start scripts are still available:
41 |
42 | **macOS/Linux:**
43 | ```bash
44 | # Make sure DaVinci Resolve is already running before executing this script
45 | ./run-now.sh
46 | ```
47 |
48 | **Windows:**
49 | ```bash
50 | # Make sure DaVinci Resolve is already running before executing this script
51 | run-now.bat
52 | ```
53 |
54 | ### 3. Manual Setup (Advanced)
55 |
56 | If you prefer to set up the integration manually or if you encounter issues with the automatic methods:
57 |
58 | #### Step 3.1: Create a Python Virtual Environment
59 |
60 | ```bash
61 | python3 -m venv venv
62 | source venv/bin/activate # On Windows: venv\Scripts\activate
63 | ```
64 |
65 | #### Step 3.2: Install Dependencies
66 |
67 | ```bash
68 | # Install all required dependencies from requirements.txt
69 | pip install -r requirements.txt
70 | ```
71 |
72 | Alternatively, you can install just the MCP SDK:
73 |
74 | ```bash
75 | pip install "mcp[cli]"
76 | ```
77 |
78 | #### Step 3.3: Set Environment Variables
79 |
80 | On macOS/Linux, add the following to your `~/.zshrc` or `~/.bashrc`:
81 |
82 | ```bash
83 | export RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
84 | export RESOLVE_SCRIPT_LIB="/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
85 | export PYTHONPATH="$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/"
86 | ```
87 |
88 | On Windows, set these environment variables in PowerShell or through System Properties:
89 |
90 | ```powershell
91 | $env:RESOLVE_SCRIPT_API = "C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting"
92 | $env:RESOLVE_SCRIPT_LIB = "C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll"
93 | $env:PYTHONPATH = "$env:PYTHONPATH;$env:RESOLVE_SCRIPT_API\Modules\"
94 | ```
95 |
96 | #### Step 3.4: Configure Cursor
97 |
98 | The installation creates two MCP configuration files:
99 |
100 | **System-level configuration**:
101 | - macOS/Linux: `~/.cursor/mcp/config.json`
102 | - Windows: `%APPDATA%\Cursor\mcp\config.json`
103 |
104 | **Project-level configuration**:
105 | - In the project root: `.cursor/mcp.json`
106 |
107 | Both configurations use absolute paths to the Python interpreter and script. This ensures Cursor can find the correct files regardless of how the project is opened.
108 |
109 | #### Sample configuration:
110 | ```json
111 | {
112 | "mcpServers": {
113 | "davinci-resolve": {
114 | "name": "DaVinci Resolve MCP",
115 | "command": "/Users/username/davinci-resolve-mcp/venv/bin/python",
116 | "args": ["/Users/username/davinci-resolve-mcp/resolve_mcp_server.py"]
117 | }
118 | }
119 | }
120 | ```
121 |
122 | The installation scripts automatically create both configuration files with the correct absolute paths for your system. If you need to move the project to a new location, you'll need to run the installation script again to update the paths.
123 |
124 | ### 4. Start the Integration
125 |
126 | For a more controlled setup with additional options:
127 |
128 | **macOS/Linux:**
129 | ```bash
130 | # From the scripts directory
131 | cd scripts
132 | ./mcp_resolve-cursor_start
133 | ```
134 |
135 | ### 5. Verify Your Installation
136 |
137 | After completing the installation steps, you can verify that everything is set up correctly by running:
138 |
139 | **macOS/Linux:**
140 | ```bash
141 | ./scripts/verify-installation.sh
142 | ```
143 |
144 | **Windows:**
145 | ```bash
146 | scripts\verify-installation.bat
147 | ```
148 |
149 | This verification script checks:
150 | - Python virtual environment setup
151 | - MCP SDK installation
152 | - DaVinci Resolve running status
153 | - Cursor configuration
154 | - Environment variables
155 | - Server script presence
156 |
157 | If all checks pass, you're ready to use the integration. If any checks fail, the script will provide guidance on how to fix the issues.
158 |
159 | ## Troubleshooting
160 |
161 | ### DaVinci Resolve Detection Issues
162 |
163 | If the script cannot detect that DaVinci Resolve is running:
164 |
165 | 1. Make sure DaVinci Resolve is actually running before executing scripts
166 | 2. The detection method has been updated to use `ps -ef | grep -i "[D]aVinci Resolve"` instead of `pgrep`, which provides more reliable detection
167 |
168 | ### Path Resolution Issues
169 |
170 | If you see errors related to file paths:
171 |
172 | 1. The scripts now use the directory where they're located as the reference point
173 | 2. Check that the `resolve_mcp_server.py` file exists in the expected location
174 | 3. Verify that your Cursor MCP configuration points to the correct paths
175 | 4. If you move the project to a new location, you'll need to run the installation script again to update the paths
176 |
177 | ### Environment Variables
178 |
179 | If you encounter Python import errors:
180 |
181 | 1. Verify that the environment variables are correctly set
182 | 2. The paths may differ depending on your DaVinci Resolve installation location
183 | 3. You can check the log file at `scripts/cursor_resolve_server.log` for details
184 |
185 | ### Cursor Configuration Issues
186 |
187 | If Cursor isn't connecting to the MCP server:
188 |
189 | 1. Check both the system-level and project-level configuration files
190 | 2. Ensure the paths in the configurations match your actual installation
191 | 3. The absolute paths must be correct - verify they point to your actual installation location
192 | 4. After moving the project, run `./install.sh` or `install.bat` again to update the paths
193 |
194 | ## Configuration Reference
195 |
196 | The integration creates two configuration files:
197 |
198 | 1. **System-level config** (for global use): `~/.cursor/mcp/config.json` (macOS/Linux) or `%APPDATA%\Cursor\mcp\config.json` (Windows)
199 | 2. **Project-level config** (for specific project use): `.cursor/mcp.json` in the project root
200 |
201 | Both configurations have the same structure:
202 |
203 | ```json
204 | {
205 | "mcpServers": {
206 | "davinci-resolve": {
207 | "name": "DaVinci Resolve MCP",
208 | "command": "<absolute-path-to-python-interpreter>",
209 | "args": ["<absolute-path-to-resolve_mcp_server.py>"]
210 | }
211 | }
212 | }
213 | ```
214 |
215 | ## Support
216 |
217 | If you encounter any issues not covered in this guide, please file an issue on the GitHub repository.
```
--------------------------------------------------------------------------------
/examples/markers/add_spaced_markers.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | Add markers at regular intervals using proper timecode conversion
4 | This script adds markers at specified intervals starting from a given timecode
5 | """
6 |
7 | import os
8 | import sys
9 | import argparse
10 |
11 | # Set environment variables for DaVinci Resolve scripting
12 | RESOLVE_API_PATH = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
13 | RESOLVE_LIB_PATH = "/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
14 | RESOLVE_MODULES_PATH = os.path.join(RESOLVE_API_PATH, "Modules")
15 |
16 | os.environ["RESOLVE_SCRIPT_API"] = RESOLVE_API_PATH
17 | os.environ["RESOLVE_SCRIPT_LIB"] = RESOLVE_LIB_PATH
18 | sys.path.append(RESOLVE_MODULES_PATH)
19 |
20 | # Import DaVinci Resolve scripting
21 | import DaVinciResolveScript as dvr_script
22 |
23 | def tc_to_frame(tc_str, fps):
24 | """Convert timecode string to frame number"""
25 | if not tc_str:
26 | return 0
27 |
28 | # Handle timecode format "HH:MM:SS:FF"
29 | parts = tc_str.split(":")
30 | if len(parts) != 4:
31 | return 0
32 |
33 | hours = int(parts[0])
34 | minutes = int(parts[1])
35 | seconds = int(parts[2])
36 | frames = int(parts[3])
37 |
38 | total_frames = int(round(
39 | (hours * 3600 + minutes * 60 + seconds) * fps + frames
40 | ))
41 |
42 | return total_frames
43 |
44 | def frame_to_tc(frame, fps):
45 | """Convert frame number to timecode string"""
46 | total_seconds = frame / fps
47 | hours = int(total_seconds // 3600)
48 | minutes = int((total_seconds % 3600) // 60)
49 | seconds = int(total_seconds % 60)
50 | frames = int((total_seconds - int(total_seconds)) * fps)
51 |
52 | return f"{hours:02d}:{minutes:02d}:{seconds:02d}:{frames:02d}"
53 |
54 | def add_markers(start_tc="01:00:00:00", interval_seconds=10, count=7, clear_existing=True):
55 | """Add markers at regular intervals"""
56 | print(f"\n===== ADDING {count} MARKERS AT {interval_seconds}-SECOND INTERVALS =====\n")
57 | print(f"Starting at: {start_tc}")
58 |
59 | # Connect to Resolve
60 | resolve = dvr_script.scriptapp("Resolve")
61 | if not resolve:
62 | print("Error: Failed to connect to DaVinci Resolve")
63 | return
64 |
65 | print(f"Connected to: {resolve.GetProductName()} {resolve.GetVersionString()}")
66 |
67 | # Get project manager
68 | project_manager = resolve.GetProjectManager()
69 | current_project = project_manager.GetCurrentProject()
70 |
71 | if not current_project:
72 | print("Error: No project currently open")
73 | return
74 |
75 | print(f"Current project: {current_project.GetName()}")
76 |
77 | # Get current timeline
78 | current_timeline = current_project.GetCurrentTimeline()
79 | if not current_timeline:
80 | print("Error: No timeline currently active")
81 | return
82 |
83 | timeline_name = current_timeline.GetName()
84 | print(f"Timeline: {timeline_name}")
85 |
86 | # Get frame rate
87 | fps = float(current_timeline.GetSetting("timelineFrameRate"))
88 | print(f"Frame rate: {fps} fps")
89 |
90 | # Get timeline start timecode
91 | timeline_start_tc = current_timeline.GetStartTimecode()
92 | if not timeline_start_tc:
93 | timeline_start_tc = "01:00:00:00" # Default
94 |
95 | print(f"Timeline start timecode: {timeline_start_tc}")
96 |
97 | # Clear existing markers if requested
98 | if clear_existing:
99 | existing_markers = current_timeline.GetMarkers() or {}
100 | print(f"Clearing {len(existing_markers)} existing markers")
101 |
102 | for frame in existing_markers:
103 | current_timeline.DeleteMarkerAtFrame(frame)
104 |
105 | # Get clips to check if frames are valid
106 | clips = []
107 | for track_idx in range(1, 5): # Check first 4 video tracks
108 | try:
109 | track_clips = current_timeline.GetItemListInTrack("video", track_idx)
110 | if track_clips and len(track_clips) > 0:
111 | clips.extend(track_clips)
112 | except:
113 | continue
114 |
115 | if not clips:
116 | print("Error: No clips found in timeline")
117 | return
118 |
119 | # Convert start timecode to frame
120 | start_frame = tc_to_frame(start_tc, fps)
121 | print(f"Start position: {start_tc} (frame {start_frame})")
122 |
123 | # Define colors
124 | colors = ["Blue", "Red", "Green", "Yellow", "Purple", "Cyan", "Pink"]
125 |
126 | # Calculate interval in frames
127 | interval_frames = int(interval_seconds * fps)
128 | print(f"Interval: {interval_seconds} seconds ({interval_frames} frames)")
129 |
130 | # Add markers
131 | print("\n--- Adding Markers ---")
132 | markers_added = 0
133 |
134 | for i in range(count):
135 | # Calculate frame position
136 | frame = start_frame + (i * interval_frames)
137 | target_tc = frame_to_tc(frame, fps)
138 |
139 | # Validate frame is within a clip
140 | frame_in_clip = False
141 | clip_name = ""
142 | for clip in clips:
143 | if clip.GetStart() <= frame <= clip.GetEnd():
144 | frame_in_clip = True
145 | clip_name = clip.GetName()
146 | break
147 |
148 | if not frame_in_clip:
149 | print(f"Skipping position {target_tc} (frame {frame}): Not within any clip")
150 | continue
151 |
152 | # Select color
153 | color_index = i % len(colors)
154 | color = colors[color_index]
155 |
156 | # Create marker note
157 | note = f"Marker {i+1}: {interval_seconds*i} seconds from start"
158 |
159 | print(f"Adding {color} marker at {target_tc} (frame {frame}) in clip: {clip_name}")
160 | result = current_timeline.AddMarker(
161 | frame,
162 | color,
163 | note,
164 | note,
165 | 1,
166 | ""
167 | )
168 |
169 | if result:
170 | print(f"✓ Successfully added marker")
171 | markers_added += 1
172 | else:
173 | print(f"✗ Failed to add marker - checking if position already has a marker")
174 |
175 | # Check if a marker already exists
176 | markers = current_timeline.GetMarkers() or {}
177 | if frame in markers:
178 | # Try alternate position
179 | alt_frame = frame + 1
180 | alt_tc = frame_to_tc(alt_frame, fps)
181 | print(f"Trying alternate position: {alt_tc} (frame {alt_frame})")
182 |
183 | alt_result = current_timeline.AddMarker(
184 | alt_frame,
185 | color,
186 | note,
187 | note,
188 | 1,
189 | ""
190 | )
191 |
192 | if alt_result:
193 | print(f"✓ Successfully added marker at alternate position")
194 | markers_added += 1
195 |
196 | # Get final count of markers
197 | final_markers = current_timeline.GetMarkers() or {}
198 |
199 | print(f"\nAdded {markers_added} new markers")
200 | print(f"Timeline now has {len(final_markers)} total markers")
201 | print("\n===== COMPLETED =====")
202 |
203 | def main():
204 | # Parse command line arguments
205 | parser = argparse.ArgumentParser(description="Add markers at regular intervals")
206 | parser.add_argument("--start", "-s", default="01:00:00:00", help="Start timecode (HH:MM:SS:FF)")
207 | parser.add_argument("--interval", "-i", type=int, default=10, help="Interval in seconds between markers")
208 | parser.add_argument("--count", "-c", type=int, default=7, help="Number of markers to add")
209 | parser.add_argument("--keep", "-k", action="store_true", help="Keep existing markers (don't clear)")
210 |
211 | args = parser.parse_args()
212 |
213 | # Validate timecode format
214 | if len(args.start.split(":")) != 4:
215 | print("Error: Invalid start timecode format. Use HH:MM:SS:FF format.")
216 | return
217 |
218 | # Add the markers
219 | add_markers(args.start, args.interval, args.count, not args.keep)
220 |
221 | if __name__ == "__main__":
222 | main()
```
--------------------------------------------------------------------------------
/src/utils/object_inspection.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve MCP Server - Object Inspection Utilities
4 |
5 | This module provides functions for inspecting DaVinci Resolve API objects:
6 | - Exploring available methods and properties
7 | - Generating structured documentation
8 | - Inspecting nested objects
9 | - Converting between Python and Lua objects if needed
10 | """
11 |
12 | import sys
13 | import inspect
14 | from typing import Any, Dict, List, Optional, Union, Callable
15 |
16 |
17 | def get_object_methods(obj: Any) -> Dict[str, Dict[str, Any]]:
18 | """
19 | Get all methods of a DaVinci Resolve object with their documentation.
20 |
21 | Args:
22 | obj: A DaVinci Resolve API object
23 |
24 | Returns:
25 | A dictionary of method names and their details
26 | """
27 | if obj is None:
28 | return {"error": "Cannot inspect None object"}
29 |
30 | methods = {}
31 |
32 | # Get all object attributes
33 | for attr_name in dir(obj):
34 | # Skip private/internal attributes
35 | if attr_name.startswith('_'):
36 | continue
37 |
38 | try:
39 | attr = getattr(obj, attr_name)
40 |
41 | # Check if it's a callable method
42 | if callable(attr):
43 | # Get the method signature if possible
44 | try:
45 | signature = str(inspect.signature(attr))
46 | except (ValueError, TypeError):
47 | signature = "()"
48 |
49 | # Get the docstring if available
50 | doc = inspect.getdoc(attr) or ""
51 |
52 | methods[attr_name] = {
53 | "signature": signature,
54 | "doc": doc,
55 | "type": "method"
56 | }
57 | except Exception as e:
58 | methods[attr_name] = {
59 | "error": str(e),
60 | "type": "error"
61 | }
62 |
63 | return methods
64 |
65 |
66 | def get_object_properties(obj: Any) -> Dict[str, Dict[str, Any]]:
67 | """
68 | Get all properties (non-callable attributes) of a DaVinci Resolve object.
69 |
70 | Args:
71 | obj: A DaVinci Resolve API object
72 |
73 | Returns:
74 | A dictionary of property names and their details
75 | """
76 | if obj is None:
77 | return {"error": "Cannot inspect None object"}
78 |
79 | properties = {}
80 |
81 | # Get all object attributes
82 | for attr_name in dir(obj):
83 | # Skip private/internal attributes
84 | if attr_name.startswith('_'):
85 | continue
86 |
87 | try:
88 | attr = getattr(obj, attr_name)
89 |
90 | # Skip if it's a method
91 | if callable(attr):
92 | continue
93 |
94 | # Get the property value and type
95 | properties[attr_name] = {
96 | "value": str(attr),
97 | "type": type(attr).__name__,
98 | "type_category": "property"
99 | }
100 | except Exception as e:
101 | properties[attr_name] = {
102 | "error": str(e),
103 | "type_category": "error"
104 | }
105 |
106 | return properties
107 |
108 |
109 | def inspect_object(obj: Any, max_depth: int = 1) -> Dict[str, Any]:
110 | """
111 | Inspect a DaVinci Resolve API object and return its methods and properties.
112 |
113 | Args:
114 | obj: A DaVinci Resolve API object
115 | max_depth: Maximum depth for nested object inspection
116 |
117 | Returns:
118 | A dictionary containing the object's methods and properties
119 | """
120 | if obj is None:
121 | return {"error": "Cannot inspect None object"}
122 |
123 | result = {
124 | "type": type(obj).__name__,
125 | "methods": get_object_methods(obj),
126 | "properties": get_object_properties(obj),
127 | }
128 |
129 | # Add string representation
130 | try:
131 | result["str"] = str(obj)
132 | except Exception as e:
133 | result["str_error"] = str(e)
134 |
135 | # Add repr representation
136 | try:
137 | result["repr"] = repr(obj)
138 | except Exception as e:
139 | result["repr_error"] = str(e)
140 |
141 | return result
142 |
143 |
144 | def get_lua_table_keys(lua_table: Any) -> List[str]:
145 | """
146 | Get all keys from a Lua table object (if the object supports Lua table iteration).
147 |
148 | Args:
149 | lua_table: A Lua table object from DaVinci Resolve API
150 |
151 | Returns:
152 | A list of keys from the Lua table
153 | """
154 | if lua_table is None:
155 | return []
156 |
157 | keys = []
158 |
159 | # Check for DaVinci-specific Lua table iteration methods
160 | if hasattr(lua_table, 'GetKeyList'):
161 | try:
162 | # Some DaVinci Resolve objects have a GetKeyList() method
163 | return lua_table.GetKeyList()
164 | except:
165 | pass
166 |
167 | # Try different iteration methods that might work with Lua tables
168 | try:
169 | # Some Lua tables can be iterated directly
170 | for key in lua_table:
171 | keys.append(key)
172 | return keys
173 | except:
174 | pass
175 |
176 | # Try manual iteration with pairs-like behavior (if available)
177 | # This is a fallback for APIs that don't support Python-style iteration
178 | return []
179 |
180 |
181 | def convert_lua_to_python(lua_obj: Any) -> Any:
182 | """
183 | Convert a Lua object from DaVinci Resolve API to a Python object.
184 |
185 | Args:
186 | lua_obj: A Lua object from DaVinci Resolve API
187 |
188 | Returns:
189 | The converted Python object
190 | """
191 | # Handle None
192 | if lua_obj is None:
193 | return None
194 |
195 | # Handle primitive types
196 | if isinstance(lua_obj, (str, int, float, bool)):
197 | return lua_obj
198 |
199 | # Try to convert Lua tables to Python dicts or lists
200 | if hasattr(lua_obj, 'GetKeyList') or hasattr(lua_obj, '__iter__'):
201 | keys = get_lua_table_keys(lua_obj)
202 |
203 | # If we found keys, convert to dict
204 | if keys:
205 | result = {}
206 | for key in keys:
207 | try:
208 | # Get the value for this key
209 | value = lua_obj[key]
210 | # Recursively convert nested Lua objects
211 | result[key] = convert_lua_to_python(value)
212 | except:
213 | result[key] = None
214 | return result
215 |
216 | # Try to convert to list if it appears numeric-indexed
217 | try:
218 | # Common Lua pattern for numeric arrays (1-indexed)
219 | result = []
220 | index = 1 # Lua arrays typically start at 1
221 | while True:
222 | try:
223 | value = lua_obj[index]
224 | result.append(convert_lua_to_python(value))
225 | index += 1
226 | except:
227 | break
228 |
229 | # If we found items, return as list
230 | if result:
231 | return result
232 | except:
233 | pass
234 |
235 | # If conversion failed, return string representation
236 | return str(lua_obj)
237 |
238 |
239 | def print_object_help(obj: Any) -> str:
240 | """
241 | Generate a human-readable help string for a DaVinci Resolve API object.
242 |
243 | Args:
244 | obj: A DaVinci Resolve API object
245 |
246 | Returns:
247 | A formatted help string describing the object's methods and properties
248 | """
249 | if obj is None:
250 | return "Cannot provide help for None object"
251 |
252 | obj_type = type(obj).__name__
253 | methods = get_object_methods(obj)
254 | properties = get_object_properties(obj)
255 |
256 | help_text = [f"Help for {obj_type} object:"]
257 | help_text.append("\n" + "=" * 40 + "\n")
258 |
259 | # Add methods
260 | if methods:
261 | help_text.append("METHODS:")
262 | help_text.append("-" * 40)
263 | for name, info in sorted(methods.items()):
264 | if "error" in info:
265 | continue
266 | signature = info.get("signature", "()")
267 | doc = info.get("doc", "").strip()
268 | help_text.append(f"{name}{signature}")
269 | if doc:
270 | help_text.append(f" {doc}\n")
271 | else:
272 | help_text.append("")
273 |
274 | # Add properties
275 | if properties:
276 | help_text.append("\nPROPERTIES:")
277 | help_text.append("-" * 40)
278 | for name, info in sorted(properties.items()):
279 | if "error" in info:
280 | continue
281 | value = info.get("value", "")
282 | type_name = info.get("type", "")
283 | help_text.append(f"{name}: {type_name} = {value}")
284 |
285 | return "\n".join(help_text)
```
--------------------------------------------------------------------------------
/scripts/mcp_resolve_launcher.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # mcp_resolve_launcher.sh
3 | # Interactive launcher script for DaVinci Resolve MCP servers
4 | # Allows selecting which server(s) to start or stop
5 |
6 | # Colors for terminal output
7 | GREEN='\033[0;32m'
8 | YELLOW='\033[0;33m'
9 | BLUE='\033[0;34m'
10 | RED='\033[0;31m'
11 | NC='\033[0m' # No Color
12 | BOLD='\033[1m'
13 |
14 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
15 | CURSOR_SCRIPT="$SCRIPT_DIR/mcp_resolve-cursor_start"
16 | CLAUDE_SCRIPT="$SCRIPT_DIR/mcp_resolve-claude_start"
17 | # Get repository root directory (parent of scripts directory)
18 | REPO_ROOT="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )"
19 |
20 | # Display banner
21 | echo -e "${BLUE}=============================================${NC}"
22 | echo -e "${BLUE} DaVinci Resolve - MCP Server Launcher ${NC}"
23 | echo -e "${BLUE}=============================================${NC}"
24 |
25 | # Check if DaVinci Resolve is running
26 | check_resolve_running() {
27 | # Try to detect with pgrep
28 | if pgrep -q "Resolve"; then
29 | echo -e "${GREEN}✓ DaVinci Resolve is running${NC}"
30 | return 0
31 | fi
32 |
33 | # Fallback: use ps to check for Resolve process
34 | if ps -ef | grep -q "[R]esolve" || ps -ef | grep -q "[D]aVinci Resolve"; then
35 | echo -e "${GREEN}✓ DaVinci Resolve is running${NC}"
36 | return 0
37 | fi
38 |
39 | echo -e "${RED}✗ DaVinci Resolve is not running${NC}"
40 | echo -e "${YELLOW}Please start DaVinci Resolve before continuing${NC}"
41 | return 1
42 | }
43 |
44 | # Find server PIDs
45 | find_server_pids() {
46 | # Look for cursor server (using mcp dev with resolve_mcp_server.py)
47 | CURSOR_PID=$(pgrep -f "mcp dev.*resolve_mcp_server.py" | head -1)
48 |
49 | # Look for Claude server (using mcp dev with resolve_mcp_server.py)
50 | CLAUDE_PID=$(pgrep -f "mcp dev.*resolve_mcp_server.py" | head -1)
51 |
52 | # If both are found and they're the same, set one to empty
53 | if [ "$CURSOR_PID" = "$CLAUDE_PID" ] && [ -n "$CURSOR_PID" ]; then
54 | # We need to disambiguate - look at log files
55 | if ps -p "$CURSOR_PID" -o command= | grep -q "cursor"; then
56 | CLAUDE_PID=""
57 | else
58 | # If we can't determine, just assume it's the Cursor server
59 | CLAUDE_PID=""
60 | fi
61 | fi
62 | }
63 |
64 | # Display server status
65 | show_status() {
66 | find_server_pids
67 |
68 | echo -e "\n${BOLD}Current Server Status:${NC}"
69 |
70 | if [ -n "$CURSOR_PID" ]; then
71 | echo -e "${GREEN}● Cursor MCP Server: Running (PID: $CURSOR_PID)${NC}"
72 | else
73 | echo -e "${RED}○ Cursor MCP Server: Not running${NC}"
74 | fi
75 |
76 | if [ -n "$CLAUDE_PID" ]; then
77 | echo -e "${GREEN}● Claude Desktop MCP Server: Running (PID: $CLAUDE_PID)${NC}"
78 | else
79 | echo -e "${RED}○ Claude Desktop MCP Server: Not running${NC}"
80 | fi
81 |
82 | echo ""
83 | }
84 |
85 | # Stop server by PID
86 | stop_server() {
87 | local pid=$1
88 | local name=$2
89 |
90 | if [ -n "$pid" ]; then
91 | echo -e "${YELLOW}Stopping $name MCP Server (PID: $pid)...${NC}"
92 | kill "$pid" 2>/dev/null
93 |
94 | # Wait for process to exit
95 | for i in {1..5}; do
96 | if ! ps -p "$pid" > /dev/null 2>&1; then
97 | echo -e "${GREEN}✓ $name MCP Server stopped${NC}"
98 | return 0
99 | fi
100 | sleep 1
101 | done
102 |
103 | # Force kill if still running
104 | echo -e "${YELLOW}Server still running. Force killing...${NC}"
105 | kill -9 "$pid" 2>/dev/null
106 | echo -e "${GREEN}✓ $name MCP Server force stopped${NC}"
107 | else
108 | echo -e "${YELLOW}$name MCP Server is not running${NC}"
109 | fi
110 | }
111 |
112 | # Start Cursor server
113 | start_cursor() {
114 | local force_flag=""
115 | local project_flag=""
116 |
117 | if [ "$1" = "force" ]; then
118 | force_flag="--force"
119 | fi
120 |
121 | if [ -n "$2" ]; then
122 | project_flag="--project \"$2\""
123 | fi
124 |
125 | echo -e "${YELLOW}Starting Cursor MCP Server...${NC}"
126 |
127 | # Check if script exists
128 | if [ ! -f "$CURSOR_SCRIPT" ]; then
129 | echo -e "${RED}✗ Cursor startup script not found: $CURSOR_SCRIPT${NC}"
130 | return 1
131 | fi
132 |
133 | # Execute the script
134 | command="$CURSOR_SCRIPT $force_flag $project_flag"
135 | eval "$command" &
136 |
137 | echo -e "${GREEN}✓ Cursor MCP Server starting in the background${NC}"
138 | }
139 |
140 | # Start Claude server
141 | start_claude() {
142 | local force_flag=""
143 | local project_flag=""
144 |
145 | if [ "$1" = "force" ]; then
146 | force_flag="--force"
147 | fi
148 |
149 | if [ -n "$2" ]; then
150 | project_flag="--project \"$2\""
151 | fi
152 |
153 | echo -e "${YELLOW}Starting Claude Desktop MCP Server...${NC}"
154 |
155 | # Check if script exists
156 | if [ ! -f "$CLAUDE_SCRIPT" ]; then
157 | echo -e "${RED}✗ Claude Desktop startup script not found: $CLAUDE_SCRIPT${NC}"
158 | return 1
159 | fi
160 |
161 | # Execute the script
162 | command="$CLAUDE_SCRIPT $force_flag $project_flag"
163 | eval "$command" &
164 |
165 | echo -e "${GREEN}✓ Claude Desktop MCP Server starting in the background${NC}"
166 | }
167 |
168 | # Interactive menu
169 | show_menu() {
170 | echo -e "${BOLD}DaVinci Resolve MCP Server Launcher${NC}"
171 | echo -e "${YELLOW}Select an option:${NC}"
172 | echo "1) Start Cursor MCP Server"
173 | echo "2) Start Claude Desktop MCP Server"
174 | echo "3) Start both servers"
175 | echo "4) Stop Cursor MCP Server"
176 | echo "5) Stop Claude Desktop MCP Server"
177 | echo "6) Stop both servers"
178 | echo "7) Show server status"
179 | echo "8) Exit"
180 | echo -e "${YELLOW}Enter your choice [1-8]:${NC} "
181 | }
182 |
183 | # Process menu selection
184 | process_selection() {
185 | local choice="$1"
186 | local force_mode="$2"
187 | local project_name="$3"
188 |
189 | case "$choice" in
190 | 1)
191 | find_server_pids
192 | if [ -n "$CURSOR_PID" ]; then
193 | echo -e "${YELLOW}Cursor MCP Server is already running (PID: $CURSOR_PID)${NC}"
194 | echo -e "${YELLOW}Stop it first before starting a new instance.${NC}"
195 | else
196 | start_cursor "$force_mode" "$project_name"
197 | fi
198 | ;;
199 | 2)
200 | find_server_pids
201 | if [ -n "$CLAUDE_PID" ]; then
202 | echo -e "${YELLOW}Claude Desktop MCP Server is already running (PID: $CLAUDE_PID)${NC}"
203 | echo -e "${YELLOW}Stop it first before starting a new instance.${NC}"
204 | else
205 | start_claude "$force_mode" "$project_name"
206 | fi
207 | ;;
208 | 3)
209 | find_server_pids
210 | if [ -n "$CURSOR_PID" ]; then
211 | echo -e "${YELLOW}Cursor MCP Server is already running (PID: $CURSOR_PID)${NC}"
212 | else
213 | start_cursor "$force_mode" "$project_name"
214 | fi
215 |
216 | if [ -n "$CLAUDE_PID" ]; then
217 | echo -e "${YELLOW}Claude Desktop MCP Server is already running (PID: $CLAUDE_PID)${NC}"
218 | else
219 | start_claude "$force_mode" "$project_name"
220 | fi
221 | ;;
222 | 4)
223 | find_server_pids
224 | stop_server "$CURSOR_PID" "Cursor"
225 | ;;
226 | 5)
227 | find_server_pids
228 | stop_server "$CLAUDE_PID" "Claude Desktop"
229 | ;;
230 | 6)
231 | find_server_pids
232 | stop_server "$CURSOR_PID" "Cursor"
233 | stop_server "$CLAUDE_PID" "Claude Desktop"
234 | ;;
235 | 7)
236 | show_status
237 | ;;
238 | 8)
239 | echo -e "${GREEN}Exiting. Goodbye!${NC}"
240 | exit 0
241 | ;;
242 | *)
243 | echo -e "${RED}Invalid option. Please try again.${NC}"
244 | ;;
245 | esac
246 | }
247 |
248 | # Check command line arguments
249 | PROJECT_NAME=""
250 | FORCE_MODE=""
251 | MENU_OPTION=""
252 |
253 | while [[ $# -gt 0 ]]; do
254 | case $1 in
255 | --project|-p)
256 | PROJECT_NAME="$2"
257 | echo -e "${YELLOW}Will use project: $2${NC}"
258 | shift 2
259 | ;;
260 | --force|-f)
261 | FORCE_MODE="force"
262 | echo -e "${YELLOW}Force mode enabled: Will skip Resolve running check${NC}"
263 | shift
264 | ;;
265 | --start-cursor)
266 | MENU_OPTION="1"
267 | shift
268 | ;;
269 | --start-claude)
270 | MENU_OPTION="2"
271 | shift
272 | ;;
273 | --start-both)
274 | MENU_OPTION="3"
275 | shift
276 | ;;
277 | --stop-cursor)
278 | MENU_OPTION="4"
279 | shift
280 | ;;
281 | --stop-claude)
282 | MENU_OPTION="5"
283 | shift
284 | ;;
285 | --stop-all)
286 | MENU_OPTION="6"
287 | shift
288 | ;;
289 | --status)
290 | MENU_OPTION="7"
291 | shift
292 | ;;
293 | *)
294 | echo -e "${YELLOW}Unknown argument: $1${NC}"
295 | shift
296 | ;;
297 | esac
298 | done
299 |
300 | # Check Resolve is running (unless we're stopping servers)
301 | if [[ "$FORCE_MODE" != "force" && "$MENU_OPTION" != "4" && "$MENU_OPTION" != "5" && "$MENU_OPTION" != "6" && "$MENU_OPTION" != "7" ]]; then
302 | check_resolve_running
303 | fi
304 |
305 | # Non-interactive mode if an option was provided via command line
306 | if [ -n "$MENU_OPTION" ]; then
307 | process_selection "$MENU_OPTION" "$FORCE_MODE" "$PROJECT_NAME"
308 | exit 0
309 | fi
310 |
311 | # Interactive mode
312 | while true; do
313 | show_status
314 | show_menu
315 | read -r choice
316 | process_selection "$choice" "$FORCE_MODE" "$PROJECT_NAME"
317 | echo -e "\nPress Enter to continue..."
318 | read -r
319 | clear
320 | done
```
--------------------------------------------------------------------------------
/src/utils/app_control.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve MCP Server - Application Control Utilities
4 |
5 | This module provides functions for controlling DaVinci Resolve application:
6 | - Quitting the application
7 | - Checking application state
8 | - Handling basic application functions
9 | """
10 |
11 | import os
12 | import logging
13 | import time
14 | import sys
15 | import platform
16 | import subprocess
17 | from typing import Dict, Any, Optional, Union, List
18 |
19 | # Configure logging
20 | logger = logging.getLogger("davinci-resolve-mcp.app_control")
21 |
22 | def quit_resolve_app(resolve_obj, force: bool = False, save_project: bool = True) -> bool:
23 | """
24 | Quit DaVinci Resolve application.
25 |
26 | Args:
27 | resolve_obj: DaVinci Resolve API object
28 | force: Whether to force quit even if unsaved changes (potentially dangerous)
29 | save_project: Whether to save the project before quitting
30 |
31 | Returns:
32 | True if the quit command was sent successfully
33 | """
34 | try:
35 | logger.info("Attempting to quit DaVinci Resolve")
36 |
37 | # Check if a project is open
38 | pm = resolve_obj.GetProjectManager()
39 | if pm:
40 | project = pm.GetCurrentProject()
41 | if project and save_project:
42 | logger.info("Saving project before quitting")
43 | # Try to save the project
44 | try:
45 | project.SaveProject()
46 | except Exception as e:
47 | logger.error(f"Failed to save project: {str(e)}")
48 | if not force:
49 | logger.error("Aborting quit due to save failure")
50 | return False
51 |
52 | # Attempt to quit using the API
53 | if hasattr(resolve_obj, 'Quit') and callable(getattr(resolve_obj, 'Quit')):
54 | logger.info("Using Resolve.Quit() API")
55 | resolve_obj.Quit()
56 | return True
57 |
58 | # If Quit method isn't available or fails, use platform-specific methods
59 | sys_platform = platform.system().lower()
60 |
61 | if sys_platform == 'darwin':
62 | # macOS - use AppleScript
63 | logger.info("Using AppleScript to quit Resolve on macOS")
64 | cmd = [
65 | 'osascript',
66 | '-e', 'tell application "DaVinci Resolve" to quit'
67 | ]
68 | if force:
69 | # Add force option if requested
70 | cmd = [
71 | 'osascript',
72 | '-e', 'tell application "DaVinci Resolve" to quit with saving'
73 | ]
74 |
75 | subprocess.run(cmd)
76 | return True
77 |
78 | elif sys_platform == 'windows':
79 | # Windows - use taskkill
80 | logger.info("Using taskkill to quit Resolve on Windows")
81 | if force:
82 | subprocess.run(['taskkill', '/F', '/IM', 'Resolve.exe'])
83 | else:
84 | subprocess.run(['taskkill', '/IM', 'Resolve.exe'])
85 | return True
86 |
87 | elif sys_platform == 'linux':
88 | # Linux - use pkill
89 | logger.info("Using pkill to quit Resolve on Linux")
90 | if force:
91 | subprocess.run(['pkill', '-9', 'resolve'])
92 | else:
93 | subprocess.run(['pkill', 'resolve'])
94 | return True
95 |
96 | # If all methods fail, return False
97 | logger.error("Failed to quit Resolve via any method")
98 | return False
99 |
100 | except Exception as e:
101 | logger.error(f"Error quitting DaVinci Resolve: {str(e)}")
102 | return False
103 |
104 | def get_app_state(resolve_obj) -> Dict[str, Any]:
105 | """
106 | Get DaVinci Resolve application state information.
107 |
108 | Args:
109 | resolve_obj: DaVinci Resolve API object
110 |
111 | Returns:
112 | Dictionary with application state information
113 | """
114 | state = {
115 | "connected": resolve_obj is not None,
116 | "version": "Unknown",
117 | "product_name": "Unknown",
118 | "platform": platform.system(),
119 | "python_version": sys.version,
120 | }
121 |
122 | if resolve_obj:
123 | try:
124 | state["version"] = resolve_obj.GetVersionString()
125 | except:
126 | pass
127 |
128 | try:
129 | state["product_name"] = resolve_obj.GetProductName()
130 | except:
131 | pass
132 |
133 | try:
134 | state["current_page"] = resolve_obj.GetCurrentPage()
135 | except:
136 | state["current_page"] = "Unknown"
137 |
138 | # Get project manager and project information
139 | try:
140 | pm = resolve_obj.GetProjectManager()
141 | if pm:
142 | state["project_manager_available"] = True
143 |
144 | current_project = pm.GetCurrentProject()
145 | if current_project:
146 | state["project_open"] = True
147 | state["project_name"] = current_project.GetName()
148 |
149 | # Check if timeline is open
150 | current_timeline = current_project.GetCurrentTimeline()
151 | if current_timeline:
152 | state["timeline_open"] = True
153 | state["timeline_name"] = current_timeline.GetName()
154 | else:
155 | state["timeline_open"] = False
156 | else:
157 | state["project_open"] = False
158 | else:
159 | state["project_manager_available"] = False
160 | except Exception as e:
161 | state["project_error"] = str(e)
162 |
163 | return state
164 |
165 | def restart_resolve_app(resolve_obj, wait_seconds: int = 5) -> bool:
166 | """
167 | Restart DaVinci Resolve application.
168 |
169 | Args:
170 | resolve_obj: DaVinci Resolve API object
171 | wait_seconds: Seconds to wait between quit and restart
172 |
173 | Returns:
174 | True if restart was initiated successfully
175 | """
176 | try:
177 | # Get Resolve executable path for restart
178 | if platform.system().lower() == 'darwin':
179 | resolve_path = '/Applications/DaVinci Resolve/DaVinci Resolve.app'
180 | elif platform.system().lower() == 'windows':
181 | # Default path, may need to be customized
182 | resolve_path = r'C:\Program Files\Blackmagic Design\DaVinci Resolve\Resolve.exe'
183 | elif platform.system().lower() == 'linux':
184 | # Default path, may need to be customized
185 | resolve_path = '/opt/resolve/bin/resolve'
186 | else:
187 | return False
188 |
189 | # Quit Resolve
190 | if not quit_resolve_app(resolve_obj, force=False, save_project=True):
191 | logger.error("Failed to quit Resolve for restart")
192 | return False
193 |
194 | # Wait for the app to close
195 | logger.info(f"Waiting {wait_seconds} seconds for Resolve to close")
196 | time.sleep(wait_seconds)
197 |
198 | # Start Resolve again
199 | logger.info("Attempting to start Resolve")
200 |
201 | if platform.system().lower() == 'darwin':
202 | subprocess.Popen(['open', resolve_path])
203 | elif platform.system().lower() == 'windows':
204 | subprocess.Popen([resolve_path])
205 | elif platform.system().lower() == 'linux':
206 | subprocess.Popen([resolve_path])
207 |
208 | return True
209 | except Exception as e:
210 | logger.error(f"Error restarting DaVinci Resolve: {str(e)}")
211 | return False
212 |
213 | def open_project_settings(resolve_obj) -> bool:
214 | """
215 | Open the Project Settings dialog in DaVinci Resolve.
216 |
217 | Args:
218 | resolve_obj: DaVinci Resolve API object
219 |
220 | Returns:
221 | True if successful, False otherwise
222 | """
223 | try:
224 | # Check if UI Manager is available
225 | ui_manager = resolve_obj.GetUIManager()
226 | if not ui_manager:
227 | logger.error("Failed to get UI Manager")
228 | return False
229 |
230 | # Open Project Settings dialog
231 | if hasattr(ui_manager, 'OpenProjectSettings') and callable(getattr(ui_manager, 'OpenProjectSettings')):
232 | ui_manager.OpenProjectSettings()
233 | return True
234 |
235 | # Alternative method - send keyboard shortcut based on platform
236 | current_page = resolve_obj.GetCurrentPage()
237 |
238 | # Ensure we're on a page that supports project settings
239 | if current_page not in ['media', 'cut', 'edit', 'fusion', 'color', 'fairlight', 'deliver']:
240 | logger.error(f"Can't open settings from page: {current_page}")
241 | return False
242 |
243 | return False # Keyboard shortcuts not implemented yet
244 | except Exception as e:
245 | logger.error(f"Error opening project settings: {str(e)}")
246 | return False
247 |
248 | def open_preferences(resolve_obj) -> bool:
249 | """
250 | Open the Preferences dialog in DaVinci Resolve.
251 |
252 | Args:
253 | resolve_obj: DaVinci Resolve API object
254 |
255 | Returns:
256 | True if successful, False otherwise
257 | """
258 | try:
259 | # Check if UI Manager is available
260 | ui_manager = resolve_obj.GetUIManager()
261 | if not ui_manager:
262 | logger.error("Failed to get UI Manager")
263 | return False
264 |
265 | # Open Preferences dialog
266 | if hasattr(ui_manager, 'OpenPreferences') and callable(getattr(ui_manager, 'OpenPreferences')):
267 | ui_manager.OpenPreferences()
268 | return True
269 |
270 | # Alternative method - send keyboard shortcut based on platform
271 | return False # Keyboard shortcuts not implemented yet
272 | except Exception as e:
273 | logger.error(f"Error opening preferences: {str(e)}")
274 | return False
```
--------------------------------------------------------------------------------
/scripts/setup/install.bat:
--------------------------------------------------------------------------------
```
1 | @echo off
2 | REM install.bat - One-step installation for DaVinci Resolve MCP Integration
3 | REM This script handles the entire installation process with improved error detection
4 |
5 | setlocal EnableDelayedExpansion
6 |
7 | REM Colors for terminal output
8 | set GREEN=[92m
9 | set YELLOW=[93m
10 | set BLUE=[94m
11 | set RED=[91m
12 | set BOLD=[1m
13 | set NC=[0m
14 |
15 | REM Get the absolute path of this script's location
16 | set "INSTALL_DIR=%~dp0"
17 | set "INSTALL_DIR=%INSTALL_DIR:~0,-1%"
18 | set "VENV_DIR=%INSTALL_DIR%\venv"
19 | set "CURSOR_CONFIG_DIR=%APPDATA%\Cursor\mcp"
20 | set "CURSOR_CONFIG_FILE=%CURSOR_CONFIG_DIR%\config.json"
21 | set "PROJECT_CURSOR_DIR=%INSTALL_DIR%\.cursor"
22 | set "PROJECT_CONFIG_FILE=%PROJECT_CURSOR_DIR%\mcp.json"
23 | set "LOG_FILE=%INSTALL_DIR%\install.log"
24 |
25 | REM Banner
26 | echo %BLUE%%BOLD%=================================================%NC%
27 | echo %BLUE%%BOLD% DaVinci Resolve MCP Integration Installer %NC%
28 | echo %BLUE%%BOLD%=================================================%NC%
29 | echo %YELLOW%Installation directory: %INSTALL_DIR%%NC%
30 | echo Installation log: %LOG_FILE%
31 | echo.
32 |
33 | REM Initialize log
34 | echo === DaVinci Resolve MCP Installation Log === > "%LOG_FILE%"
35 | echo Date: %date% %time% >> "%LOG_FILE%"
36 | echo Install directory: %INSTALL_DIR% >> "%LOG_FILE%"
37 | echo User: %USERNAME% >> "%LOG_FILE%"
38 | echo System: %OS% Windows %PROCESSOR_ARCHITECTURE% >> "%LOG_FILE%"
39 | echo. >> "%LOG_FILE%"
40 |
41 | REM Function to log messages (call :log "Message")
42 | :log
43 | echo [%time%] %~1 >> "%LOG_FILE%"
44 | exit /b 0
45 |
46 | REM Check if DaVinci Resolve is running
47 | :check_resolve_running
48 | call :log "Checking if DaVinci Resolve is running"
49 | echo %YELLOW%Checking if DaVinci Resolve is running... %NC%
50 |
51 | tasklist /FI "IMAGENAME eq Resolve.exe" 2>NUL | find /I /N "Resolve.exe">NUL
52 | if %ERRORLEVEL% == 0 (
53 | echo %GREEN%OK%NC%
54 | call :log "DaVinci Resolve is running"
55 | set RESOLVE_RUNNING=1
56 | ) else (
57 | echo %RED%NOT RUNNING%NC%
58 | echo %YELLOW%DaVinci Resolve must be running to complete the installation.%NC%
59 | echo %YELLOW%Please start DaVinci Resolve and try again.%NC%
60 | call :log "DaVinci Resolve is not running - installation cannot proceed"
61 | set RESOLVE_RUNNING=0
62 | )
63 | exit /b %RESOLVE_RUNNING%
64 |
65 | REM Create Python virtual environment
66 | :create_venv
67 | call :log "Creating/checking Python virtual environment"
68 | echo %YELLOW%Setting up Python virtual environment... %NC%
69 |
70 | if exist "%VENV_DIR%\Scripts\python.exe" (
71 | echo %GREEN%ALREADY EXISTS%NC%
72 | call :log "Virtual environment already exists"
73 | set VENV_STATUS=1
74 | ) else (
75 | echo %YELLOW%CREATING%NC%
76 | python -m venv "%VENV_DIR%" >> "%LOG_FILE%" 2>&1
77 |
78 | if %ERRORLEVEL% == 0 (
79 | echo %GREEN%OK%NC%
80 | call :log "Virtual environment created successfully"
81 | set VENV_STATUS=1
82 | ) else (
83 | echo %RED%FAILED%NC%
84 | echo %RED%Failed to create Python virtual environment.%NC%
85 | echo %YELLOW%Check that Python 3.9+ is installed.%NC%
86 | call :log "Failed to create virtual environment"
87 | set VENV_STATUS=0
88 | )
89 | )
90 | exit /b %VENV_STATUS%
91 |
92 | REM Install MCP SDK
93 | :install_mcp
94 | call :log "Installing MCP SDK"
95 | echo %YELLOW%Installing MCP SDK... %NC%
96 |
97 | "%VENV_DIR%\Scripts\pip" install "mcp[cli]" >> "%LOG_FILE%" 2>&1
98 |
99 | if %ERRORLEVEL% == 0 (
100 | echo %GREEN%OK%NC%
101 | call :log "MCP SDK installed successfully"
102 | set MCP_STATUS=1
103 | ) else (
104 | echo %RED%FAILED%NC%
105 | echo %RED%Failed to install MCP SDK.%NC%
106 | echo %YELLOW%Check the log file for details: %LOG_FILE%%NC%
107 | call :log "Failed to install MCP SDK"
108 | set MCP_STATUS=0
109 | )
110 | exit /b %MCP_STATUS%
111 |
112 | REM Set environment variables
113 | :setup_env_vars
114 | call :log "Setting up environment variables"
115 | echo %YELLOW%Setting up environment variables... %NC%
116 |
117 | REM Generate environment variables file
118 | set "ENV_FILE=%INSTALL_DIR%\.env.bat"
119 | (
120 | echo @echo off
121 | echo set "RESOLVE_SCRIPT_API=C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting"
122 | echo set "RESOLVE_SCRIPT_LIB=C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll"
123 | echo set "PYTHONPATH=%%PYTHONPATH%%;%%RESOLVE_SCRIPT_API%%\Modules"
124 | ) > "%ENV_FILE%"
125 |
126 | REM Source the environment variables
127 | call "%ENV_FILE%"
128 |
129 | echo %GREEN%OK%NC%
130 | call :log "Environment variables set:"
131 | call :log "RESOLVE_SCRIPT_API=%RESOLVE_SCRIPT_API%"
132 | call :log "RESOLVE_SCRIPT_LIB=%RESOLVE_SCRIPT_LIB%"
133 |
134 | REM Suggest adding to system variables
135 | echo %YELLOW%Consider adding these environment variables to your system:%NC%
136 | echo %BLUE% RESOLVE_SCRIPT_API = C:\ProgramData\Blackmagic Design\DaVinci Resolve\Support\Developer\Scripting%NC%
137 | echo %BLUE% RESOLVE_SCRIPT_LIB = C:\Program Files\Blackmagic Design\DaVinci Resolve\fusionscript.dll%NC%
138 | echo %BLUE% Add to PYTHONPATH: %%RESOLVE_SCRIPT_API%%\Modules%NC%
139 |
140 | exit /b 1
141 |
142 | REM Setup Cursor MCP configuration
143 | :setup_cursor_config
144 | call :log "Setting up Cursor MCP configuration"
145 | echo %YELLOW%Setting up Cursor MCP configuration... %NC%
146 |
147 | REM Create system-level directory if it doesn't exist
148 | if not exist "%CURSOR_CONFIG_DIR%" mkdir "%CURSOR_CONFIG_DIR%"
149 |
150 | REM Create system-level config file with the absolute paths
151 | (
152 | echo {
153 | echo "mcpServers": {
154 | echo "davinci-resolve": {
155 | echo "name": "DaVinci Resolve MCP",
156 | echo "command": "%INSTALL_DIR:\=\\%\\venv\\Scripts\\python.exe",
157 | echo "args": ["%INSTALL_DIR:\=\\%\\resolve_mcp_server.py"]
158 | echo }
159 | echo }
160 | echo }
161 | ) > "%CURSOR_CONFIG_FILE%"
162 |
163 | REM Create project-level directory if it doesn't exist
164 | if not exist "%PROJECT_CURSOR_DIR%" mkdir "%PROJECT_CURSOR_DIR%"
165 |
166 | REM Create project-level config with absolute paths (same as system-level config)
167 | (
168 | echo {
169 | echo "mcpServers": {
170 | echo "davinci-resolve": {
171 | echo "name": "DaVinci Resolve MCP",
172 | echo "command": "%INSTALL_DIR:\=\\%\\venv\\Scripts\\python.exe",
173 | echo "args": ["%INSTALL_DIR:\=\\%\\resolve_mcp_server.py"]
174 | echo }
175 | echo }
176 | echo }
177 | ) > "%PROJECT_CONFIG_FILE%"
178 |
179 | if exist "%CURSOR_CONFIG_FILE%" if exist "%PROJECT_CONFIG_FILE%" (
180 | echo %GREEN%OK%NC%
181 | echo %GREEN%Cursor MCP config created at: %CURSOR_CONFIG_FILE%%NC%
182 | echo %GREEN%Project MCP config created at: %PROJECT_CONFIG_FILE%%NC%
183 | call :log "Cursor MCP configuration created successfully"
184 | call :log "System config file: %CURSOR_CONFIG_FILE%"
185 | call :log "Project config file: %PROJECT_CONFIG_FILE%"
186 |
187 | REM Show the paths that were set
188 | echo %YELLOW%Paths configured:%NC%
189 | echo %BLUE% Python: %INSTALL_DIR%\venv\Scripts\python.exe%NC%
190 | echo %BLUE% Script: %INSTALL_DIR%\resolve_mcp_server.py%NC%
191 |
192 | set CONFIG_STATUS=1
193 | ) else (
194 | echo %RED%FAILED%NC%
195 | echo %RED%Failed to create Cursor MCP configuration.%NC%
196 | call :log "Failed to create Cursor MCP configuration"
197 | set CONFIG_STATUS=0
198 | )
199 | exit /b %CONFIG_STATUS%
200 |
201 | REM Verify installation
202 | :verify_installation
203 | call :log "Verifying installation"
204 | echo %BLUE%%BOLD%=================================================%NC%
205 | echo %YELLOW%%BOLD%Verifying installation...%NC%
206 |
207 | REM Run the verification script
208 | call "%INSTALL_DIR%\scripts\verify-installation.bat"
209 | set VERIFY_RESULT=%ERRORLEVEL%
210 |
211 | call :log "Verification completed with result: %VERIFY_RESULT%"
212 |
213 | exit /b %VERIFY_RESULT%
214 |
215 | REM Run server if verification succeeds
216 | :run_server
217 | call :log "Starting server"
218 | echo %BLUE%%BOLD%=================================================%NC%
219 | echo %GREEN%%BOLD%Starting DaVinci Resolve MCP Server...%NC%
220 | echo.
221 |
222 | REM Run the server using the virtual environment
223 | "%VENV_DIR%\Scripts\python.exe" "%INSTALL_DIR%\resolve_mcp_server.py"
224 |
225 | set SERVER_EXIT=%ERRORLEVEL%
226 | call :log "Server exited with code: %SERVER_EXIT%"
227 |
228 | exit /b %SERVER_EXIT%
229 |
230 | REM Main installation process
231 | :main
232 | call :log "Starting installation process"
233 |
234 | REM Check if DaVinci Resolve is running
235 | call :check_resolve_running
236 | if %RESOLVE_RUNNING% == 0 (
237 | echo %YELLOW%Waiting 10 seconds for DaVinci Resolve to start...%NC%
238 | timeout /t 10 /nobreak > nul
239 | call :check_resolve_running
240 | if %RESOLVE_RUNNING% == 0 (
241 | call :log "Installation aborted - DaVinci Resolve not running"
242 | echo %RED%Installation aborted.%NC%
243 | exit /b 1
244 | )
245 | )
246 |
247 | REM Create virtual environment
248 | call :create_venv
249 | if %VENV_STATUS% == 0 (
250 | call :log "Installation aborted - virtual environment setup failed"
251 | echo %RED%Installation aborted.%NC%
252 | exit /b 1
253 | )
254 |
255 | REM Install MCP SDK
256 | call :install_mcp
257 | if %MCP_STATUS% == 0 (
258 | call :log "Installation aborted - MCP SDK installation failed"
259 | echo %RED%Installation aborted.%NC%
260 | exit /b 1
261 | )
262 |
263 | REM Set up environment variables
264 | call :setup_env_vars
265 |
266 | REM Set up Cursor configuration
267 | call :setup_cursor_config
268 | if %CONFIG_STATUS% == 0 (
269 | call :log "Installation aborted - Cursor configuration failed"
270 | echo %RED%Installation aborted.%NC%
271 | exit /b 1
272 | )
273 |
274 | REM Verify installation
275 | call :verify_installation
276 | set VERIFY_RESULT=%ERRORLEVEL%
277 | if %VERIFY_RESULT% NEQ 0 (
278 | call :log "Installation completed with verification warnings"
279 | echo %YELLOW%Installation completed with warnings.%NC%
280 | echo %YELLOW%Please fix any issues before starting the server.%NC%
281 | echo %YELLOW%You can run the verification script again:%NC%
282 | echo %BLUE% scripts\verify-installation.bat%NC%
283 | exit /b 1
284 | )
285 |
286 | REM Installation successful
287 | call :log "Installation completed successfully"
288 | echo %GREEN%%BOLD%Installation completed successfully!%NC%
289 | echo %YELLOW%You can now start the server with:%NC%
290 | echo %BLUE% run-now.bat%NC%
291 |
292 | REM Ask if the user wants to start the server now
293 | echo.
294 | set /p START_SERVER="Do you want to start the server now? (y/n) "
295 | if /i "%START_SERVER%" == "y" (
296 | call :run_server
297 | ) else (
298 | call :log "User chose not to start the server"
299 | echo %YELLOW%You can start the server later with:%NC%
300 | echo %BLUE% run-now.bat%NC%
301 | )
302 |
303 | exit /b 0
304 |
305 | REM Call the main process
306 | call :main
```
--------------------------------------------------------------------------------
/tests/benchmark_server.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve MCP Server Benchmark
4 | -----------------------------------
5 | This script benchmarks the MCP server's performance and responsiveness.
6 | It measures:
7 | - Connection time
8 | - Response time for various operations
9 | - Stability under load
10 | - Memory usage
11 |
12 | Usage:
13 | python benchmark_server.py [--iterations=N] [--delay=SECONDS]
14 |
15 | Requirements:
16 | - DaVinci Resolve must be running with a project open
17 | - DaVinci Resolve MCP Server must be running
18 | - requests, psutil modules (pip install requests psutil)
19 | """
20 |
21 | import os
22 | import sys
23 | import time
24 | import json
25 | import argparse
26 | import statistics
27 | import requests
28 | import logging
29 | import psutil
30 | from typing import Dict, Any, List, Tuple, Optional
31 | from datetime import datetime
32 |
33 | # Configure logging
34 | logging.basicConfig(
35 | level=logging.INFO,
36 | format='%(asctime)s - %(levelname)s - %(message)s',
37 | handlers=[
38 | logging.FileHandler(f"mcp_benchmark_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"),
39 | logging.StreamHandler()
40 | ]
41 | )
42 | logger = logging.getLogger(__name__)
43 |
44 | # Server configuration
45 | SERVER_URL = "http://localhost:8000/api"
46 |
47 | def send_request(tool_name: str, params: Dict[str, Any]) -> Tuple[Dict[str, Any], float]:
48 | """Send a request to the MCP server and measure response time."""
49 | try:
50 | payload = {
51 | "tool": tool_name,
52 | "params": params
53 | }
54 | start_time = time.time()
55 | response = requests.post(SERVER_URL, json=payload)
56 | end_time = time.time()
57 | response_time = end_time - start_time
58 |
59 | response.raise_for_status()
60 | return response.json(), response_time
61 | except requests.exceptions.RequestException as e:
62 | logger.error(f"Request error: {e}")
63 | return {"success": False, "error": str(e)}, time.time() - start_time
64 |
65 | def measure_system_resources() -> Dict[str, float]:
66 | """Measure system resources used by the server process."""
67 | resources = {}
68 |
69 | try:
70 | # Find the server process
71 | for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
72 | if proc.info['cmdline'] and any('resolve_mcp_server.py' in cmd for cmd in proc.info['cmdline'] if cmd):
73 | # Get memory usage
74 | process = psutil.Process(proc.info['pid'])
75 | mem_info = process.memory_info()
76 | resources['memory_mb'] = mem_info.rss / (1024 * 1024) # Convert to MB
77 | resources['cpu_percent'] = process.cpu_percent(interval=0.5)
78 | resources['threads'] = process.num_threads()
79 | resources['connections'] = len(process.connections())
80 | break
81 | except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess) as e:
82 | logger.error(f"Error measuring system resources: {e}")
83 |
84 | return resources
85 |
86 | def benchmark_operation(operation_name: str, tool_name: str, params: Dict[str, Any],
87 | iterations: int = 10, delay: float = 0.5) -> Dict[str, Any]:
88 | """Benchmark a specific operation."""
89 | logger.info(f"Benchmarking {operation_name}...")
90 |
91 | response_times = []
92 | success_count = 0
93 | error_count = 0
94 | error_messages = []
95 |
96 | for i in range(iterations):
97 | logger.info(f" Iteration {i+1}/{iterations}")
98 | result, response_time = send_request(tool_name, params)
99 |
100 | response_times.append(response_time)
101 |
102 | if "error" not in result or not result["error"]:
103 | success_count += 1
104 | else:
105 | error_count += 1
106 | error_message = str(result.get("error", "Unknown error"))
107 | error_messages.append(error_message)
108 | logger.warning(f" Error: {error_message}")
109 |
110 | if i < iterations - 1: # Don't delay after the last iteration
111 | time.sleep(delay)
112 |
113 | # Calculate statistics
114 | stats = {
115 | "min_time": min(response_times),
116 | "max_time": max(response_times),
117 | "avg_time": statistics.mean(response_times),
118 | "median_time": statistics.median(response_times),
119 | "std_dev": statistics.stdev(response_times) if len(response_times) > 1 else 0,
120 | "success_rate": success_count / iterations,
121 | "error_rate": error_count / iterations,
122 | "error_messages": error_messages[:5] # Limit to first 5 errors
123 | }
124 |
125 | # Log results
126 | logger.info(f"Results for {operation_name}:")
127 | logger.info(f" Success rate: {stats['success_rate'] * 100:.1f}%")
128 | logger.info(f" Avg response time: {stats['avg_time'] * 1000:.2f}ms")
129 | logger.info(f" Min/Max: {stats['min_time'] * 1000:.2f}ms / {stats['max_time'] * 1000:.2f}ms")
130 |
131 | return stats
132 |
133 | def run_benchmarks(iterations: int = 10, delay: float = 0.5) -> Dict[str, Any]:
134 | """Run benchmarks for various operations."""
135 | benchmark_results = {}
136 |
137 | # Measure system resources at start
138 | initial_resources = measure_system_resources()
139 | benchmark_results["initial_resources"] = initial_resources
140 |
141 | # Define operations to benchmark
142 | operations = [
143 | {
144 | "name": "Get Current Page",
145 | "tool": "mcp_davinci_resolve_switch_page",
146 | "params": {"page": "media"}
147 | },
148 | {
149 | "name": "Switch to Edit Page",
150 | "tool": "mcp_davinci_resolve_switch_page",
151 | "params": {"page": "edit"}
152 | },
153 | {
154 | "name": "List Timelines",
155 | "tool": "mcp_davinci_resolve_list_timelines_tool",
156 | "params": {"random_string": "benchmark"}
157 | },
158 | {
159 | "name": "Project Settings - Integer",
160 | "tool": "mcp_davinci_resolve_set_project_setting",
161 | "params": {"setting_name": "timelineFrameRate", "setting_value": 24}
162 | },
163 | {
164 | "name": "Project Settings - String",
165 | "tool": "mcp_davinci_resolve_set_project_setting",
166 | "params": {"setting_name": "timelineFrameRate", "setting_value": "24"}
167 | },
168 | {
169 | "name": "Clear Render Queue",
170 | "tool": "mcp_davinci_resolve_clear_render_queue",
171 | "params": {"random_string": "benchmark"}
172 | }
173 | ]
174 |
175 | # Run benchmarks for each operation
176 | for op in operations:
177 | benchmark_results[op["name"]] = benchmark_operation(
178 | op["name"], op["tool"], op["params"], iterations, delay
179 | )
180 |
181 | # Measure system resources at end
182 | final_resources = measure_system_resources()
183 | benchmark_results["final_resources"] = final_resources
184 |
185 | # Calculate resource usage difference
186 | resource_diff = {}
187 | for key in initial_resources:
188 | if key in final_resources:
189 | resource_diff[key] = final_resources[key] - initial_resources[key]
190 |
191 | benchmark_results["resource_change"] = resource_diff
192 |
193 | return benchmark_results
194 |
195 | def print_summary(results: Dict[str, Any]) -> None:
196 | """Print a summary of benchmark results."""
197 | logger.info("\n" + "=" * 50)
198 | logger.info("BENCHMARK SUMMARY")
199 | logger.info("=" * 50)
200 |
201 | # Calculate overall statistics
202 | response_times = []
203 | success_rates = []
204 |
205 | for key, value in results.items():
206 | if key not in ["initial_resources", "final_resources", "resource_change"] and isinstance(value, dict):
207 | if "avg_time" in value:
208 | response_times.append(value["avg_time"])
209 | if "success_rate" in value:
210 | success_rates.append(value["success_rate"])
211 |
212 | # Overall stats
213 | if response_times:
214 | logger.info(f"Overall average response time: {statistics.mean(response_times) * 1000:.2f}ms")
215 | if success_rates:
216 | logger.info(f"Overall success rate: {statistics.mean(success_rates) * 100:.1f}%")
217 |
218 | # Operation ranking by speed
219 | operation_times = []
220 | for key, value in results.items():
221 | if key not in ["initial_resources", "final_resources", "resource_change"] and isinstance(value, dict):
222 | if "avg_time" in value:
223 | operation_times.append((key, value["avg_time"]))
224 |
225 | if operation_times:
226 | logger.info("\nOperations ranked by speed (fastest first):")
227 | for op, time in sorted(operation_times, key=lambda x: x[1]):
228 | logger.info(f" {op}: {time * 1000:.2f}ms")
229 |
230 | # Resource usage
231 | if "resource_change" in results and results["resource_change"]:
232 | logger.info("\nResource usage change during benchmark:")
233 | for key, value in results["resource_change"].items():
234 | if key == "memory_mb":
235 | logger.info(f" Memory: {value:.2f}MB")
236 | elif key == "cpu_percent":
237 | logger.info(f" CPU: {value:.1f}%")
238 | else:
239 | logger.info(f" {key}: {value}")
240 |
241 | logger.info("=" * 50)
242 |
243 | def main() -> None:
244 | """Run the benchmark suite."""
245 | parser = argparse.ArgumentParser(description="Benchmark the DaVinci Resolve MCP Server")
246 | parser.add_argument("--iterations", type=int, default=10,
247 | help="Number of iterations for each benchmark (default: 10)")
248 | parser.add_argument("--delay", type=float, default=0.5,
249 | help="Delay between benchmark iterations in seconds (default: 0.5)")
250 | args = parser.parse_args()
251 |
252 | logger.info("Starting DaVinci Resolve MCP Server Benchmark")
253 | logger.info("=" * 50)
254 | logger.info(f"Iterations: {args.iterations}, Delay: {args.delay}s")
255 |
256 | # Run benchmarks
257 | results = run_benchmarks(args.iterations, args.delay)
258 |
259 | # Print summary
260 | print_summary(results)
261 |
262 | # Save results to file
263 | result_file = f"benchmark_results_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
264 | with open(result_file, 'w') as f:
265 | json.dump(results, f, indent=2)
266 |
267 | logger.info(f"Benchmark results saved to {result_file}")
268 |
269 | if __name__ == "__main__":
270 | main()
```
--------------------------------------------------------------------------------
/src/utils/layout_presets.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve MCP Server - Layout Presets Utilities
4 |
5 | This module provides functions for working with DaVinci Resolve UI layout presets:
6 | - Saving layout presets
7 | - Loading layout presets
8 | - Exporting/importing preset files
9 | - Managing layout configurations
10 | """
11 |
12 | import os
13 | import json
14 | import logging
15 | from typing import Dict, List, Any, Optional, Union
16 |
17 | # Configure logging
18 | logger = logging.getLogger("davinci-resolve-mcp.layout_presets")
19 |
20 | # Default preset locations by platform
21 | DEFAULT_PRESET_PATHS = {
22 | "darwin": "~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Presets/",
23 | "win32": "C:\\ProgramData\\Blackmagic Design\\DaVinci Resolve\\Presets\\",
24 | "linux": "~/.local/share/DaVinciResolve/Presets/"
25 | }
26 |
27 | def get_layout_preset_path(platform: str = None) -> str:
28 | """
29 | Get the path to layout presets for the current platform.
30 |
31 | Args:
32 | platform: Override the detected platform (darwin, win32, linux)
33 |
34 | Returns:
35 | Path to the layout presets directory
36 | """
37 | import platform as platform_module
38 | import os
39 |
40 | # Determine platform if not specified
41 | if platform is None:
42 | platform = platform_module.system().lower()
43 | if platform == "darwin":
44 | platform = "darwin"
45 | elif platform == "windows":
46 | platform = "win32"
47 | elif platform == "linux":
48 | platform = "linux"
49 | else:
50 | platform = "darwin" # Default to macOS if unknown
51 |
52 | # Get default path for platform
53 | preset_path = DEFAULT_PRESET_PATHS.get(platform, DEFAULT_PRESET_PATHS["darwin"])
54 |
55 | # Expand user directory if needed
56 | preset_path = os.path.expanduser(preset_path)
57 |
58 | # Ensure directory exists
59 | if not os.path.exists(preset_path):
60 | os.makedirs(preset_path, exist_ok=True)
61 |
62 | return preset_path
63 |
64 | def get_ui_layout_path(preset_path: str = None) -> str:
65 | """
66 | Get the path to UI layout presets.
67 |
68 | Args:
69 | preset_path: Base preset directory path (determined automatically if None)
70 |
71 | Returns:
72 | Path to the UI layout presets directory
73 | """
74 | if preset_path is None:
75 | preset_path = get_layout_preset_path()
76 |
77 | # UI layouts are in a specific subdirectory
78 | ui_layout_path = os.path.join(preset_path, "UILayouts")
79 |
80 | # Ensure directory exists
81 | if not os.path.exists(ui_layout_path):
82 | os.makedirs(ui_layout_path, exist_ok=True)
83 |
84 | return ui_layout_path
85 |
86 | def list_layout_presets(layout_type: str = "ui") -> List[Dict[str, Any]]:
87 | """
88 | List available layout presets.
89 |
90 | Args:
91 | layout_type: Type of layout presets to list ('ui', 'window', 'workspace')
92 |
93 | Returns:
94 | List of preset information dictionaries
95 | """
96 | # Get appropriate preset directory
97 | if layout_type.lower() == "ui":
98 | preset_dir = get_ui_layout_path()
99 | else:
100 | # Other layout types would be handled here
101 | preset_dir = get_ui_layout_path()
102 |
103 | # List files in the directory
104 | if not os.path.exists(preset_dir):
105 | return []
106 |
107 | presets = []
108 | for filename in os.listdir(preset_dir):
109 | # Only include layout preset files
110 | if filename.endswith(".layout"):
111 | preset_path = os.path.join(preset_dir, filename)
112 | presets.append({
113 | "name": os.path.splitext(filename)[0],
114 | "path": preset_path,
115 | "type": layout_type,
116 | "size": os.path.getsize(preset_path)
117 | })
118 |
119 | return presets
120 |
121 | def save_layout_preset(resolve_obj, preset_name: str, layout_type: str = "ui") -> bool:
122 | """
123 | Save the current layout as a preset.
124 |
125 | Args:
126 | resolve_obj: DaVinci Resolve API object
127 | preset_name: Name for the saved preset
128 | layout_type: Type of layout to save ('ui', 'window', 'workspace')
129 |
130 | Returns:
131 | True if successful, False otherwise
132 | """
133 | try:
134 | # Ensure preset name has no spaces or special characters
135 | safe_name = preset_name.replace(" ", "_").replace("/", "_").replace("\\", "_")
136 |
137 | # Different layout types have different save methods
138 | if layout_type.lower() == "ui":
139 | # For UI layouts, use the UI Manager
140 | ui_manager = resolve_obj.GetUIManager()
141 | if not ui_manager:
142 | logger.error("Failed to get UI Manager")
143 | return False
144 |
145 | # Save the current UI layout
146 | return ui_manager.SaveUILayout(safe_name)
147 | else:
148 | # Other layout types would be handled here
149 | logger.error(f"Unsupported layout type: {layout_type}")
150 | return False
151 | except Exception as e:
152 | logger.error(f"Error saving layout preset: {str(e)}")
153 | return False
154 |
155 | def load_layout_preset(resolve_obj, preset_name: str, layout_type: str = "ui") -> bool:
156 | """
157 | Load a layout preset.
158 |
159 | Args:
160 | resolve_obj: DaVinci Resolve API object
161 | preset_name: Name of the preset to load
162 | layout_type: Type of layout to load ('ui', 'window', 'workspace')
163 |
164 | Returns:
165 | True if successful, False otherwise
166 | """
167 | try:
168 | # Different layout types have different load methods
169 | if layout_type.lower() == "ui":
170 | # For UI layouts, use the UI Manager
171 | ui_manager = resolve_obj.GetUIManager()
172 | if not ui_manager:
173 | logger.error("Failed to get UI Manager")
174 | return False
175 |
176 | # Load the specified UI layout
177 | return ui_manager.LoadUILayout(preset_name)
178 | else:
179 | # Other layout types would be handled here
180 | logger.error(f"Unsupported layout type: {layout_type}")
181 | return False
182 | except Exception as e:
183 | logger.error(f"Error loading layout preset: {str(e)}")
184 | return False
185 |
186 | def export_layout_preset(preset_name: str, export_path: str, layout_type: str = "ui") -> bool:
187 | """
188 | Export a layout preset to a file.
189 |
190 | Args:
191 | preset_name: Name of the preset to export
192 | export_path: Path to export the preset file to
193 | layout_type: Type of layout to export ('ui', 'window', 'workspace')
194 |
195 | Returns:
196 | True if successful, False otherwise
197 | """
198 | try:
199 | # Get the source preset path
200 | if layout_type.lower() == "ui":
201 | preset_dir = get_ui_layout_path()
202 | else:
203 | # Other layout types would be handled here
204 | preset_dir = get_ui_layout_path()
205 |
206 | # Construct source and destination paths
207 | source_path = os.path.join(preset_dir, f"{preset_name}.layout")
208 |
209 | # Ensure source file exists
210 | if not os.path.exists(source_path):
211 | logger.error(f"Preset file not found: {source_path}")
212 | return False
213 |
214 | # Ensure destination directory exists
215 | export_dir = os.path.dirname(export_path)
216 | if export_dir and not os.path.exists(export_dir):
217 | os.makedirs(export_dir, exist_ok=True)
218 |
219 | # Copy the preset file
220 | import shutil
221 | shutil.copy2(source_path, export_path)
222 |
223 | return True
224 | except Exception as e:
225 | logger.error(f"Error exporting layout preset: {str(e)}")
226 | return False
227 |
228 | def import_layout_preset(import_path: str, preset_name: str = None, layout_type: str = "ui") -> bool:
229 | """
230 | Import a layout preset from a file.
231 |
232 | Args:
233 | import_path: Path to the preset file to import
234 | preset_name: Name to save the imported preset as (uses filename if None)
235 | layout_type: Type of layout to import ('ui', 'window', 'workspace')
236 |
237 | Returns:
238 | True if successful, False otherwise
239 | """
240 | try:
241 | # Ensure source file exists
242 | if not os.path.exists(import_path):
243 | logger.error(f"Import file not found: {import_path}")
244 | return False
245 |
246 | # Get the destination preset path
247 | if layout_type.lower() == "ui":
248 | preset_dir = get_ui_layout_path()
249 | else:
250 | # Other layout types would be handled here
251 | preset_dir = get_ui_layout_path()
252 |
253 | # Use filename as preset name if not specified
254 | if preset_name is None:
255 | preset_name = os.path.splitext(os.path.basename(import_path))[0]
256 |
257 | # Ensure preset name has no spaces or special characters
258 | safe_name = preset_name.replace(" ", "_").replace("/", "_").replace("\\", "_")
259 |
260 | # Construct destination path
261 | dest_path = os.path.join(preset_dir, f"{safe_name}.layout")
262 |
263 | # Copy the preset file
264 | import shutil
265 | shutil.copy2(import_path, dest_path)
266 |
267 | return True
268 | except Exception as e:
269 | logger.error(f"Error importing layout preset: {str(e)}")
270 | return False
271 |
272 | def delete_layout_preset(preset_name: str, layout_type: str = "ui") -> bool:
273 | """
274 | Delete a layout preset.
275 |
276 | Args:
277 | preset_name: Name of the preset to delete
278 | layout_type: Type of layout to delete ('ui', 'window', 'workspace')
279 |
280 | Returns:
281 | True if successful, False otherwise
282 | """
283 | try:
284 | # Get the preset path
285 | if layout_type.lower() == "ui":
286 | preset_dir = get_ui_layout_path()
287 | else:
288 | # Other layout types would be handled here
289 | preset_dir = get_ui_layout_path()
290 |
291 | # Construct the preset file path
292 | preset_path = os.path.join(preset_dir, f"{preset_name}.layout")
293 |
294 | # Ensure file exists
295 | if not os.path.exists(preset_path):
296 | logger.error(f"Preset file not found: {preset_path}")
297 | return False
298 |
299 | # Delete the file
300 | os.remove(preset_path)
301 |
302 | return True
303 | except Exception as e:
304 | logger.error(f"Error deleting layout preset: {str(e)}")
305 | return False
```
--------------------------------------------------------------------------------
/scripts/setup/install.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # install.sh - One-step installation for DaVinci Resolve MCP Integration
3 | # This script handles the entire installation process with improved error detection
4 |
5 | # Colors for terminal output
6 | GREEN='\033[0;32m'
7 | YELLOW='\033[0;33m'
8 | BLUE='\033[0;34m'
9 | RED='\033[0;31m'
10 | BOLD='\033[1m'
11 | NC='\033[0m' # No Color
12 |
13 | # Get the absolute path of this script's location
14 | INSTALL_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
15 | VENV_DIR="$INSTALL_DIR/venv"
16 | CURSOR_CONFIG_DIR="$HOME/.cursor/mcp"
17 | CURSOR_CONFIG_FILE="$CURSOR_CONFIG_DIR/config.json"
18 | PROJECT_CURSOR_DIR="$INSTALL_DIR/.cursor"
19 | PROJECT_CONFIG_FILE="$PROJECT_CURSOR_DIR/mcp.json"
20 | LOG_FILE="$INSTALL_DIR/install.log"
21 |
22 | # Banner
23 | echo -e "${BLUE}${BOLD}=================================================${NC}"
24 | echo -e "${BLUE}${BOLD} DaVinci Resolve MCP Integration Installer ${NC}"
25 | echo -e "${BLUE}${BOLD}=================================================${NC}"
26 | echo -e "${YELLOW}Installation directory: ${INSTALL_DIR}${NC}"
27 | echo -e "Installation log: ${LOG_FILE}"
28 | echo ""
29 |
30 | # Initialize log
31 | echo "=== DaVinci Resolve MCP Installation Log ===" > "$LOG_FILE"
32 | echo "Date: $(date)" >> "$LOG_FILE"
33 | echo "Install directory: $INSTALL_DIR" >> "$LOG_FILE"
34 | echo "User: $(whoami)" >> "$LOG_FILE"
35 | echo "System: $(uname -a)" >> "$LOG_FILE"
36 | echo "" >> "$LOG_FILE"
37 |
38 | # Function to log messages
39 | log() {
40 | echo "[$(date +%T)] $1" >> "$LOG_FILE"
41 | }
42 |
43 | # Function to check if DaVinci Resolve is running
44 | check_resolve_running() {
45 | log "Checking if DaVinci Resolve is running"
46 | echo -ne "${YELLOW}Checking if DaVinci Resolve is running... ${NC}"
47 |
48 | if ps -ef | grep -i "[D]aVinci Resolve" > /dev/null; then
49 | echo -e "${GREEN}OK${NC}"
50 | log "DaVinci Resolve is running"
51 | return 0
52 | else
53 | echo -e "${RED}NOT RUNNING${NC}"
54 | echo -e "${YELLOW}DaVinci Resolve must be running to complete the installation.${NC}"
55 | echo -e "${YELLOW}Please start DaVinci Resolve and try again.${NC}"
56 | log "DaVinci Resolve is not running - installation cannot proceed"
57 | return 1
58 | fi
59 | }
60 |
61 | # Function to create Python virtual environment
62 | create_venv() {
63 | log "Creating/checking Python virtual environment"
64 | echo -ne "${YELLOW}Setting up Python virtual environment... ${NC}"
65 |
66 | if [ -d "$VENV_DIR" ] && [ -f "$VENV_DIR/bin/python" ]; then
67 | echo -e "${GREEN}ALREADY EXISTS${NC}"
68 | log "Virtual environment already exists"
69 | else
70 | echo -ne "${YELLOW}CREATING${NC}"
71 | python3 -m venv "$VENV_DIR" >> "$LOG_FILE" 2>&1
72 |
73 | if [ $? -eq 0 ]; then
74 | echo -e "${GREEN}OK${NC}"
75 | log "Virtual environment created successfully"
76 | else
77 | echo -e "${RED}FAILED${NC}"
78 | echo -e "${RED}Failed to create Python virtual environment.${NC}"
79 | echo -e "${YELLOW}Check that Python 3.9+ is installed.${NC}"
80 | log "Failed to create virtual environment"
81 | return 1
82 | fi
83 | fi
84 |
85 | return 0
86 | }
87 |
88 | # Function to install MCP SDK
89 | install_mcp() {
90 | log "Installing MCP SDK"
91 | echo -ne "${YELLOW}Installing MCP SDK... ${NC}"
92 |
93 | "$VENV_DIR/bin/pip" install "mcp[cli]" >> "$LOG_FILE" 2>&1
94 |
95 | if [ $? -eq 0 ]; then
96 | echo -e "${GREEN}OK${NC}"
97 | log "MCP SDK installed successfully"
98 | return 0
99 | else
100 | echo -e "${RED}FAILED${NC}"
101 | echo -e "${RED}Failed to install MCP SDK.${NC}"
102 | echo -e "${YELLOW}Check the log file for details: $LOG_FILE${NC}"
103 | log "Failed to install MCP SDK"
104 | return 1
105 | fi
106 | }
107 |
108 | # Function to set environment variables
109 | setup_env_vars() {
110 | log "Setting up environment variables"
111 | echo -ne "${YELLOW}Setting up environment variables... ${NC}"
112 |
113 | # Generate environment variables file
114 | ENV_FILE="$INSTALL_DIR/.env"
115 | cat > "$ENV_FILE" << EOF
116 | RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
117 | RESOLVE_SCRIPT_LIB="/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
118 | PYTHONPATH="\$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/"
119 | EOF
120 |
121 | # Source the environment variables
122 | source "$ENV_FILE"
123 |
124 | # Export them for the current session
125 | export RESOLVE_SCRIPT_API="/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting"
126 | export RESOLVE_SCRIPT_LIB="/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so"
127 | export PYTHONPATH="$PYTHONPATH:$RESOLVE_SCRIPT_API/Modules/"
128 |
129 | echo -e "${GREEN}OK${NC}"
130 | log "Environment variables set:"
131 | log "RESOLVE_SCRIPT_API=$RESOLVE_SCRIPT_API"
132 | log "RESOLVE_SCRIPT_LIB=$RESOLVE_SCRIPT_LIB"
133 |
134 | # Suggest adding to shell profile
135 | echo -e "${YELLOW}Consider adding these environment variables to your shell profile:${NC}"
136 | echo -e "${BLUE} echo 'export RESOLVE_SCRIPT_API=\"/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting\"' >> ~/.zshrc${NC}"
137 | echo -e "${BLUE} echo 'export RESOLVE_SCRIPT_LIB=\"/Applications/DaVinci Resolve/DaVinci Resolve.app/Contents/Libraries/Fusion/fusionscript.so\"' >> ~/.zshrc${NC}"
138 | echo -e "${BLUE} echo 'export PYTHONPATH=\"\$PYTHONPATH:\$RESOLVE_SCRIPT_API/Modules/\"' >> ~/.zshrc${NC}"
139 |
140 | return 0
141 | }
142 |
143 | # Function to setup Cursor MCP configuration
144 | setup_cursor_config() {
145 | log "Setting up Cursor MCP configuration"
146 | echo -ne "${YELLOW}Setting up Cursor MCP configuration... ${NC}"
147 |
148 | # Create system-level directory if it doesn't exist
149 | mkdir -p "$CURSOR_CONFIG_DIR"
150 |
151 | # Create system-level config file with the absolute paths
152 | cat > "$CURSOR_CONFIG_FILE" << EOF
153 | {
154 | "mcpServers": {
155 | "davinci-resolve": {
156 | "name": "DaVinci Resolve MCP",
157 | "command": "${INSTALL_DIR}/venv/bin/python",
158 | "args": ["${INSTALL_DIR}/resolve_mcp_server.py"]
159 | }
160 | }
161 | }
162 | EOF
163 |
164 | # Create project-level directory if it doesn't exist
165 | mkdir -p "$PROJECT_CURSOR_DIR"
166 |
167 | # Create project-level config file with absolute paths (same as system-level)
168 | cat > "$PROJECT_CONFIG_FILE" << EOF
169 | {
170 | "mcpServers": {
171 | "davinci-resolve": {
172 | "name": "DaVinci Resolve MCP",
173 | "command": "${INSTALL_DIR}/venv/bin/python",
174 | "args": ["${INSTALL_DIR}/resolve_mcp_server.py"]
175 | }
176 | }
177 | }
178 | EOF
179 |
180 | if [ -f "$CURSOR_CONFIG_FILE" ] && [ -f "$PROJECT_CONFIG_FILE" ]; then
181 | echo -e "${GREEN}OK${NC}"
182 | echo -e "${GREEN}Cursor MCP config created at: $CURSOR_CONFIG_FILE${NC}"
183 | echo -e "${GREEN}Project MCP config created at: $PROJECT_CONFIG_FILE${NC}"
184 | log "Cursor MCP configuration created successfully"
185 | log "System config file: $CURSOR_CONFIG_FILE"
186 | log "Project config file: $PROJECT_CONFIG_FILE"
187 |
188 | # Show the paths that were set
189 | echo -e "${YELLOW}Paths configured:${NC}"
190 | echo -e "${BLUE} Python: ${INSTALL_DIR}/venv/bin/python${NC}"
191 | echo -e "${BLUE} Script: ${INSTALL_DIR}/resolve_mcp_server.py${NC}"
192 |
193 | return 0
194 | else
195 | echo -e "${RED}FAILED${NC}"
196 | echo -e "${RED}Failed to create Cursor MCP configuration.${NC}"
197 | log "Failed to create Cursor MCP configuration"
198 | return 1
199 | fi
200 | }
201 |
202 | # Make server script executable
203 | make_script_executable() {
204 | log "Making server script executable"
205 | echo -ne "${YELLOW}Making server script executable... ${NC}"
206 |
207 | chmod +x "$INSTALL_DIR/resolve_mcp_server.py"
208 | chmod +x "$INSTALL_DIR/scripts/mcp_resolve-cursor_start"
209 | chmod +x "$INSTALL_DIR/scripts/verify-installation.sh"
210 |
211 | echo -e "${GREEN}OK${NC}"
212 | log "Server scripts made executable"
213 | return 0
214 | }
215 |
216 | # Verify installation
217 | verify_installation() {
218 | log "Verifying installation"
219 | echo -e "${BLUE}${BOLD}=================================================${NC}"
220 | echo -e "${YELLOW}${BOLD}Verifying installation...${NC}"
221 |
222 | # Run the verification script
223 | "$INSTALL_DIR/scripts/verify-installation.sh"
224 | VERIFY_RESULT=$?
225 |
226 | log "Verification completed with result: $VERIFY_RESULT"
227 |
228 | return $VERIFY_RESULT
229 | }
230 |
231 | # Run server if verification succeeds
232 | run_server() {
233 | log "Starting server"
234 | echo -e "${BLUE}${BOLD}=================================================${NC}"
235 | echo -e "${GREEN}${BOLD}Starting DaVinci Resolve MCP Server...${NC}"
236 | echo ""
237 |
238 | # Run the server using the virtual environment
239 | "$VENV_DIR/bin/python" "$INSTALL_DIR/resolve_mcp_server.py"
240 |
241 | SERVER_EXIT=$?
242 | log "Server exited with code: $SERVER_EXIT"
243 |
244 | return $SERVER_EXIT
245 | }
246 |
247 | # Main installation process
248 | main() {
249 | log "Starting installation process"
250 |
251 | # Check if DaVinci Resolve is running
252 | if ! check_resolve_running; then
253 | echo -e "${YELLOW}Waiting 10 seconds for DaVinci Resolve to start...${NC}"
254 | sleep 10
255 | if ! check_resolve_running; then
256 | log "Installation aborted - DaVinci Resolve not running"
257 | echo -e "${RED}Installation aborted.${NC}"
258 | exit 1
259 | fi
260 | fi
261 |
262 | # Create virtual environment
263 | if ! create_venv; then
264 | log "Installation aborted - virtual environment setup failed"
265 | echo -e "${RED}Installation aborted.${NC}"
266 | exit 1
267 | fi
268 |
269 | # Install MCP SDK
270 | if ! install_mcp; then
271 | log "Installation aborted - MCP SDK installation failed"
272 | echo -e "${RED}Installation aborted.${NC}"
273 | exit 1
274 | fi
275 |
276 | # Set up environment variables
277 | if ! setup_env_vars; then
278 | log "Installation aborted - environment variable setup failed"
279 | echo -e "${RED}Installation aborted.${NC}"
280 | exit 1
281 | fi
282 |
283 | # Set up Cursor configuration
284 | if ! setup_cursor_config; then
285 | log "Installation aborted - Cursor configuration failed"
286 | echo -e "${RED}Installation aborted.${NC}"
287 | exit 1
288 | fi
289 |
290 | # Make scripts executable
291 | if ! make_script_executable; then
292 | log "Installation aborted - failed to make scripts executable"
293 | echo -e "${RED}Installation aborted.${NC}"
294 | exit 1
295 | fi
296 |
297 | # Verify installation
298 | if ! verify_installation; then
299 | log "Installation completed with verification warnings"
300 | echo -e "${YELLOW}Installation completed with warnings.${NC}"
301 | echo -e "${YELLOW}Please fix any issues before starting the server.${NC}"
302 | echo -e "${YELLOW}You can run the verification script again:${NC}"
303 | echo -e "${BLUE} ./scripts/verify-installation.sh${NC}"
304 | exit 1
305 | fi
306 |
307 | # Installation successful
308 | log "Installation completed successfully"
309 | echo -e "${GREEN}${BOLD}Installation completed successfully!${NC}"
310 | echo -e "${YELLOW}You can now start the server with:${NC}"
311 | echo -e "${BLUE} ./run-now.sh${NC}"
312 | echo -e "${YELLOW}Or for more options:${NC}"
313 | echo -e "${BLUE} ./scripts/mcp_resolve-cursor_start${NC}"
314 |
315 | # Ask if the user wants to start the server now
316 | echo ""
317 | read -p "Do you want to start the server now? (y/n) " -n 1 -r
318 | echo ""
319 | if [[ $REPLY =~ ^[Yy]$ ]]; then
320 | run_server
321 | else
322 | log "User chose not to start the server"
323 | echo -e "${YELLOW}You can start the server later with:${NC}"
324 | echo -e "${BLUE} ./run-now.sh${NC}"
325 | fi
326 | }
327 |
328 | # Run the main installation process
329 | main
```
--------------------------------------------------------------------------------
/scripts/launch.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 | # DaVinci Resolve MCP Server Launch Script for macOS
3 | # Easy launcher that handles environment setup, checking and starting the server
4 |
5 | # Get the absolute path to the project directory
6 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
7 | PROJECT_DIR="$( cd "$SCRIPT_DIR/.." &> /dev/null && pwd )"
8 | VENV_DIR="$PROJECT_DIR/venv"
9 | MCP_PORT=8080
10 |
11 | # Load utility functions
12 | source "$SCRIPT_DIR/utils.sh"
13 |
14 | # Main menu function
15 | show_main_menu() {
16 | clear
17 | print_header "DaVinci Resolve MCP Server Launcher"
18 |
19 | echo -e "${CYAN}What would you like to do?${NC}"
20 | echo ""
21 | echo -e "${WHITE}1${NC}. Start MCP Server"
22 | echo -e "${WHITE}2${NC}. Stop MCP Server(s)"
23 | echo -e "${WHITE}3${NC}. Check Environment"
24 | echo -e "${WHITE}4${NC}. Setup Environment"
25 | echo -e "${WHITE}5${NC}. View Readme"
26 | echo -e "${WHITE}0${NC}. Exit"
27 | echo ""
28 |
29 | read -p "Enter your choice [1-5 or 0]: " choice
30 |
31 | case "$choice" in
32 | 1) start_server ;;
33 | 2) stop_servers ;;
34 | 3) check_environment ;;
35 | 4) setup_environment ;;
36 | 5) view_readme ;;
37 | 0) exit 0 ;;
38 | *)
39 | echo -e "${YELLOW}Invalid choice. Please try again.${NC}"
40 | sleep 1
41 | show_main_menu
42 | ;;
43 | esac
44 | }
45 |
46 | # Function to start the server
47 | start_server() {
48 | print_header "Starting DaVinci Resolve MCP Server"
49 |
50 | # Check if Python is available
51 | if ! check_python; then
52 | print_status "error" "Cannot start the server without Python."
53 | read -p "Press Enter to return to the main menu..." dummy
54 | show_main_menu
55 | return
56 | fi
57 |
58 | # Check if the virtual environment exists
59 | if ! check_venv "$VENV_DIR"; then
60 | print_status "warning" "Virtual environment not found. Setting up now..."
61 | setup_virtual_environment
62 | fi
63 |
64 | # Check for required packages
65 | if ! is_package_installed "$VENV_DIR" "mcp"; then
66 | print_status "warning" "MCP package not installed. Installing now..."
67 | install_package "$VENV_DIR" "mcp[cli]"
68 | fi
69 |
70 | # Check if Resolve environment variables are set
71 | if ! check_resolve_env; then
72 | print_status "warning" "Resolve environment variables not set. Setting defaults..."
73 | set_resolve_env
74 | fi
75 |
76 | # Check if DaVinci Resolve is running
77 | if ! is_resolve_running; then
78 | print_status "warning" "DaVinci Resolve is not running. The server will start but won't be able to connect to Resolve."
79 | print_status "info" "Please start DaVinci Resolve and then restart the server."
80 | else
81 | print_status "success" "DaVinci Resolve is running."
82 | fi
83 |
84 | # Check if the port is in use
85 | if is_port_in_use "$MCP_PORT"; then
86 | print_status "warning" "Port $MCP_PORT is already in use."
87 | read -p "Do you want to stop the process using this port? (y/n): " stop_process
88 | if [[ "$stop_process" =~ ^[Yy]$ ]]; then
89 | kill_port_process "$MCP_PORT"
90 | else
91 | print_status "error" "Cannot start the server on port $MCP_PORT as it's already in use."
92 | read -p "Press Enter to return to the main menu..." dummy
93 | show_main_menu
94 | return
95 | fi
96 | fi
97 |
98 | # Run the server in development mode
99 | print_status "info" "Starting the MCP server in development mode..."
100 |
101 | # Navigate to the project directory and start the server
102 | cd "$PROJECT_DIR"
103 |
104 | # Run the server in background or foreground?
105 | read -p "Run server in background? (y/n): " bg_choice
106 | if [[ "$bg_choice" =~ ^[Yy]$ ]]; then
107 | print_status "info" "Starting server in background mode..."
108 | nohup "$VENV_DIR/bin/mcp" dev "$PROJECT_DIR/src/resolve_mcp_server.py" > "$PROJECT_DIR/mcp_server.log" 2>&1 &
109 | print_status "success" "Server is running in the background."
110 | print_status "info" "Logs are being written to: $PROJECT_DIR/mcp_server.log"
111 | print_status "info" "To stop the server, use option 2 from the main menu."
112 | else
113 | print_status "info" "Starting server in foreground mode..."
114 | print_status "info" "Press Ctrl+C to stop the server"
115 | print_status "info" "Server is starting..."
116 | sleep 1
117 | "$VENV_DIR/bin/mcp" dev "$PROJECT_DIR/src/resolve_mcp_server.py"
118 | fi
119 |
120 | read -p "Press Enter to return to the main menu..." dummy
121 | show_main_menu
122 | }
123 |
124 | # Function to stop all running servers
125 | stop_servers() {
126 | print_header "Stopping DaVinci Resolve MCP Server(s)"
127 |
128 | # Check if any MCP servers are running
129 | local pids=$(pgrep -f "mcp dev")
130 |
131 | if [ -z "$pids" ]; then
132 | print_status "info" "No MCP servers are currently running."
133 | else
134 | print_status "info" "Found MCP server processes: $pids"
135 | print_status "info" "Stopping servers..."
136 |
137 | kill $pids 2>/dev/null
138 | sleep 1
139 |
140 | # Check if they're still running and force kill if needed
141 | pids=$(pgrep -f "mcp dev")
142 | if [ -n "$pids" ]; then
143 | print_status "warning" "Forcing termination of server processes..."
144 | kill -9 $pids 2>/dev/null
145 | fi
146 |
147 | print_status "success" "All MCP servers have been stopped."
148 | fi
149 |
150 | # Also check for any processes using the MCP port
151 | if is_port_in_use "$MCP_PORT"; then
152 | print_status "warning" "Port $MCP_PORT is still in use."
153 | read -p "Do you want to stop processes using this port? (y/n): " stop_port
154 | if [[ "$stop_port" =~ ^[Yy]$ ]]; then
155 | kill_port_process "$MCP_PORT"
156 | print_status "success" "Freed port $MCP_PORT."
157 | fi
158 | else
159 | print_status "success" "Port $MCP_PORT is free."
160 | fi
161 |
162 | read -p "Press Enter to return to the main menu..." dummy
163 | show_main_menu
164 | }
165 |
166 | # Function to check the environment
167 | check_environment() {
168 | print_header "Checking Environment"
169 |
170 | # Check Python
171 | if check_python; then
172 | python_version=$(python3 --version)
173 | print_status "success" "Python is available: $python_version"
174 | else
175 | print_status "error" "Python check failed."
176 | fi
177 |
178 | # Check virtual environment
179 | if check_venv "$VENV_DIR"; then
180 | print_status "success" "Virtual environment exists at $VENV_DIR"
181 |
182 | # Check for MCP package
183 | if is_package_installed "$VENV_DIR" "mcp"; then
184 | mcp_version=$("$VENV_DIR/bin/pip" show mcp | grep Version | awk '{print $2}')
185 | print_status "success" "MCP package is installed (version $mcp_version)"
186 | else
187 | print_status "error" "MCP package is not installed"
188 | fi
189 | else
190 | print_status "error" "Virtual environment not found or incomplete"
191 | fi
192 |
193 | # Check Resolve environment variables
194 | if check_resolve_env; then
195 | print_status "success" "Resolve environment variables are set"
196 | echo -e "${BLUE}RESOLVE_SCRIPT_API${NC} = $RESOLVE_SCRIPT_API"
197 | echo -e "${BLUE}RESOLVE_SCRIPT_LIB${NC} = $RESOLVE_SCRIPT_LIB"
198 | else
199 | print_status "error" "Missing Resolve environment variables"
200 | fi
201 |
202 | # Check if DaVinci Resolve is running
203 | if is_resolve_running; then
204 | print_status "success" "DaVinci Resolve is running"
205 | else
206 | print_status "error" "DaVinci Resolve is not running"
207 | fi
208 |
209 | # Check if port is in use
210 | if is_port_in_use "$MCP_PORT"; then
211 | process_info=$(lsof -i ":$MCP_PORT" | tail -n +2 | head -1)
212 | print_status "warning" "Port $MCP_PORT is in use: $process_info"
213 | else
214 | print_status "success" "Port $MCP_PORT is free"
215 | fi
216 |
217 | read -p "Press Enter to return to the main menu..." dummy
218 | show_main_menu
219 | }
220 |
221 | # Function to set up the environment
222 | setup_environment() {
223 | print_header "Setting Up Environment"
224 |
225 | # Check Python first
226 | if ! check_python; then
227 | print_status "error" "Cannot set up the environment without Python."
228 | read -p "Press Enter to return to the main menu..." dummy
229 | show_main_menu
230 | return
231 | fi
232 |
233 | # Set up virtual environment
234 | setup_virtual_environment
235 |
236 | # Set up Resolve environment variables
237 | print_status "info" "Setting up Resolve environment variables..."
238 | set_resolve_env
239 |
240 | # Add environment variables to shell profile if needed
241 | shell_profile=""
242 | if [ -f "$HOME/.zshrc" ]; then
243 | shell_profile="$HOME/.zshrc"
244 | elif [ -f "$HOME/.bash_profile" ]; then
245 | shell_profile="$HOME/.bash_profile"
246 | elif [ -f "$HOME/.bashrc" ]; then
247 | shell_profile="$HOME/.bashrc"
248 | fi
249 |
250 | if [ -n "$shell_profile" ]; then
251 | print_status "info" "Checking for environment variables in $shell_profile..."
252 |
253 | if grep -q "RESOLVE_SCRIPT_API" "$shell_profile"; then
254 | print_status "success" "Environment variables already exist in $shell_profile"
255 | else
256 | print_status "info" "Adding environment variables to $shell_profile..."
257 | echo "" >> "$shell_profile"
258 | echo "# DaVinci Resolve MCP Server environment variables" >> "$shell_profile"
259 | echo "export RESOLVE_SCRIPT_API=\"$RESOLVE_SCRIPT_API\"" >> "$shell_profile"
260 | echo "export RESOLVE_SCRIPT_LIB=\"$RESOLVE_SCRIPT_LIB\"" >> "$shell_profile"
261 | echo "export PYTHONPATH=\"\$PYTHONPATH:\$RESOLVE_SCRIPT_API/Modules/\"" >> "$shell_profile"
262 | print_status "success" "Environment variables added to $shell_profile"
263 | fi
264 | else
265 | print_status "warning" "No shell profile found for setting persistent environment variables"
266 | print_status "info" "You'll need to set these variables manually in your shell profile:"
267 | echo "export RESOLVE_SCRIPT_API=\"$RESOLVE_SCRIPT_API\""
268 | echo "export RESOLVE_SCRIPT_LIB=\"$RESOLVE_SCRIPT_LIB\""
269 | echo "export PYTHONPATH=\"\$PYTHONPATH:\$RESOLVE_SCRIPT_API/Modules/\""
270 | fi
271 |
272 | print_status "success" "Environment setup complete!"
273 | read -p "Press Enter to return to the main menu..." dummy
274 | show_main_menu
275 | }
276 |
277 | # Function to set up virtual environment
278 | setup_virtual_environment() {
279 | print_status "info" "Setting up Python virtual environment..."
280 |
281 | if check_venv "$VENV_DIR"; then
282 | print_status "info" "Virtual environment already exists at $VENV_DIR"
283 | else
284 | print_status "info" "Creating virtual environment at $VENV_DIR"
285 | python3 -m venv "$VENV_DIR"
286 |
287 | if ! check_venv "$VENV_DIR"; then
288 | print_status "error" "Failed to create virtual environment"
289 | return 1
290 | fi
291 | fi
292 |
293 | # Install or upgrade pip
294 | print_status "info" "Updating pip in virtual environment..."
295 | "$VENV_DIR/bin/pip" install --upgrade pip
296 |
297 | # Install MCP with CLI support
298 | print_status "info" "Installing MCP SDK with CLI support..."
299 | "$VENV_DIR/bin/pip" install "mcp[cli]"
300 |
301 | print_status "success" "Virtual environment is ready"
302 | return 0
303 | }
304 |
305 | # Function to view README
306 | view_readme() {
307 | print_header "DaVinci Resolve MCP Server Documentation"
308 |
309 | # Check if less is available
310 | if command_exists less; then
311 | less "$PROJECT_DIR/README.md"
312 | else
313 | cat "$PROJECT_DIR/README.md"
314 | fi
315 |
316 | read -p "Press Enter to return to the main menu..." dummy
317 | show_main_menu
318 | }
319 |
320 | # Make all scripts executable
321 | chmod +x "$SCRIPT_DIR"/*.sh
322 | chmod +x "$PROJECT_DIR/src/resolve_mcp_server.py"
323 |
324 | # Start the main menu
325 | show_main_menu
```
--------------------------------------------------------------------------------
/docs/CHANGELOG.md:
--------------------------------------------------------------------------------
```markdown
1 | # Changelog
2 |
3 | All notable changes to the DaVinci Resolve MCP Server project will be documented in this file.
4 |
5 | ## [1.3.6] - 2025-03-29
6 |
7 | ### Added
8 | - Comprehensive Feature Additions:
9 | - Complete MediaPoolItem functionality:
10 | - LinkProxyMedia/UnlinkProxyMedia for proxy media workflow
11 | - ReplaceClip functionality for swapping media files
12 | - TranscribeAudio/ClearTranscription for audio transcription
13 | - Complete Folder object methods:
14 | - Export functionality for DRB folder archives
15 | - TranscribeAudio for batch audio transcription
16 | - ClearTranscription for managing folder-level transcriptions
17 | - Cache Management implementation:
18 | - Get/set cache mode (auto, on, off)
19 | - Control optimized media settings
20 | - Manage proxy media settings including quality
21 | - Configure cache file paths (local/network)
22 | - Generate and delete optimized media for specific clips
23 | - Timeline Item Properties implementation:
24 | - Transform properties (position, scale, rotation, anchor point)
25 | - Crop controls (left, right, top, bottom)
26 | - Composite mode and opacity settings
27 | - Retime controls including speed and process type
28 | - Stabilization controls including method and strength
29 | - Audio properties including volume, pan, and EQ settings
30 | - Keyframe Control implementation:
31 | - Resource endpoint for retrieving all keyframes for timeline items
32 | - Add/modify/delete keyframe tools for precise animation control
33 | - Keyframe interpolation control (Linear, Bezier, Ease-In, Ease-Out)
34 | - Keyframe mode selection (All, Color, Sizing)
35 | - Support for all keyframeable properties (transform, crop, composite)
36 | - Color Preset Management implementation:
37 | - Resource endpoint for retrieving all color presets in albums
38 | - Save color presets from timeline clips with customizable naming
39 | - Apply color presets to timeline clips by ID or name
40 | - Delete color presets from albums
41 | - Create and manage color preset albums
42 | - LUT Export functionality:
43 | - Tool for exporting grades from clips as LUT files
44 | - Support for multiple LUT formats (Cube, DaVinci, 3DL, Panasonic)
45 | - Variable LUT sizing options (17-point, 33-point, 65-point)
46 | - Batch export functionality for PowerGrades
47 | - Added helper functions for recursively accessing media pool items
48 | - Updated FEATURES.md with comprehensive documentation
49 |
50 | ### Changed
51 | - Project directory restructuring to better organize files:
52 | - Moved documentation files to `docs/` directory
53 | - Moved test scripts to `scripts/tests/` directory
54 | - Moved configuration templates to `config-templates/` directory
55 | - Moved utility scripts to `scripts/` directory
56 | - Updated all scripts to work with the new directory structure
57 | - Created simpler launcher scripts in the root directory
58 | - Updated Implementation Progress Summary to reflect 100% completion of MediaPoolItem and Folder features
59 | - Enhanced project documentation with better usage examples
60 | - Improved media management functionality with expanded clip operations
61 | - Enhanced timeline item handling with ID-based lookup
62 | - Improved property validation with comprehensive error reporting
63 | - Added support for both video and audio timeline item properties
64 | - Enhanced color workflow efficiency with preset system
65 | - Improved organization of saved grades with album management
66 | - Enhanced cross-application compatibility with industry-standard LUT formats
67 |
68 | ## [1.3.5] - 2025-03-29
69 |
70 | ### Added
71 | - Updated Cursor integration with new templating system
72 | - Improved client-specific launcher scripts for better usability
73 | - Added automatic Cursor MCP configuration generation
74 | - Enhanced cross-platform compatibility in launcher scripts
75 |
76 | ### Changed
77 | - Updated Cursor integration script to use project root relative paths
78 | - Simplified launcher script by removing dependencies on intermediate scripts
79 | - Improved virtual environment detection and validation
80 |
81 | ### Fixed
82 | - Path handling in Cursor configuration for more reliable connections
83 | - Virtual environment validation to prevent launch failures
84 | - Environment variable checking with more robust validation
85 |
86 | ## [1.3.4] - 2025-03-28
87 |
88 | ### Changed
89 | - Improved template configuration for MCP clients with better documentation
90 | - Fixed Cursor integration templates to use direct Python path instead of MCP CLI
91 | - Simplified configuration process by removing environment variable requirements
92 | - Added clearer warnings in templates and README about path replacement
93 | - Created VERSION.md file for easier version tracking
94 |
95 | ### Fixed
96 | - Connection issues with Cursor MCP integration
97 | - Path variable handling in configuration templates
98 | - Configuration templates now use consistent variable naming
99 |
100 | ## [1.3.3] - 2025-03-27
101 |
102 | ### Fixed
103 | - Improved Windows compatibility for the run-now.bat script:
104 | - Fixed ANSI color code syntax errors in Windows command prompt
105 | - Made the npm/Node.js check a warning instead of an error
106 | - Simplified environment variable handling for better Windows compatibility
107 | - Fixed command syntax in batch file for more reliable execution
108 | - Improved DaVinci Resolve process detection for Windows
109 | - Added support for detecting multiple possible DaVinci Resolve executable names
110 | - Enhanced batch file error handling and robustness
111 | - Fixed issue with running the MCP server executable on Windows
112 | - Increased timeout waiting for DaVinci Resolve to start
113 | - Added Windows specific templates in config-templates
114 |
115 | ## [1.3.2] - 2025-03-28
116 |
117 | ### Added
118 | - Experimental Windows support with platform-specific path detection
119 | - Dynamic environment setup based on operating system
120 | - Platform utility module for handling OS-specific paths and configurations
121 | - Enhanced error messages with platform-specific environment setup instructions
122 | - Windows pre-launch check script (PowerShell) with automatic environment configuration
123 | - Windows batch file launcher for easy execution of the pre-launch check
124 |
125 | ### Changed
126 | - Refactored path setup code to use platform detection
127 | - Improved logging with platform-specific information
128 | - Updated documentation to reflect Windows compatibility status
129 | - Enhanced README with Windows-specific configuration instructions
130 |
131 | ### Fixed
132 | - Platform-dependent path issues that prevented Windows compatibility
133 | - Environment variable handling for cross-platform use
134 | - Windows-specific configuration paths for Cursor integration
135 |
136 | ## [1.3.1] - 2025-03-27
137 |
138 | ### Added
139 | - Universal launcher script (`mcp_resolve_launcher.sh`) that provides both interactive and command-line interfaces for:
140 | - Starting and stopping Cursor MCP server
141 | - Starting and stopping Claude Desktop MCP server
142 | - Running both servers simultaneously
143 | - Checking server status
144 | - Forcing server start even if DaVinci Resolve isn't detected
145 | - Specifying a project to open on server start
146 | - Improved Claude Desktop integration script with better error handling and force mode
147 | - Enhanced detection for running DaVinci Resolve process
148 |
149 | ### Changed
150 | - Updated documentation to include new universal launcher functionality
151 | - Improved server startup process with better error handling and logging
152 | - Enhanced cross-client compatibility between Cursor and Claude Desktop
153 | - Relocated and improved marker test script from root to examples/markers directory with better documentation and organization
154 |
155 | ### Fixed
156 | - Process detection issues when looking for running DaVinci Resolve
157 | - Signal handling in server scripts for cleaner termination
158 |
159 | ## [1.3.0] - 2025-03-26
160 |
161 | ### Added
162 | - Support for adding clips to timeline directly by name
163 | - Intelligent marker placement with frame detection
164 | - Enhanced logging and error reporting
165 | - Improved code organization with modular architecture
166 |
167 | ### Changed
168 | - Reorganized project structure for better maintainability
169 | - Enhanced Claude Desktop integration with better error handling
170 | - Optimized connection to DaVinci Resolve for faster response times
171 | - Updated documentation to include more examples
172 |
173 | ### Fixed
174 | - Issues with marker placement on empty timelines
175 | - Media pool navigation in complex project structures
176 | - Timing issues when rapidly sending commands to DaVinci Resolve
177 |
178 | ## [1.1.0] - 2025-03-26
179 |
180 | ### Added
181 | - Claude Desktop integration with claude_desktop_config.json support
182 | - Consolidated server management script (scripts/server.sh)
183 | - Project structure reorganization for better maintenance
184 | - Configuration templates for easier setup
185 |
186 | ### Changed
187 | - Moved scripts to dedicated scripts/ directory
188 | - Organized example files into examples/ directory
189 | - Updated README with Claude Desktop instructions
190 | - Updated FEATURES.md to reflect Claude Desktop compatibility
191 |
192 | ### Fixed
193 | - Environment variable handling in server scripts
194 | - Path references in documentation
195 |
196 | ## [1.0.0] - 2025-03-24
197 |
198 | ### Added
199 | - Initial release with Cursor integration
200 | - DaVinci Resolve connection functionality
201 | - Project management features (list, open, create projects)
202 | - Timeline operations (create, list, switch timelines)
203 | - Marker functionality with advanced frame detection
204 | - Media Pool operations (import media, create bins)
205 | - Comprehensive setup scripts
206 | - Pre-launch check script
207 |
208 | ### Changed
209 | - Switched from original MCP framework to direct JSON-RPC for improved reliability
210 |
211 | ### Fixed
212 | - Save Project functionality with multi-method approach
213 | - Environment variable setup for consistent connection
214 |
215 | ## [0.1.0] - 2025-03-26
216 |
217 | ### Added
218 | - Initial release with core functionality
219 | - Connection to DaVinci Resolve via MCP
220 | - Project management features (list, open, create projects)
221 | - Timeline operations (list, create, switch timelines, add markers)
222 | - Media pool operations (list clips, import media, create bins)
223 | - Setup script for easier installation and configuration
224 | - Comprehensive documentation
225 |
226 | ### Implemented Features
227 | - [x] **Get Resolve Version** – Resource that returns the Resolve version string
228 | - [x] **Get Current Page** – Resource that identifies which page is currently open in the UI
229 | - [x] **Switch Page** – Tool to change the UI to a specified page
230 |
231 | - [x] **List Projects** – Resource that lists available project names
232 | - [x] **Get Current Project Name** – Resource that retrieves the name of the currently open project
233 | - [x] **Open Project** – Tool to open a project by name
234 | - [x] **Create New Project** – Tool to create a new project with a given name
235 | - [x] **Save Project** – Tool to save the current project
236 |
237 | - [x] **List Timelines** – Resource that lists all timeline names in the current project
238 | - [x] **Get Current Timeline** – Resource that gets information about the current timeline
239 | - [x] **Create Timeline** – Tool to create a new timeline
240 | - [x] **Set Current Timeline** – Tool to switch to a different timeline by name
241 | - [x] **Add Marker** – Tool to add a marker at a specified time on the current timeline
242 |
243 | - [x] **List Media Pool Clips** – Resource that lists clips in the media pool
244 | - [x] **Import Media** – Tool to import a media file into the current project
245 | - [x] **Create Bin** – Tool to create a new bin/folder in the media pool
246 |
247 | ### Future Work
248 | - [ ] Move Clip to Timeline – Tool to take a clip from the media pool and place it on the timeline
249 | - [ ] Windows and Linux Support
250 | - [ ] Claude Desktop Integration
251 | - [ ] Color Page Operations
252 | - [ ] Fusion Operations
253 |
254 | ## [0.3.0] - 2025-03-26
255 | ### Changed
256 | - Cleaned up project structure by removing redundant test scripts and log files
257 | - Removed duplicate server implementations to focus on the main MCP server
258 | - Consolidated server startup scripts to simplify usage
259 | - Created backup directories for removed files (cleanup_backup and logs_backup)
260 | - Improved marker functionality with better frame detection and clip-aware positioning
261 |
262 | ### Added
263 | - Implemented "Add Clip to Timeline" feature to allow adding media pool clips to timelines
264 | - Added test script for validating timeline operations
265 | - Added comprehensive marker testing to improve functionality
266 |
267 | ## [0.3.1] - 2025-03-27
268 | ### Enhanced
269 | - Completely overhauled marker functionality:
270 | - Added intelligent frame selection when frame is not specified
271 | - Improved error handling for marker placement
272 | - Added collision detection to avoid overwriting existing markers
273 | - Added suggestions for alternate frames when a marker already exists
274 | - Implemented validation to ensure markers are placed on actual clips
275 | - Better debugging and detailed error messages
276 |
277 | ### Fixed
278 | - Resolved issue with markers failing silently when trying to place on invalid frames
279 | - Fixed marker placement to only allow adding markers on actual media content
280 |
281 | ## [0.2.0] - 2025-03-25
282 |
283 | ### Added
284 | - Added new features and improvements
285 | - Updated documentation
286 |
287 | ### Implemented Features
288 | - [x] **New Feature 1** – Description of the new feature
289 | - [x] **New Feature 2** – Description of the new feature
290 |
291 | ### Future Work
292 | - [ ] Task 1 – Description of the task
293 | - [ ] Task 2 – Description of the task
294 |
295 | ## Unreleased
296 |
297 | ### Added
298 | - Project directory restructuring to better organize files:
299 | - Moved documentation files to `docs/` directory
300 | - Moved test scripts to `scripts/tests/` directory
301 | - Moved configuration templates to `config-templates/` directory
302 | - Moved utility scripts to `scripts/` directory
303 | - Updated all scripts to work with the new directory structure
304 | - Created simpler launcher scripts in the root directory
305 |
```
--------------------------------------------------------------------------------
/tests/test_improvements.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve MCP Server Test Script
4 | --------------------------------------
5 | This script tests the improvements made to the DaVinci Resolve MCP Server
6 | by systematically checking each enhanced feature.
7 |
8 | Usage:
9 | python test_improvements.py
10 |
11 | Requirements:
12 | - DaVinci Resolve must be running with a project open
13 | - DaVinci Resolve MCP Server must be running (after restart)
14 | - requests module (pip install requests)
15 | """
16 |
17 | import json
18 | import time
19 | import sys
20 | import requests
21 | import logging
22 | from typing import Dict, Any, List, Tuple, Optional
23 |
24 | # Configure logging
25 | logging.basicConfig(
26 | level=logging.INFO,
27 | format='%(asctime)s - %(levelname)s - %(message)s',
28 | handlers=[
29 | logging.FileHandler("mcp_test_results.log"),
30 | logging.StreamHandler()
31 | ]
32 | )
33 | logger = logging.getLogger(__name__)
34 |
35 | # Server configuration
36 | SERVER_URL = "http://localhost:8000/api"
37 |
38 | def send_request(tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
39 | """Send a request to the MCP server."""
40 | try:
41 | payload = {
42 | "tool": tool_name,
43 | "params": params
44 | }
45 | response = requests.post(SERVER_URL, json=payload)
46 | response.raise_for_status()
47 | return response.json()
48 | except requests.exceptions.RequestException as e:
49 | logger.error(f"Request error: {e}")
50 | return {"success": False, "error": str(e)}
51 |
52 | def test_server_connection() -> bool:
53 | """Test basic connection to DaVinci Resolve via the server."""
54 | logger.info("Testing server connection...")
55 |
56 | # Try switching to media page as a basic connectivity test
57 | result = send_request("mcp_davinci_resolve_switch_page", {"page": "media"})
58 |
59 | if result.get("success", False) or "content" in result:
60 | logger.info("✅ Server connection successful")
61 | return True
62 | else:
63 | logger.error(f"❌ Server connection failed: {result.get('error', 'Unknown error')}")
64 | return False
65 |
66 | def test_project_settings() -> bool:
67 | """Test setting project settings with different parameter types."""
68 | logger.info("Testing project settings parameter handling...")
69 |
70 | # Test with numeric value
71 | logger.info("Testing numeric parameter...")
72 | result1 = send_request("mcp_davinci_resolve_set_project_setting",
73 | {"setting_name": "timelineFrameRate", "setting_value": 24})
74 |
75 | # Test with string value
76 | logger.info("Testing string parameter...")
77 | result2 = send_request("mcp_davinci_resolve_set_project_setting",
78 | {"setting_name": "timelineFrameRate", "setting_value": "24"})
79 |
80 | # Test with float value
81 | logger.info("Testing float parameter...")
82 | result3 = send_request("mcp_davinci_resolve_set_project_setting",
83 | {"setting_name": "colorScienceMode", "setting_value": 0})
84 |
85 | success1 = "error" not in result1 or not result1.get("error")
86 | success2 = "error" not in result2 or not result2.get("error")
87 | success3 = "error" not in result3 or not result3.get("error")
88 |
89 | if success1 and success2 and success3:
90 | logger.info("✅ Project settings parameter handling is working")
91 | return True
92 | else:
93 | logger.error(f"❌ Project settings parameter handling failed")
94 | logger.error(f" Numeric test: {'✅ Passed' if success1 else '❌ Failed'}")
95 | logger.error(f" String test: {'✅ Passed' if success2 else '❌ Failed'}")
96 | logger.error(f" Float test: {'✅ Passed' if success3 else '❌ Failed'}")
97 | return False
98 |
99 | def test_color_page_operations() -> bool:
100 | """Test color page operations with automatic clip selection."""
101 | logger.info("Testing color page operations...")
102 |
103 | # Switch to color page
104 | result1 = send_request("mcp_davinci_resolve_switch_page", {"page": "color"})
105 |
106 | # Try adding a serial node (should use automatic clip selection)
107 | time.sleep(1) # Give it a moment to switch pages
108 | result2 = send_request("mcp_davinci_resolve_add_node",
109 | {"node_type": "serial", "label": "AutoTest"})
110 |
111 | # Try setting color wheel parameter
112 | result3 = send_request("mcp_davinci_resolve_set_color_wheel_param",
113 | {"wheel": "gain", "param": "red", "value": 0.1})
114 |
115 | success1 = "error" not in result1 or not result1.get("error")
116 | success2 = "error" not in result2 or not result2.get("error")
117 | success3 = "error" not in result3 or not result3.get("error")
118 |
119 | # Check if automatic clip selection messages are present
120 | auto_select_working = False
121 | if not success2:
122 | error_msg = str(result2.get("error", ""))
123 | # Even if it failed, check if we see the right error message
124 | if "ensure_clip_selected" in error_msg or "Selected first clip" in error_msg:
125 | logger.info("✅ Automatic clip selection is being attempted")
126 | auto_select_working = True
127 |
128 | if success1 and (success2 or auto_select_working):
129 | logger.info("✅ Color page operations are working or properly reporting selection issues")
130 | return True
131 | else:
132 | logger.error(f"❌ Color page operations test failed")
133 | logger.error(f" Switch to color page: {'✅ Passed' if success1 else '❌ Failed'}")
134 | logger.error(f" Add node: {'✅ Passed' if success2 else '❌ Failed but proper error' if auto_select_working else '❌ Failed'}")
135 | logger.error(f" Set color wheel: {'✅ Passed' if success3 else '❌ Failed'}")
136 | return False
137 |
138 | def test_render_queue_operations() -> bool:
139 | """Test render queue operations with improved helpers."""
140 | logger.info("Testing render queue operations...")
141 |
142 | # Switch to deliver page
143 | result1 = send_request("mcp_davinci_resolve_switch_page", {"page": "deliver"})
144 |
145 | # Clear render queue first (known to be working)
146 | result2 = send_request("mcp_davinci_resolve_clear_render_queue", {"random_string": "test"})
147 |
148 | # Try adding a timeline to the render queue
149 | time.sleep(1) # Give it a moment to switch pages
150 | result3 = send_request("mcp_davinci_resolve_add_to_render_queue",
151 | {"preset_name": "YouTube 1080p", "timeline_name": None, "use_in_out_range": False})
152 |
153 | success1 = "error" not in result1 or not result1.get("error")
154 | success2 = "error" not in result2 or not result2.get("error")
155 | success3 = "error" not in result3 or not result3.get("error")
156 |
157 | # Check if our helpers are being used
158 | helper_working = False
159 | if not success3:
160 | error_msg = str(result3.get("error", ""))
161 | # Even if it failed, check if we see messages from our helpers
162 | if "ensure_render_settings" in error_msg or "validate_render_preset" in error_msg:
163 | logger.info("✅ Render queue helpers are being used")
164 | helper_working = True
165 |
166 | if success1 and success2 and (success3 or helper_working):
167 | logger.info("✅ Render queue operations are working or properly using helpers")
168 | return True
169 | else:
170 | logger.error(f"❌ Render queue operations test failed")
171 | logger.error(f" Switch to deliver page: {'✅ Passed' if success1 else '❌ Failed'}")
172 | logger.error(f" Clear render queue: {'✅ Passed' if success2 else '❌ Failed'}")
173 | logger.error(f" Add to render queue: {'✅ Passed' if success3 else '❌ Failed but helpers working' if helper_working else '❌ Failed'}")
174 | return False
175 |
176 | def test_error_handling_with_empty_timeline() -> bool:
177 | """Test how the color operations handle an empty timeline."""
178 | logger.info("Testing error handling with empty timeline...")
179 |
180 | # First create a new empty timeline
181 | empty_timeline_name = f"Empty_Test_Timeline_{int(time.time())}"
182 | create_result = send_request("mcp_davinci_resolve_create_timeline",
183 | {"name": empty_timeline_name})
184 |
185 | # Set it as current
186 | set_result = send_request("mcp_davinci_resolve_set_current_timeline",
187 | {"name": empty_timeline_name})
188 |
189 | # Try to perform color operations on empty timeline
190 | result1 = send_request("mcp_davinci_resolve_switch_page", {"page": "color"})
191 | time.sleep(1) # Give it a moment to switch pages
192 |
193 | # Try adding a node - this should fail but with proper error message
194 | result2 = send_request("mcp_davinci_resolve_add_node",
195 | {"node_type": "serial", "label": "EmptyTest"})
196 |
197 | success1 = "error" not in result1 or not result1.get("error")
198 |
199 | # Check for improved error handling
200 | improved_error = False
201 | expected_phrases = ["no clip", "empty timeline", "select clip", "ensure_clip_selected"]
202 |
203 | if "error" in result2 and result2.get("error"):
204 | error_msg = str(result2.get("error", "")).lower()
205 | for phrase in expected_phrases:
206 | if phrase in error_msg:
207 | improved_error = True
208 | logger.info(f"✅ Proper error handling detected: '{phrase}' found in error message")
209 | break
210 |
211 | # Clean up - delete the test timeline
212 | delete_result = send_request("mcp_davinci_resolve_delete_timeline",
213 | {"name": empty_timeline_name})
214 |
215 | if success1 and improved_error:
216 | logger.info("✅ Error handling for empty timeline is working properly")
217 | return True
218 | else:
219 | logger.error("❌ Error handling for empty timeline test failed")
220 | logger.error(f" Switch to color page: {'✅ Passed' if success1 else '❌ Failed'}")
221 | logger.error(f" Improved error message: {'✅ Passed' if improved_error else '❌ Failed'}")
222 | return False
223 |
224 | def test_parameter_validation() -> bool:
225 | """Test parameter validation with various types and edge cases."""
226 | logger.info("Testing parameter validation with various types...")
227 |
228 | # Test with different types
229 | tests = [
230 | {"type": "integer", "value": 24, "setting": "timelineFrameRate"},
231 | {"type": "string", "value": "24", "setting": "timelineFrameRate"},
232 | {"type": "float", "value": 23.976, "setting": "timelineFrameRate"},
233 | {"type": "string float", "value": "23.976", "setting": "timelineFrameRate"},
234 | {"type": "boolean", "value": True, "setting": "timelineResolutionWidth"},
235 | {"type": "string boolean", "value": "true", "setting": "timelineResolutionWidth"}
236 | ]
237 |
238 | results = []
239 |
240 | for test in tests:
241 | logger.info(f"Testing {test['type']} parameter: {test['value']}")
242 | result = send_request("mcp_davinci_resolve_set_project_setting",
243 | {"setting_name": test['setting'], "setting_value": test['value']})
244 |
245 | # Consider it a success if no error or if error doesn't mention type validation
246 | success = "error" not in result or not result.get("error") or "type" not in str(result.get("error", "")).lower()
247 | results.append(success)
248 |
249 | if success:
250 | logger.info(f"✅ {test['type']} parameter accepted")
251 | else:
252 | logger.error(f"❌ {test['type']} parameter rejected: {result.get('error', 'Unknown error')}")
253 |
254 | # Final judgment based on how many tests passed
255 | passed = sum(results)
256 | total = len(results)
257 | success_rate = passed / total
258 |
259 | if success_rate >= 0.5: # At least half of the tests should pass
260 | logger.info(f"✅ Parameter validation is working for {passed}/{total} types")
261 | return True
262 | else:
263 | logger.error(f"❌ Parameter validation test failed for {total - passed}/{total} types")
264 | return False
265 |
266 | def print_test_summary(results: Dict[str, bool]) -> None:
267 | """Print a summary of all test results."""
268 | logger.info("\n" + "=" * 50)
269 | logger.info("TEST SUMMARY")
270 | logger.info("=" * 50)
271 |
272 | total = len(results)
273 | passed = sum(results.values())
274 |
275 | for test_name, result in results.items():
276 | status = "✅ PASSED" if result else "❌ FAILED"
277 | logger.info(f"{status} - {test_name}")
278 |
279 | logger.info("-" * 50)
280 | logger.info(f"Total Tests: {total}")
281 | logger.info(f"Passed: {passed}")
282 | logger.info(f"Failed: {total - passed}")
283 | logger.info(f"Success Rate: {passed/total*100:.1f}%")
284 | logger.info("=" * 50)
285 |
286 | def main() -> None:
287 | """Run all tests and report results."""
288 | logger.info("Starting DaVinci Resolve MCP Server tests")
289 | logger.info("=" * 50)
290 |
291 | # Store test results
292 | results = {}
293 |
294 | # Test server connection first
295 | connection_result = test_server_connection()
296 | results["Server Connection"] = connection_result
297 |
298 | # Only continue with other tests if connection is successful
299 | if connection_result:
300 | # Test project settings
301 | results["Project Settings"] = test_project_settings()
302 |
303 | # Test color page operations
304 | results["Color Page Operations"] = test_color_page_operations()
305 |
306 | # Test render queue operations
307 | results["Render Queue Operations"] = test_render_queue_operations()
308 |
309 | # Test error handling with empty timeline
310 | results["Empty Timeline Error Handling"] = test_error_handling_with_empty_timeline()
311 |
312 | # Test parameter validation
313 | results["Parameter Validation"] = test_parameter_validation()
314 | else:
315 | logger.error("Skipping remaining tests due to server connection failure")
316 | results["Project Settings"] = False
317 | results["Color Page Operations"] = False
318 | results["Render Queue Operations"] = False
319 | results["Empty Timeline Error Handling"] = False
320 | results["Parameter Validation"] = False
321 |
322 | # Print test summary
323 | print_test_summary(results)
324 |
325 | # Exit with appropriate status
326 | if all(results.values()):
327 | logger.info("All tests passed!")
328 | sys.exit(0)
329 | else:
330 | logger.error("Some tests failed. Check the logs for details.")
331 | sys.exit(1)
332 |
333 | if __name__ == "__main__":
334 | main()
```
--------------------------------------------------------------------------------
/scripts/batch_automation.py:
--------------------------------------------------------------------------------
```python
1 | #!/usr/bin/env python3
2 | """
3 | DaVinci Resolve MCP Batch Automation
4 | -----------------------------------
5 | This script demonstrates automation of common DaVinci Resolve workflows
6 | using the MCP server API. It provides a command-line interface for
7 | executing predefined sequences of operations.
8 |
9 | Usage:
10 | python batch_automation.py [--workflow=NAME] [--config=FILE]
11 |
12 | Available Workflows:
13 | - color_grade: Apply basic color grading to all clips
14 | - create_proxies: Create proxies for selected media
15 | - render_timeline: Render a timeline with specific settings
16 | - organize_media: Organize media into bins by type
17 |
18 | Requirements:
19 | - DaVinci Resolve must be running
20 | - DaVinci Resolve MCP Server must be running
21 | - requests module (pip install requests)
22 | """
23 |
24 | import os
25 | import sys
26 | import time
27 | import json
28 | import argparse
29 | import logging
30 | import requests
31 | from typing import Dict, Any, List, Tuple, Optional, Callable
32 | from datetime import datetime
33 |
34 | # Configure logging
35 | logging.basicConfig(
36 | level=logging.INFO,
37 | format='%(asctime)s - %(levelname)s - %(message)s',
38 | handlers=[
39 | logging.FileHandler(f"mcp_batch_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"),
40 | logging.StreamHandler()
41 | ]
42 | )
43 | logger = logging.getLogger(__name__)
44 |
45 | # Server configuration
46 | SERVER_URL = "http://localhost:8000/api"
47 |
48 | def send_request(tool_name: str, params: Dict[str, Any]) -> Dict[str, Any]:
49 | """Send a request to the MCP server."""
50 | try:
51 | payload = {
52 | "tool": tool_name,
53 | "params": params
54 | }
55 | logger.info(f"Sending request: {tool_name} with params {params}")
56 | response = requests.post(SERVER_URL, json=payload)
57 | response.raise_for_status()
58 | result = response.json()
59 |
60 | if "error" in result and result["error"]:
61 | logger.error(f"Error in response: {result['error']}")
62 | else:
63 | logger.info(f"Request successful: {tool_name}")
64 |
65 | return result
66 | except requests.exceptions.RequestException as e:
67 | logger.error(f"Request error: {e}")
68 | return {"success": False, "error": str(e)}
69 |
70 | def workflow_step(description: str) -> Callable:
71 | """Decorator for workflow steps with descriptive logging."""
72 | def decorator(func: Callable) -> Callable:
73 | def wrapper(*args, **kwargs):
74 | logger.info(f"STEP: {description}")
75 | logger.info("-" * 40)
76 | result = func(*args, **kwargs)
77 | logger.info("-" * 40)
78 | return result
79 | return wrapper
80 | return decorator
81 |
82 | class WorkflowManager:
83 | """Manages and executes predefined workflows."""
84 |
85 | def __init__(self, config_file: Optional[str] = None):
86 | """Initialize with optional config file."""
87 | self.config = {}
88 | if config_file and os.path.exists(config_file):
89 | with open(config_file, 'r') as f:
90 | self.config = json.load(f)
91 | logger.info(f"Loaded configuration from {config_file}")
92 |
93 | @workflow_step("Creating new project")
94 | def create_project(self, name: str) -> Dict[str, Any]:
95 | """Create a new project with the given name."""
96 | return send_request("mcp_davinci_resolve_create_project", {"name": name})
97 |
98 | @workflow_step("Opening existing project")
99 | def open_project(self, name: str) -> Dict[str, Any]:
100 | """Open an existing project by name."""
101 | return send_request("mcp_davinci_resolve_open_project", {"name": name})
102 |
103 | @workflow_step("Creating new timeline")
104 | def create_timeline(self, name: str) -> Dict[str, Any]:
105 | """Create a new timeline with the given name."""
106 | return send_request("mcp_davinci_resolve_create_timeline", {"name": name})
107 |
108 | @workflow_step("Switching to timeline")
109 | def switch_timeline(self, name: str) -> Dict[str, Any]:
110 | """Switch to a timeline by name."""
111 | return send_request("mcp_davinci_resolve_set_current_timeline", {"name": name})
112 |
113 | @workflow_step("Importing media")
114 | def import_media(self, file_path: str) -> Dict[str, Any]:
115 | """Import a media file into the media pool."""
116 | return send_request("mcp_davinci_resolve_import_media", {"file_path": file_path})
117 |
118 | @workflow_step("Creating media bin")
119 | def create_bin(self, name: str) -> Dict[str, Any]:
120 | """Create a new bin in the media pool."""
121 | return send_request("mcp_davinci_resolve_create_bin", {"name": name})
122 |
123 | @workflow_step("Moving media to bin")
124 | def move_to_bin(self, clip_name: str, bin_name: str) -> Dict[str, Any]:
125 | """Move a media clip to a bin."""
126 | return send_request("mcp_davinci_resolve_move_media_to_bin",
127 | {"clip_name": clip_name, "bin_name": bin_name})
128 |
129 | @workflow_step("Adding clip to timeline")
130 | def add_to_timeline(self, clip_name: str, timeline_name: Optional[str] = None) -> Dict[str, Any]:
131 | """Add a clip to the timeline."""
132 | params = {"clip_name": clip_name}
133 | if timeline_name:
134 | params["timeline_name"] = timeline_name
135 | return send_request("mcp_davinci_resolve_add_clip_to_timeline", params)
136 |
137 | @workflow_step("Switching page")
138 | def switch_page(self, page: str) -> Dict[str, Any]:
139 | """Switch to a specific page in DaVinci Resolve."""
140 | return send_request("mcp_davinci_resolve_switch_page", {"page": page})
141 |
142 | @workflow_step("Adding node")
143 | def add_node(self, node_type: str = "serial", label: Optional[str] = None) -> Dict[str, Any]:
144 | """Add a node to the current grade."""
145 | params = {"node_type": node_type}
146 | if label:
147 | params["label"] = label
148 | return send_request("mcp_davinci_resolve_add_node", params)
149 |
150 | @workflow_step("Setting color wheel parameter")
151 | def set_color_param(self, wheel: str, param: str, value: float,
152 | node_index: Optional[int] = None) -> Dict[str, Any]:
153 | """Set a color wheel parameter for a node."""
154 | params = {"wheel": wheel, "param": param, "value": value}
155 | if node_index is not None:
156 | params["node_index"] = node_index
157 | return send_request("mcp_davinci_resolve_set_color_wheel_param", params)
158 |
159 | @workflow_step("Adding to render queue")
160 | def add_to_render_queue(self, preset_name: str,
161 | timeline_name: Optional[str] = None,
162 | use_in_out_range: bool = False) -> Dict[str, Any]:
163 | """Add a timeline to the render queue."""
164 | params = {"preset_name": preset_name, "use_in_out_range": use_in_out_range}
165 | if timeline_name:
166 | params["timeline_name"] = timeline_name
167 | return send_request("mcp_davinci_resolve_add_to_render_queue", params)
168 |
169 | @workflow_step("Starting render")
170 | def start_render(self) -> Dict[str, Any]:
171 | """Start rendering the jobs in the render queue."""
172 | return send_request("mcp_davinci_resolve_start_render", {"random_string": "batch"})
173 |
174 | @workflow_step("Clearing render queue")
175 | def clear_render_queue(self) -> Dict[str, Any]:
176 | """Clear all jobs from the render queue."""
177 | return send_request("mcp_davinci_resolve_clear_render_queue", {"random_string": "batch"})
178 |
179 | @workflow_step("Setting project setting")
180 | def set_project_setting(self, setting_name: str, setting_value: Any) -> Dict[str, Any]:
181 | """Set a project setting to the specified value."""
182 | return send_request("mcp_davinci_resolve_set_project_setting",
183 | {"setting_name": setting_name, "setting_value": setting_value})
184 |
185 | @workflow_step("Saving project")
186 | def save_project(self) -> Dict[str, Any]:
187 | """Save the current project."""
188 | return send_request("mcp_davinci_resolve_save_project", {"random_string": "batch"})
189 |
190 | def run_workflow_color_grade(self) -> None:
191 | """Run a basic color grading workflow."""
192 | logger.info("Running color grading workflow")
193 |
194 | # Configure parameters from config or use defaults
195 | project_name = self.config.get("project_name", "Color Grade Example")
196 | timeline_name = self.config.get("timeline_name", "Color Timeline")
197 |
198 | # Create or open project
199 | try:
200 | self.open_project(project_name)
201 | except:
202 | self.create_project(project_name)
203 |
204 | # Set up timeline
205 | self.create_timeline(timeline_name)
206 | self.switch_timeline(timeline_name)
207 |
208 | # Import sample media if provided in config
209 | media_files = self.config.get("media_files", [])
210 | for file_path in media_files:
211 | if os.path.exists(file_path):
212 | self.import_media(file_path)
213 | clip_name = os.path.basename(file_path)
214 | self.add_to_timeline(clip_name, timeline_name)
215 |
216 | # Switch to color page and apply basic grade
217 | self.switch_page("color")
218 |
219 | # Add a serial node for primary correction
220 | self.add_node("serial", "Primary")
221 |
222 | # Set some color parameters
223 | # Slightly warm up the midtones
224 | self.set_color_param("gamma", "red", 0.05, 1)
225 | self.set_color_param("gamma", "blue", -0.03, 1)
226 |
227 | # Add another node for contrast
228 | self.add_node("serial", "Contrast")
229 |
230 | # Increase contrast a bit
231 | self.set_color_param("gain", "master", 0.1, 2)
232 | self.set_color_param("lift", "master", -0.05, 2)
233 |
234 | # Save the project
235 | self.save_project()
236 |
237 | logger.info("Color grading workflow completed")
238 |
239 | def run_workflow_render_timeline(self) -> None:
240 | """Run a workflow to render a timeline."""
241 | logger.info("Running render timeline workflow")
242 |
243 | # Configure parameters from config or use defaults
244 | project_name = self.config.get("project_name", "Render Example")
245 | timeline_name = self.config.get("timeline_name", "Render Timeline")
246 | preset_name = self.config.get("render_preset", "YouTube 1080p")
247 |
248 | # Create or open project
249 | try:
250 | self.open_project(project_name)
251 | except:
252 | self.create_project(project_name)
253 |
254 | # Make sure we have a timeline
255 | timeline_exists = False
256 | try:
257 | self.switch_timeline(timeline_name)
258 | timeline_exists = True
259 | except:
260 | self.create_timeline(timeline_name)
261 | self.switch_timeline(timeline_name)
262 |
263 | # If we need to add media, do it only if timeline is new
264 | if not timeline_exists:
265 | media_files = self.config.get("media_files", [])
266 | for file_path in media_files:
267 | if os.path.exists(file_path):
268 | self.import_media(file_path)
269 | clip_name = os.path.basename(file_path)
270 | self.add_to_timeline(clip_name, timeline_name)
271 |
272 | # Configure project settings
273 | self.set_project_setting("timelineFrameRate", "24")
274 |
275 | # Switch to deliver page
276 | self.switch_page("deliver")
277 |
278 | # Clear any existing render jobs
279 | self.clear_render_queue()
280 |
281 | # Add timeline to render queue
282 | self.add_to_render_queue(preset_name, timeline_name)
283 |
284 | # Start rendering
285 | self.start_render()
286 |
287 | logger.info("Render timeline workflow completed")
288 |
289 | def run_workflow_organize_media(self) -> None:
290 | """Run a workflow to organize media into bins."""
291 | logger.info("Running organize media workflow")
292 |
293 | # Configure parameters from config or use defaults
294 | project_name = self.config.get("project_name", "Media Organization")
295 |
296 | # Create or open project
297 | try:
298 | self.open_project(project_name)
299 | except:
300 | self.create_project(project_name)
301 |
302 | # Make sure we're on media page
303 | self.switch_page("media")
304 |
305 | # Create organizational bins
306 | bins = {
307 | "Video": [".mp4", ".mov", ".mxf", ".avi"],
308 | "Audio": [".wav", ".mp3", ".aac", ".m4a"],
309 | "Images": [".png", ".jpg", ".jpeg", ".tiff", ".tif", ".exr"],
310 | "Graphics": [".psd", ".ai", ".eps"]
311 | }
312 |
313 | for bin_name in bins.keys():
314 | self.create_bin(bin_name)
315 |
316 | # Import media if specified
317 | media_files = self.config.get("media_files", [])
318 | imported_clips = []
319 |
320 | for file_path in media_files:
321 | if os.path.exists(file_path):
322 | self.import_media(file_path)
323 | clip_name = os.path.basename(file_path)
324 | imported_clips.append((clip_name, file_path))
325 |
326 | # Organize clips into bins based on extension
327 | for clip_name, file_path in imported_clips:
328 | ext = os.path.splitext(file_path)[1].lower()
329 |
330 | for bin_name, extensions in bins.items():
331 | if ext in extensions:
332 | self.move_to_bin(clip_name, bin_name)
333 | break
334 |
335 | # Save the project
336 | self.save_project()
337 |
338 | logger.info("Media organization workflow completed")
339 |
340 | def run_workflow(self, workflow_name: str) -> None:
341 | """Run a specified workflow by name."""
342 | workflows = {
343 | "color_grade": self.run_workflow_color_grade,
344 | "render_timeline": self.run_workflow_render_timeline,
345 | "organize_media": self.run_workflow_organize_media
346 | }
347 |
348 | if workflow_name in workflows:
349 | logger.info(f"Starting workflow: {workflow_name}")
350 | workflows[workflow_name]()
351 | logger.info(f"Workflow {workflow_name} completed")
352 | else:
353 | logger.error(f"Unknown workflow: {workflow_name}")
354 | logger.info(f"Available workflows: {', '.join(workflows.keys())}")
355 |
356 | def main() -> None:
357 | """Run the batch automation script."""
358 | parser = argparse.ArgumentParser(
359 | description="Automate DaVinci Resolve workflows using MCP Server"
360 | )
361 | parser.add_argument("--workflow", type=str, default="color_grade",
362 | help="Workflow to run (color_grade, render_timeline, organize_media)")
363 | parser.add_argument("--config", type=str, default=None,
364 | help="Path to JSON configuration file")
365 | args = parser.parse_args()
366 |
367 | logger.info("Starting DaVinci Resolve MCP Batch Automation")
368 | logger.info("=" * 50)
369 | logger.info(f"Workflow: {args.workflow}")
370 | logger.info(f"Config file: {args.config or 'Using defaults'}")
371 |
372 | try:
373 | manager = WorkflowManager(args.config)
374 | manager.run_workflow(args.workflow)
375 | except Exception as e:
376 | logger.error(f"Workflow failed: {str(e)}", exc_info=True)
377 |
378 | logger.info("=" * 50)
379 | logger.info("Batch automation completed")
380 |
381 | if __name__ == "__main__":
382 | main()
```