# Directory Structure ``` ├── .gitignore ├── examples │ └── basic_usage.py ├── README.md ├── requirements.txt ├── setup.py └── src └── resolve_mcp ├── __init__.py ├── resolve_api.py └── server.py ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | wheels/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # Virtual environments 24 | venv/ 25 | env/ 26 | ENV/ 27 | 28 | # IDE specific files 29 | .idea/ 30 | .vscode/ 31 | *.swp 32 | *.swo 33 | 34 | # OS specific files 35 | .DS_Store 36 | Thumbs.db 37 | 38 | # Logs 39 | *.log 40 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # DaVinci Resolve MCP Server 2 | 3 | A Model Context Protocol (MCP) server for interacting with DaVinci Resolve and Fusion. This server allows AI assistants like Claude to directly interact with and control DaVinci Resolve through the Model Context Protocol. 4 | 5 | ## Features 6 | 7 | - Two-way communication: Connect Claude AI to DaVinci Resolve through the MCP protocol 8 | - Project management: Create, open, and manage DaVinci Resolve projects 9 | - Timeline manipulation: Create, modify, and navigate timelines 10 | - Media management: Import, organize, and manage media in the Media Pool 11 | - Fusion integration: Create and modify Fusion compositions 12 | - Scene inspection: Get detailed information about the current DaVinci Resolve project 13 | - Code execution: Run arbitrary Python code in DaVinci Resolve from Claude 14 | 15 | ## Installation 16 | 17 | ### Prerequisites 18 | 19 | - DaVinci Resolve Studio (version 17 or higher recommended) 20 | - Python 3.8 or higher 21 | - Claude Desktop (for AI integration) 22 | 23 | ### Setup 24 | 25 | 1. Clone this repository: 26 | ``` 27 | git clone https://github.com/apvlv/davinci-resolve-mcp.git 28 | cd davinci-resolve-mcp 29 | ``` 30 | 31 | 2. Install the required dependencies: 32 | ``` 33 | pip install -r requirements.txt 34 | ``` 35 | 36 | 3. Install the MCP server in Claude Desktop: 37 | ``` 38 | mcp install src/resolve_mcp/server.py 39 | ``` 40 | 41 | Alternatively, you can install with the editable flag for development: 42 | ``` 43 | mcp install src/resolve_mcp/server.py --with-editable . 44 | ``` 45 | 46 | ## Usage 47 | 48 | ### With Claude Desktop 49 | 50 | 1. Start DaVinci Resolve 51 | 2. In Claude Desktop, connect to the "DaVinci Resolve MCP" server 52 | 3. You can now interact with DaVinci Resolve through Claude 53 | 54 | ### With 5ire 55 | 56 | [5ire](https://5ire.app/) is an open-source cross-platform desktop AI assistant and MCP client that's compatible with this server. 57 | 58 | 1. Install 5ire from [GitHub](https://github.com/nanbingxyz/5ire) or using Homebrew on macOS: 59 | ``` 60 | brew tap brewforge/extras 61 | brew install --cask 5ire 62 | ``` 63 | 2. Start DaVinci Resolve 64 | 3. In 5ire, add the DaVinci Resolve MCP server 65 | 4. Connect to the server using your preferred AI model (OpenAI, Claude, etc.) 66 | 5. You can now interact with DaVinci Resolve through 5ire 67 | 68 | ## Available Commands 69 | 70 | ### Resources (Information Retrieval) 71 | 72 | - `project://current` - Get information about the current project 73 | - `project://timelines` - Get a list of timelines in the current project 74 | - `timeline://current` - Get information about the current timeline 75 | - `mediapool://folders` - Get a list of folders in the media pool 76 | - `mediapool://current` - Get information about the current media pool folder 77 | - `storage://volumes` - Get a list of mounted volumes in the media storage 78 | - `system://status` - Get the current status of the DaVinci Resolve connection 79 | 80 | ### Project Management 81 | 82 | - `create_project(name)` - Create a new DaVinci Resolve project 83 | - `load_project(name)` - Load an existing DaVinci Resolve project 84 | - `save_project()` - Save the current DaVinci Resolve project 85 | 86 | ### Timeline Management 87 | 88 | - `create_timeline(name)` - Create a new timeline in the current project 89 | - `set_current_timeline(index)` - Set the current timeline by index (1-based) 90 | 91 | ### Media Management 92 | 93 | - `import_media(file_paths)` - Import media files into the current media pool folder 94 | - `create_folder(name)` - Create a new folder in the current media pool folder 95 | - `create_timeline_from_clips(name, clip_indices)` - Create a new timeline from clips in the current media pool folder 96 | 97 | ### Fusion Integration 98 | 99 | - `add_fusion_comp_to_clip(timeline_index, track_type, track_index, item_index)` - Add a Fusion composition to a clip in the timeline 100 | - `create_fusion_node(node_type, parameters)` - Create a specific Fusion node in the current composition 101 | - `create_fusion_node_chain(node_chain)` - Create a chain of connected Fusion nodes in the current composition 102 | 103 | ### Page Navigation 104 | 105 | - `open_page(page_name)` - Open a specific page in DaVinci Resolve (media, edit, fusion, color, fairlight, deliver) 106 | 107 | ### Advanced Operations 108 | 109 | - `execute_python(code)` - Execute arbitrary Python code in DaVinci Resolve 110 | - `execute_lua(script)` - Execute a Lua script in DaVinci Resolve's Fusion 111 | 112 | ## Examples 113 | 114 | - "Create a new project named 'My Documentary'" 115 | - "Import all video files from the Downloads folder" 116 | - "Create a new timeline with the selected clips" 117 | - "Apply a Fusion effect to the selected clip" 118 | - "Get information about the current project" 119 | - "Switch to the Color page" 120 | - "Save the current project" 121 | - "Create a folder named 'Raw Footage' in the media pool" 122 | - "Create a Blur node in the current Fusion composition" 123 | - "Create a Text node with the content 'Hello World'" 124 | - "Create a chain of nodes: MediaIn -> Blur -> ColorCorrector -> MediaOut" 125 | 126 | ## Technical Details 127 | 128 | The server uses the Model Context Protocol to communicate between Claude and DaVinci Resolve. It leverages DaVinci Resolve's Python API to control the application. 129 | 130 | ## License 131 | 132 | MIT 133 | ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | mcp>=0.3.0 2 | pydantic>=2.0.0 3 | typing-extensions>=4.0.0 4 | ``` -------------------------------------------------------------------------------- /src/resolve_mcp/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | DaVinci Resolve MCP Server - Model Context Protocol integration for DaVinci Resolve 3 | """ 4 | 5 | __version__ = "0.1.0" 6 | ``` -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- ```python 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="resolve-mcp", 5 | version="0.1.0", 6 | description="Model Context Protocol (MCP) server for DaVinci Resolve", 7 | author="Your Name", 8 | author_email="[email protected]", 9 | packages=find_packages(where="src"), 10 | package_dir={"": "src"}, 11 | install_requires=[ 12 | "mcp>=0.3.0", 13 | "pydantic>=2.0.0", 14 | "typing-extensions>=4.0.0", 15 | ], 16 | entry_points={ 17 | "console_scripts": [ 18 | "resolve-mcp=resolve_mcp.server:main", 19 | ], 20 | }, 21 | python_requires=">=3.8", 22 | classifiers=[ 23 | "Development Status :: 3 - Alpha", 24 | "Intended Audience :: Developers", 25 | "Programming Language :: Python :: 3", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | ], 30 | ) 31 | ``` -------------------------------------------------------------------------------- /examples/basic_usage.py: -------------------------------------------------------------------------------- ```python 1 | #!/usr/bin/env python 2 | """ 3 | Basic example of using the DaVinci Resolve MCP Server. 4 | 5 | This script demonstrates how to manually connect to the MCP server 6 | and execute some basic operations with DaVinci Resolve. 7 | """ 8 | 9 | import sys 10 | import os 11 | import time 12 | import json 13 | from typing import Dict, Any 14 | 15 | # Add the parent directory to the path so we can import the resolve_mcp package 16 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 17 | 18 | from src.resolve_mcp.resolve_api import ResolveAPI 19 | 20 | def main(): 21 | """Main function to demonstrate basic usage of the DaVinci Resolve API.""" 22 | print("Connecting to DaVinci Resolve...") 23 | resolve_api = ResolveAPI() 24 | 25 | if not resolve_api.is_connected(): 26 | print("Failed to connect to DaVinci Resolve. Make sure DaVinci Resolve is running.") 27 | return 28 | 29 | print("Successfully connected to DaVinci Resolve.") 30 | 31 | # Get the current project 32 | project = resolve_api.get_current_project() 33 | if project: 34 | print(f"Current project: {project.GetName()}") 35 | else: 36 | print("No project is currently open.") 37 | 38 | # Create a new project 39 | project_name = f"Example Project {int(time.time())}" 40 | print(f"Creating a new project: {project_name}") 41 | if resolve_api.create_project(project_name): 42 | print(f"Successfully created project: {project_name}") 43 | project = resolve_api.get_current_project() 44 | else: 45 | print(f"Failed to create project: {project_name}") 46 | return 47 | 48 | # Create a new timeline 49 | timeline_name = f"Example Timeline {int(time.time())}" 50 | print(f"Creating a new timeline: {timeline_name}") 51 | if resolve_api.create_timeline(timeline_name): 52 | print(f"Successfully created timeline: {timeline_name}") 53 | else: 54 | print(f"Failed to create timeline: {timeline_name}") 55 | 56 | # Get the current timeline 57 | timeline = resolve_api.get_current_timeline() 58 | if timeline: 59 | print(f"Current timeline: {timeline.GetName()}") 60 | print(f"Timeline duration: {timeline.GetEndFrame() - timeline.GetStartFrame() + 1} frames") 61 | print(f"Video tracks: {timeline.GetTrackCount('video')}") 62 | print(f"Audio tracks: {timeline.GetTrackCount('audio')}") 63 | else: 64 | print("No timeline is currently open.") 65 | 66 | # Get media storage information 67 | print("\nMedia Storage Information:") 68 | volumes = resolve_api.get_mounted_volumes() 69 | if volumes: 70 | print("Mounted volumes:") 71 | for i, volume in enumerate(volumes, 1): 72 | print(f" {i}. {volume}") 73 | else: 74 | print("No mounted volumes available.") 75 | 76 | # Get media pool information 77 | print("\nMedia Pool Information:") 78 | media_pool = resolve_api.get_media_pool() 79 | if media_pool: 80 | root_folder = media_pool.GetRootFolder() 81 | if root_folder: 82 | print(f"Root folder: {root_folder.GetName()}") 83 | 84 | # Print folder structure 85 | def print_folder_structure(folder, indent=""): 86 | name = folder.GetName() 87 | print(f"{indent}- {name}") 88 | 89 | subfolders = folder.GetSubFolders() 90 | for subfolder in subfolders: 91 | print_folder_structure(subfolder, indent + " ") 92 | 93 | print("Folder structure:") 94 | print_folder_structure(root_folder) 95 | else: 96 | print("No root folder available.") 97 | else: 98 | print("No media pool available.") 99 | 100 | # Save the project 101 | if resolve_api.save_project(): 102 | print(f"Successfully saved project: {project.GetName()}") 103 | else: 104 | print(f"Failed to save project: {project.GetName()}") 105 | 106 | if __name__ == "__main__": 107 | main() 108 | ``` -------------------------------------------------------------------------------- /src/resolve_mcp/resolve_api.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | DaVinci Resolve API connector module. 3 | 4 | This module provides functions to connect to DaVinci Resolve's Python API 5 | and interact with its various components. 6 | """ 7 | 8 | import sys 9 | import os 10 | import platform 11 | from typing import Optional, Dict, List, Any, Union, Tuple 12 | 13 | class ResolveAPI: 14 | """Class to handle connection and interaction with DaVinci Resolve API.""" 15 | 16 | def __init__(self): 17 | """Initialize the ResolveAPI class and connect to DaVinci Resolve.""" 18 | self.resolve = None 19 | self.fusion = None 20 | self.project_manager = None 21 | self.current_project = None 22 | self.media_storage = None 23 | self.media_pool = None 24 | 25 | self._connect_to_resolve() 26 | 27 | def _connect_to_resolve(self) -> None: 28 | """ 29 | Connect to DaVinci Resolve based on the current operating system. 30 | 31 | This function adds the appropriate paths to sys.path and imports the 32 | DaVinciResolveScript module to establish a connection to Resolve. 33 | """ 34 | # Determine the appropriate path based on the operating system 35 | if platform.system() == "Windows": 36 | resolve_script_dir = os.path.join( 37 | os.environ.get("PROGRAMDATA", "C:\\ProgramData"), 38 | "Blackmagic Design", "DaVinci Resolve", "Support", "Developer", "Scripting" 39 | ) 40 | script_api_path = os.path.join(resolve_script_dir, "Modules") 41 | 42 | # Add the API directory to the system path 43 | if script_api_path not in sys.path: 44 | sys.path.append(script_api_path) 45 | 46 | # Import the DaVinciResolveScript module 47 | try: 48 | import DaVinciResolveScript as dvr_script 49 | self.resolve = dvr_script.scriptapp("Resolve") 50 | except ImportError: 51 | print("Error: Could not find DaVinciResolveScript module on Windows.") 52 | self.resolve = None 53 | 54 | elif platform.system() == "Darwin": 55 | # macOS path 56 | resolve_script_dir = "/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting" 57 | script_api_path = os.path.join(resolve_script_dir, "Modules") 58 | 59 | # Add the API directory to the system path 60 | if script_api_path not in sys.path: 61 | sys.path.append(script_api_path) 62 | 63 | # Import the DaVinciResolveScript module 64 | try: 65 | import DaVinciResolveScript as dvr_script 66 | self.resolve = dvr_script.scriptapp("Resolve") 67 | except ImportError: 68 | # Try the user-specific path if the system-wide path fails 69 | user_script_api_path = os.path.join( 70 | os.path.expanduser("~"), 71 | "Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules" 72 | ) 73 | if user_script_api_path not in sys.path: 74 | sys.path.append(user_script_api_path) 75 | 76 | try: 77 | import DaVinciResolveScript as dvr_script 78 | self.resolve = dvr_script.scriptapp("Resolve") 79 | except ImportError: 80 | print("Error: Could not find DaVinciResolveScript module on macOS.") 81 | self.resolve = None 82 | 83 | elif platform.system() == "Linux": 84 | # Linux path 85 | resolve_script_dir = "/opt/resolve/Developer/Scripting" 86 | script_api_path = os.path.join(resolve_script_dir, "Modules") 87 | 88 | # Add the API directory to the system path 89 | if script_api_path not in sys.path: 90 | sys.path.append(script_api_path) 91 | 92 | # Import the DaVinciResolveScript module 93 | try: 94 | import DaVinciResolveScript as dvr_script 95 | self.resolve = dvr_script.scriptapp("Resolve") 96 | except ImportError: 97 | print("Error: Could not find DaVinciResolveScript module on Linux.") 98 | self.resolve = None 99 | 100 | # Initialize other components if Resolve is connected 101 | if self.resolve: 102 | self.project_manager = self.resolve.GetProjectManager() 103 | self.current_project = self.project_manager.GetCurrentProject() 104 | self.media_storage = self.resolve.GetMediaStorage() 105 | self.fusion = self.resolve.Fusion() 106 | 107 | # Initialize media pool if a project is open 108 | if self.current_project: 109 | self.media_pool = self.current_project.GetMediaPool() 110 | 111 | def is_connected(self) -> bool: 112 | """Check if connected to DaVinci Resolve.""" 113 | return self.resolve is not None 114 | 115 | def get_project_manager(self): 116 | """Get the project manager object.""" 117 | return self.project_manager 118 | 119 | def get_current_project(self): 120 | """Get the current project object.""" 121 | if self.project_manager: 122 | self.current_project = self.project_manager.GetCurrentProject() 123 | return self.current_project 124 | 125 | def get_media_storage(self): 126 | """Get the media storage object.""" 127 | return self.media_storage 128 | 129 | def get_media_pool(self): 130 | """Get the media pool object for the current project.""" 131 | if self.current_project: 132 | self.media_pool = self.current_project.GetMediaPool() 133 | return self.media_pool 134 | 135 | def get_fusion(self): 136 | """Get the Fusion object.""" 137 | return self.fusion 138 | 139 | def open_page(self, page_name: str) -> bool: 140 | """ 141 | Open a specific page in DaVinci Resolve. 142 | 143 | Args: 144 | page_name (str): The name of the page to open. 145 | Valid values: "media", "edit", "fusion", "color", "fairlight", "deliver" 146 | 147 | Returns: 148 | bool: True if successful, False otherwise. 149 | """ 150 | if not self.resolve: 151 | return False 152 | 153 | valid_pages = ["media", "edit", "fusion", "color", "fairlight", "deliver"] 154 | if page_name.lower() not in valid_pages: 155 | return False 156 | 157 | self.resolve.OpenPage(page_name.lower()) 158 | return True 159 | 160 | def create_project(self, project_name: str) -> bool: 161 | """ 162 | Create a new project with the given name. 163 | 164 | Args: 165 | project_name (str): The name of the project to create. 166 | 167 | Returns: 168 | bool: True if successful, False otherwise. 169 | """ 170 | if not self.project_manager: 171 | return False 172 | 173 | new_project = self.project_manager.CreateProject(project_name) 174 | if new_project: 175 | self.current_project = new_project 176 | self.media_pool = self.current_project.GetMediaPool() 177 | return True 178 | 179 | return False 180 | 181 | def load_project(self, project_name: str) -> bool: 182 | """ 183 | Load an existing project with the given name. 184 | 185 | Args: 186 | project_name (str): The name of the project to load. 187 | 188 | Returns: 189 | bool: True if successful, False otherwise. 190 | """ 191 | if not self.project_manager: 192 | return False 193 | 194 | loaded_project = self.project_manager.LoadProject(project_name) 195 | if loaded_project: 196 | self.current_project = loaded_project 197 | self.media_pool = self.current_project.GetMediaPool() 198 | return True 199 | 200 | return False 201 | 202 | def save_project(self) -> bool: 203 | """ 204 | Save the current project. 205 | 206 | Returns: 207 | bool: True if successful, False otherwise. 208 | """ 209 | if not self.current_project: 210 | return False 211 | 212 | return self.current_project.SaveProject() 213 | 214 | def get_project_name(self) -> Optional[str]: 215 | """ 216 | Get the name of the current project. 217 | 218 | Returns: 219 | Optional[str]: The project name, or None if no project is open. 220 | """ 221 | if not self.current_project: 222 | return None 223 | 224 | return self.current_project.GetName() 225 | 226 | def create_timeline(self, timeline_name: str) -> bool: 227 | """ 228 | Create a new timeline with the given name. 229 | 230 | Args: 231 | timeline_name (str): The name of the timeline to create. 232 | 233 | Returns: 234 | bool: True if successful, False otherwise. 235 | """ 236 | if not self.media_pool: 237 | return False 238 | 239 | new_timeline = self.media_pool.CreateEmptyTimeline(timeline_name) 240 | return new_timeline is not None 241 | 242 | def get_current_timeline(self): 243 | """ 244 | Get the current timeline. 245 | 246 | Returns: 247 | The current timeline object, or None if no timeline is open. 248 | """ 249 | if not self.current_project: 250 | return None 251 | 252 | return self.current_project.GetCurrentTimeline() 253 | 254 | def get_timeline_count(self) -> int: 255 | """ 256 | Get the number of timelines in the current project. 257 | 258 | Returns: 259 | int: The number of timelines, or 0 if no project is open. 260 | """ 261 | if not self.current_project: 262 | return 0 263 | 264 | return self.current_project.GetTimelineCount() 265 | 266 | def get_timeline_by_index(self, index: int): 267 | """ 268 | Get a timeline by its index. 269 | 270 | Args: 271 | index (int): The index of the timeline (1-based). 272 | 273 | Returns: 274 | The timeline object, or None if the index is invalid. 275 | """ 276 | if not self.current_project: 277 | return None 278 | 279 | return self.current_project.GetTimelineByIndex(index) 280 | 281 | def set_current_timeline(self, timeline) -> bool: 282 | """ 283 | Set the current timeline. 284 | 285 | Args: 286 | timeline: The timeline object to set as current. 287 | 288 | Returns: 289 | bool: True if successful, False otherwise. 290 | """ 291 | if not self.current_project: 292 | return False 293 | 294 | return self.current_project.SetCurrentTimeline(timeline) 295 | 296 | def get_mounted_volumes(self) -> List[str]: 297 | """ 298 | Get a list of mounted volumes. 299 | 300 | Returns: 301 | List[str]: A list of mounted volume paths. 302 | """ 303 | if not self.media_storage: 304 | return [] 305 | 306 | return self.media_storage.GetMountedVolumes() 307 | 308 | def get_sub_folders(self, folder_path: str) -> List[str]: 309 | """ 310 | Get a list of subfolders in the given folder path. 311 | 312 | Args: 313 | folder_path (str): The path of the folder to get subfolders from. 314 | 315 | Returns: 316 | List[str]: A list of subfolder paths. 317 | """ 318 | if not self.media_storage: 319 | return [] 320 | 321 | return self.media_storage.GetSubFolders(folder_path) 322 | 323 | def get_files(self, folder_path: str) -> List[str]: 324 | """ 325 | Get a list of files in the given folder path. 326 | 327 | Args: 328 | folder_path (str): The path of the folder to get files from. 329 | 330 | Returns: 331 | List[str]: A list of file paths. 332 | """ 333 | if not self.media_storage: 334 | return [] 335 | 336 | return self.media_storage.GetFiles(folder_path) 337 | 338 | def add_items_to_media_pool(self, file_paths: List[str]) -> List[Any]: 339 | """ 340 | Add items from the media storage to the media pool. 341 | 342 | Args: 343 | file_paths (List[str]): A list of file paths to add. 344 | 345 | Returns: 346 | List[Any]: A list of added media pool items. 347 | """ 348 | if not self.media_storage or not self.media_pool: 349 | return [] 350 | 351 | return self.media_storage.AddItemsToMediaPool(file_paths) 352 | 353 | def get_root_folder(self): 354 | """ 355 | Get the root folder of the media pool. 356 | 357 | Returns: 358 | The root folder object, or None if no media pool is available. 359 | """ 360 | if not self.media_pool: 361 | return None 362 | 363 | return self.media_pool.GetRootFolder() 364 | 365 | def get_current_folder(self): 366 | """ 367 | Get the current folder of the media pool. 368 | 369 | Returns: 370 | The current folder object, or None if no media pool is available. 371 | """ 372 | if not self.media_pool: 373 | return None 374 | 375 | return self.media_pool.GetCurrentFolder() 376 | 377 | def add_sub_folder(self, parent_folder, folder_name: str): 378 | """ 379 | Add a subfolder to the given parent folder. 380 | 381 | Args: 382 | parent_folder: The parent folder object. 383 | folder_name (str): The name of the subfolder to create. 384 | 385 | Returns: 386 | The created subfolder object, or None if unsuccessful. 387 | """ 388 | if not self.media_pool: 389 | return None 390 | 391 | return self.media_pool.AddSubFolder(parent_folder, folder_name) 392 | 393 | def get_folder_clips(self, folder) -> List[Any]: 394 | """ 395 | Get a list of clips in the given folder. 396 | 397 | Args: 398 | folder: The folder object. 399 | 400 | Returns: 401 | List[Any]: A list of media pool items in the folder. 402 | """ 403 | if not folder: 404 | return [] 405 | 406 | return folder.GetClips() 407 | 408 | def get_folder_name(self, folder) -> Optional[str]: 409 | """ 410 | Get the name of the given folder. 411 | 412 | Args: 413 | folder: The folder object. 414 | 415 | Returns: 416 | Optional[str]: The folder name, or None if the folder is invalid. 417 | """ 418 | if not folder: 419 | return None 420 | 421 | return folder.GetName() 422 | 423 | def get_folder_sub_folders(self, folder) -> List[Any]: 424 | """ 425 | Get a list of subfolders in the given folder. 426 | 427 | Args: 428 | folder: The folder object. 429 | 430 | Returns: 431 | List[Any]: A list of subfolder objects. 432 | """ 433 | if not folder: 434 | return [] 435 | 436 | return folder.GetSubFolders() 437 | 438 | def append_to_timeline(self, clips: List[Any]) -> bool: 439 | """ 440 | Append the given clips to the current timeline. 441 | 442 | Args: 443 | clips (List[Any]): A list of media pool items to append. 444 | 445 | Returns: 446 | bool: True if successful, False otherwise. 447 | """ 448 | if not self.media_pool: 449 | return False 450 | 451 | return self.media_pool.AppendToTimeline(clips) 452 | 453 | def create_timeline_from_clips(self, timeline_name: str, clips: List[Any]): 454 | """ 455 | Create a new timeline with the given name and append the given clips. 456 | 457 | Args: 458 | timeline_name (str): The name of the timeline to create. 459 | clips (List[Any]): A list of media pool items to append. 460 | 461 | Returns: 462 | The created timeline object, or None if unsuccessful. 463 | """ 464 | if not self.media_pool: 465 | return None 466 | 467 | return self.media_pool.CreateTimelineFromClips(timeline_name, clips) 468 | 469 | def import_timeline_from_file(self, file_path: str): 470 | """ 471 | Import a timeline from a file. 472 | 473 | Args: 474 | file_path (str): The path of the file to import. 475 | 476 | Returns: 477 | The imported timeline object, or None if unsuccessful. 478 | """ 479 | if not self.media_pool: 480 | return None 481 | 482 | return self.media_pool.ImportTimelineFromFile(file_path) 483 | 484 | def execute_lua(self, script: str) -> Any: 485 | """ 486 | Execute a Lua script in DaVinci Resolve. 487 | 488 | Args: 489 | script (str): The Lua script to execute. 490 | 491 | Returns: 492 | Any: The result of the script execution. 493 | """ 494 | if not self.fusion: 495 | return None 496 | 497 | return self.fusion.Execute(script) 498 | 499 | def create_fusion_node(self, comp, node_type: str, inputs: Dict[str, Any] = None) -> Any: 500 | """ 501 | Create a Fusion node in the given composition. 502 | 503 | Args: 504 | comp: The Fusion composition to add the node to. 505 | node_type (str): The type of node to create (e.g., 'Blur', 'ColorCorrector'). 506 | inputs (Dict[str, Any], optional): Dictionary of input parameters for the node. 507 | 508 | Returns: 509 | Any: The created node, or None if unsuccessful. 510 | """ 511 | if not self.fusion or not comp: 512 | return None 513 | 514 | try: 515 | # Create the node in the composition 516 | node = comp.AddTool(node_type) 517 | 518 | # Set input parameters if provided 519 | if inputs and node: 520 | for key, value in inputs.items(): 521 | node[key] = value 522 | 523 | return node 524 | except Exception as e: 525 | print(f"Error creating Fusion node: {e}") 526 | return None 527 | 528 | def get_current_comp(self) -> Any: 529 | """ 530 | Get the current Fusion composition. 531 | 532 | Returns: 533 | Any: The current Fusion composition, or None if not available. 534 | """ 535 | if not self.fusion: 536 | return None 537 | 538 | try: 539 | return self.fusion.CurrentComp 540 | except Exception as e: 541 | print(f"Error getting current composition: {e}") 542 | return None 543 | ``` -------------------------------------------------------------------------------- /src/resolve_mcp/server.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | DaVinci Resolve MCP Server 3 | 4 | This module implements a Model Context Protocol (MCP) server for DaVinci Resolve, 5 | allowing AI assistants like Claude to interact with DaVinci Resolve through the MCP protocol. 6 | """ 7 | 8 | import os 9 | import sys 10 | import json 11 | import logging 12 | import traceback 13 | from typing import Dict, List, Any, Optional, Union, Tuple 14 | 15 | # Print debugging information 16 | print(f"Python version: {sys.version}", file=sys.stderr) 17 | print(f"Python executable: {sys.executable}", file=sys.stderr) 18 | print(f"Python path: {sys.path}", file=sys.stderr) 19 | print(f"Current working directory: {os.getcwd()}", file=sys.stderr) 20 | 21 | try: 22 | from mcp.server.fastmcp import FastMCP, Context, Image 23 | from pydantic import BaseModel 24 | print(f"Successfully imported MCP", file=sys.stderr) 25 | except ImportError as e: 26 | print(f"Error importing MCP: {e}", file=sys.stderr) 27 | traceback.print_exc(file=sys.stderr) 28 | raise 29 | 30 | try: 31 | # Try absolute import first (when installed as a package) 32 | from resolve_mcp.resolve_api import ResolveAPI 33 | print(f"Successfully imported ResolveAPI using absolute import", file=sys.stderr) 34 | except ImportError as e: 35 | print(f"Error with absolute import: {e}", file=sys.stderr) 36 | try: 37 | # Fall back to relative import (when running from source) 38 | from .resolve_api import ResolveAPI 39 | print(f"Successfully imported ResolveAPI using relative import", file=sys.stderr) 40 | except ImportError as e2: 41 | print(f"Error with relative import: {e2}", file=sys.stderr) 42 | traceback.print_exc(file=sys.stderr) 43 | raise 44 | 45 | # Configure logging 46 | logging.basicConfig( 47 | level=logging.INFO, 48 | format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 49 | ) 50 | logger = logging.getLogger("resolve_mcp") 51 | 52 | # Create the MCP server 53 | mcp = FastMCP("DaVinci Resolve") 54 | 55 | # Initialize the Resolve API 56 | resolve_api = ResolveAPI() 57 | 58 | # Check if connected to Resolve 59 | if not resolve_api.is_connected(): 60 | logger.error("Failed to connect to DaVinci Resolve. Make sure DaVinci Resolve is running.") 61 | else: 62 | logger.info("Successfully connected to DaVinci Resolve.") 63 | 64 | # Define resource and tool functions 65 | 66 | # System Information Resources 67 | 68 | @mcp.resource("system://status") 69 | def get_system_status() -> str: 70 | """Get the current status of the DaVinci Resolve connection.""" 71 | if resolve_api.is_connected(): 72 | project_name = resolve_api.get_project_name() or "No project open" 73 | timeline = resolve_api.get_current_timeline() 74 | timeline_name = timeline.GetName() if timeline else "No timeline open" 75 | 76 | return f""" 77 | DaVinci Resolve Status: 78 | - Connection: Connected 79 | - Current Project: {project_name} 80 | - Current Timeline: {timeline_name} 81 | """ 82 | else: 83 | return """ 84 | DaVinci Resolve Status: 85 | - Connection: Not connected 86 | - Error: DaVinci Resolve is not running or not accessible 87 | """ 88 | 89 | # Project Resources 90 | 91 | @mcp.resource("project://current") 92 | def get_current_project() -> str: 93 | """Get information about the current project.""" 94 | if not resolve_api.is_connected(): 95 | return "Error: Not connected to DaVinci Resolve." 96 | 97 | project = resolve_api.get_current_project() 98 | if not project: 99 | return "No project is currently open." 100 | 101 | project_name = project.GetName() 102 | timeline_count = project.GetTimelineCount() 103 | 104 | current_timeline = project.GetCurrentTimeline() 105 | timeline_name = current_timeline.GetName() if current_timeline else "None" 106 | 107 | return f""" 108 | Current Project: {project_name} 109 | Timeline Count: {timeline_count} 110 | Current Timeline: {timeline_name} 111 | """ 112 | 113 | @mcp.resource("project://timelines") 114 | def get_project_timelines() -> str: 115 | """Get a list of timelines in the current project.""" 116 | if not resolve_api.is_connected(): 117 | return "Error: Not connected to DaVinci Resolve." 118 | 119 | project = resolve_api.get_current_project() 120 | if not project: 121 | return "No project is currently open." 122 | 123 | timeline_count = project.GetTimelineCount() 124 | if timeline_count == 0: 125 | return "No timelines in the current project." 126 | 127 | timelines = [] 128 | for i in range(1, timeline_count + 1): 129 | timeline = project.GetTimelineByIndex(i) 130 | if timeline: 131 | timelines.append(f"{i}. {timeline.GetName()}") 132 | 133 | return "\n".join(timelines) 134 | 135 | @mcp.resource("timeline://current") 136 | def get_current_timeline() -> str: 137 | """Get information about the current timeline.""" 138 | if not resolve_api.is_connected(): 139 | return "Error: Not connected to DaVinci Resolve." 140 | 141 | project = resolve_api.get_current_project() 142 | if not project: 143 | return "No project is currently open." 144 | 145 | timeline = project.GetCurrentTimeline() 146 | if not timeline: 147 | return "No timeline is currently open." 148 | 149 | timeline_name = timeline.GetName() 150 | start_frame = timeline.GetStartFrame() 151 | end_frame = timeline.GetEndFrame() 152 | duration = end_frame - start_frame + 1 153 | 154 | video_track_count = timeline.GetTrackCount("video") 155 | audio_track_count = timeline.GetTrackCount("audio") 156 | subtitle_track_count = timeline.GetTrackCount("subtitle") 157 | 158 | return f""" 159 | Timeline: {timeline_name} 160 | Duration: {duration} frames ({start_frame} - {end_frame}) 161 | Video Tracks: {video_track_count} 162 | Audio Tracks: {audio_track_count} 163 | Subtitle Tracks: {subtitle_track_count} 164 | """ 165 | 166 | @mcp.resource("mediapool://folders") 167 | def get_media_pool_folders() -> str: 168 | """Get a list of folders in the media pool.""" 169 | if not resolve_api.is_connected(): 170 | return "Error: Not connected to DaVinci Resolve." 171 | 172 | media_pool = resolve_api.get_media_pool() 173 | if not media_pool: 174 | return "No media pool available." 175 | 176 | root_folder = media_pool.GetRootFolder() 177 | if not root_folder: 178 | return "No root folder available." 179 | 180 | def get_folder_structure(folder, indent=""): 181 | result = [] 182 | name = folder.GetName() 183 | result.append(f"{indent}- {name}") 184 | 185 | subfolders = folder.GetSubFolders() 186 | for subfolder in subfolders: 187 | result.extend(get_folder_structure(subfolder, indent + " ")) 188 | 189 | return result 190 | 191 | folder_structure = get_folder_structure(root_folder) 192 | return "\n".join(folder_structure) 193 | 194 | @mcp.resource("mediapool://current") 195 | def get_current_media_pool_folder() -> str: 196 | """Get information about the current media pool folder.""" 197 | if not resolve_api.is_connected(): 198 | return "Error: Not connected to DaVinci Resolve." 199 | 200 | media_pool = resolve_api.get_media_pool() 201 | if not media_pool: 202 | return "No media pool available." 203 | 204 | current_folder = media_pool.GetCurrentFolder() 205 | if not current_folder: 206 | return "No current folder available." 207 | 208 | folder_name = current_folder.GetName() 209 | clips = current_folder.GetClips() 210 | clip_count = len(clips) if clips else 0 211 | 212 | clip_info = [] 213 | if clips: 214 | for i, clip in enumerate(clips, 1): 215 | if i > 10: # Limit to 10 clips to avoid overwhelming response 216 | clip_info.append(f"... and {clip_count - 10} more clips") 217 | break 218 | clip_info.append(f"{i}. {clip.GetName()}") 219 | 220 | return f""" 221 | Current Folder: {folder_name} 222 | Clip Count: {clip_count} 223 | Clips: 224 | {"No clips in this folder." if clip_count == 0 else "\n".join(clip_info)} 225 | """ 226 | 227 | @mcp.resource("storage://volumes") 228 | def get_mounted_volumes() -> str: 229 | """Get a list of mounted volumes in the media storage.""" 230 | if not resolve_api.is_connected(): 231 | return "Error: Not connected to DaVinci Resolve." 232 | 233 | media_storage = resolve_api.get_media_storage() 234 | if not media_storage: 235 | return "No media storage available." 236 | 237 | volumes = media_storage.GetMountedVolumes() 238 | if not volumes: 239 | return "No mounted volumes available." 240 | 241 | volume_list = [] 242 | for i, volume in enumerate(volumes, 1): 243 | volume_list.append(f"{i}. {volume}") 244 | 245 | return "\n".join(volume_list) 246 | 247 | # Tools for Project Management 248 | 249 | @mcp.tool() 250 | def create_project(name: str) -> str: 251 | """ 252 | Create a new DaVinci Resolve project. 253 | 254 | Args: 255 | name: The name of the project to create 256 | 257 | Returns: 258 | A message indicating success or failure 259 | """ 260 | if not resolve_api.is_connected(): 261 | return "Error: Not connected to DaVinci Resolve." 262 | 263 | success = resolve_api.create_project(name) 264 | if success: 265 | return f"Successfully created project '{name}'." 266 | else: 267 | return f"Failed to create project '{name}'. The project may already exist." 268 | 269 | @mcp.tool() 270 | def load_project(name: str) -> str: 271 | """ 272 | Load an existing DaVinci Resolve project. 273 | 274 | Args: 275 | name: The name of the project to load 276 | 277 | Returns: 278 | A message indicating success or failure 279 | """ 280 | if not resolve_api.is_connected(): 281 | return "Error: Not connected to DaVinci Resolve." 282 | 283 | success = resolve_api.load_project(name) 284 | if success: 285 | return f"Successfully loaded project '{name}'." 286 | else: 287 | return f"Failed to load project '{name}'. The project may not exist." 288 | 289 | @mcp.tool() 290 | def save_project() -> str: 291 | """ 292 | Save the current DaVinci Resolve project. 293 | 294 | Returns: 295 | A message indicating success or failure 296 | """ 297 | if not resolve_api.is_connected(): 298 | return "Error: Not connected to DaVinci Resolve." 299 | 300 | project = resolve_api.get_current_project() 301 | if not project: 302 | return "No project is currently open." 303 | 304 | success = resolve_api.save_project() 305 | if success: 306 | return f"Successfully saved project '{project.GetName()}'." 307 | else: 308 | return f"Failed to save project '{project.GetName()}'." 309 | 310 | # Tools for Timeline Management 311 | 312 | @mcp.tool() 313 | def create_timeline(name: str) -> str: 314 | """ 315 | Create a new timeline in the current project. 316 | 317 | Args: 318 | name: The name of the timeline to create 319 | 320 | Returns: 321 | A message indicating success or failure 322 | """ 323 | if not resolve_api.is_connected(): 324 | return "Error: Not connected to DaVinci Resolve." 325 | 326 | project = resolve_api.get_current_project() 327 | if not project: 328 | return "No project is currently open." 329 | 330 | media_pool = resolve_api.get_media_pool() 331 | if not media_pool: 332 | return "No media pool available." 333 | 334 | timeline = media_pool.CreateEmptyTimeline(name) 335 | if timeline: 336 | project.SetCurrentTimeline(timeline) 337 | return f"Successfully created timeline '{name}'." 338 | else: 339 | return f"Failed to create timeline '{name}'. The timeline may already exist." 340 | 341 | @mcp.tool() 342 | def set_current_timeline(index: int) -> str: 343 | """ 344 | Set the current timeline by index. 345 | 346 | Args: 347 | index: The index of the timeline (1-based) 348 | 349 | Returns: 350 | A message indicating success or failure 351 | """ 352 | if not resolve_api.is_connected(): 353 | return "Error: Not connected to DaVinci Resolve." 354 | 355 | project = resolve_api.get_current_project() 356 | if not project: 357 | return "No project is currently open." 358 | 359 | timeline_count = project.GetTimelineCount() 360 | if index < 1 or index > timeline_count: 361 | return f"Invalid timeline index. Valid range is 1-{timeline_count}." 362 | 363 | timeline = project.GetTimelineByIndex(index) 364 | if not timeline: 365 | return f"Failed to get timeline at index {index}." 366 | 367 | success = project.SetCurrentTimeline(timeline) 368 | if success: 369 | return f"Successfully set current timeline to '{timeline.GetName()}'." 370 | else: 371 | return f"Failed to set current timeline to '{timeline.GetName()}'." 372 | 373 | # Tools for Media Management 374 | 375 | @mcp.tool() 376 | def import_media(file_paths: List[str]) -> str: 377 | """ 378 | Import media files into the current media pool folder. 379 | 380 | Args: 381 | file_paths: A list of file paths to import 382 | 383 | Returns: 384 | A message indicating success or failure 385 | """ 386 | if not resolve_api.is_connected(): 387 | return "Error: Not connected to DaVinci Resolve." 388 | 389 | media_storage = resolve_api.get_media_storage() 390 | if not media_storage: 391 | return "No media storage available." 392 | 393 | media_pool = resolve_api.get_media_pool() 394 | if not media_pool: 395 | return "No media pool available." 396 | 397 | clips = media_storage.AddItemsToMediaPool(file_paths) 398 | if clips: 399 | return f"Successfully imported {len(clips)} media files." 400 | else: 401 | return "Failed to import media files. Check that the file paths are valid." 402 | 403 | @mcp.tool() 404 | def create_folder(name: str) -> str: 405 | """ 406 | Create a new folder in the current media pool folder. 407 | 408 | Args: 409 | name: The name of the folder to create 410 | 411 | Returns: 412 | A message indicating success or failure 413 | """ 414 | if not resolve_api.is_connected(): 415 | return "Error: Not connected to DaVinci Resolve." 416 | 417 | media_pool = resolve_api.get_media_pool() 418 | if not media_pool: 419 | return "No media pool available." 420 | 421 | current_folder = media_pool.GetCurrentFolder() 422 | if not current_folder: 423 | return "No current folder available." 424 | 425 | new_folder = media_pool.AddSubFolder(current_folder, name) 426 | if new_folder: 427 | return f"Successfully created folder '{name}'." 428 | else: 429 | return f"Failed to create folder '{name}'. The folder may already exist." 430 | 431 | @mcp.tool() 432 | def create_timeline_from_clips(name: str, clip_indices: List[int]) -> str: 433 | """ 434 | Create a new timeline from clips in the current media pool folder. 435 | 436 | Args: 437 | name: The name of the timeline to create 438 | clip_indices: A list of clip indices (1-based) to include in the timeline 439 | 440 | Returns: 441 | A message indicating success or failure 442 | """ 443 | if not resolve_api.is_connected(): 444 | return "Error: Not connected to DaVinci Resolve." 445 | 446 | media_pool = resolve_api.get_media_pool() 447 | if not media_pool: 448 | return "No media pool available." 449 | 450 | current_folder = media_pool.GetCurrentFolder() 451 | if not current_folder: 452 | return "No current folder available." 453 | 454 | clips = current_folder.GetClips() 455 | if not clips: 456 | return "No clips in the current folder." 457 | 458 | clips_list = list(clips.values()) 459 | selected_clips = [] 460 | 461 | for index in clip_indices: 462 | if index < 1 or index > len(clips_list): 463 | return f"Invalid clip index {index}. Valid range is 1-{len(clips_list)}." 464 | selected_clips.append(clips_list[index - 1]) 465 | 466 | timeline = media_pool.CreateTimelineFromClips(name, selected_clips) 467 | if timeline: 468 | return f"Successfully created timeline '{name}' with {len(selected_clips)} clips." 469 | else: 470 | return f"Failed to create timeline '{name}'." 471 | 472 | # Tools for Fusion Integration 473 | 474 | @mcp.tool() 475 | def add_fusion_comp_to_clip(timeline_index: int, track_type: str, track_index: int, item_index: int) -> str: 476 | """ 477 | Add a Fusion composition to a clip in the timeline. 478 | 479 | Args: 480 | timeline_index: The index of the timeline (1-based) 481 | track_type: The type of track ("video", "audio", or "subtitle") 482 | track_index: The index of the track (1-based) 483 | item_index: The index of the item in the track (1-based) 484 | 485 | Returns: 486 | A message indicating success or failure 487 | """ 488 | if not resolve_api.is_connected(): 489 | return "Error: Not connected to DaVinci Resolve." 490 | 491 | project = resolve_api.get_current_project() 492 | if not project: 493 | return "No project is currently open." 494 | 495 | timeline_count = project.GetTimelineCount() 496 | if timeline_index < 1 or timeline_index > timeline_count: 497 | return f"Invalid timeline index. Valid range is 1-{timeline_count}." 498 | 499 | timeline = project.GetTimelineByIndex(timeline_index) 500 | if not timeline: 501 | return f"Failed to get timeline at index {timeline_index}." 502 | 503 | if track_type not in ["video", "audio", "subtitle"]: 504 | return "Invalid track type. Valid types are 'video', 'audio', or 'subtitle'." 505 | 506 | track_count = timeline.GetTrackCount(track_type) 507 | if track_index < 1 or track_index > track_count: 508 | return f"Invalid track index. Valid range is 1-{track_count}." 509 | 510 | items = timeline.GetItemsInTrack(track_type, track_index) 511 | if not items: 512 | return f"No items in {track_type} track {track_index}." 513 | 514 | if item_index < 1 or item_index > len(items): 515 | return f"Invalid item index. Valid range is 1-{len(items)}." 516 | 517 | item = items[item_index - 1] 518 | fusion_comp = item.AddFusionComp() 519 | 520 | if fusion_comp: 521 | # Switch to the Fusion page to edit the composition 522 | resolve_api.open_page("fusion") 523 | return f"Successfully added Fusion composition to {track_type} track {track_index}, item {item_index}." 524 | else: 525 | return f"Failed to add Fusion composition to {track_type} track {track_index}, item {item_index}." 526 | 527 | @mcp.tool() 528 | def create_fusion_node(node_type: str, parameters: Dict[str, Any] = None) -> str: 529 | """ 530 | Create a Fusion node in the current composition. 531 | 532 | Args: 533 | node_type: The type of node to create (e.g., 'Blur', 'ColorCorrector', 'Text') 534 | parameters: Optional dictionary of parameters to set on the node 535 | 536 | Returns: 537 | A message indicating success or failure 538 | """ 539 | if not resolve_api.is_connected(): 540 | return "Error: Not connected to DaVinci Resolve." 541 | 542 | # Get the current Fusion composition 543 | comp = resolve_api.get_current_comp() 544 | if not comp: 545 | return "No active Fusion composition. Please open the Fusion page and select a composition first." 546 | 547 | # Create the node 548 | node = resolve_api.create_fusion_node(comp, node_type, parameters) 549 | if node: 550 | return f"Successfully created {node_type} node in the Fusion composition." 551 | else: 552 | return f"Failed to create {node_type} node. Check that the node type is valid." 553 | 554 | @mcp.tool() 555 | def create_fusion_node_chain(node_chain: List[Dict[str, Any]]) -> str: 556 | """ 557 | Create a chain of connected Fusion nodes in the current composition. 558 | 559 | Args: 560 | node_chain: A list of dictionaries, each containing: 561 | - 'type': The type of node to create 562 | - 'name': Optional name for the node 563 | - 'params': Optional dictionary of parameters to set on the node 564 | 565 | Returns: 566 | A message indicating success or failure 567 | """ 568 | if not resolve_api.is_connected(): 569 | return "Error: Not connected to DaVinci Resolve." 570 | 571 | # Get the current Fusion composition 572 | comp = resolve_api.get_current_comp() 573 | if not comp: 574 | return "No active Fusion composition. Please open the Fusion page and select a composition first." 575 | 576 | if not node_chain or len(node_chain) == 0: 577 | return "No nodes specified in the chain." 578 | 579 | try: 580 | # Create the first node 581 | prev_node = None 582 | nodes_created = [] 583 | 584 | for node_info in node_chain: 585 | node_type = node_info.get('type') 586 | if not node_type: 587 | continue 588 | 589 | # Create the node 590 | node = resolve_api.create_fusion_node(comp, node_type, node_info.get('params')) 591 | 592 | if not node: 593 | continue 594 | 595 | # Set the node name if provided 596 | if 'name' in node_info and node_info['name']: 597 | node.SetAttrs({'TOOLS_Name': node_info['name']}) 598 | 599 | # Connect to previous node if this isn't the first node 600 | if prev_node: 601 | # Connect the main output of the previous node to the main input of this node 602 | node.ConnectInput('Input', prev_node) 603 | 604 | prev_node = node 605 | nodes_created.append(node_type) 606 | 607 | if not nodes_created: 608 | return "Failed to create any nodes in the chain." 609 | 610 | return f"Successfully created node chain: {' -> '.join(nodes_created)}" 611 | except Exception as e: 612 | return f"Error creating node chain: {str(e)}" 613 | 614 | # Tools for Page Navigation 615 | 616 | @mcp.tool() 617 | def open_page(page_name: str) -> str: 618 | """ 619 | Open a specific page in DaVinci Resolve. 620 | 621 | Args: 622 | page_name: The name of the page to open (media, edit, fusion, color, fairlight, deliver) 623 | 624 | Returns: 625 | A message indicating success or failure 626 | """ 627 | if not resolve_api.is_connected(): 628 | return "Error: Not connected to DaVinci Resolve." 629 | 630 | valid_pages = ["media", "edit", "fusion", "color", "fairlight", "deliver"] 631 | if page_name.lower() not in valid_pages: 632 | return f"Invalid page name. Valid pages are: {', '.join(valid_pages)}." 633 | 634 | success = resolve_api.open_page(page_name.lower()) 635 | if success: 636 | return f"Successfully opened the {page_name.capitalize()} page." 637 | else: 638 | return f"Failed to open the {page_name.capitalize()} page." 639 | 640 | # Tools for Advanced Operations 641 | 642 | @mcp.tool() 643 | def execute_python(code: str) -> str: 644 | """ 645 | Execute arbitrary Python code in DaVinci Resolve. 646 | 647 | Args: 648 | code: The Python code to execute 649 | 650 | Returns: 651 | The result of the code execution 652 | """ 653 | if not resolve_api.is_connected(): 654 | return "Error: Not connected to DaVinci Resolve." 655 | 656 | # Create a local namespace with access to the Resolve API 657 | local_namespace = { 658 | "resolve_api": resolve_api, 659 | "resolve": resolve_api.resolve, 660 | "fusion": resolve_api.fusion, 661 | "project_manager": resolve_api.project_manager, 662 | "current_project": resolve_api.current_project, 663 | "media_storage": resolve_api.media_storage, 664 | "media_pool": resolve_api.media_pool, 665 | } 666 | 667 | try: 668 | # Execute the code in the local namespace 669 | exec_result = {} 670 | exec(code, globals(), local_namespace) 671 | 672 | # Update the Resolve API objects with any changes made in the code 673 | resolve_api.resolve = local_namespace.get("resolve", resolve_api.resolve) 674 | resolve_api.fusion = local_namespace.get("fusion", resolve_api.fusion) 675 | resolve_api.project_manager = local_namespace.get("project_manager", resolve_api.project_manager) 676 | resolve_api.current_project = local_namespace.get("current_project", resolve_api.current_project) 677 | resolve_api.media_storage = local_namespace.get("media_storage", resolve_api.media_storage) 678 | resolve_api.media_pool = local_namespace.get("media_pool", resolve_api.media_pool) 679 | 680 | # Check for a result variable 681 | if "result" in local_namespace: 682 | result = local_namespace["result"] 683 | return str(result) 684 | 685 | return "Code executed successfully." 686 | except Exception as e: 687 | return f"Error executing code: {str(e)}" 688 | 689 | @mcp.tool() 690 | def execute_lua(script: str) -> str: 691 | """ 692 | Execute a Lua script in DaVinci Resolve's Fusion. 693 | 694 | Args: 695 | script: The Lua script to execute 696 | 697 | Returns: 698 | The result of the script execution 699 | """ 700 | if not resolve_api.is_connected(): 701 | return "Error: Not connected to DaVinci Resolve." 702 | 703 | if not resolve_api.fusion: 704 | return "Fusion is not available." 705 | 706 | try: 707 | result = resolve_api.execute_lua(script) 708 | return str(result) if result is not None else "Script executed successfully." 709 | except Exception as e: 710 | return f"Error executing Lua script: {str(e)}" 711 | 712 | # Main entry point function for the MCP server 713 | def main(): 714 | """Main entry point for the MCP server.""" 715 | return mcp.run() 716 | 717 | if __name__ == "__main__": 718 | main() 719 | ```