#
tokens: 33041/50000 4/85 files (page 3/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 3 of 4. Use http://codebase.md/samuelgursky/davinci-resolve-mcp?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

--------------------------------------------------------------------------------
/src/api/delivery_operations.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
DaVinci Resolve Delivery Page Operations
"""

import logging
from typing import Dict, Any, List, Optional, Union, Tuple

logger = logging.getLogger("davinci-resolve-mcp.delivery")

def get_render_presets(resolve) -> List[Dict[str, Any]]:
    """Get all available render presets in the current project.
    
    Args:
        resolve: DaVinci Resolve instance
        
    Returns:
        List of dictionaries containing preset information
    """
    if not resolve:
        logger.error("No connection to DaVinci Resolve")
        return {"error": "No connection to DaVinci Resolve"}
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project is currently open")
        return {"error": "No project is currently open"}
    
    # Switch to the Deliver page
    page = resolve.GetCurrentPage()
    if page != "deliver":
        logger.info(f"Switching from {page} page to deliver page")
        resolve.OpenPage("deliver")
    
    render_settings = current_project.GetRenderSettings()
    if not render_settings:
        logger.error("Failed to get render settings")
        return {"error": "Failed to get render settings"}
    
    presets = []
    
    # Get project presets
    try:
        project_presets = render_settings.GetRenderPresetList()
        for preset in project_presets:
            preset_info = {
                "name": preset,
                "type": "project",
                "details": {}
            }
            
            # Try to get detailed information about the preset
            try:
                if render_settings.SetRenderSettings({"SelectPreset": preset}):
                    format_info = render_settings.GetCurrentRenderFormatAndCodec()
                    if format_info:
                        preset_info["details"]["format"] = format_info["format"]
                        preset_info["details"]["codec"] = format_info["codec"]
                    
                    resolution = render_settings.GetCurrentRenderResolution()
                    if resolution:
                        preset_info["details"]["resolution"] = resolution
                    
                    frame_rate = render_settings.GetCurrentRenderFrameRate()
                    if frame_rate:
                        preset_info["details"]["frame_rate"] = frame_rate
            except Exception as e:
                logger.warning(f"Could not get detailed information for preset {preset}: {str(e)}")
            
            presets.append(preset_info)
    except Exception as e:
        logger.warning(f"Could not get project presets: {str(e)}")
    
    # Get system presets
    try:
        system_presets = render_settings.GetSystemPresetList()
        for preset in system_presets:
            preset_info = {
                "name": preset,
                "type": "system",
                "details": {}
            }
            
            # Similar detailed information retrieval could be added here
            presets.append(preset_info)
    except Exception as e:
        logger.warning(f"Could not get system presets: {str(e)}")
    
    return presets

def add_to_render_queue(
    resolve, 
    preset_name: str, 
    timeline_name: Optional[str] = None,
    use_in_out_range: bool = False,
    render_settings: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """Add the current timeline or a specific timeline to the render queue.
    
    Args:
        resolve: DaVinci Resolve instance
        preset_name: Name of the render preset to use
        timeline_name: Name of the timeline to render (uses current if None)
        use_in_out_range: Whether to render only the in/out range instead of entire timeline
        render_settings: Additional render settings to apply (optional)
        
    Returns:
        Dictionary with status information about the added job
    """
    if not resolve:
        logger.error("No connection to DaVinci Resolve")
        return {"error": "No connection to DaVinci Resolve"}
    
    logger.info(f"Adding timeline to render queue with preset: {preset_name}")
    if timeline_name:
        logger.info(f"Using specified timeline: {timeline_name}")
    else:
        logger.info("Using current timeline")
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project is currently open")
        return {"error": "No project is currently open"}
    
    # Switch to the Deliver page
    page = resolve.GetCurrentPage()
    if page != "deliver":
        logger.info(f"Switching from {page} page to deliver page")
        resolve.OpenPage("deliver")
    
    # Get the timeline to render
    if timeline_name:
        timeline = current_project.GetTimelineByName(timeline_name)
        if not timeline:
            logger.error(f"Timeline '{timeline_name}' not found")
            return {"error": f"Timeline '{timeline_name}' not found"}
        
        # Set it as the current timeline
        current_project.SetCurrentTimeline(timeline)
    else:
        timeline = current_project.GetCurrentTimeline()
        if not timeline:
            logger.error("No timeline is currently open")
            return {"error": "No timeline is currently open"}
    
    # Get timeline name for reporting
    actual_timeline_name = timeline.GetName()
    logger.info(f"Using timeline: {actual_timeline_name}")
    
    # Use our helper function to ensure render settings are initialized
    success, render_settings_interface, message = ensure_render_settings(resolve, current_project)
    if not success or not render_settings_interface:
        logger.error(f"Failed to initialize render settings: {message}")
        return {"error": message}
    
    # Use our helper function to validate the preset
    preset_valid, available_presets, preset_message = validate_render_preset(
        render_settings_interface, preset_name
    )
    if not preset_valid:
        logger.error(f"Invalid preset: {preset_message}")
        return {"error": preset_message}
    
    # Apply the render preset
    settings_to_apply = {"SelectPreset": preset_name}
    logger.info(f"Applying render preset: {preset_name}")
    
    # Add any additional render settings if provided
    if render_settings:
        logger.info(f"Adding additional render settings: {render_settings}")
        settings_to_apply.update(render_settings)
    
    try:
        if not render_settings_interface.SetRenderSettings(settings_to_apply):
            logger.error(f"Failed to apply render preset '{preset_name}'")
            return {"error": f"Failed to apply render preset '{preset_name}'"}
        logger.info("Successfully applied render preset")
    except Exception as e:
        logger.error(f"Error applying render preset: {str(e)}")
        return {"error": f"Error applying render preset: {str(e)}"}
    
    # Add to render queue
    result = False
    logger.info("Adding timeline to render queue")
    try:
        if use_in_out_range:
            logger.info("Using in/out range for render")
            result = current_project.AddRenderJobToRenderQueue()
        else:
            logger.info(f"Adding entire timeline '{actual_timeline_name}' to render queue")
            result = current_project.AddTimelineToRenderQueue(actual_timeline_name)
    except Exception as e:
        logger.error(f"Exception while adding to render queue: {str(e)}")
        
        # Try alternative approach - sometimes a different order helps
        try:
            logger.info("Trying alternative approach for adding to render queue")
            # Sometimes re-applying the preset helps
            render_settings_interface.SetRenderSettings(settings_to_apply)
            
            # Try adding again after a small delay
            import time
            time.sleep(0.5)
            
            if use_in_out_range:
                result = current_project.AddRenderJobToRenderQueue()
            else:
                result = current_project.AddTimelineToRenderQueue(actual_timeline_name)
        except Exception as nested_e:
            logger.error(f"Alternative approach also failed: {str(nested_e)}")
            return {"error": f"Failed to add to render queue: {str(e)}. Alternative approach also failed: {str(nested_e)}"}
    
    if result:
        logger.info(f"Successfully added '{actual_timeline_name}' to render queue with preset '{preset_name}'")
        return {
            "success": True,
            "message": f"Added '{actual_timeline_name}' to render queue with preset '{preset_name}'",
            "timeline": actual_timeline_name,
            "preset": preset_name,
            "in_out_range_only": use_in_out_range
        }
    else:
        logger.error("Failed to add to render queue")
        return {"error": "Failed to add to render queue", "timeline": actual_timeline_name, "preset": preset_name}

def start_render(resolve) -> Dict[str, Any]:
    """Start rendering jobs in the render queue.
    
    Args:
        resolve: DaVinci Resolve instance
        
    Returns:
        Dictionary with status information about the render process
    """
    if not resolve:
        logger.error("No connection to DaVinci Resolve")
        return {"error": "No connection to DaVinci Resolve"}
    
    logger.info("Starting render process")
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project is currently open")
        return {"error": "No project is currently open"}
    
    # Switch to the Deliver page
    page = resolve.GetCurrentPage()
    if page != "deliver":
        logger.info(f"Switching from {page} page to deliver page")
        resolve.OpenPage("deliver")
    
    # Start rendering
    try:
        # Check if there are jobs in the queue
        logger.info("Checking for jobs in render queue")
        try:
            queue_items = current_project.GetRenderJobList()
            if queue_items:
                logger.info(f"Found {len(queue_items)} jobs in render queue")
            else:
                logger.warning("GetRenderJobList() returned None or empty list")
                queue_items = []
        except Exception as e:
            logger.error(f"Error getting render job list: {str(e)}")
            queue_items = []
        
        if not queue_items or len(queue_items) == 0:
            logger.warning("No jobs in render queue")
            return {"warning": "No jobs in render queue", "jobs_count": 0}
        
        # Start the render process
        logger.info("Starting render process")
        try:
            result = current_project.StartRendering()  # Try the newer API first
            if result is None:
                # If newer API returns None, try the older API
                logger.info("Newer StartRendering() API returned None, trying StartRenderingJob()")
                result = current_project.StartRenderingJob()
        except AttributeError:
            # If newer API doesn't exist, fall back to older API
            logger.info("Newer StartRendering() API not available, using StartRenderingJob()")
            result = current_project.StartRenderingJob()
        except Exception as e:
            logger.error(f"Error starting render: {str(e)}")
            result = False
            
        if result:
            logger.info("Render started successfully")
            return {
                "success": True,
                "message": "Render started successfully",
                "jobs_count": len(queue_items)
            }
        else:
            logger.error("Failed to start rendering")
            return {"error": "Failed to start rendering", "jobs_count": len(queue_items)}
            
    except Exception as e:
        logger.error(f"Exception while starting render: {str(e)}")
        return {"error": f"Failed to start rendering: {str(e)}"}

def get_render_queue_status(resolve) -> Dict[str, Any]:
    """Get the status of jobs in the render queue.
    
    Args:
        resolve: DaVinci Resolve instance
        
    Returns:
        Dictionary with information about the render queue status
    """
    if not resolve:
        logger.error("No connection to DaVinci Resolve")
        return {"error": "No connection to DaVinci Resolve"}
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project is currently open")
        return {"error": "No project is currently open"}
    
    # Switch to the Deliver page
    page = resolve.GetCurrentPage()
    if page != "deliver":
        logger.info(f"Switching from {page} page to deliver page")
        resolve.OpenPage("deliver")
    
    try:
        # Get render queue items
        queue_items = current_project.GetRenderJobList()
        
        if not queue_items:
            return {
                "status": "empty",
                "message": "Render queue is empty",
                "jobs": []
            }
            
        # Get details for each job
        jobs = []
        is_rendering = False
        
        for job_id in queue_items:
            job_info = {
                "id": job_id,
                "name": "Unknown",
                "status": "Unknown"
            }
            
            try:
                # Try to get job name (usually timeline name)
                job_info["name"] = current_project.GetRenderJobName(job_id)
                
                # Try to get job status
                status = current_project.GetRenderJobStatus(job_id)
                job_info["status"] = status
                
                # Check if any job is currently rendering
                if status == "Rendering":
                    is_rendering = True
                
                # Try to get additional job properties if available
                try:
                    # Frame progress might be available for rendering jobs
                    progress = current_project.GetRenderJobFrameProgress(job_id)
                    if progress:
                        job_info["progress"] = progress
                        
                    # Get estimated remaining time if available
                    time_remaining = current_project.GetRenderJobEstimatedTimeRemaining(job_id)
                    if time_remaining:
                        job_info["time_remaining"] = time_remaining
                except:
                    # Not all properties are available for all job states
                    pass
                
            except Exception as e:
                logger.warning(f"Could not get details for job {job_id}: {str(e)}")
            
            jobs.append(job_info)
        
        # Determine overall render queue status
        if is_rendering:
            queue_status = "rendering"
        elif any(job["status"] == "Complete" for job in jobs):
            if all(job["status"] == "Complete" for job in jobs):
                queue_status = "complete"
            else:
                queue_status = "partial_complete"
        else:
            queue_status = "ready"
        
        return {
            "status": queue_status,
            "jobs_count": len(jobs),
            "jobs": jobs,
            "is_rendering": is_rendering
        }
            
    except Exception as e:
        logger.error(f"Exception while getting render queue status: {str(e)}")
        return {"error": f"Failed to get render queue status: {str(e)}"}

def clear_render_queue(resolve) -> Dict[str, Any]:
    """Clear all jobs from the render queue.
    
    Args:
        resolve: DaVinci Resolve instance
        
    Returns:
        Dictionary with status information about the operation
    """
    if not resolve:
        logger.error("No connection to DaVinci Resolve")
        return {"error": "No connection to DaVinci Resolve"}
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project is currently open")
        return {"error": "No project is currently open"}
    
    # Switch to the Deliver page
    page = resolve.GetCurrentPage()
    if page != "deliver":
        logger.info(f"Switching from {page} page to deliver page")
        resolve.OpenPage("deliver")
    
    try:
        # Get current jobs count for reporting
        queue_items = current_project.GetRenderJobList()
        initial_count = len(queue_items) if queue_items else 0
        
        if initial_count == 0:
            return {
                "success": True,
                "message": "Render queue is already empty",
                "jobs_removed": 0
            }
        
        # Check if any jobs are currently rendering
        is_rendering = False
        for job_id in queue_items:
            try:
                status = current_project.GetRenderJobStatus(job_id)
                if status == "Rendering":
                    is_rendering = True
                    break
            except:
                pass
        
        # If jobs are rendering, we need to stop rendering first
        if is_rendering:
            logger.info("Stopping active rendering before clearing queue")
            try:
                current_project.StopRendering()
                # Small delay to allow DaVinci Resolve to update job statuses
                import time
                time.sleep(0.5)  
            except Exception as e:
                logger.warning(f"Issue stopping rendering: {str(e)}")
        
        # Clear the render queue
        result = current_project.DeleteAllRenderJobs()
        
        if result:
            return {
                "success": True,
                "message": "Render queue cleared successfully",
                "jobs_removed": initial_count,
                "was_rendering": is_rendering
            }
        else:
            return {"error": "Failed to clear render queue", "jobs_count": initial_count}
            
    except Exception as e:
        logger.error(f"Exception while clearing render queue: {str(e)}")
        return {"error": f"Failed to clear render queue: {str(e)}"}

def ensure_render_settings(resolve, current_project) -> Tuple[bool, Optional[Any], str]:
    """Ensures render settings interface is properly initialized.
    
    Args:
        resolve: The DaVinci Resolve instance
        current_project: The current project
        
    Returns:
        Tuple containing (success, render_settings_object, message)
    """
    logger.info("Attempting to get render settings interface")
    try:
        render_settings_interface = current_project.GetRenderSettings()
        if render_settings_interface:
            logger.info("Successfully got render settings interface")
            return True, render_settings_interface, "Render settings interface successfully obtained"
        else:
            logger.warning("GetRenderSettings() returned None")
    except Exception as e:
        logger.error(f"Error getting render settings interface: {str(e)}")
        render_settings_interface = None
    
    # Alternative approach if render settings is None
    logger.warning("Failed to get render settings interface, trying alternative approaches")
    
    # Try refreshing the deliver page
    try:
        logger.info("Trying to refresh the deliver page")
        current_page = resolve.GetCurrentPage()
        if current_page != "deliver":
            resolve.OpenPage("deliver")
        else:
            # If already on deliver, try switching to another page and back
            resolve.OpenPage("edit")
            resolve.OpenPage("deliver")
        
        # Try getting render settings again
        render_settings_interface = current_project.GetRenderSettings()
        if render_settings_interface:
            logger.info("Successfully got render settings interface after page refresh")
            return True, render_settings_interface, "Render settings interface obtained after page refresh"
    except Exception as e:
        logger.error(f"Error in page refresh approach: {str(e)}")
    
    # Try one more approach - sometimes a delay helps
    try:
        logger.info("Trying with a small delay")
        import time
        time.sleep(1.0)  # Short delay
        
        render_settings_interface = current_project.GetRenderSettings()
        if render_settings_interface:
            logger.info("Successfully got render settings interface after delay")
            return True, render_settings_interface, "Render settings interface obtained after delay"
    except Exception as e:
        logger.error(f"Error in delay approach: {str(e)}")
    
    # If still no render settings, we've failed
    logger.error("Could not get render settings interface after multiple attempts")
    return False, None, "Failed to access render settings. Deliver page functionality may not be fully initialized."

def validate_render_preset(render_settings_interface, preset_name: str) -> Tuple[bool, List[str], str]:
    """Validates that a render preset exists and returns available presets.
    
    Args:
        render_settings_interface: The render settings interface
        preset_name: Name of the preset to validate
        
    Returns:
        Tuple containing (is_valid, available_presets, message)
    """
    logger.info("Checking if preset exists")
    try:
        project_presets = render_settings_interface.GetRenderPresetList() or []
        system_presets = render_settings_interface.GetSystemPresetList() or []
        
        all_presets = project_presets + system_presets
        logger.info(f"Found {len(project_presets)} project presets and {len(system_presets)} system presets")
        
        if preset_name in project_presets:
            logger.info(f"Found '{preset_name}' in project presets")
            return True, all_presets, f"Valid project preset: {preset_name}"
        elif preset_name in system_presets:
            logger.info(f"Found '{preset_name}' in system presets")
            return True, all_presets, f"Valid system preset: {preset_name}"
        else:
            logger.error(f"Render preset '{preset_name}' not found")
            return False, all_presets, f"Preset '{preset_name}' not found. Available presets: {', '.join(all_presets)}"
    except Exception as e:
        logger.error(f"Error while checking presets: {str(e)}")
        return False, [], f"Error checking render presets: {str(e)}" 
```

--------------------------------------------------------------------------------
/src/api/media_operations.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
DaVinci Resolve Media Operations
"""

import logging
import os
from typing import List, Dict, Any

logger = logging.getLogger("davinci-resolve-mcp.media")

def list_media_pool_clips(resolve) -> List[Dict[str, Any]]:
    """List all clips in the media pool of the current project."""
    if resolve is None:
        return [{"error": "Not connected to DaVinci Resolve"}]
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return [{"error": "Failed to get Project Manager"}]
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return [{"error": "No project currently open"}]
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return [{"error": "Failed to get Media Pool"}]
    
    # Get the root folder and all its clips
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return [{"error": "Failed to get Root Folder"}]
    
    clips = root_folder.GetClipList()
    
    # Format clip info
    clip_info = []
    for clip in clips:
        if clip:
            clip_info.append({
                "name": clip.GetName(),
                "type": clip.GetClipProperty()["Type"],
                "duration": clip.GetClipProperty()["Duration"],
                "fps": clip.GetClipProperty().get("FPS", "Unknown")
            })
    
    return clip_info if clip_info else [{"info": "No clips found in the media pool"}]

def import_media(resolve, file_path: str) -> str:
    """Import a media file into the current project's media pool."""
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    # Validate file path
    if not file_path:
        return "Error: File path cannot be empty"
    
    if not os.path.exists(file_path):
        return f"Error: File '{file_path}' does not exist"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Import the media file
    # DaVinci Resolve API expects a list of file paths
    imported_media = media_pool.ImportMedia([file_path])
    
    if imported_media and len(imported_media) > 0:
        return f"Successfully imported '{os.path.basename(file_path)}'"
    else:
        return f"Failed to import '{file_path}'. The file may be in an unsupported format."

def create_bin(resolve, name: str) -> str:
    """Create a new bin/folder in the media pool."""
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    if not name:
        return "Error: Bin name cannot be empty"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get the root folder to add the bin to
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    # Check if bin already exists (by checking the subfolders)
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder.GetName() == name:
            return f"Error: Bin '{name}' already exists"
    
    # Create the bin
    new_bin = media_pool.AddSubFolder(root_folder, name)
    
    if new_bin:
        return f"Successfully created bin '{name}'"
    else:
        return f"Failed to create bin '{name}'"

def list_bins(resolve) -> List[Dict[str, Any]]:
    """List all bins/folders in the media pool of the current project."""
    if resolve is None:
        return [{"error": "Not connected to DaVinci Resolve"}]
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return [{"error": "Failed to get Project Manager"}]
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return [{"error": "No project currently open"}]
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return [{"error": "Failed to get Media Pool"}]
    
    # Get the root folder
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return [{"error": "Failed to get Root Folder"}]
    
    # Get all subfolders (bins) from the root folder
    folders = root_folder.GetSubFolderList()
    
    # Format bin info
    bin_info = []
    
    # Add root folder information
    bin_info.append({
        "name": root_folder.GetName() or "Master",
        "is_root": True,
        "clip_count": len(root_folder.GetClipList())
    })
    
    # Add all subfolders (bins)
    for folder in folders:
        if folder:
            bin_info.append({
                "name": folder.GetName(),
                "is_root": False,
                "clip_count": len(folder.GetClipList())
            })
    
    return bin_info if len(bin_info) > 1 else [{"info": "No bins found in the media pool"}]

def get_bin_contents(resolve, bin_name: str) -> List[Dict[str, Any]]:
    """Get the contents of a specific bin/folder in the media pool.
    
    Args:
        resolve: The DaVinci Resolve instance
        bin_name: The name of the bin to get contents from. Use 'Master' for the root folder.
    """
    if resolve is None:
        return [{"error": "Not connected to DaVinci Resolve"}]
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return [{"error": "Failed to get Project Manager"}]
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return [{"error": "No project currently open"}]
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return [{"error": "Failed to get Media Pool"}]
    
    # Get the root folder
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return [{"error": "Failed to get Root Folder"}]
    
    # Check if we want the root folder (Master)
    if bin_name.lower() == "master" or bin_name == root_folder.GetName():
        clips = root_folder.GetClipList()
        return format_clip_list(clips, "Master")
    
    # Otherwise search for the bin in subfolders
    folders = root_folder.GetSubFolderList()
    target_folder = None
    
    for folder in folders:
        if folder and folder.GetName() == bin_name:
            target_folder = folder
            break
    
    if not target_folder:
        return [{"error": f"Bin '{bin_name}' not found in Media Pool"}]
    
    # Get clips from the target folder
    clips = target_folder.GetClipList()
    return format_clip_list(clips, bin_name)

def format_clip_list(clips, bin_name: str) -> List[Dict[str, Any]]:
    """Helper function to format clip info from a clip list."""
    if not clips:
        return [{"info": f"No clips found in bin '{bin_name}'"}]
    
    clip_info = []
    for clip in clips:
        if clip:
            properties = clip.GetClipProperty()
            clip_info.append({
                "name": clip.GetName(),
                "type": properties.get("Type", "Unknown"),
                "duration": properties.get("Duration", "Unknown"),
                "fps": properties.get("FPS", "Unknown"),
                "resolution": f"{properties.get('Width', '?')}x{properties.get('Height', '?')}",
                "bin": bin_name
            })
    
    return clip_info

def list_timeline_clips(resolve) -> List[Dict[str, Any]]:
    """List all clips in the current timeline."""
    if resolve is None:
        return [{"error": "Not connected to DaVinci Resolve"}]
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return [{"error": "Failed to get Project Manager"}]
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return [{"error": "No project currently open"}]
    
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        return [{"error": "No timeline currently active"}]
    
    # Get all video tracks
    video_tracks = current_timeline.GetTrackCount("video")
    
    clip_info = []
    for track_index in range(1, video_tracks + 1):
        # Note: Track indices in Resolve API are 1-based
        clips = current_timeline.GetItemListInTrack("video", track_index)
        
        for clip in clips:
            if clip:
                clip_info.append({
                    "name": clip.GetName(),
                    "track": f"V{track_index}",
                    "start_frame": clip.GetStart(),
                    "end_frame": clip.GetEnd(),
                    "duration": clip.GetDuration()
                })
    
    # Get audio tracks as well
    audio_tracks = current_timeline.GetTrackCount("audio")
    for track_index in range(1, audio_tracks + 1):
        clips = current_timeline.GetItemListInTrack("audio", track_index)
        
        for clip in clips:
            if clip:
                clip_info.append({
                    "name": clip.GetName(),
                    "track": f"A{track_index}",
                    "start_frame": clip.GetStart(),
                    "end_frame": clip.GetEnd(),
                    "duration": clip.GetDuration()
                })
    
    return clip_info if clip_info else [{"info": "No clips found in the current timeline"}]

def add_clip_to_timeline(resolve, clip_name: str, timeline_name: str = None) -> str:
    """Add a media pool clip to the timeline.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_name: Name of the clip in the media pool
        timeline_name: Optional timeline to target (uses current if not specified)
    """
    if not resolve:
        return "Error: Not connected to DaVinci Resolve"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
        
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get all clips in root folder
    root_folder = media_pool.GetRootFolder()
    clips = root_folder.GetClipList()
    
    target_clip = None
    for clip in clips:
        if clip.GetName() == clip_name:
            target_clip = clip
            break
    
    if not target_clip:
        return f"Error: Clip '{clip_name}' not found in Media Pool"
    
    # Get the target timeline
    timeline = None
    if timeline_name:
        # Switch to the specified timeline
        timeline_count = current_project.GetTimelineCount()
        for i in range(1, timeline_count + 1):
            t = current_project.GetTimelineByIndex(i)
            if t and t.GetName() == timeline_name:
                timeline = t
                current_project.SetCurrentTimeline(timeline)
                break
        
        if not timeline:
            return f"Error: Timeline '{timeline_name}' not found"
    else:
        # Use current timeline
        timeline = current_project.GetCurrentTimeline()
        if not timeline:
            return "Error: No timeline currently active"
    
    # Add clip to timeline
    # We need to use media_pool.AppendToTimeline() which expects a list of clips
    result = media_pool.AppendToTimeline([target_clip])
    
    if result and len(result) > 0:
        return f"Successfully added clip '{clip_name}' to timeline"
    else:
        return f"Failed to add clip '{clip_name}' to timeline"

def delete_media(resolve, clip_name: str) -> str:
    """Delete a media clip from the media pool by name.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_name: Name of the clip to delete
        
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get all clips in root folder and all subfolders
    all_clips = []
    target_clip = None
    
    # Get the root folder
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    # Get clips from root folder
    root_clips = root_folder.GetClipList()
    if root_clips:
        all_clips.extend(root_clips)
    
    # Get clips from subfolders
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder:
            folder_clips = folder.GetClipList()
            if folder_clips:
                all_clips.extend(folder_clips)
    
    # Find the clip by name
    for clip in all_clips:
        if clip and clip.GetName() == clip_name:
            target_clip = clip
            break
    
    if not target_clip:
        return f"Error: Clip '{clip_name}' not found in Media Pool"
    
    # Delete the clip
    try:
        result = media_pool.DeleteClips([target_clip])
        if result:
            return f"Successfully deleted clip '{clip_name}' from Media Pool"
        else:
            return f"Failed to delete clip '{clip_name}' from Media Pool"
    except Exception as e:
        return f"Error deleting clip: {str(e)}"

def move_media_to_bin(resolve, clip_name: str, bin_name: str) -> str:
    """Move a media clip to a specific bin in the media pool.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_name: Name of the clip to move
        bin_name: Name of the target bin
        
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get the root folder
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    # Find the target bin
    target_folder = None
    
    # Check if we want the root folder
    if bin_name.lower() == "master" or bin_name == root_folder.GetName():
        target_folder = root_folder
    else:
        # Search in subfolders
        folders = root_folder.GetSubFolderList()
        for folder in folders:
            if folder and folder.GetName() == bin_name:
                target_folder = folder
                break
    
    if not target_folder:
        return f"Error: Bin '{bin_name}' not found in Media Pool"
    
    # Find the clip by name
    all_clips = []
    target_clip = None
    
    # Get clips from root folder
    root_clips = root_folder.GetClipList()
    if root_clips:
        all_clips.extend(root_clips)
    
    # Get clips from subfolders
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder:
            folder_clips = folder.GetClipList()
            if folder_clips:
                all_clips.extend(folder_clips)
    
    # Find the clip by name
    for clip in all_clips:
        if clip and clip.GetName() == clip_name:
            target_clip = clip
            break
    
    if not target_clip:
        return f"Error: Clip '{clip_name}' not found in Media Pool"
    
    # Move the clip to the target bin
    try:
        result = media_pool.MoveClips([target_clip], target_folder)
        if result:
            return f"Successfully moved clip '{clip_name}' to bin '{bin_name}'"
        else:
            return f"Failed to move clip '{clip_name}' to bin '{bin_name}'"
    except Exception as e:
        return f"Error moving clip: {str(e)}"

def auto_sync_audio(resolve, clip_names: List[str], sync_method: str = "waveform", 
                   append_mode: bool = False, target_bin: str = None) -> str:
    """Sync audio between clips with customizable settings.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_names: List of clip names to sync
        sync_method: Method to use for synchronization - 'waveform' or 'timecode'
        append_mode: Whether to append the audio or replace it
        target_bin: Optional bin to move synchronized clips to
        
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    if not clip_names or len(clip_names) < 2:
        return "Error: At least two clips are required for audio synchronization"
    
    # Validate sync method
    if sync_method not in ["waveform", "timecode"]:
        return "Error: Sync method must be 'waveform' or 'timecode'"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get all clips from media pool
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    all_clips = []
    
    # Get clips from root folder
    root_clips = root_folder.GetClipList()
    if root_clips:
        all_clips.extend(root_clips)
    
    # Get clips from subfolders
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder:
            folder_clips = folder.GetClipList()
            if folder_clips:
                all_clips.extend(folder_clips)
    
    # Find clips by name
    clips_to_sync = []
    for name in clip_names:
        found = False
        for clip in all_clips:
            if clip and clip.GetName() == name:
                clips_to_sync.append(clip)
                found = True
                break
        if not found:
            return f"Error: Clip '{name}' not found in Media Pool"
    
    # Set the clips as selected in media pool
    try:
        result = media_pool.SetCurrentFolder(root_folder)
        if not result:
            return "Error: Failed to set current folder to root"
            
        result = media_pool.SelectClips(clips_to_sync)
        if not result:
            return "Error: Failed to select clips for syncing"
        
        # Set sync options
        sync_options = {
            "syncMethod": 0 if sync_method == "waveform" else 1,  # 0 for waveform, 1 for timecode
            "appendMode": append_mode  # True to append audio, False to replace
        }
        
        # Perform the auto sync
        result = media_pool.AutoSyncAudio(sync_options)
        
        if not result:
            return "Error: Failed to sync audio for the selected clips"
        
        # If a target bin is specified, move the synced clips there
        if target_bin:
            target_folder = None
            
            # Check if we want the root folder
            if target_bin.lower() == "master" or target_bin == root_folder.GetName():
                target_folder = root_folder
            else:
                # Search in subfolders
                for folder in folders:
                    if folder and folder.GetName() == target_bin:
                        target_folder = folder
                        break
            
            if not target_folder:
                return f"Warning: Synced clips but bin '{target_bin}' not found for moving clips"
            
            # Move the synced clips to the target bin
            move_result = media_pool.MoveClips(clips_to_sync, target_folder)
            if not move_result:
                return f"Warning: Synced clips but failed to move them to bin '{target_bin}'"
        
        return f"Successfully synced audio for {len(clips_to_sync)} clips using {sync_method} method"
        
    except Exception as e:
        return f"Error syncing audio: {str(e)}"

def unlink_clips(resolve, clip_names: List[str]) -> str:
    """Unlink specified clips, disconnecting them from their media files.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_names: List of clip names to unlink
        
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    if not clip_names or len(clip_names) == 0:
        return "Error: No clip names provided for unlinking"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get all clips from media pool
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    all_clips = []
    
    # Get clips from root folder
    root_clips = root_folder.GetClipList()
    if root_clips:
        all_clips.extend(root_clips)
    
    # Get clips from subfolders
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder:
            folder_clips = folder.GetClipList()
            if folder_clips:
                all_clips.extend(folder_clips)
    
    # Find clips by name
    clips_to_unlink = []
    not_found_clips = []
    
    for name in clip_names:
        found = False
        for clip in all_clips:
            if clip and clip.GetName() == name:
                clips_to_unlink.append(clip)
                found = True
                break
        if not found:
            not_found_clips.append(name)
    
    if not_found_clips:
        return f"Error: Clips not found in Media Pool: {', '.join(not_found_clips)}"
    
    if not clips_to_unlink:
        return "Error: No valid clips found to unlink"
    
    try:
        # Unlink the clips
        result = media_pool.UnlinkClips(clips_to_unlink)
        
        if result:
            return f"Successfully unlinked {len(clips_to_unlink)} clips"
        else:
            return "Error: Failed to unlink clips"
        
    except Exception as e:
        return f"Error unlinking clips: {str(e)}"

def relink_clips(resolve, clip_names: List[str], media_paths: List[str] = None, 
                folder_path: str = None, recursive: bool = False) -> str:
    """Relink specified clips to their media files.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_names: List of clip names to relink
        media_paths: Optional list of specific media file paths to use for relinking
        folder_path: Optional folder path to search for media files
        recursive: Whether to search the folder path recursively
        
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    if not clip_names or len(clip_names) == 0:
        return "Error: No clip names provided for relinking"
    
    if media_paths is None and folder_path is None:
        return "Error: Either media_paths or folder_path must be provided for relinking"
    
    if media_paths is not None and folder_path is not None:
        return "Error: Cannot specify both media_paths and folder_path, choose one approach"
    
    if media_paths is not None and len(media_paths) > 0 and len(media_paths) != len(clip_names):
        return "Error: If providing media_paths, the number must match the number of clip_names"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get all clips from media pool
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    all_clips = []
    
    # Get clips from root folder
    root_clips = root_folder.GetClipList()
    if root_clips:
        all_clips.extend(root_clips)
    
    # Get clips from subfolders
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder:
            folder_clips = folder.GetClipList()
            if folder_clips:
                all_clips.extend(folder_clips)
    
    # Find clips by name
    clips_to_relink = []
    not_found_clips = []
    
    for name in clip_names:
        found = False
        for clip in all_clips:
            if clip and clip.GetName() == name:
                clips_to_relink.append(clip)
                found = True
                break
        if not found:
            not_found_clips.append(name)
    
    if not_found_clips:
        return f"Error: Clips not found in Media Pool: {', '.join(not_found_clips)}"
    
    if not clips_to_relink:
        return "Error: No valid clips found to relink"
    
    try:
        # Relink the clips
        result = False
        
        if media_paths:
            # Relink with specific media paths
            # Note: The API expects clips and paths to be matched by index
            result = media_pool.RelinkClips(clips_to_relink, media_paths)
        else:
            # Relink by searching in folder path
            relink_options = {"recursive": recursive}
            result = media_pool.RelinkClips(clips_to_relink, [], folder_path, relink_options)
        
        if result:
            if media_paths:
                return f"Successfully relinked {len(clips_to_relink)} clips to specified media paths"
            else:
                return f"Successfully relinked {len(clips_to_relink)} clips using media from {folder_path}"
        else:
            if media_paths:
                return "Error: Failed to relink clips to specified media paths"
            else:
                return f"Error: Failed to relink clips using media from {folder_path}"
        
    except Exception as e:
        return f"Error relinking clips: {str(e)}"

def create_sub_clip(resolve, clip_name: str, start_frame: int, end_frame: int, 
                    sub_clip_name: str = None, bin_name: str = None) -> str:
    """Create a subclip from the specified clip using in and out points.
    
    Args:
        resolve: The DaVinci Resolve instance
        clip_name: Name of the source clip
        start_frame: Start frame (in point)
        end_frame: End frame (out point)
        sub_clip_name: Optional name for the subclip (defaults to original name with '_subclip')
        bin_name: Optional bin to place the subclip in
        
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    if not clip_name:
        return "Error: Clip name cannot be empty"
    
    if start_frame >= end_frame:
        return "Error: Start frame must be less than end frame"
    
    if start_frame < 0:
        return "Error: Start frame cannot be negative"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    media_pool = current_project.GetMediaPool()
    if not media_pool:
        return "Error: Failed to get Media Pool"
    
    # Get all clips from media pool
    root_folder = media_pool.GetRootFolder()
    if not root_folder:
        return "Error: Failed to get Root Folder"
    
    all_clips = []
    
    # Get clips from root folder
    root_clips = root_folder.GetClipList()
    if root_clips:
        all_clips.extend(root_clips)
    
    # Get clips from subfolders
    folders = root_folder.GetSubFolderList()
    for folder in folders:
        if folder:
            folder_clips = folder.GetClipList()
            if folder_clips:
                all_clips.extend(folder_clips)
    
    # Find the source clip
    source_clip = None
    for clip in all_clips:
        if clip and clip.GetName() == clip_name:
            source_clip = clip
            break
    
    if not source_clip:
        return f"Error: Source clip '{clip_name}' not found in Media Pool"
    
    # Set the target folder for the subclip
    target_folder = root_folder
    if bin_name:
        target_folder = None
        # Check if it's the root folder
        if bin_name.lower() == "master" or bin_name == root_folder.GetName():
            target_folder = root_folder
        else:
            # Search for the bin in subfolders
            for folder in folders:
                if folder and folder.GetName() == bin_name:
                    target_folder = folder
                    break
            
            if not target_folder:
                return f"Error: Target bin '{bin_name}' not found in Media Pool"
    
    # Set the current folder to the target folder
    result = media_pool.SetCurrentFolder(target_folder)
    if not result:
        return f"Error: Failed to set current folder to '{target_folder.GetName()}'"
    
    # Generate a subclip name if not provided
    if not sub_clip_name:
        sub_clip_name = f"{clip_name}_subclip"
    
    # Set in/out points for the source clip
    result = source_clip.SetMarkInOut(start_frame, end_frame)
    if not result:
        return f"Error: Failed to set in/out points on clip '{clip_name}'"
    
    try:
        # Create the subclip using CreateSubClip
        sub_clip = media_pool.CreateSubClip(sub_clip_name, source_clip)
        
        if sub_clip:
            # Clear the mark in/out points from the source clip
            source_clip.ClearMarkInOut()
            
            return f"Successfully created subclip '{sub_clip_name}' from frames {start_frame} to {end_frame}"
        else:
            # Clear the mark in/out points if subclip creation failed
            source_clip.ClearMarkInOut()
            
            return f"Error: Failed to create subclip from '{clip_name}'"
        
    except Exception as e:
        # Always clear the mark in/out points in case of exceptions
        try:
            source_clip.ClearMarkInOut()
        except:
            pass
        
        return f"Error creating subclip: {str(e)}" 
```

--------------------------------------------------------------------------------
/docs/FEATURES.md:
--------------------------------------------------------------------------------

```markdown
# DaVinci Resolve MCP Server Features

This document tracks the implementation status of features in the DaVinci Resolve MCP (Multi-Client Protocol) Server. It is organized by feature categories and provides details on implementation status, compatibility with clients, and any known issues.

## Implementation Status

The MCP server implements nearly all features from the DaVinci Resolve scripting API, but our testing has revealed that while we have implemented 202 features (100%), only a small percentage have been verified working on macOS (8%), with many features still needing verification (82%) or having known issues (10%).

Testing has primarily been conducted on macOS, with Windows support implemented but requiring thorough testing. Each feature in this document is marked with symbols indicating its current status:

**Status Key:**
- ✅ - Implemented and verified working
- ⚠️ - Implemented but needs testing/verification
- 🐞 - Implemented but has known issues
- 🟡 - Planned feature
- 🚫 - Not implemented/supported

The compatibility columns indicate whether a feature is known to work with specific clients (Cursor/Claude) on specific platforms (Mac/Windows).

## Feature Categories

## Status Definitions

✅ - **Implemented & Verified**: Feature is fully implemented and verified working  
⚠️ - **Implemented with Limitations**: Feature works but has known limitations or requirements  
🔄 - **In progress**: Feature is in development or testing phase  
🟡 - **Planned**: Feature is planned but not yet implemented  
❌ - **Not implemented**: Feature will not be implemented  
🚫 - **Not applicable**: Feature is not applicable to the current platform  
🐞 - **Implementation Issues**: Feature is implemented but has known bugs  

## Client/Platform Compatibility Update

| Client | macOS | Windows | Linux |
|--------|-------|---------|-------|
| Cursor | ✅ Stable | ⚠️ Needs Testing | ❌ |
| Claude Desktop | ✅ Stable | ⚠️ Needs Testing | ❌ |

## Implementation Methods

| Method | Status | Notes |
|--------|--------|-------|
| MCP Framework | 🐞 | Original implementation - connection issues |
| Direct JSON-RPC | ✅ | Current implementation - more reliable |

## Feature Statistics

| Category | Total Features | Implemented | Verified (Mac) | Verified (Win) | Not Verified | Failed |
|----------|----------------|-------------|----------------|----------------|--------------|--------|
| Core Features | 9 | 9 (100%) | 4 (44%) | 0 (0%) | 3 (33%) | 2 (22%) |
| General Resolve API | 14 | 14 (100%) | 6 (43%) | 0 (0%) | 5 (36%) | 3 (21%) |
| Project Management | 18 | 18 (100%) | 2 (11%) | 0 (0%) | 15 (83%) | 1 (6%) |
| Timeline Operations | 12 | 12 (100%) | 2 (17%) | 0 (0%) | 8 (67%) | 2 (16%) |
| Media Pool Operations | 18 | 18 (100%) | 0 (0%) | 0 (0%) | 16 (89%) | 2 (11%) |
| Color Page Operations | 16 | 16 (100%) | 0 (0%) | 0 (0%) | 14 (88%) | 2 (12%) |
| Delivery Page Operations | 12 | 12 (100%) | 1 (8%) | 0 (0%) | 10 (84%) | 1 (8%) |
| Fusion Page Operations | 0 | 0 (0%) | 0 (0%) | 0 (0%) | 0 (0%) | 0 (0%) |
| Fairlight Page Operations | 0 | 0 (0%) | 0 (0%) | 0 (0%) | 0 (0%) | 0 (0%) |
| Media Storage Operations | 0 | 0 (0%) | 0 (0%) | 0 (0%) | 0 (0%) | 0 (0%) |
| Audio Sync | 4 | 4 (100%) | 0 (0%) | 0 (0%) | 4 (100%) | 0 (0%) |
| Cache Management | 3 | 3 (100%) | 1 (33%) | 0 (0%) | 2 (67%) | 0 (0%) |
| Proxy Media Management | 6 | 6 (100%) | 0 (0%) | 0 (0%) | 5 (83%) | 1 (17%) |
| Transcription Services | 6 | 6 (100%) | 0 (0%) | 0 (0%) | 5 (83%) | 1 (17%) |
| Object Methods | 84 | 84 (100%) | 1 (1%) | 0 (0%) | 79 (94%) | 4 (5%) |
| **TOTAL** | **202** | **202 (100%)** | **17 (8%)** | **0 (0%)** | **166 (82%)** | **19 (10%)** |

**Status Key:**
- ✅ - Implemented and verified working
- ⚠️ - Implemented but needs testing/verification
- 🐞 - Implemented but has known issues
- 🟡 - Planned feature
- 🚫 - Not implemented/supported

## Core Features

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Connect to Resolve | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Establish connection to DaVinci Resolve |
| Switch to Page | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Switch between Media, Edit, Color, etc. - Verified working |
| Get Current Page | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get current active page |
| Get Resolve Version | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get DaVinci Resolve version |
| Get Product Name | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get product name (Studio or free) |
| Object Inspection | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Introspect API objects, methods, and properties |
| Error Handling | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Error messages exist but could be more informative |

### Project Management

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| List Projects | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get list of available projects |
| Get Current Project Name | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get name of currently open project |
| Open Project | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Open project by name - Verified working |
| Create Project | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create new project - Cannot recreate existing projects |
| Save Project | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Save current project |
| Close Project | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Close current project |
| Project Properties | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Get and set project settings - Parameter type issues |
| SuperScale Settings | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Control super scale quality - Not verified |
| Timeline Frame Rate | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Control timeline frame rates - Not verified |
| Export/Import Project | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Import/export project files - Not verified |
| Archive Project | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Archive projects with media - Not verified |
| Cloud Project Operations | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create and manage cloud projects - Not verified |
| Project Folders | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create and navigate project folders - Not verified |
| Project Presets | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Apply and manage project presets - Not verified |
| Load Time/Performance | 🟡 | - | - | - | - | Project load time and performance metrics |
| Project Analytics | 🟡 | - | - | - | - | Project usage and statistics |
| Collaborative Projects | 🟡 | - | - | - | - | Manage collaborative workflows |
| Database Management | 🟡 | - | - | - | - | PostgreSQL and local database operations |
| Project Templates | 🟡 | - | - | - | - | Save and load project templates |

### Timeline Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Create Timeline | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Create timeline - Failed with existing names without clear error |
| List Timelines | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get all timelines in project - Verified working |
| Get Current Timeline | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get current active timeline |
| Set Current Timeline | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Switch to specified timeline - Verified working |
| Add Timeline Marker | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add marker at position - Requires valid frame within timeline bounds |
| Delete Timeline Marker | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Delete marker at position - Not verified |
| Manage Timeline Tracks | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add/remove video and audio tracks - Not verified |
| Get Timeline Items | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get clips in timeline - Not verified |
| Timecode Operations | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get/set current timecode - Not verified |
| Timeline Settings | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Manage timeline settings - Not verified |
| Timeline Generators | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Insert generators into timeline - Not verified |
| Timeline OFX | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Insert OFX plugins into timeline - Not verified |
| Timeline Import/Export | 🟡 | - | - | - | - | Import/export timeline formats |
| Scene Detection | 🟡 | - | - | - | - | Detect scene cuts automatically |
| Auto Subtitle Creation | 🟡 | - | - | - | - | Generate subtitles from audio |

### Media Pool Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Import Media | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Import media files |
| List Media | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | List media pool clips |
| Create Bins | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Create folders in media pool - Verified working |
| Organize Media | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Move clips between folders |
| Add to Timeline | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Add clips to timeline |
| Clip Properties | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get/set clip properties |
| Clip Markers | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Add/manage clip markers |
| Metadata Management | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Get/set clip metadata |
| Media Relinking | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Relink/unlink media files |
| Audio Sync | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Sync audio between clips |
| Proxy Media | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Link/unlink proxy media |
| Clip Transcription | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Transcribe audio in clips |
| Bulk Import | 🟡 | - | - | - | - | Batch import operations |
| Smart Bins | 🟡 | - | - | - | - | Create/manage smart bins |

### Media Storage Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Get Mounted Volumes | 🟡 | - | - | - | - | List mounted storage devices |
| Browse Folders | 🟡 | - | - | - | - | Navigate folder structure |
| List Media Files | 🟡 | - | - | - | - | List media in folders |
| Reveal in Storage | 🟡 | - | - | - | - | Highlight file in browser |

### Color Page Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Apply LUTs | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Apply LUTs to clips |
| Color Correction | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Adjust color parameters |
| Get/Set Grades | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Manage color grades |
| Node Management | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Work with node graph - Note: May require clips with existing grade objects |
| Gallery Operations | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Save/load looks from gallery |
| Color Wheels | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Adjust lift/gamma/gain - Note: Requires clips with existing grade objects |
| Grade Versions | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Manage color versions |
| Export Grades | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Export grades as files |
| Color Groups | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Group clips for color |
| Node Cache | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Control node caching |
| Flag Management | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Add/remove clip flags |
| Color Space | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Color space controls |
| Magic Mask | 🟡 | - | - | - | - | AI-based masking |
| Track/Window | 🟡 | - | - | - | - | Motion tracking operations |
| HDR Grading | 🟡 | - | - | - | - | High dynamic range controls |
| Face Refinement | 🟡 | - | - | - | - | Automated face enhancement |

### Delivery Page Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Add Render Job | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Add job to render queue - Failed with "'NoneType' object is not callable" |
| Start Rendering | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Start render process - Not verified |
| List Render Jobs | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get all queued render jobs - Not verified |
| Delete Render Jobs | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove jobs from queue - Not verified |
| Clear Render Queue | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Clear render queue - Verified working |
| Get Render Presets | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List available presets - Not verified |
| Render Status | 🟡 | - | - | - | - | Check render progress |
| Export Settings | 🟡 | - | - | - | - | Configure render settings |
| Format Control | 🟡 | - | - | - | - | Control output format/codec |
| Quick Export | 🟡 | - | - | - | - | RenderWithQuickExport |
| Batch Rendering | 🟡 | - | - | - | - | Manage multiple render jobs |

### Specialized Features

#### Object Inspection

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Get Object Properties | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get object properties - Not verified |
| List Available Methods | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List API methods for object - Not verified |
| Get API Version | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get DaVinci Resolve API version - Not verified |
| Get Supported Objects | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List supported API object types - Not verified |
| Interactive Inspection | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Testing/debugging interface - Not verified |

#### Layout Presets

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Get UI Layout Presets | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List available layout presets - Not verified |
| Set UI Layout Preset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Switch to a specific UI layout - Not verified |
| Save Current Layout | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Save current UI as layout preset - Not verified |
| Delete Layout Preset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove a custom layout preset - Not verified |

#### App Control

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Quit Application | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Safely close DaVinci Resolve - Not verified (not testing to avoid closing app) |
| Restart Application | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Restart DaVinci Resolve - Not verified (not testing to avoid disruption) |
| Save All Projects | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Save all open projects - Not verified |
| Check Application Status | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Verify if application is running - Not verified |

#### Cloud Project Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| List Cloud Projects | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List projects in cloud library - Not verified |
| Create Cloud Project | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create new project in cloud - Not verified |
| Open Cloud Project | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Open project from cloud library - Not verified |
| Delete Cloud Project | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove project from cloud - Not verified |
| Export Project to Cloud | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Upload local project to cloud - Not verified |
| Import Project from Cloud | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Download cloud project locally - Not verified |

#### Audio Sync Features

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Auto-sync Audio | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Automatic audio synchronization - Not verified |
| Waveform Analysis | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Sync based on waveform matching - Not verified |
| Timecode Sync | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Sync based on embedded timecode - Not verified |
| Multi-clip Sync | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Sync multiple clips simultaneously - Not verified |
| Append Track Mode | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Option to append or replace audio - Not verified |
| Manual Offset Adjustment | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Fine-tune sync with manual offset - Not verified |

#### Proxy Media Management

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Link Proxy Media | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Link proxy files to clips - Not verified |
| Unlink Proxy Media | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove proxy file associations - Not verified |
| Set Proxy Mode | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Toggle between proxy/original - Failed during testing |
| Set Proxy Quality | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Configure proxy resolution - Failed with "Failed to set proxy quality" |
| Proxy Generation | 🟡 | - | - | - | - | Generate proxy media files |
| Batch Proxy Operations | 🟡 | - | - | - | - | Process multiple clips at once |

#### Cache Management

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Set Cache Mode | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Control cache utilization - Note: May require specific project setup |
| Set Optimized Media Mode | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Toggle optimized media usage - Note: May require specific project setup |
| Set Proxy Mode | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Toggle proxy mode - Note: May require specific project setup |
| Set Proxy Quality | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Configure proxy quality |
| Clear Cache | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Delete cached render files |
| Cache Settings | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Configure cache parameters |
| Generate Optimized Media | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Create optimized media |
| Delete Optimized Media | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Remove optimized media files |

#### Transcription Services

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Transcribe Audio | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Generate text from audio - Failed with clip not found error |
| Clear Transcription | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove existing transcription - Not verified |
| Set Transcription Language | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Select language for transcription - Not verified |
| Export Transcription | 🟡 | - | - | - | - | Save transcription to file |
| Transcribe Multiple Clips | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Batch transcription processing - Not verified |
| Edit Transcription | 🟡 | - | - | - | - | Modify generated text |

## Object-Specific Methods

### Timeline Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| GetName | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get timeline name - Not verified |
| GetStartFrame | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get first frame number - Not verified |
| GetEndFrame | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get last frame number - Not verified |
| GetTrackCount | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Count tracks by type - Not verified |
| GetItemListInTrack | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get clips in track - Not verified |
| AddMarker | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add marker at frame - Not verified |
| GetMarkers | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get all timeline markers - Not verified |
| DeleteMarkerAtFrame | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove marker at position - Not verified |
| DeleteMarkersByColor | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove markers by color - Not verified |
| DeleteAllMarkers | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Clear all markers - Not verified |
| ApplyGradeFromDRX | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Apply grade from file - Not verified |
| GetSetting | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get timeline setting - Not verified |
| SetSetting | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Change timeline setting - Not verified |
| InsertGeneratorIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add generator clip - Not verified |
| InsertOFXGeneratorIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add OFX generator - Not verified |
| InsertFusionGeneratorIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add Fusion generator - Not verified |
| InsertTitleIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add title clip - Not verified |
| InsertFusionTitleIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add Fusion title - Not verified |
| InsertOFXTitleIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add OFX title - Not verified |
| DuplicateTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create timeline copy - Not verified |
| CreateCompoundClip | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Group clips together - Not verified |
| CreateFusionClip | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Convert to Fusion clip - Not verified |
| ImportIntoTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Import timeline file - Not verified |
| Export | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Export timeline file - Not verified |

### TimelineItem Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| GetName | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get clip name - Not verified |
| GetDuration | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get clip duration - Not verified |
| GetStart | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get start frame - Not verified |
| GetEnd | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get end frame - Not verified |
| GetLeftOffset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get left handle length - Not verified |
| GetRightOffset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get right handle length - Not verified |
| GetProperty | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get clip property - Not verified |
| SetProperty | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Change clip property - Not verified |
| AddMarker | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add marker at offset - Not verified |
| GetMarkers | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get all clip markers - Not verified |
| DeleteMarkerAtFrame | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove marker at position - Not verified |
| DeleteMarkersByColor | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove markers by color - Not verified |
| DeleteAllMarkers | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Clear all markers - Not verified |
| AddFusionComp | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add Fusion composition - Not verified |
| ImportFusionComp | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Import Fusion composition - Not verified |
| ExportFusionComp | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Export Fusion composition - Not verified |

### Project Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| GetName | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get project name - Not verified |
| GetPresetList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get available presets - Not verified |
| SetPreset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Apply preset to project - Not verified |
| AddRenderJob | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Add job to render queue - Failed in our testing |
| DeleteAllRenderJobs | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Clear render queue - Verified working |
| StartRendering | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Begin render process - Not verified |
| StopRendering | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Abort render process - Not verified |
| IsRenderingInProgress | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Check render status - Not verified |
| SetRenderFormat | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Set output format - Not verified |
| LoadLayoutPreset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Apply UI layout - Not verified |
| SaveLayoutPreset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Store current UI layout - Not verified |
| ExportLayoutPreset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Save layout to file - Not verified |
| DeleteLayoutPreset | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove saved layout - Not verified |
| GetSetting | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get project setting - Not verified |
| SetSetting | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Change project setting - Failed with parameter type issues |
| GetRenderJobStatus | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get job progress info - Not verified |
| GetRenderPresetList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List render presets - Not verified |
| ImportRenderPresets | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Import presets file - Not verified |
| ExportRenderPresets | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Export presets to file - Not verified |
| GetCurrentRenderFormatAndCodec | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get format settings - Not verified |
| SetCurrentRenderFormatAndCodec | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Set format settings - Not verified |

### MediaPool Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| GetRootFolder | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get root media folder - Not verified |
| AddSubFolder | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Create new subfolder - Failed with existing folder name |
| CreateEmptyTimeline | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Create blank timeline - Failed with existing name |
| AppendToTimeline | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add clips to timeline - Not verified |
| ImportMedia | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Import media files - Not verified |
| ExportMetadata | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Export clip metadata - Not verified |
| DeleteClips | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove clips from pool - Not verified |
| MoveClips | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Move clips between bins - Not verified |
| GetCurrentFolder | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get active folder - Not verified |
| SetCurrentFolder | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Switch active folder - Not verified |
| GetClipMatteList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get clip matte files - Not verified |
| AddClipMatte | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Add matte to clip - Not verified |
| DeleteClipMatte | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove clip matte - Not verified |
| RelinkClips | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Reconnect media files - Not verified |
| UnlinkClips | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Disconnect media files - Not verified |
| LinkProxyMedia | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Connect proxy media - Not verified |
| UnlinkProxyMedia | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove proxy links - Not verified |
| ReplaceClip | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Replace with new media - Not verified |

### Gallery Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| GetAlbumName | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get current album name - Not verified |
| SetAlbumName | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Rename current album - Not verified |
| GetCurrentAlbum | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get active album - Not verified |
| SetCurrentAlbum | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Switch to album - Not verified |
| GetAlbumList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List all albums - Not verified |
| CreateAlbum | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create new album - Not verified |
| DeleteAlbum | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove album - Not verified |
| GetStillList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List album stills - Not verified |
| DeleteStill | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Delete still - Not verified |
| ExportStills | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Save stills to files - Not verified |
| ImportStills | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Load stills from files - Not verified |

### ColorPage Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| GetLUTs | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get available LUTs - Not verified |
| GetCurrentNode | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get active color node - Not verified |
| GetNodeList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List all color nodes - Not verified |
| SelectNode | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Switch active node - Not verified |
| AddNode | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Add new node - Failed with "Cannot access grade object" |
| DeleteNode | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove node - Not verified |
| SetPrimaryColorGrade | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Apply primary correction - Not verified |
| SetColorWheelPrimaryParam | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Adjust primary wheel - Failed with "Cannot access grade object" |
| SetColorWheelLogParam | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Adjust log wheel - Not verified |
| GetKeyframeMode | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get keyframe mode - Not verified |
| SetKeyframeMode | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Set keyframe mode - Not verified |
| ApplyLUT | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Apply LUT to node - Not verified |
| ExportLUT | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Export node as LUT - Not verified |
| GetColorVersion | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get current version - Not verified |
| GetColorVersions | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List all versions - Not verified |
| CreateColorVersion | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create new version - Not verified |
| DeleteColorVersion | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove version - Not verified |
| LoadColorVersion | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Switch to version - Not verified |
| GetColorGroupList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List color groups - Not verified |
| CreateColorGroup | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Create new group - Not verified |
| DeleteColorGroup | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove group - Not verified |

### Delivery Object Methods

| Method | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|--------|---------------|--------------|--------------|--------------|--------------|-------|
| AddRenderJob | 🐞 | 🐞 | 🐞 | ⚠️ | ⚠️ | Add to render queue - Failed in our testing |
| DeleteRenderJob | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Remove render job - Not verified |
| DeleteAllRenderJobs | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Clear render queue - Verified working |
| GetRenderJobList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List queued jobs - Not verified |
| GetRenderPresetList | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List available presets - Not verified |
| GetRenderFormats | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List output formats - Not verified |
| GetRenderCodecs | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | List available codecs - Not verified |
| RenderJobStatus | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Get job status - Not verified |
| IsRenderingInProgress | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Check render activity - Not verified |
| StartRendering | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Begin render process - Not verified |
| StopRendering | ⚠️ | ⚠️ | ⚠️ | ⚠️ | ⚠️ | Cancel rendering - Not verified |

## Implementation Details

### Object Inspection

The object inspection implementation provides comprehensive functionality for:

1. **API Exploration** - Inspect Resolve API objects to discover methods and properties
2. **Method Analysis** - Get detailed information about object methods and their parameters
3. **Property Inspection** - Access object properties with type information
4. **Python Integration** - Combines Python's introspection with structured output
5. **Documentation Generation** - Can be used to create documentation for API objects

### Layout Presets

The layout presets implementation enables:

1. **Preset Management** - List, save, load, export, and import UI layout presets
2. **User Interface Customization** - Store and recall different UI layouts for different tasks
3. **Workflow Optimization** - Quick switching between different interface configurations
4. **Cross-Project Sharing** - Export and import layouts between different projects or systems

### App Control

The app control implementation provides:

1. **Application Management** - Functions to control the Resolve application itself
2. **State Monitoring** - Check application state and version information
3. **Settings Access** - Open project settings and preferences dialogs
4. **Session Control** - Safely quit or restart the application programmatically

### Cloud Project Operations

The cloud project operations implementation provides:

1. **Cloud Project Creation** - Create new cloud projects with specified settings
2. **Project Restoration** - Restore cloud projects from online storage
3. **Import Functionality** - Import cloud projects into the local database
4. **User Management** - Add, remove, and manage users for collaborative workflow
5. **Export Functions** - Export local projects to cloud storage

### Audio Synchronization

The audio synchronization implementation supports:

1. **Multi-camera workflows** - Synchronizing video clips from multiple cameras with separate audio
2. **External audio sources** - Integrating audio from external recorders
3. **Sync method options** - Support for both waveform analysis and timecode-based synchronization
4. **Organization workflow** - Automatic organization of synced clips into dedicated bins

The implementation supports these parameters:

1. **clip_names** - List of clips to synchronize
2. **sync_method** - "waveform" (audio pattern matching) or "timecode" (TC matching)
3. **append_mode** - Toggle between appending audio tracks or replacing audio
4. **target_bin** - Optional bin name for organization

### Proxy Media Management

Comprehensive proxy media functionality for:

1. **Proxy workflow support** - Connecting high-resolution clips to lower-resolution proxy media
2. **Performance optimization** - Improving playback performance with proxy media
3. **Quality toggling** - Easily switching between proxy and full-resolution media
4. **Path management** - Maintaining proper file paths for proxies
5. **Quality settings** - Control proxy generation quality (quarter, half, three-quarter, full)

### Cache Management  

The cache management implementation provides detailed control over:

1. **Cache Modes** - Control over cache usage with Auto/On/Off settings  
2. **Optimized Media** - Management of optimized media settings and generation
3. **Proxy Media** - Control of proxy media settings, quality, and usage
4. **Mode Selection** - Simple mode selection with human-friendly options

## Planned Features

Next development priorities:

1. **Fusion Page Integration** - Access to Fusion scripting and composition management
2. **Fairlight Page Operations** - Audio editing and mixing functionality
3. **Media Storage Management** - Advanced media storage and organization tools
4. **Render Job Operations** - Comprehensive render queue management with job ID support
5. **Timeline Export Properties** - Export formats including AAF, XML, EDL, etc.
6. **Windows Platform Compatibility** - Ensuring full functionality across platforms

### Fairlight Page Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Audio Levels | 🟡 | - | - | - | - | Control audio levels |
| Audio Effects | 🟡 | - | - | - | - | Apply audio effects |
| Audio Routing | 🟡 | - | - | - | - | Configure audio routing |
| Audio Metering | 🟡 | - | - | - | - | Monitor audio levels |
| Track Management | 🟡 | - | - | - | - | Add/remove/edit audio tracks |
| Sound Libraries | 🟡 | - | - | - | - | Access sound effects libraries |
| Voice Isolation | 🟡 | - | - | - | - | AI-powered voice separation |
| Noise Removal | 🟡 | - | - | - | - | Audio cleanup tools |

### Fusion Page Integration

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Fusion Composition | 🟡 | - | - | - | - | Create/edit Fusion compositions |
| Node Graph | 🟡 | - | - | - | - | Work with Fusion node graph |
| Add Effects | 🟡 | - | - | - | - | Add visual effects nodes |
| Animation | 🟡 | - | - | - | - | Animate properties and parameters |
| Templates | 🟡 | - | - | - | - | Use/save effect templates |
| 3D Objects | 🟡 | - | - | - | - | Work with 3D elements |
| Particle Systems | 🟡 | - | - | - | - | Create and edit particle effects |
| Text Generation | 🟡 | - | - | - | - | Create text effects and animations |

### Edit Page Operations

| Feature | Implementation | Cursor (Mac) | Claude (Mac) | Cursor (Win) | Claude (Win) | Notes |
|---------|---------------|--------------|--------------|--------------|--------------|-------|
| Clip Editing | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Edit clip properties |
| Transitions | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Add/edit transitions |
| Effects | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Apply video effects |
| Generators | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Add titles, solids, etc. |
| Speed Effects | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | Control clip playback speed |
| Dynamic Zoom | 🟡 | - | - | - | - | Ken Burns style effects |
| Stabilization | 🟡 | - | - | - | - | Video stabilization tools |
| Smart Reframe | 🟡 | - | - | - | - | AI-based reframing for different aspect ratios |

## Testing Summary

During our testing process, we've identified several key issues and limitations:

1. **Color Page Operations**: Several color-related operations failed with "Cannot access grade object" errors, including AddNode and SetColorWheelPrimaryParam. These issues may be related to the current project state or clip selection.

2. **Delivery Operations**: Adding render jobs to the queue consistently failed in our tests, though clearing the render queue works correctly.

3. **Media Pool Operations**: Some operations such as creating new bins and timelines failed when existing items with the same name were present, indicating a need for better error handling or checking.

4. **Proxy and Transcription**: Proxy and transcription operations failed with various issues, including "Clip not found" errors, suggesting the need for better media management integration.

5. **Project Settings**: Setting project settings failed with parameter type issues, suggesting a mismatch between the expected and provided parameter formats.

### Next Steps

Based on our testing, we recommend:

1. Implementing better error handling and validation in the API endpoints
2. Adding more robust logging for troubleshooting
3. Creating comprehensive test cases for each feature category
4. Focusing on fixing the most critical issues in color grading and rendering first
5. Adding better documentation for parameter types and expected formats

The MCP server has good fundamental implementation but requires significant testing and debugging to reach production readiness.

```

--------------------------------------------------------------------------------
/src/api/color_operations.py:
--------------------------------------------------------------------------------

```python
#!/usr/bin/env python3
"""
DaVinci Resolve Color Page Operations
"""

import logging
from typing import Dict, Any, List, Optional, Tuple, Union

logger = logging.getLogger("davinci-resolve-mcp.color")

def get_current_node(resolve) -> Dict[str, Any]:
    """Get information about the current node in the color page.
    
    Args:
        resolve: The DaVinci Resolve instance
    
    Returns:
        Dictionary with current node information
    """
    if resolve is None:
        return {"error": "Not connected to DaVinci Resolve"}
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return {"error": "No project currently open"}
    
    # First, ensure we're on the color page
    current_page = resolve.GetCurrentPage()
    if current_page.lower() != "color":
        return {"error": f"Not on Color page. Current page is: {current_page}"}
    
    # Get the current timeline
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        return {"error": "No timeline currently active"}
    
    try:
        # Access color-specific functionality through the timeline
        # First get the current clip in the timeline
        current_clip = current_timeline.GetCurrentVideoItem()
        if not current_clip:
            return {"error": "No clip is currently selected in the timeline"}
        
        # Get the clip's grade
        current_grade = current_clip.GetCurrentGrade()
        if not current_grade:
            return {"error": "Failed to get current grade"}
        
        # Get the currently selected node
        current_node_index = current_grade.GetCurrentNode()
        if current_node_index < 1:
            return {"error": "No node is currently selected"}
        
        # Get node count
        node_count = current_grade.GetNodeCount()
        
        # Get information about the current node
        node_info = {
            "clip_name": current_clip.GetName(),
            "node_index": current_node_index,
            "node_count": node_count,
            "is_serial": current_grade.IsSerial(current_node_index),
            "is_parallel": current_grade.IsParallel(current_node_index),
            "is_layer": current_grade.IsLayer(current_node_index),
        }
        
        # Try to get node name
        try:
            node_name = current_grade.GetNodeName(current_node_index)
            node_info["name"] = node_name
        except:
            node_info["name"] = f"Node {current_node_index}"
        
        # Try to get additional node properties if available
        try:
            # Check for common node properties that might be available
            properties = {}
            
            # Check if node is enabled
            try:
                properties["enabled"] = current_grade.IsNodeEnabled(current_node_index)
            except:
                pass
            
            # Get node type if available
            try:
                properties["type"] = current_grade.GetNodeType(current_node_index)
            except:
                pass
            
            # Add properties if we found any
            if properties:
                node_info["properties"] = properties
        except:
            pass
        
        return node_info
        
    except Exception as e:
        return {"error": f"Error getting current node: {str(e)}"}

def apply_lut(resolve, lut_path: str, node_index: int = None) -> str:
    """Apply a LUT to a node in the color page.
    
    Args:
        resolve: The DaVinci Resolve instance
        lut_path: Path to the LUT file to apply
        node_index: Index of the node to apply the LUT to (uses current node if None)
    
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    # Validate LUT path
    if not lut_path:
        return "Error: LUT path cannot be empty"
    
    import os
    if not os.path.exists(lut_path):
        return f"Error: LUT file '{lut_path}' does not exist"
    
    # Check file extension for supported LUT types
    valid_extensions = ['.cube', '.3dl', '.lut', '.mga']
    file_extension = os.path.splitext(lut_path)[1].lower()
    if file_extension not in valid_extensions:
        return f"Error: Unsupported LUT file format. Supported formats: {', '.join(valid_extensions)}"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    # First, ensure we're on the color page
    current_page = resolve.GetCurrentPage()
    if current_page.lower() != "color":
        # Try to switch to color page
        result = resolve.OpenPage("color")
        if not result:
            return f"Error: Failed to switch to Color page. Current page is: {current_page}"
    
    # Get the current timeline
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        return "Error: No timeline currently active"
    
    try:
        # Get the current clip in the timeline
        current_clip = current_timeline.GetCurrentVideoItem()
        if not current_clip:
            return "Error: No clip is currently selected in the timeline"
        
        # Get the clip's grade
        current_grade = current_clip.GetCurrentGrade()
        if not current_grade:
            return "Error: Failed to get current grade"
        
        # Determine which node to apply the LUT to
        target_node_index = node_index
        if target_node_index is None:
            # Use the currently selected node
            target_node_index = current_grade.GetCurrentNode()
            if target_node_index < 1:
                return "Error: No node is currently selected"
        else:
            # Validate the provided node index
            node_count = current_grade.GetNodeCount()
            if target_node_index < 1 or target_node_index > node_count:
                return f"Error: Invalid node index {target_node_index}. Valid range: 1-{node_count}"
        
        # Apply the LUT to the node
        result = current_grade.ApplyLUT(target_node_index, lut_path)
        
        if result:
            # Try to get the node name for a better message
            try:
                node_name = current_grade.GetNodeName(target_node_index)
                return f"Successfully applied LUT '{os.path.basename(lut_path)}' to node '{node_name}' (index {target_node_index})"
            except:
                return f"Successfully applied LUT '{os.path.basename(lut_path)}' to node {target_node_index}"
        else:
            return f"Failed to apply LUT to node {target_node_index}"
        
    except Exception as e:
        return f"Error applying LUT: {str(e)}"

def add_node(resolve, node_type: str = "serial", label: str = None) -> str:
    """Add a new node to the current grade in the color page.
    
    Args:
        resolve: The DaVinci Resolve instance
        node_type: Type of node to add. Options: 'serial', 'parallel', 'layer'
        label: Optional label/name for the new node
    
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    # Validate node type
    valid_node_types = ['serial', 'parallel', 'layer']
    if node_type.lower() not in valid_node_types:
        return f"Error: Invalid node type. Must be one of: {', '.join(valid_node_types)}"
    
    logger.info(f"Adding {node_type} node with label: {label if label else 'None'}")
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project currently open")
        return "Error: No project currently open"
    
    # First, ensure we're on the color page
    current_page = resolve.GetCurrentPage()
    if current_page.lower() != "color":
        # Try to switch to color page
        logger.info(f"Currently on {current_page} page, switching to color page")
        result = resolve.OpenPage("color")
        if not result:
            logger.error(f"Failed to switch to Color page. Current page is: {current_page}")
            return f"Error: Failed to switch to Color page. Current page is: {current_page}"
        logger.info("Successfully switched to color page")
    
    # Get the current timeline
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        logger.error("No timeline currently active")
        return "Error: No timeline currently active"
    
    try:
        # Use the helper function to ensure a clip is selected
        clip_selected, current_clip, message = ensure_clip_selected(resolve, current_timeline)
        
        if not clip_selected or not current_clip:
            logger.error("No clip could be selected automatically")
            return f"Error: {message}. Please select a clip manually in DaVinci Resolve."
        
        logger.info(f"Working with clip: {current_clip.GetName()}")
        
        # Get the clip's grade
        # This is where the NoneType error typically occurs
        logger.info("Attempting to get current grade")
        
        # First method: Direct approach
        try:
            current_grade = current_clip.GetCurrentGrade()
            if current_grade:
                logger.info("Successfully got current grade using GetCurrentGrade()")
            else:
                logger.warning("GetCurrentGrade() returned None")
        except Exception as e:
            logger.error(f"Error getting current grade via GetCurrentGrade(): {str(e)}")
            current_grade = None
        
        # Alternative approach if the first method failed
        if not current_grade:
            logger.info("Attempting alternative methods to access grade functionality")
            
            # Try to select the clip first to ensure it's active
            try:
                # Ensure clip is selected in the timeline
                logger.info("Trying to select the clip in timeline again")
                current_timeline.SetCurrentVideoItem(current_clip)
                logger.info(f"Selected clip {current_clip.GetName()} in timeline")
                
                # Try to get grade again after selection
                current_grade = current_clip.GetCurrentGrade()
                if current_grade:
                    logger.info("Successfully got current grade after selection")
            except Exception as e:
                logger.error(f"Error in alternative selection approach: {str(e)}")
        
        # Direct node creation if we still don't have a grade object
        if not current_grade:
            logger.warning("Could not get grade object, attempting direct node creation")
            
            try:
                # Try using the node graph directly through ColorPage
                color_page = project_manager.GetCurrentPage()
                if color_page and hasattr(color_page, "GetNodeGraph"):
                    node_graph = color_page.GetNodeGraph()
                    if node_graph:
                        logger.info("Successfully got node graph through ColorPage")
                        
                        # Direct node creation using node graph
                        if node_type.lower() == "serial":
                            result = node_graph.AddSerialNode()
                        elif node_type.lower() == "parallel":
                            result = node_graph.AddParallelNode()
                        elif node_type.lower() == "layer":
                            result = node_graph.AddLayerNode()
                        
                        if result:
                            return f"Successfully added {node_type} node using direct NodeGraph approach"
                        else:
                            logger.error(f"Failed to add {node_type} node using NodeGraph")
            except Exception as e:
                logger.error(f"Error in direct node creation attempt: {str(e)}")
            
            return f"Error adding {node_type} node: Cannot access grade object. The clip may not be properly graded yet."
        
        logger.info("Proceeding with node addition with valid grade object")
        # Add the appropriate type of node
        result = False
        method_name = ""
        
        if node_type.lower() == "serial":
            # Add a serial node after the current node
            method_name = "AddSerialNode"
            logger.info(f"Calling {method_name}()")
            result = current_grade.AddSerialNode()
        elif node_type.lower() == "parallel":
            # Add a parallel node
            method_name = "AddParallelNode"
            logger.info(f"Calling {method_name}()")
            result = current_grade.AddParallelNode()
        elif node_type.lower() == "layer":
            # Add a layer node
            method_name = "AddLayerNode"
            logger.info(f"Calling {method_name}()")
            result = current_grade.AddLayerNode()
        
        if not result:
            logger.error(f"Failed to add {node_type} node using {method_name}()")
            return f"Failed to add {node_type} node using {method_name}()"
        
        # Get the new node count and find the newly added node
        new_node_count = current_grade.GetNodeCount()
        logger.info(f"New node count: {new_node_count}")
        
        # Get the new node index - it should be the currently selected node
        new_node_index = current_grade.GetCurrentNode()
        logger.info(f"New node index: {new_node_index}")
        
        # Set label if provided
        if label and new_node_index > 0:
            try:
                logger.info(f"Setting node label to '{label}'")
                current_grade.SetNodeLabel(new_node_index, label)
                node_label_info = f" with label '{label}'"
            except Exception as e:
                logger.warning(f"Failed to set node label: {str(e)}")
                node_label_info = f" (couldn't set label to '{label}')"
        else:
            node_label_info = ""
        
        logger.info(f"Successfully added {node_type} node (index {new_node_index}){node_label_info}")
        return f"Successfully added {node_type} node (index {new_node_index}){node_label_info}"
        
    except Exception as e:
        logger.error(f"Error adding {node_type} node: {str(e)}")
        return f"Error adding {node_type} node: {str(e)}"

def copy_grade(resolve, source_clip_name: str = None, target_clip_name: str = None, mode: str = "full") -> str:
    """Copy a grade from one clip to another in the color page.
    
    Args:
        resolve: The DaVinci Resolve instance
        source_clip_name: Name of the source clip to copy grade from (uses current clip if None)
        target_clip_name: Name of the target clip to apply grade to (uses current clip if None)
        mode: What to copy - 'full' (entire grade), 'current_node', or 'all_nodes'
    
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    # Validate copy mode
    valid_modes = ['full', 'current_node', 'all_nodes']
    if mode.lower() not in valid_modes:
        return f"Error: Invalid copy mode. Must be one of: {', '.join(valid_modes)}"
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return "Error: No project currently open"
    
    # First, ensure we're on the color page
    current_page = resolve.GetCurrentPage()
    if current_page.lower() != "color":
        # Try to switch to color page
        result = resolve.OpenPage("color")
        if not result:
            return f"Error: Failed to switch to Color page. Current page is: {current_page}"
    
    # Get the current timeline
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        return "Error: No timeline currently active"
    
    try:
        # Get all clips in the timeline
        all_video_clips = []
        
        # Get video track count
        video_track_count = current_timeline.GetTrackCount("video")
        
        # Gather all clips from video tracks
        for track_index in range(1, video_track_count + 1):
            track_items = current_timeline.GetItemListInTrack("video", track_index)
            if track_items:
                all_video_clips.extend(track_items)
        
        # Get the source clip
        source_clip = None
        if source_clip_name:
            # Find the source clip by name
            for clip in all_video_clips:
                if clip and clip.GetName() == source_clip_name:
                    source_clip = clip
                    break
            
            if not source_clip:
                return f"Error: Source clip '{source_clip_name}' not found in timeline"
        else:
            # Use the current clip as source
            source_clip = current_timeline.GetCurrentVideoItem()
            if not source_clip:
                return "Error: No clip is currently selected to use as source"
            source_clip_name = source_clip.GetName()
        
        # Get the source grade
        source_grade = source_clip.GetCurrentGrade()
        if not source_grade:
            return f"Error: Failed to get grade from source clip '{source_clip_name}'"
        
        # Get the target clip
        target_clip = None
        if target_clip_name:
            # Check if target is same as source
            if target_clip_name == source_clip_name:
                return f"Error: Source and target clips cannot be the same (both are '{source_clip_name}')"
            
            # Find the target clip by name
            for clip in all_video_clips:
                if clip and clip.GetName() == target_clip_name:
                    target_clip = clip
                    break
            
            if not target_clip:
                return f"Error: Target clip '{target_clip_name}' not found in timeline"
        else:
            # Use the current clip as target (need to select a different clip first)
            current_clip = current_timeline.GetCurrentVideoItem()
            
            if not current_clip:
                return "Error: No clip is currently selected to use as target"
            
            if current_clip.GetName() == source_clip_name:
                return "Error: Cannot copy grade to the same clip. Please specify a different target clip."
            
            target_clip = current_clip
            target_clip_name = target_clip.GetName()
        
        # Get the target grade
        target_grade = target_clip.GetCurrentGrade()
        if not target_grade:
            return f"Error: Failed to get grade from target clip '{target_clip_name}'"
        
        # Select the target clip to make it active for grade operations
        current_timeline.SetCurrentVideoItem(target_clip)
        
        # Execute the copy based on the specified mode
        result = False
        if mode.lower() == "full":
            # Copy the entire grade including all nodes
            result = target_clip.CopyGrade(source_clip)
        elif mode.lower() == "current_node":
            # Copy only the current node from source to target
            source_node_index = source_grade.GetCurrentNode()
            target_node_index = target_grade.GetCurrentNode()
            
            if source_node_index < 1:
                return "Error: No node selected in source clip"
            
            if target_node_index < 1:
                return "Error: No node selected in target clip"
            
            # Copy the current node
            result = target_grade.CopyFromNodeToNode(source_grade, source_node_index, target_node_index)
        elif mode.lower() == "all_nodes":
            # Copy all nodes but keep other grade settings
            source_node_count = source_grade.GetNodeCount()
            
            if source_node_count < 1:
                return "Error: Source clip has no nodes to copy"
            
            # First, clear all nodes in target
            target_node_count = target_grade.GetNodeCount()
            for i in range(target_node_count, 0, -1):
                target_grade.DeleteNode(i)
            
            # Then, add nodes matching the source structure
            for i in range(1, source_node_count + 1):
                # Determine node type
                if source_grade.IsSerial(i):
                    target_grade.AddSerialNode()
                elif source_grade.IsParallel(i):
                    target_grade.AddParallelNode()
                elif source_grade.IsLayer(i):
                    target_grade.AddLayerNode()
                
                # Copy node settings
                new_node_index = target_grade.GetCurrentNode()
                if new_node_index > 0:
                    target_grade.CopyFromNodeToNode(source_grade, i, new_node_index)
            
            result = True
        
        if result:
            return f"Successfully copied grade from '{source_clip_name}' to '{target_clip_name}' using mode '{mode}'"
        else:
            return f"Failed to copy grade from '{source_clip_name}' to '{target_clip_name}' using mode '{mode}'"
        
    except Exception as e:
        return f"Error copying grade: {str(e)}"

def get_color_wheels(resolve, node_index: int = None) -> Dict[str, Any]:
    """Get color wheel parameters for a specific node.
    
    Args:
        resolve: The DaVinci Resolve instance
        node_index: Index of the node to get color wheels from (uses current node if None)
    
    Returns:
        Dictionary with color wheel parameters
    """
    if resolve is None:
        return {"error": "Not connected to DaVinci Resolve"}
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        return {"error": "Failed to get Project Manager"}
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        return {"error": "No project currently open"}
    
    # First, ensure we're on the color page
    current_page = resolve.GetCurrentPage()
    if current_page.lower() != "color":
        return {"error": f"Not on Color page. Current page is: {current_page}"}
    
    # Get the current timeline
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        return {"error": "No timeline currently active"}
    
    try:
        # Get the current clip in the timeline
        current_clip = current_timeline.GetCurrentVideoItem()
        if not current_clip:
            return {"error": "No clip is currently selected in the timeline"}
        
        # Get the clip's grade
        current_grade = current_clip.GetCurrentGrade()
        if not current_grade:
            return {"error": "Failed to get current grade"}
        
        # Determine which node to get color wheels from
        target_node_index = node_index
        if target_node_index is None:
            # Use the currently selected node
            target_node_index = current_grade.GetCurrentNode()
            if target_node_index < 1:
                return {"error": "No node is currently selected"}
        else:
            # Validate the provided node index
            node_count = current_grade.GetNodeCount()
            if target_node_index < 1 or target_node_index > node_count:
                return {"error": f"Invalid node index {target_node_index}. Valid range: 1-{node_count}"}
        
        # Get node name if available
        node_name = ""
        try:
            node_name = current_grade.GetNodeName(target_node_index)
        except:
            node_name = f"Node {target_node_index}"
        
        # Get color wheel parameters
        color_wheels = {
            "node_index": target_node_index,
            "node_name": node_name,
            "clip_name": current_clip.GetName(),
            "wheels": {}
        }
        
        # Try to get each of the color wheels
        wheels_to_get = [
            {"name": "lift", "function_prefix": "GetLift"},
            {"name": "gamma", "function_prefix": "GetGamma"},
            {"name": "gain", "function_prefix": "GetGain"},
            {"name": "offset", "function_prefix": "GetOffset"},
        ]
        
        for wheel in wheels_to_get:
            wheel_name = wheel["name"]
            prefix = wheel["function_prefix"]
            
            wheel_data = {}
            try:
                # Try to get R, G, B, and Y (master) values
                for channel, channel_name in [("R", "red"), ("G", "green"), ("B", "blue"), ("Y", "master")]:
                    # Build the function name dynamically
                    function_name = f"{prefix}{channel}"
                    
                    if hasattr(current_grade, function_name):
                        # Call the function with the node index
                        getter_func = getattr(current_grade, function_name)
                        value = getter_func(target_node_index)
                        wheel_data[channel_name] = value
                
                if wheel_data:
                    color_wheels["wheels"][wheel_name] = wheel_data
            except Exception as e:
                color_wheels["wheels"][wheel_name] = {"error": f"Could not get {wheel_name} wheel: {str(e)}"}
        
        # Try to get additional common color controls
        try:
            additional_controls = {}
            
            # Try to get contrast
            try:
                if hasattr(current_grade, "GetContrast"):
                    additional_controls["contrast"] = current_grade.GetContrast(target_node_index)
            except:
                pass
            
            # Try to get saturation
            try:
                if hasattr(current_grade, "GetSaturation"):
                    additional_controls["saturation"] = current_grade.GetSaturation(target_node_index)
            except:
                pass
            
            # Try to get color temperature
            try:
                if hasattr(current_grade, "GetColorTemp"):
                    additional_controls["color_temp"] = current_grade.GetColorTemp(target_node_index)
            except:
                pass
            
            # Try to get tint
            try:
                if hasattr(current_grade, "GetTint"):
                    additional_controls["tint"] = current_grade.GetTint(target_node_index)
            except:
                pass
            
            # Add additional controls if any were found
            if additional_controls:
                color_wheels["additional_controls"] = additional_controls
        except:
            pass
        
        return color_wheels
        
    except Exception as e:
        return {"error": f"Error getting color wheel parameters: {str(e)}"}

def set_color_wheel_param(resolve, wheel: str, param: str, value: float, node_index: int = None) -> str:
    """Set a color wheel parameter for a node.
    
    Args:
        resolve: The DaVinci Resolve instance
        wheel: Which color wheel to adjust ('lift', 'gamma', 'gain', 'offset')
        param: Which parameter to adjust ('red', 'green', 'blue', 'master')
        value: The value to set (typically between -1.0 and 1.0)
        node_index: Index of the node to set parameter for (uses current node if None)
    
    Returns:
        String indicating success or failure with detailed error message
    """
    if resolve is None:
        return "Error: Not connected to DaVinci Resolve"
    
    # Validate wheel
    valid_wheels = ['lift', 'gamma', 'gain', 'offset']
    if wheel.lower() not in valid_wheels:
        return f"Error: Invalid wheel name. Must be one of: {', '.join(valid_wheels)}"
    
    # Validate parameter
    valid_params = ['red', 'green', 'blue', 'master']
    if param.lower() not in valid_params:
        return f"Error: Invalid parameter name. Must be one of: {', '.join(valid_params)}"
    
    logger.info(f"Setting {wheel} {param} to {value}")
    
    # Map parameter names to channel identifiers used in the API
    param_to_channel = {
        'red': 'R',
        'green': 'G',
        'blue': 'B',
        'master': 'Y'
    }
    
    # Map wheel names to function name prefixes used in the API
    wheel_to_function_prefix = {
        'lift': 'SetLift',
        'gamma': 'SetGamma',
        'gain': 'SetGain',
        'offset': 'SetOffset'
    }
    
    project_manager = resolve.GetProjectManager()
    if not project_manager:
        logger.error("Failed to get Project Manager")
        return "Error: Failed to get Project Manager"
    
    current_project = project_manager.GetCurrentProject()
    if not current_project:
        logger.error("No project currently open")
        return "Error: No project currently open"
    
    # First, ensure we're on the color page
    current_page = resolve.GetCurrentPage()
    if current_page.lower() != "color":
        # Try to switch to color page
        logger.info(f"Currently on {current_page} page, switching to color page")
        result = resolve.OpenPage("color")
        if not result:
            logger.error(f"Failed to switch to Color page. Current page is: {current_page}")
            return f"Error: Failed to switch to Color page. Current page is: {current_page}"
        logger.info("Successfully switched to color page")
    
    # Get the current timeline
    current_timeline = current_project.GetCurrentTimeline()
    if not current_timeline:
        logger.error("No timeline currently active")
        return "Error: No timeline currently active"
    
    try:
        # Use the helper function to ensure a clip is selected
        clip_selected, current_clip, message = ensure_clip_selected(resolve, current_timeline)
        
        if not clip_selected or not current_clip:
            logger.error("No clip could be selected automatically")
            return f"Error: {message}. Please select a clip manually in DaVinci Resolve."
        
        logger.info(f"Working with clip: {current_clip.GetName()}")
        
        # Get the clip's grade
        # This is where the NoneType error typically occurs
        logger.info("Attempting to get current grade")
        
        # First method: Direct approach
        try:
            current_grade = current_clip.GetCurrentGrade()
            if current_grade:
                logger.info("Successfully got current grade using GetCurrentGrade()")
            else:
                logger.warning("GetCurrentGrade() returned None")
        except Exception as e:
            logger.error(f"Error getting current grade via GetCurrentGrade(): {str(e)}")
            current_grade = None
        
        # Alternative approach if the first method failed
        if not current_grade:
            logger.info("Attempting alternative methods to access grade functionality")
            
            # Try to select the clip first to ensure it's active
            try:
                # Ensure clip is selected in the timeline
                logger.info("Trying to select the clip in timeline again")
                current_timeline.SetCurrentVideoItem(current_clip)
                logger.info(f"Selected clip {current_clip.GetName()} in timeline")
                
                # Try to get grade again after selection
                current_grade = current_clip.GetCurrentGrade()
                if current_grade:
                    logger.info("Successfully got current grade after selection")
            except Exception as e:
                logger.error(f"Error in alternative selection approach: {str(e)}")
        
        # Check if we have a valid grade object
        if not current_grade:
            logger.error("Could not get grade object after multiple attempts")
            return "Error setting color wheel parameter: Cannot access grade object. The clip may not be properly graded yet."
        
        logger.info("Proceeding with parameter setting with valid grade object")
        
        # Determine which node to set parameter for
        target_node_index = node_index
        if target_node_index is None:
            # Use the currently selected node
            logger.info("Getting current node index")
            target_node_index = current_grade.GetCurrentNode()
            if target_node_index < 1:
                logger.error("No node is currently selected")
                return "Error: No node is currently selected"
            logger.info(f"Using current node: {target_node_index}")
        else:
            # Validate the provided node index
            logger.info(f"Validating provided node index: {target_node_index}")
            node_count = current_grade.GetNodeCount()
            if target_node_index < 1 or target_node_index > node_count:
                logger.error(f"Invalid node index {target_node_index}. Valid range: 1-{node_count}")
                return f"Error: Invalid node index {target_node_index}. Valid range: 1-{node_count}"
        
        # Get node name for better reporting
        node_name = ""
        try:
            logger.info(f"Getting name for node {target_node_index}")
            node_name = current_grade.GetNodeName(target_node_index) or f"Node {target_node_index}"
            logger.info(f"Node name: {node_name}")
        except Exception as e:
            logger.warning(f"Could not get node name: {str(e)}")
            node_name = f"Node {target_node_index}"
        
        # Build the function name to call
        channel = param_to_channel[param.lower()]
        function_prefix = wheel_to_function_prefix[wheel.lower()]
        function_name = f"{function_prefix}{channel}"
        logger.info(f"Function to call: {function_name}")
        
        # Check if the function exists
        if not hasattr(current_grade, function_name):
            logger.error(f"Function '{function_name}' not found in DaVinci Resolve API")
            return f"Error: Function '{function_name}' not found in DaVinci Resolve API for setting {wheel} {param}"
        
        # Get the setter function
        setter_func = getattr(current_grade, function_name)
        
        # Set the parameter value
        logger.info(f"Calling {function_name}({target_node_index}, {value})")
        result = setter_func(target_node_index, value)
        
        if result:
            logger.info(f"Successfully set {wheel} {param} to {value} for {node_name}")
            return f"Successfully set {wheel} {param} to {value} for {node_name}"
        else:
            logger.error(f"Failed to set {wheel} {param} to {value} for {node_name}")
            return f"Failed to set {wheel} {param} to {value} for {node_name}"
        
    except Exception as e:
        logger.error(f"Error setting color wheel parameter: {str(e)}")
        return f"Error setting color wheel parameter: {str(e)}"

def ensure_clip_selected(resolve, timeline) -> Tuple[bool, Optional[Any], str]:
    """Ensures a clip is selected in the timeline, selecting the first clip if needed.
    
    Args:
        resolve: The DaVinci Resolve instance
        timeline: The current timeline
        
    Returns:
        Tuple containing (success, clip_object, message)
    """
    # First check if there's already a clip selected
    current_clip = timeline.GetCurrentVideoItem()
    if current_clip:
        logger.info(f"Clip already selected: {current_clip.GetName()}")
        return True, current_clip, f"Using currently selected clip: {current_clip.GetName()}"
    
    # No clip selected, try to select the first clip
    logger.info("No clip currently selected, attempting to select first clip")
    try:
        # Get video tracks
        video_track_count = timeline.GetTrackCount("video")
        logger.info(f"Timeline has {video_track_count} video tracks")
        
        # Check each track for clips
        for track_index in range(1, video_track_count + 1):
            logger.info(f"Checking video track {track_index}")
            
            # Get clips in this track
            track_items = timeline.GetItemListInTrack("video", track_index)
            if not track_items or len(track_items) == 0:
                logger.info(f"No clips in track {track_index}")
                continue
                
            logger.info(f"Found {len(track_items)} clips in track {track_index}")
            
            # Try to select the first clip
            first_clip = track_items[0]
            if first_clip:
                clip_name = first_clip.GetName()
                logger.info(f"Attempting to select clip: {clip_name}")
                
                # Set it as the current clip
                timeline.SetCurrentVideoItem(first_clip)
                
                # Verify selection
                selected_clip = timeline.GetCurrentVideoItem()
                if selected_clip and selected_clip.GetName() == clip_name:
                    logger.info(f"Successfully selected first clip: {clip_name}")
                    return True, selected_clip, f"Automatically selected clip: {clip_name}"
                else:
                    logger.warning("Failed to verify clip selection")
            
            # If we got here, we couldn't select a clip in this track
            logger.warning(f"Could not select a clip in track {track_index}")
        
        # If we reach here, we couldn't find or select any clips
        logger.warning("No clips found in any video track, or could not select any")
        return False, None, "Could not find any clips in the timeline to select"
        
    except Exception as e:
        logger.error(f"Error attempting to select a clip: {str(e)}")
        return False, None, f"Error selecting clip: {str(e)}" 
```
Page 3/4FirstPrevNextLast