#
tokens: 49013/50000 17/85 files (page 2/5)
lines: on (toggle) GitHub
raw markdown copy reset
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() 
```
Page 2/5FirstPrevNextLast