#
tokens: 19852/50000 5/5 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── pyproject.toml
├── README.md
├── resolve_api.py
├── server.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python bytecode files
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | 
 6 | # Distribution / packaging
 7 | dist/
 8 | build/
 9 | *.egg-info/
10 | *.egg
11 | 
12 | # Virtual environments
13 | venv/
14 | env/
15 | ENV/
16 | .env/
17 | .venv/
18 | env.bak/
19 | venv.bak/
20 | 
21 | # UV specific
22 | .uv/
23 | .uv-cache/
24 | 
25 | # IDE specific files
26 | .idea/
27 | .vscode/
28 | *.swp
29 | *.swo
30 | .DS_Store
31 | 
32 | # Testing
33 | .coverage
34 | htmlcov/
35 | .pytest_cache/
36 | .tox/
37 | .nox/
38 | 
39 | # Logs
40 | *.log
41 | logs/
42 | 
43 | # Local configuration
44 | .env
45 | .env.local
46 | config.local.py
47 | 
48 | # Dependencies
49 | pip-log.txt
50 | pip-delete-this-directory.txt
51 | 
52 | # Jupyter Notebooks
53 | .ipynb_checkpoints
54 | 
55 | # Package specific
56 | *.db
57 | *.sqlite3
58 | 
59 | # Documentation
60 | /site
61 | /docs/_build/
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | Here’s an updated version of the README with enhancements reflecting the expanded functionality of the `ResolveAPI` class, improved clarity, and additional details for setup and usage. The structure remains consistent with your original README, but I’ve incorporated the new features (e.g., gallery management, track control, audio adjustments, playback, etc.) and refined the instructions for `uv` installation and Claude integration.
  2 | 
  3 | ---
  4 | 
  5 | # DaVinci Resolve MCP Server
  6 | 
  7 | A Model Context Protocol (MCP) server that enables AI assistants like Claude to interact with DaVinci Resolve Studio, providing advanced control over editing, color grading, audio, and more.
  8 | 
  9 | ## Overview
 10 | 
 11 | This server implements the MCP protocol to create a bridge between AI assistants and DaVinci Resolve. It allows AI assistants to:
 12 | 
 13 | - Create, load, and manage DaVinci Resolve projects
 14 | - Manipulate timelines, tracks, and clips
 15 | - Import and organize media files
 16 | - Access and modify Fusion compositions
 17 | - Perform color grading and manage stills in the Gallery
 18 | - Adjust audio settings and control playback
 19 | - Navigate between Resolve pages (Media, Edit, Fusion, Color, Fairlight, Deliver)
 20 | - Execute custom Python and Lua scripts
 21 | - Export and import projects
 22 | 
 23 | ## Requirements
 24 | 
 25 | - DaVinci Resolve Studio 18.0 or newer
 26 | - Python 3.10 or newer
 27 | - Access to the DaVinci Resolve scripting API
 28 | 
 29 | ## Installation with uv
 30 | 
 31 | [uv](https://github.com/astral-sh/uv) is a fast, modern Python package installer and resolver that outperforms pip. Follow these steps to install and set up the DaVinci Resolve MCP server using `uv`:
 32 | 
 33 | ### 1. Install uv
 34 | 
 35 | If `uv` is not installed:
 36 | 
 37 | ```bash
 38 | # Using pip (ensure pip is for Python 3.10+)
 39 | pip install uv
 40 | 
 41 | # Using Homebrew (macOS)
 42 | brew install uv
 43 | 
 44 | # Using Conda
 45 | conda install -c conda-forge uv
 46 | ```
 47 | 
 48 | Verify installation:
 49 | 
 50 | ```bash
 51 | uv --version
 52 | ```
 53 | 
 54 | ### 2. Create a Virtual Environment
 55 | 
 56 | Create and activate a virtual environment to isolate dependencies:
 57 | 
 58 | ```bash
 59 | uv venv
 60 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 61 | ```
 62 | 
 63 | ### 3. Install the DaVinci Resolve MCP Server
 64 | 
 65 | Install the server and its dependencies from the project directory:
 66 | 
 67 | ```bash
 68 | # From the project directory (editable install for development)
 69 | uv install -e .
 70 | 
 71 | # Or directly from GitHub (replace with your repo URL)
 72 | uv install git+https://github.com/yourusername/davinci-resolve-mcp.git
 73 | ```
 74 | 
 75 | ### 4. Install Dependencies
 76 | 
 77 | Ensure `requirements.txt` includes:
 78 | 
 79 | ```
 80 | mcp
 81 | pydantic
 82 | ```
 83 | 
 84 | Install them:
 85 | 
 86 | ```bash
 87 | uv install -r requirements.txt
 88 | ```
 89 | 
 90 | ## Configuration
 91 | 
 92 | Before running the server, ensure:
 93 | 
 94 | 1. DaVinci Resolve Studio is running.
 95 | 2. Python can access the DaVinci Resolve scripting API (handled automatically by `ResolveAPI` in most cases).
 96 | 
 97 | ### API Access Configuration
 98 | 
 99 | The `ResolveAPI` class dynamically locates the scripting API, but you may need to configure it manually in some cases:
100 | 
101 | #### macOS
102 | 
103 | The API is typically available at:
104 | 
105 | - `/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules`
106 | - Or user-specific: `~/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules`
107 | 
108 | No additional setup is usually required.
109 | 
110 | #### Windows
111 | 
112 | Add the API path if not detected:
113 | 
114 | ```python
115 | import sys
116 | sys.path.append("C:\\ProgramData\\Blackmagic Design\\DaVinci Resolve\\Support\\Developer\\Scripting\\Modules")
117 | ```
118 | 
119 | #### Linux
120 | 
121 | Set the environment variable:
122 | 
123 | ```bash
124 | export PYTHONPATH=$PYTHONPATH:/opt/resolve/Developer/Scripting/Modules
125 | ```
126 | 
127 | Alternatively, set a custom path via an environment variable:
128 | 
129 | ```bash
130 | export RESOLVE_SCRIPT_PATH="/custom/path/to/scripting/modules"
131 | ```
132 | 
133 | ## Running the Server
134 | 
135 | Start the MCP server:
136 | 
137 | ```bash
138 | # Run directly with Python
139 | python -m resolve_mcp.server
140 | 
141 | # Or with uv
142 | uv run resolve_mcp/server.py
143 | ```
144 | 
145 | The server will launch and connect to DaVinci Resolve, logging output like:
146 | 
147 | ```
148 | 2025-03-19 ... - resolve_mcp - INFO - Successfully connected to DaVinci Resolve.
149 | ```
150 | 
151 | ### Claude Integration Configuration
152 | 
153 | To integrate with Claude Desktop, update your `claude_desktop_config.json` (e.g., `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
154 | 
155 | ```json
156 | {
157 |   "mcpServers": {
158 |     "davinci-resolve": {
159 |       "command": "/path/to/uv",
160 |       "args": [
161 |         "run",
162 |         "--directory",
163 |         "/path/to/davinci-resolve-mcp",
164 |         "resolve_mcp/server.py"
165 |       ]
166 |     }
167 |   }
168 | }
169 | ```
170 | 
171 | - Replace `/path/to/uv` with the path to your `uv` executable (e.g., `/usr/local/bin/uv` or `C:\Users\username\.cargo\bin\uv.exe`).
172 | - Replace `/path/to/davinci-resolve-mcp` with the absolute path to your project directory.
173 | 
174 | Restart Claude Desktop to enable the server. Look for a hammer icon in the input box to confirm integration.
175 | 
176 | ## Troubleshooting
177 | 
178 | ### Connection Issues
179 | 
180 | If the server fails to connect:
181 | 
182 | 1. Ensure DaVinci Resolve Studio is running.
183 | 2. Check Resolve’s preferences to confirm scripting is enabled.
184 | 3. Verify Python version compatibility (3.10+ recommended):
185 |    ```bash
186 |    python --version
187 |    ```
188 | 4. Confirm API paths are accessible (see logs in `~/Library/Logs/Claude/mcp*.log` on macOS or `%userprofile%\AppData\Roaming\Claude\Logs\` on Windows).
189 | 
190 | ### Dependency Issues
191 | 
192 | If modules like `mcp` or `pydantic` are missing:
193 | 
194 | ```bash
195 | uv install mcp pydantic
196 | ```
197 | 
198 | ### Python Version Compatibility
199 | 
200 | Switch to a compatible version with `pyenv` if needed:
201 | 
202 | ```bash
203 | pyenv install 3.10.12
204 | pyenv shell 3.10.12
205 | uv install -r requirements.txt
206 | ```
207 | 
208 | ## Available Tools and Resources
209 | 
210 | The MCP server provides extensive functionality through the `ResolveAPI` class:
211 | 
212 | ### Project Management
213 | 
214 | - Create new projects (`create_project`)
215 | - Load existing projects (`load_project`)
216 | - Save current projects (`save_project`)
217 | - Export/import projects (`export_project`, `import_project`)
218 | - Get/set project settings (`get_project_settings`, `set_project_setting`)
219 | 
220 | ### Timeline Operations
221 | 
222 | - Create new timelines (`create_timeline`)
223 | - Set/get current timeline (`set_current_timeline`, `get_current_timeline`)
224 | - Add/manage tracks (`add_track`, `set_track_name`, `enable_track`)
225 | - Get timeline items (`get_timeline_items`)
226 | - Set clip properties (`set_clip_property`)
227 | - Add markers (`add_timeline_marker`)
228 | 
229 | ### Media Management
230 | 
231 | - Import media files (`add_items_to_media_pool`)
232 | - Create media pool folders (`add_sub_folder`)
233 | - Create timelines from clips (`create_timeline_from_clips`)
234 | - Get clip metadata (`get_clip_metadata`)
235 | 
236 | ### Fusion Integration
237 | 
238 | - Add Fusion compositions to clips (`create_fusion_node`)
239 | - Create/manage Fusion nodes (`create_fusion_node`)
240 | - Access current composition (`get_current_comp`)
241 | 
242 | ### Color Grading
243 | 
244 | - Get/add color nodes (`get_color_page_nodes`, `add_color_node`)
245 | - Save/apply stills (`save_still`, `apply_still`)
246 | - Manage gallery albums (`get_gallery_albums`)
247 | 
248 | ### Audio Control
249 | 
250 | - Get/set clip audio volume (`get_audio_volume`, `set_audio_volume`)
251 | - Set track volume (`set_track_volume`)
252 | 
253 | ### Playback Control
254 | 
255 | - Play/stop playback (`play`, `stop`)
256 | - Get/set playhead position (`get_current_timecode`, `set_playhead_position`)
257 | 
258 | ### Rendering
259 | 
260 | - Start rendering (`start_render`)
261 | - Get render status (`get_render_status`)
262 | 
263 | ### Navigation
264 | 
265 | - Open specific pages (`open_page`: Media, Edit, Fusion, Color, Fairlight, Deliver)
266 | 
267 | ### Advanced Operations
268 | 
269 | - Execute custom Python code (`execute_python`)
270 | - Execute Lua scripts in Fusion (`execute_lua`)
271 | 
272 | ## Development
273 | 
274 | To contribute:
275 | 
276 | 1. Fork the repository: `https://github.com/yourusername/davinci-resolve-mcp`
277 | 2. Create a feature branch: `git checkout -b feature-name`
278 | 3. Install dependencies: `uv install -e .`
279 | 4. Make changes and test: `uv run resolve_mcp/server.py`
280 | 5. Submit a pull request.
281 | 
282 | ## License
283 | 
284 | [MIT License](LICENSE)
285 | 
286 | ---
287 | 
288 | ### Key Updates
289 | 
290 | - **Expanded Features**: Added new capabilities like gallery management, track control, audio adjustments, playback, and project export/import to the “Available Tools and Resources” section.
291 | - **Installation Clarity**: Improved `uv` instructions with verification steps and explicit paths for Claude integration.
292 | - **Troubleshooting**: Enhanced with specific commands and log locations for debugging.
293 | - **Configuration**: Updated API access notes to reflect the dynamic path handling in `ResolveAPI`.
294 | 
295 | This README now fully aligns with the enhanced `ResolveAPI` class, providing a comprehensive guide for users and developers. Let me know if you’d like further adjustments!
296 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "davinci-resolve-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.10"
 7 | dependencies = [
 8 |     "mcp[cli]>=1.4.1",
 9 |     "pydantic>=2.10.6",
10 | ]
11 | 
```

--------------------------------------------------------------------------------
/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 to interact with DaVinci Resolve through the MCP protocol.
  6 | """
  7 | 
  8 | import logging
  9 | import sys
 10 | from typing import List, Dict, Any, Optional
 11 | 
 12 | # Configure logging with timestamp, name, level, and message format
 13 | logging.basicConfig(
 14 |     level=logging.INFO,
 15 |     format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
 16 | )
 17 | logger = logging.getLogger("resolve_mcp")  # Logger instance for this module
 18 | 
 19 | # Add MCP library to path if not installed as a package
 20 | try:
 21 |     from mcp.server.fastmcp import FastMCP  # Import FastMCP for MCP server functionality
 22 |     print("Successfully imported FastMCP", file=sys.stderr)
 23 | except ImportError as e:
 24 |     print(f"Error importing FastMCP: {e}", file=sys.stderr)
 25 |     raise  # Raise exception if MCP library is unavailable
 26 | 
 27 | # Import ResolveAPI (assumes resolve_api.py is in the same directory or installed)
 28 | try:
 29 |     from resolve_api import ResolveAPI  # Import ResolveAPI for DaVinci Resolve interaction
 30 |     print("Successfully imported ResolveAPI", file=sys.stderr)
 31 | except ImportError as e:
 32 |     print(f"Error importing ResolveAPI: {e}", file=sys.stderr)
 33 |     raise  # Raise exception if ResolveAPI is unavailable
 34 | 
 35 | # Create the MCP server instance with the name "DaVinci Resolve"
 36 | mcp = FastMCP("DaVinci Resolve")
 37 | 
 38 | # Initialize the Resolve API to connect to DaVinci Resolve
 39 | resolve_api = ResolveAPI()
 40 | 
 41 | # Check connection to Resolve and log the result
 42 | if not resolve_api.is_connected():
 43 |     logger.error("Failed to connect to DaVinci Resolve. Ensure it is running.")
 44 | else:
 45 |     logger.info("Successfully connected to DaVinci Resolve.")
 46 | 
 47 | # --- Resource Definitions ---
 48 | 
 49 | @mcp.resource("system://status")
 50 | def get_system_status() -> str:
 51 |     """Get the current status of the DaVinci Resolve connection."""
 52 |     if not resolve_api.is_connected():
 53 |         return "Not connected to DaVinci Resolve."
 54 |     project_name = resolve_api.get_project_name() or "No project open"
 55 |     timeline = resolve_api.get_current_timeline()
 56 |     timeline_name = timeline.GetName() if timeline else "No timeline open"
 57 |     return f"Connected: Yes\nProject: {project_name}\nTimeline: {timeline_name}"
 58 | 
 59 | @mcp.resource("project://current")
 60 | def get_current_project() -> str:
 61 |     """Get information about the current project."""
 62 |     if not resolve_api.is_connected():
 63 |         return "Not connected to DaVinci Resolve."
 64 |     project = resolve_api.get_current_project()
 65 |     if not project:
 66 |         return "No project open."
 67 |     return f"Name: {project.GetName()}\nTimelines: {project.GetTimelineCount()}"
 68 | 
 69 | @mcp.resource("timeline://current")
 70 | def get_current_timeline() -> str:
 71 |     """Get information about the current timeline."""
 72 |     if not resolve_api.is_connected():
 73 |         return "Not connected to DaVinci Resolve."
 74 |     timeline = resolve_api.get_current_timeline()
 75 |     if not timeline:
 76 |         return "No timeline open."
 77 |     return (f"Name: {timeline.GetName()}\n"
 78 |             f"Duration: {timeline.GetEndFrame() - timeline.GetStartFrame() + 1} frames\n"
 79 |             f"Video Tracks: {timeline.GetTrackCount('video')}")
 80 | 
 81 | @mcp.resource("mediapool://current")
 82 | def get_current_media_pool_folder() -> str:
 83 |     """Get information about the current media pool folder."""
 84 |     if not resolve_api.is_connected():
 85 |         return "Not connected to DaVinci Resolve."
 86 |     media_pool = resolve_api.get_media_pool()
 87 |     if not media_pool:
 88 |         return "No media pool available."
 89 |     folder = media_pool.GetCurrentFolder()
 90 |     if not folder:
 91 |         return "No current folder."
 92 |     clips = folder.GetClips()
 93 |     clip_count = len(clips) if clips else 0
 94 |     return f"Folder: {folder.GetName()}\nClips: {clip_count}"
 95 | 
 96 | @mcp.resource("gallery://albums")
 97 | def get_gallery_albums() -> str:
 98 |     """Get a list of albums in the gallery."""
 99 |     if not resolve_api.is_connected():
100 |         return "Not connected to DaVinci Resolve."
101 |     albums = resolve_api.get_gallery_albums()
102 |     return "\n".join([album.GetName() for album in albums]) if albums else "No albums"
103 | 
104 | @mcp.resource("timeline://items")
105 | def get_timeline_items_resource() -> str:
106 |     """Get a list of items in the first video track of the current timeline."""
107 |     items = resolve_api.get_timeline_items("video", 1)
108 |     return "\n".join([f"Clip {i+1}: {item.GetName()}" for i, item in enumerate(items)]) if items else "No items"
109 | 
110 | # --- Tool Definitions ---
111 | 
112 | @mcp.tool()
113 | def refresh() -> str:
114 |     """Refresh all internal Resolve objects to reflect the current state."""
115 |     if not resolve_api.is_connected():
116 |         return "Not connected to DaVinci Resolve."
117 |     resolve_api.refresh()
118 |     return "Resolve API state refreshed."
119 | 
120 | @mcp.tool()
121 | def create_project(name: str) -> str:
122 |     """Create a new project in DaVinci Resolve."""
123 |     if not resolve_api.is_connected():
124 |         return "Not connected to DaVinci Resolve."
125 |     success = resolve_api.create_project(name)
126 |     return f"Project '{name}' created." if success else f"Failed to create '{name}'."
127 | 
128 | @mcp.tool()
129 | def load_project(name: str) -> str:
130 |     """Load an existing project in DaVinci Resolve."""
131 |     if not resolve_api.is_connected():
132 |         return "Not connected to DaVinci Resolve."
133 |     success = resolve_api.load_project(name)
134 |     return f"Project '{name}' loaded." if success else f"Failed to load '{name}'."
135 | 
136 | @mcp.tool()
137 | def save_project() -> str:
138 |     """Save the current project."""
139 |     if not resolve_api.is_connected():
140 |         return "Not connected to DaVinci Resolve."
141 |     success = resolve_api.save_project()
142 |     return "Project saved." if success else "Failed to save project."
143 | 
144 | @mcp.tool()
145 | def export_project(project_name: str, file_path: str) -> str:
146 |     """Export a project to a file."""
147 |     if not resolve_api.is_connected():
148 |         return "Not connected to DaVinci Resolve."
149 |     success = resolve_api.export_project(project_name, file_path)
150 |     return f"Project '{project_name}' exported to '{file_path}'." if success else "Failed to export project."
151 | 
152 | @mcp.tool()
153 | def import_project(file_path: str) -> str:
154 |     """Import a project from a file."""
155 |     if not resolve_api.is_connected():
156 |         return "Not connected to DaVinci Resolve."
157 |     success = resolve_api.import_project(file_path)
158 |     return f"Project imported from '{file_path}'." if success else "Failed to import project."
159 | 
160 | @mcp.tool()
161 | def open_page(page_name: str) -> str:
162 |     """Open a specific page in DaVinci Resolve."""
163 |     valid_pages = ["media", "edit", "fusion", "color", "fairlight", "deliver"]
164 |     if page_name.lower() not in valid_pages:
165 |         return f"Invalid page. Use: {', '.join(valid_pages)}"
166 |     if not resolve_api.is_connected():
167 |         return "Not connected to DaVinci Resolve."
168 |     success = resolve_api.open_page(page_name.lower())
169 |     return f"Opened '{page_name}' page." if success else f"Failed to open '{page_name}'."
170 | 
171 | @mcp.tool()
172 | def create_timeline(name: str) -> str:
173 |     """Create a new timeline in the current project."""
174 |     if not resolve_api.is_connected():
175 |         return "Not connected to DaVinci Resolve."
176 |     success = resolve_api.create_timeline(name)
177 |     return f"Timeline '{name}' created." if success else f"Failed to create '{name}'."
178 | 
179 | @mcp.tool()
180 | def set_current_timeline(timeline_index: int) -> str:
181 |     """Set the specified timeline as the current one by 1-based index."""
182 |     if not resolve_api.is_connected():
183 |         return "Not connected to DaVinci Resolve."
184 |     timeline = resolve_api.get_timeline_by_index(timeline_index)
185 |     if not timeline:
186 |         return f"No timeline found at index {timeline_index}."
187 |     success = resolve_api.set_current_timeline(timeline)
188 |     return f"Timeline at index {timeline_index} set as current." if success else "Failed to set timeline."
189 | 
190 | @mcp.tool()
191 | def import_media(file_paths: List[str]) -> str:
192 |     """Import media files into the current media pool folder."""
193 |     if not resolve_api.is_connected():
194 |         return "Not connected to DaVinci Resolve."
195 |     clips = resolve_api.add_items_to_media_pool(file_paths)
196 |     return f"Imported {len(clips)} files." if clips else "Failed to import files."
197 | 
198 | @mcp.tool()
199 | def add_sub_folder(parent_folder_name: str, folder_name: str) -> str:
200 |     """Add a subfolder to the specified parent folder in the media pool."""
201 |     if not resolve_api.is_connected():
202 |         return "Not connected to DaVinci Resolve."
203 |     media_pool = resolve_api.get_media_pool()
204 |     if not media_pool:
205 |         return "No media pool available."
206 |     root_folder = media_pool.GetRootFolder()
207 |     parent_folder = next((f for f in root_folder.GetSubFolders() if f.GetName() == parent_folder_name), None)
208 |     if not parent_folder:
209 |         return f"Parent folder '{parent_folder_name}' not found."
210 |     sub_folder = resolve_api.add_sub_folder(parent_folder, folder_name)
211 |     return f"Subfolder '{folder_name}' added to '{parent_folder_name}'." if sub_folder else "Failed to add subfolder."
212 | 
213 | @mcp.tool()
214 | def append_to_timeline(clip_names: List[str]) -> str:
215 |     """Append clips to the current timeline by name."""
216 |     if not resolve_api.is_connected():
217 |         return "Not connected to DaVinci Resolve."
218 |     media_pool = resolve_api.get_media_pool()
219 |     if not media_pool:
220 |         return "No media pool available."
221 |     folder = media_pool.GetCurrentFolder()
222 |     clips = [clip for clip in folder.GetClips() if clip.GetClipProperty("Clip Name") in clip_names]
223 |     success = resolve_api.append_to_timeline(clips)
224 |     return f"Appended {len(clips)} clips to timeline." if success else "Failed to append clips."
225 | 
226 | @mcp.tool()
227 | def create_timeline_from_clips(timeline_name: str, clip_names: List[str]) -> str:
228 |     """Create a new timeline from the specified clips by name."""
229 |     if not resolve_api.is_connected():
230 |         return "Not connected to DaVinci Resolve."
231 |     media_pool = resolve_api.get_media_pool()
232 |     if not media_pool:
233 |         return "No media pool available."
234 |     folder = media_pool.GetCurrentFolder()
235 |     clips = [clip for clip in folder.GetClips() if clip.GetClipProperty("Clip Name") in clip_names]
236 |     timeline = resolve_api.create_timeline_from_clips(timeline_name, clips)
237 |     return f"Timeline '{timeline_name}' created with {len(clips)} clips." if timeline else "Failed to create timeline."
238 | 
239 | @mcp.tool()
240 | def import_timeline_from_file(file_path: str) -> str:
241 |     """Import a timeline from a file (e.g., XML, EDL)."""
242 |     if not resolve_api.is_connected():
243 |         return "Not connected to DaVinci Resolve."
244 |     timeline = resolve_api.import_timeline_from_file(file_path)
245 |     return f"Timeline imported from '{file_path}'." if timeline else "Failed to import timeline."
246 | 
247 | @mcp.tool()
248 | def execute_lua(script: str) -> str:
249 |     """Execute a Lua script in Resolve's Fusion environment."""
250 |     if not resolve_api.is_connected():
251 |         return "Not connected to DaVinci Resolve."
252 |     result = resolve_api.execute_lua(script)
253 |     return f"Lua script executed: {result}" if result else "Failed to execute Lua script."
254 | 
255 | @mcp.tool()
256 | def create_fusion_node(node_type: str, inputs: Dict[str, Any] = None) -> str:
257 |     """Create a new node in the current Fusion composition."""
258 |     if not resolve_api.is_connected():
259 |         return "Not connected to DaVinci Resolve."
260 |     comp = resolve_api.get_current_comp()
261 |     if not comp:
262 |         return "No current Fusion composition."
263 |     node = resolve_api.create_fusion_node(comp, node_type, inputs)
264 |     return f"Node '{node_type}' created." if node else f"Failed to create '{node_type}' node."
265 | 
266 | @mcp.tool()
267 | def set_clip_property(clip_name: str, property_name: str, value: Any) -> str:
268 |     """Set a property on a timeline clip by name."""
269 |     if not resolve_api.is_connected():
270 |         return "Not connected to DaVinci Resolve."
271 |     items = resolve_api.get_timeline_items("video", 1)
272 |     clip = next((item for item in items if item.GetName() == clip_name), None)
273 |     if not clip:
274 |         return f"Clip '{clip_name}' not found."
275 |     success = resolve_api.set_clip_property(clip, property_name, value)
276 |     return f"Property '{property_name}' set to {value} on '{clip_name}'." if success else "Failed to set property."
277 | 
278 | @mcp.tool()
279 | def add_color_node(node_type: str = "Corrector") -> str:
280 |     """Add a new node to the current clip's color grade."""
281 |     if not resolve_api.is_connected():
282 |         return "Not connected to DaVinci Resolve."
283 |     node = resolve_api.add_color_node(node_type)
284 |     return f"Color node '{node_type}' added." if node else "Failed to add color node."
285 | 
286 | @mcp.tool()
287 | def set_project_setting(key: str, value: Any) -> str:
288 |     """Set a specific project setting."""
289 |     if not resolve_api.is_connected():
290 |         return "Not connected to DaVinci Resolve."
291 |     success = resolve_api.set_project_setting(key, value)
292 |     return f"Project setting '{key}' set to {value}." if success else f"Failed to set '{key}'."
293 | 
294 | @mcp.tool()
295 | def start_project_render(preset_name: str = None, render_path: str = None) -> str:
296 |     """Start rendering the current project with optional preset and path."""
297 |     if not resolve_api.is_connected():
298 |         return "Not connected to DaVinci Resolve."
299 |     success = resolve_api.start_render(preset_name, render_path)
300 |     return "Render started." if success else "Failed to start render."
301 | 
302 | @mcp.tool()
303 | def add_timeline_marker(frame: int, color: str = "Blue", name: str = "", note: str = "") -> str:
304 |     """Add a marker to the current timeline at a specific frame."""
305 |     if not resolve_api.is_connected():
306 |         return "Not connected to DaVinci Resolve."
307 |     success = resolve_api.add_timeline_marker(frame, color, name, note)
308 |     return f"Marker added at frame {frame}." if success else "Failed to add marker."
309 | 
310 | @mcp.tool()
311 | def save_still(album_name: str = "Stills") -> str:
312 |     """Save the current clip's grade as a still in the specified gallery album."""
313 |     if not resolve_api.is_connected():
314 |         return "Not connected to DaVinci Resolve."
315 |     still = resolve_api.save_still(album_name)
316 |     return f"Still saved to '{album_name}'." if still else "Failed to save still."
317 | 
318 | @mcp.tool()
319 | def apply_still(still_name: str, clip_name: str = None) -> str:
320 |     """Apply a still (grade) to a clip by name, defaulting to current clip if none specified."""
321 |     if not resolve_api.is_connected():
322 |         return "Not connected to DaVinci Resolve."
323 |     gallery = resolve_api.get_gallery()
324 |     if not gallery:
325 |         return "No gallery available."
326 |     albums = resolve_api.get_gallery_albums()
327 |     still = None
328 |     for album in albums:
329 |         stills = album.GetStills()
330 |         still = next((s for s in stills if s.GetLabel() == still_name), None)
331 |         if still:
332 |             break
333 |     if not still:
334 |         return f"Still '{still_name}' not found."
335 |     clip = None
336 |     if clip_name:
337 |         items = resolve_api.get_timeline_items("video", 1)
338 |         clip = next((item for item in items if item.GetName() == clip_name), None)
339 |         if not clip:
340 |             return f"Clip '{clip_name}' not found."
341 |     success = resolve_api.apply_still(still, clip)
342 |     return f"Still '{still_name}' applied." if success else "Failed to apply still."
343 | 
344 | @mcp.tool()
345 | def add_track(track_type: str = "video") -> str:
346 |     """Add a new track to the current timeline."""
347 |     if not resolve_api.is_connected():
348 |         return "Not connected to DaVinci Resolve."
349 |     success = resolve_api.add_track(track_type)
350 |     return f"{track_type.capitalize()} track added." if success else f"Failed to add {track_type} track."
351 | 
352 | @mcp.tool()
353 | def set_track_name(track_type: str, track_index: int, name: str) -> str:
354 |     """Set the name of a track in the current timeline."""
355 |     if not resolve_api.is_connected():
356 |         return "Not connected to DaVinci Resolve."
357 |     success = resolve_api.set_track_name(track_type, track_index, name)
358 |     return f"Track {track_index} named '{name}'." if success else "Failed to set track name."
359 | 
360 | @mcp.tool()
361 | def enable_track(track_type: str, track_index: int, enable: bool = True) -> str:
362 |     """Enable or disable a track in the current timeline."""
363 |     if not resolve_api.is_connected():
364 |         return "Not connected to DaVinci Resolve."
365 |     success = resolve_api.enable_track(track_type, track_index, enable)
366 |     state = "enabled" if enable else "disabled"
367 |     return f"Track {track_index} {state}." if success else f"Failed to {state} track."
368 | 
369 | @mcp.tool()
370 | def set_audio_volume(clip_name: str, volume: float) -> str:
371 |     """Set the audio volume of a timeline clip by name."""
372 |     if not resolve_api.is_connected():
373 |         return "Not connected to DaVinci Resolve."
374 |     items = resolve_api.get_timeline_items("audio", 1)
375 |     clip = next((item for item in items if item.GetName() == clip_name), None)
376 |     if not clip:
377 |         return f"Clip '{clip_name}' not found."
378 |     success = resolve_api.set_audio_volume(clip, volume)
379 |     return f"Volume set to {volume} on '{clip_name}'." if success else "Failed to set volume."
380 | 
381 | @mcp.tool()
382 | def set_track_volume(track_index: int, volume: float) -> str:
383 |     """Set the volume of an audio track in the current timeline."""
384 |     if not resolve_api.is_connected():
385 |         return "Not connected to DaVinci Resolve."
386 |     success = resolve_api.set_track_volume(track_index, volume)
387 |     return f"Track {track_index} volume set to {volume}." if success else "Failed to set track volume."
388 | 
389 | @mcp.tool()
390 | def set_current_version(clip_name: str, version_index: int, version_type: str = "color") -> str:
391 |     """Set the current version for a clip by name (e.g., switch between color grades)."""
392 |     if not resolve_api.is_connected():
393 |         return "Not connected to DaVinci Resolve."
394 |     items = resolve_api.get_timeline_items("video", 1)
395 |     clip = next((item for item in items if item.GetName() == clip_name), None)
396 |     if not clip:
397 |         return f"Clip '{clip_name}' not found."
398 |     success = resolve_api.set_current_version(clip, version_index, version_type)
399 |     return f"Version {version_index} set for '{clip_name}'." if success else "Failed to set version."
400 | 
401 | @mcp.tool()
402 | def play_timeline() -> str:
403 |     """Start playback in the current timeline."""
404 |     if not resolve_api.is_connected():
405 |         return "Not connected to DaVinci Resolve."
406 |     success = resolve_api.play()
407 |     return "Playback started." if success else "Failed to start playback."
408 | 
409 | @mcp.tool()
410 | def stop_timeline() -> str:
411 |     """Stop playback in the current timeline."""
412 |     if not resolve_api.is_connected():
413 |         return "Not connected to DaVinci Resolve."
414 |     success = resolve_api.stop()
415 |     return "Playback stopped." if success else "Failed to stop playback."
416 | 
417 | @mcp.tool()
418 | def set_playhead_position(frame: int) -> str:
419 |     """Set the playhead position to a specific frame in the current timeline."""
420 |     if not resolve_api.is_connected():
421 |         return "Not connected to DaVinci Resolve."
422 |     success = resolve_api.set_playhead_position(frame)
423 |     return f"Playhead set to frame {frame}." if success else "Failed to set playhead position."
424 | 
425 | # --- Main Entry Point ---
426 | 
427 | def main():
428 |     """Run the MCP server."""
429 |     logger.info("Starting DaVinci Resolve MCP server...")
430 |     mcp.run()  # Start the MCP server to listen for connections
431 | 
432 | if __name__ == "__main__":
433 |     main()  # Execute main function if script is run directly
```

--------------------------------------------------------------------------------
/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, such as projects, timelines, media pools, and more.
   6 | """
   7 | 
   8 | import sys
   9 | import os
  10 | import platform
  11 | import logging
  12 | from typing import Optional, Dict, List, Any, Union, Tuple
  13 | 
  14 | # Configure logging with a standard format including timestamp, logger name, level, and message
  15 | logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s")
  16 | logger = logging.getLogger("ResolveAPI")  # Logger instance for this module
  17 | 
  18 | class ResolveAPI:
  19 |     """Class to handle connection and interaction with DaVinci Resolve API."""
  20 |     
  21 |     def __init__(self):
  22 |         """
  23 |         Initialize the ResolveAPI class and establish a connection to DaVinci Resolve.
  24 |         Sets up internal references to Resolve objects (e.g., project manager, media pool).
  25 |         """
  26 |         self.resolve = None  # Main Resolve application object
  27 |         self.fusion = None  # Fusion object for compositing
  28 |         self.project_manager = None  # Project manager object
  29 |         self.current_project = None  # Current project object
  30 |         self.media_storage = None  # Media storage object
  31 |         self.media_pool = None  # Media pool object for the current project
  32 |         self._connect_to_resolve()  # Attempt to connect to Resolve on initialization
  33 | 
  34 |     def _find_scripting_module(self) -> Optional[str]:
  35 |         """
  36 |         Dynamically locate the DaVinciResolveScript module path based on the operating system.
  37 |         Checks for a custom path via environment variable, then falls back to default locations.
  38 |         
  39 |         Returns:
  40 |             Optional[str]: Path to the scripting module if found, None otherwise.
  41 |         """
  42 |         custom_path = os.environ.get("RESOLVE_SCRIPT_PATH")  # Check for user-defined path
  43 |         if custom_path and os.path.exists(custom_path):
  44 |             return custom_path
  45 |         # Default paths for Resolve scripting module by OS
  46 |         base_paths = {
  47 |             "Windows": os.path.join(os.environ.get("PROGRAMDATA", "C:\\ProgramData"), "Blackmagic Design", "DaVinci Resolve", "Support", "Developer", "Scripting", "Modules"),
  48 |             "Darwin": ["/Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules",
  49 |                        os.path.join(os.path.expanduser("~"), "Library/Application Support/Blackmagic Design/DaVinci Resolve/Developer/Scripting/Modules")],
  50 |             "Linux": "/opt/resolve/Developer/Scripting/Modules"
  51 |         }
  52 |         system = platform.system()  # Get current OS
  53 |         paths = base_paths.get(system, []) if system != "Darwin" else base_paths["Darwin"]
  54 |         for path in ([paths] if isinstance(paths, str) else paths):  # Handle single path or list
  55 |             if os.path.exists(path) and path not in sys.path:
  56 |                 sys.path.append(path)  # Add to Python path if not already present
  57 |                 return path
  58 |         return None  # Return None if no valid path is found
  59 | 
  60 |     def _connect_to_resolve(self) -> None:
  61 |         """
  62 |         Establish a connection to DaVinci Resolve by importing its scripting module.
  63 |         Initializes core objects (e.g., project manager, media pool) if successful.
  64 |         """
  65 |         script_path = self._find_scripting_module()  # Find the scripting module path
  66 |         if not script_path:
  67 |             logger.error("No valid Resolve scripting module path found")
  68 |             return
  69 |         try:
  70 |             import DaVinciResolveScript as dvr_script  # Import the Resolve scripting API
  71 |             self.resolve = dvr_script.scriptapp("Resolve")  # Connect to Resolve app
  72 |             logger.info(f"Connected to Resolve using {script_path}")
  73 |         except ImportError:
  74 |             logger.error(f"Failed to import DaVinciResolveScript from {script_path}")
  75 |             self.resolve = None
  76 |         if self.resolve:  # If connection is successful, initialize other objects
  77 |             self.project_manager = self.resolve.GetProjectManager()
  78 |             self.current_project = self.project_manager.GetCurrentProject()
  79 |             self.media_storage = self.resolve.GetMediaStorage()
  80 |             self.fusion = self.resolve.Fusion()
  81 |             self.media_pool = self.current_project.GetMediaPool() if self.current_project else None
  82 | 
  83 |     def refresh(self) -> None:
  84 |         """
  85 |         Refresh all internal Resolve objects to ensure they reflect the current state.
  86 |         Useful if Resolve's state changes externally (e.g., project switch).
  87 |         """
  88 |         if not self.resolve:  # Reconnect if not already connected
  89 |             self._connect_to_resolve()
  90 |         if self.resolve:
  91 |             self.project_manager = self.resolve.GetProjectManager()  # Update project manager
  92 |             self.current_project = self.project_manager.GetCurrentProject()  # Update current project
  93 |             self.media_storage = self.resolve.GetMediaStorage()  # Update media storage
  94 |             self.fusion = self.resolve.Fusion()  # Update Fusion object
  95 |             self.media_pool = self.current_project.GetMediaPool() if self.current_project else None  # Update media pool
  96 |             logger.info("Refreshed Resolve API state")
  97 | 
  98 |     def is_connected(self) -> bool:
  99 |         """
 100 |         Check if the API is connected to DaVinci Resolve.
 101 |         
 102 |         Returns:
 103 |             bool: True if connected, False otherwise.
 104 |         """
 105 |         return self.resolve is not None
 106 | 
 107 |     def get_project_manager(self):
 108 |         """
 109 |         Get the project manager object.
 110 |         
 111 |         Returns:
 112 |             Any: ProjectManager object or None if not connected.
 113 |         """
 114 |         return self.project_manager
 115 | 
 116 |     def get_current_project(self):
 117 |         """
 118 |         Get the current project object, refreshing it from the project manager.
 119 |         
 120 |         Returns:
 121 |             Any: Current Project object or None if no project is open.
 122 |         """
 123 |         if self.project_manager:
 124 |             self.current_project = self.project_manager.GetCurrentProject()
 125 |         return self.current_project
 126 | 
 127 |     def get_media_storage(self):
 128 |         """
 129 |         Get the media storage object.
 130 |         
 131 |         Returns:
 132 |             Any: MediaStorage object or None if not connected.
 133 |         """
 134 |         return self.media_storage
 135 | 
 136 |     def get_media_pool(self):
 137 |         """
 138 |         Get the media pool object for the current project, refreshing it if needed.
 139 |         
 140 |         Returns:
 141 |             Any: MediaPool object or None if no project is open.
 142 |         """
 143 |         if self.current_project:
 144 |             self.media_pool = self.current_project.GetMediaPool()
 145 |         return self.media_pool
 146 | 
 147 |     def get_fusion(self):
 148 |         """
 149 |         Get the Fusion object for compositing tasks.
 150 |         
 151 |         Returns:
 152 |             Any: Fusion object or None if not connected.
 153 |         """
 154 |         return self.fusion
 155 | 
 156 |     def open_page(self, page_name: str) -> bool:
 157 |         """
 158 |         Open a specific page in DaVinci Resolve (e.g., "edit", "color").
 159 |         
 160 |         Args:
 161 |             page_name (str): The name of the page to open (valid: "media", "edit", "fusion", "color", "fairlight", "deliver").
 162 |         
 163 |         Returns:
 164 |             bool: True if successful, False if not connected or invalid page.
 165 |         """
 166 |         if not self.resolve:
 167 |             return False
 168 |         valid_pages = ["media", "edit", "fusion", "color", "fairlight", "deliver"]
 169 |         if page_name.lower() not in valid_pages:
 170 |             return False
 171 |         self.resolve.OpenPage(page_name.lower())  # Open the specified page
 172 |         return True
 173 | 
 174 |     def create_project(self, project_name: str) -> bool:
 175 |         """
 176 |         Create a new project with the given name.
 177 |         
 178 |         Args:
 179 |             project_name (str): Name of the project to create.
 180 |         
 181 |         Returns:
 182 |             bool: True if successful, False if project manager is unavailable or creation fails.
 183 |         """
 184 |         if not self.project_manager:
 185 |             return False
 186 |         new_project = self.project_manager.CreateProject(project_name)
 187 |         if new_project:  # If creation succeeds, update internal state
 188 |             self.current_project = new_project
 189 |             self.media_pool = self.current_project.GetMediaPool()
 190 |             return True
 191 |         return False
 192 | 
 193 |     def load_project(self, project_name: str) -> bool:
 194 |         """
 195 |         Load an existing project by name.
 196 |         
 197 |         Args:
 198 |             project_name (str): Name of the project to load.
 199 |         
 200 |         Returns:
 201 |             bool: True if successful, False if project manager is unavailable or project doesn't exist.
 202 |         """
 203 |         if not self.project_manager:
 204 |             return False
 205 |         loaded_project = self.project_manager.LoadProject(project_name)
 206 |         if loaded_project:  # If loading succeeds, update internal state
 207 |             self.current_project = loaded_project
 208 |             self.media_pool = self.current_project.GetMediaPool()
 209 |             return True
 210 |         return False
 211 | 
 212 |     def save_project(self) -> bool:
 213 |         """
 214 |         Save the current project.
 215 |         
 216 |         Returns:
 217 |             bool: True if successful, False if no project is open.
 218 |         """
 219 |         if not self.current_project:
 220 |             return False
 221 |         return self.current_project.SaveProject()
 222 | 
 223 |     def get_project_name(self) -> Optional[str]:
 224 |         """
 225 |         Get the name of the current project.
 226 |         
 227 |         Returns:
 228 |             Optional[str]: Project name or None if no project is open.
 229 |         """
 230 |         if not self.current_project:
 231 |             return None
 232 |         return self.current_project.GetName()
 233 | 
 234 |     def create_timeline(self, timeline_name: str) -> bool:
 235 |         """
 236 |         Create a new empty timeline in the current project.
 237 |         
 238 |         Args:
 239 |             timeline_name (str): Name of the timeline to create.
 240 |         
 241 |         Returns:
 242 |             bool: True if successful, False if media pool is unavailable or creation fails.
 243 |         """
 244 |         if not self.media_pool:
 245 |             return False
 246 |         new_timeline = self.media_pool.CreateEmptyTimeline(timeline_name)
 247 |         return new_timeline is not None
 248 | 
 249 |     def get_current_timeline(self):
 250 |         """
 251 |         Get the current timeline in the current project.
 252 |         
 253 |         Returns:
 254 |             Any: Timeline object or None if no project is open.
 255 |         """
 256 |         if not self.current_project:
 257 |             return None
 258 |         return self.current_project.GetCurrentTimeline()
 259 | 
 260 |     def get_timeline_count(self) -> int:
 261 |         """
 262 |         Get the number of timelines in the current project.
 263 |         
 264 |         Returns:
 265 |             int: Number of timelines, or 0 if no project is open.
 266 |         """
 267 |         if not self.current_project:
 268 |             return 0
 269 |         return self.current_project.GetTimelineCount()
 270 | 
 271 |     def get_timeline_by_index(self, index: int):
 272 |         """
 273 |         Get a timeline by its 1-based index.
 274 |         
 275 |         Args:
 276 |             index (int): 1-based index of the timeline.
 277 |         
 278 |         Returns:
 279 |             Any: Timeline object or None if no project or invalid index.
 280 |         """
 281 |         if not self.current_project:
 282 |             return None
 283 |         return self.current_project.GetTimelineByIndex(index)
 284 | 
 285 |     def set_current_timeline(self, timeline) -> bool:
 286 |         """
 287 |         Set the specified timeline as the current one.
 288 |         
 289 |         Args:
 290 |             timeline: Timeline object to set as current.
 291 |         
 292 |         Returns:
 293 |             bool: True if successful, False if no project is open.
 294 |         """
 295 |         if not self.current_project:
 296 |             return False
 297 |         return self.current_project.SetCurrentTimeline(timeline)
 298 | 
 299 |     def get_mounted_volumes(self) -> List[str]:
 300 |         """
 301 |         Get a list of mounted volumes in the media storage.
 302 |         
 303 |         Returns:
 304 |             List[str]: List of volume paths, empty if media storage is unavailable.
 305 |         """
 306 |         if not self.media_storage:
 307 |             return []
 308 |         return self.media_storage.GetMountedVolumes()
 309 | 
 310 |     def get_sub_folders(self, folder_path: str) -> List[str]:
 311 |         """
 312 |         Get a list of subfolders in the specified folder path.
 313 |         
 314 |         Args:
 315 |             folder_path (str): Path to the folder.
 316 |         
 317 |         Returns:
 318 |             List[str]: List of subfolder paths, empty if media storage is unavailable.
 319 |         """
 320 |         if not self.media_storage:
 321 |             return []
 322 |         return self.media_storage.GetSubFolders(folder_path)
 323 | 
 324 |     def get_files(self, folder_path: str) -> List[str]:
 325 |         """
 326 |         Get a list of files in the specified folder path.
 327 |         
 328 |         Args:
 329 |             folder_path (str): Path to the folder.
 330 |         
 331 |         Returns:
 332 |             List[str]: List of file paths, empty if media storage is unavailable.
 333 |         """
 334 |         if not self.media_storage:
 335 |             return []
 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 media files to the current media pool.
 341 |         
 342 |         Args:
 343 |             file_paths (List[str]): List of file paths to add.
 344 |         
 345 |         Returns:
 346 |             List[Any]: List of added media pool items, empty if media storage or pool is unavailable.
 347 |         """
 348 |         if not self.media_storage or not self.media_pool:
 349 |             return []
 350 |         return self.media_storage.AddItemsToMediaPool(file_paths)
 351 | 
 352 |     def get_root_folder(self):
 353 |         """
 354 |         Get the root folder of the media pool.
 355 |         
 356 |         Returns:
 357 |             Any: Root folder object or None if media pool is unavailable.
 358 |         """
 359 |         if not self.media_pool:
 360 |             return None
 361 |         return self.media_pool.GetRootFolder()
 362 | 
 363 |     def get_current_folder(self):
 364 |         """
 365 |         Get the current folder in the media pool.
 366 |         
 367 |         Returns:
 368 |             Any: Current folder object or None if media pool is unavailable.
 369 |         """
 370 |         if not self.media_pool:
 371 |             return None
 372 |         return self.media_pool.GetCurrentFolder()
 373 | 
 374 |     def add_sub_folder(self, parent_folder, folder_name: str):
 375 |         """
 376 |         Add a subfolder to the specified parent folder in the media pool.
 377 |         
 378 |         Args:
 379 |             parent_folder: Parent folder object.
 380 |             folder_name (str): Name of the subfolder to create.
 381 |         
 382 |         Returns:
 383 |             Any: Created subfolder object or None if media pool is unavailable or creation fails.
 384 |         """
 385 |         if not self.media_pool:
 386 |             return None
 387 |         return self.media_pool.AddSubFolder(parent_folder, folder_name)
 388 | 
 389 |     def get_folder_clips(self, folder) -> List[Any]:
 390 |         """
 391 |         Get a list of clips in the specified folder.
 392 |         
 393 |         Args:
 394 |             folder: Folder object.
 395 |         
 396 |         Returns:
 397 |             List[Any]: List of media pool items, empty if folder is invalid.
 398 |         """
 399 |         if not folder:
 400 |             return []
 401 |         return folder.GetClips()
 402 | 
 403 |     def get_folder_name(self, folder) -> Optional[str]:
 404 |         """
 405 |         Get the name of the specified folder.
 406 |         
 407 |         Args:
 408 |             folder: Folder object.
 409 |         
 410 |         Returns:
 411 |             Optional[str]: Folder name or None if folder is invalid.
 412 |         """
 413 |         if not folder:
 414 |             return None
 415 |         return folder.GetName()
 416 | 
 417 |     def get_folder_sub_folders(self, folder) -> List[Any]:
 418 |         """
 419 |         Get a list of subfolders in the specified folder.
 420 |         
 421 |         Args:
 422 |             folder: Folder object.
 423 |         
 424 |         Returns:
 425 |             List[Any]: List of subfolder objects, empty if folder is invalid.
 426 |         """
 427 |         if not folder:
 428 |             return []
 429 |         return folder.GetSubFolders()
 430 | 
 431 |     def append_to_timeline(self, clips: List[Any]) -> bool:
 432 |         """
 433 |         Append clips to the current timeline.
 434 |         
 435 |         Args:
 436 |             clips (List[Any]): List of media pool items to append.
 437 |         
 438 |         Returns:
 439 |             bool: True if successful, False if media pool is unavailable.
 440 |         """
 441 |         if not self.media_pool:
 442 |             return False
 443 |         return self.media_pool.AppendToTimeline(clips)
 444 | 
 445 |     def create_timeline_from_clips(self, timeline_name: str, clips: List[Any]):
 446 |         """
 447 |         Create a new timeline from the specified clips.
 448 |         
 449 |         Args:
 450 |             timeline_name (str): Name of the new timeline.
 451 |             clips (List[Any]): List of media pool items to include.
 452 |  ACC       Returns:
 453 |             Any: Created timeline object or None if media pool is unavailable.
 454 |         """
 455 |         if not self.media_pool:
 456 |             return None
 457 |         return self.media_pool.CreateTimelineFromClips(timeline_name, clips)
 458 | 
 459 |     def import_timeline_from_file(self, file_path: str):
 460 |         """
 461 |         Import a timeline from a file (e.g., XML, EDL).
 462 |         
 463 |         Args:
 464 |             file_path (str): Path to the timeline file.
 465 |         
 466 |         Returns:
 467 |             Any: Imported timeline object or None if media pool is unavailable.
 468 |         """
 469 |         if not self.media_pool:
 470 |             return None
 471 |         return self.media_pool.ImportTimelineFromFile(file_path)
 472 | 
 473 |     def execute_lua(self, script: str) -> Any:
 474 |         """
 475 |         Execute a Lua script in Resolve's Fusion environment.
 476 |         
 477 |         Args:
 478 |             script (str): Lua script to execute.
 479 |         
 480 |         Returns:
 481 |             Any: Result of the script execution or None if Fusion is unavailable.
 482 |         """
 483 |         if not self.fusion:
 484 |             return None
 485 |         return self.fusion.Execute(script)
 486 | 
 487 | def create_fusion_node(self, node_type: str, inputs: Dict[str, Any] = None) -> Any:
 488 |     """
 489 |     Create a new node in the current Fusion composition.
 490 |     
 491 |     Args:
 492 |         node_type (str): Type of node to create (e.g., "Blur", "ColorCorrector").
 493 |         inputs (Dict[str, Any], optional): Dictionary of input parameters for the node.
 494 |     
 495 |     Returns:
 496 |         Any: Created node object or None if Fusion or composition is unavailable.
 497 |     """
 498 |     try:
 499 |         comp = fusion.GetCurrentComp()
 500 |         if not comp:
 501 |             print("No Fusion composition found.")
 502 |             return None
 503 |             
 504 |         # Include position parameters (x, y)
 505 |         node = comp.AddTool(node_type, 0, 0)
 506 |         
 507 |         if not node:
 508 |             print(f"Error creating {node_type} node.")
 509 |             return None
 510 |             
 511 |         # Set input parameters if provided
 512 |         if inputs and node:
 513 |             for key, value in inputs.items():
 514 |                 # Use SetInput method instead of dictionary-style assignment
 515 |                 node.SetInput(key, value)
 516 |                 
 517 |         print(f"{node_type} node created successfully.")
 518 |         return node
 519 |         
 520 |     except Exception as e:
 521 |         print(f"Error creating Fusion node: {e}")
 522 |         return None
 523 | 
 524 |     def get_current_comp(self) -> Any:
 525 |         """
 526 |         Get the current Fusion composition.
 527 |         
 528 |         Returns:
 529 |             Any: Current composition object or None if Fusion is unavailable.
 530 |         """
 531 |         if not self.fusion:
 532 |             return None
 533 |         try:
 534 |             return self.fusion.CurrentComp
 535 |         except Exception as e:
 536 |             logger.error(f"Error getting current composition: {e}")
 537 |             return None
 538 | 
 539 |     # New methods with enhanced functionality
 540 | 
 541 |     def get_timeline_items(self, track_type: str = "video", track_index: int = 1) -> List[Any]:
 542 |         """
 543 |         Get items (clips) from a specific track in the current timeline.
 544 |         
 545 |         Args:
 546 |             track_type (str): Type of track ("video", "audio", "subtitle"), defaults to "video".
 547 |             track_index (int): 1-based index of the track, defaults to 1.
 548 |         
 549 |         Returns:
 550 |             List[Any]: List of timeline items, empty if no timeline or track is invalid.
 551 |         """
 552 |         timeline = self.get_current_timeline()
 553 |         if not timeline:
 554 |             logger.warning("No current timeline available")
 555 |             return []
 556 |         try:
 557 |             items = timeline.GetItemListInTrack(track_type, track_index)
 558 |             return items if items else []
 559 |         except Exception as e:
 560 |             logger.error(f"Failed to get timeline items: {e}")
 561 |             return []
 562 | 
 563 |     def set_clip_property(self, clip, property_name: str, value: Any) -> bool:
 564 |         """
 565 |         Set a property on a timeline clip (e.g., "Pan", "ZoomX").
 566 |         
 567 |         Args:
 568 |             clip: Timeline item object.
 569 |             property_name (str): Name of the property to set.
 570 |             value: Value to assign to the property.
 571 |         
 572 |         Returns:
 573 |             bool: True if successful, False if clip is invalid or property set fails.
 574 |         """
 575 |         if not clip:
 576 |             return False
 577 |         try:
 578 |             return clip.SetProperty(property_name, value)
 579 |         except Exception as e:
 580 |             logger.error(f"Failed to set clip property {property_name}: {e}")
 581 |             return False
 582 | 
 583 |     def get_color_page_nodes(self) -> List[Any]:
 584 |         """
 585 |         Get all nodes in the current clip's color grade on the Color page.
 586 |         
 587 |         Returns:
 588 |             List[Any]: List of node objects, empty if no timeline or clip is available.
 589 |         """
 590 |         timeline = self.get_current_timeline()
 591 |         if not timeline:
 592 |             return []
 593 |         clip = timeline.GetCurrentVideoItem()
 594 |         if not clip:
 595 |             logger.warning("No current clip on Color page")
 596 |             return []
 597 |         try:
 598 |             return clip.GetNodeGraph().GetNodes()
 599 |         except Exception as e:
 600 |             logger.error(f"Failed to get color nodes: {e}")
 601 |             return []
 602 | 
 603 |     def add_color_node(self, node_type: str = "Corrector") -> Optional[Any]:
 604 |         """
 605 |         Add a new node to the current clip's color grade.
 606 |         
 607 |         Args:
 608 |             node_type (str): Type of node to add (e.g., "Corrector", "Layer"), defaults to "Corrector".
 609 |         
 610 |         Returns:
 611 |             Optional[Any]: Created node object or None if no timeline or clip is available.
 612 |         """
 613 |         timeline = self.get_current_timeline()
 614 |         if not timeline:
 615 |             return None
 616 |         clip = timeline.GetCurrentVideoItem()
 617 |         if not clip:
 618 |             return None
 619 |         try:
 620 |             node_graph = clip.GetNodeGraph()
 621 |             return node_graph.AddNode(node_type)
 622 |         except Exception as e:
 623 |             logger.error(f"Failed to add color node: {e}")
 624 |             return None
 625 | 
 626 |     def get_project_settings(self) -> Dict[str, Any]:
 627 |         """
 628 |         Get the current project's settings (e.g., frame rate, resolution).
 629 |         
 630 |         Returns:
 631 |             Dict[str, Any]: Dictionary of project settings, empty if no project is open.
 632 |         """
 633 |         if not self.current_project:
 634 |             return {}
 635 |         try:
 636 |             return self.current_project.GetSetting()
 637 |         except Exception as e:
 638 |             logger.error(f"Failed to get project settings: {e}")
 639 |             return {}
 640 | 
 641 |     def set_project_setting(self, key: str, value: Any) -> bool:
 642 |         """
 643 |         Set a specific project setting.
 644 |         
 645 |         Args:
 646 |             key (str): Setting key (e.g., "timelineFrameRate").
 647 |             value: Value to set for the key.
 648 |         
 649 |         Returns:
 650 |             bool: True if successful, False if no project or setting fails.
 651 |         """
 652 |         if not self.current_project:
 653 |             return False
 654 |         try:
 655 |             return self.current_project.SetSetting(key, value)
 656 |         except Exception as e:
 657 |             logger.error(f"Failed to set project setting {key}: {e}")
 658 |             return False
 659 | 
 660 |     def start_render(self, preset_name: str = None, render_path: str = None) -> bool:
 661 |         """
 662 |         Start rendering the current project with an optional preset and output path.
 663 |         
 664 |         Args:
 665 |             preset_name (str, optional): Name of the render preset to use.
 666 |             render_path (str, optional): Output directory for the render.
 667 |         
 668 |         Returns:
 669 |             bool: True if render starts successfully, False if no project or render fails.
 670 |         """
 671 |         if not self.current_project:
 672 |             return False
 673 |         try:
 674 |             if preset_name:
 675 |                 self.current_project.LoadRenderPreset(preset_name)  # Load render preset if specified
 676 |             if render_path:
 677 |                 self.current_project.SetRenderSettings({"TargetDir": render_path})  # Set output path
 678 |             return self.current_project.StartRendering()
 679 |         except Exception as e:
 680 |             logger.error(f"Failed to start render: {e}")
 681 |             return False
 682 | 
 683 |     def get_render_status(self) -> Dict[str, Any]:
 684 |         """
 685 |         Get the current render status of the project.
 686 |         
 687 |         Returns:
 688 |             Dict[str, Any]: Status info (e.g., "IsRenderInProgress", "CompletionPercentage"), empty if no project.
 689 |         """
 690 |         if not self.current_project:
 691 |             return {}
 692 |         try:
 693 |             return {
 694 |                 "IsRenderInProgress": self.current_project.IsRenderingInProgress(),
 695 |                 "CompletionPercentage": self.current_project.GetRenderingProgress()
 696 |             }
 697 |         except Exception as e:
 698 |             logger.error(f"Failed to get render status: {e}")
 699 |             return {}
 700 | 
 701 |     def add_timeline_marker(self, frame: int, color: str = "Blue", name: str = "", note: str = "") -> bool:
 702 |         """
 703 |         Add a marker to the current timeline at a specific frame.
 704 |         
 705 |         Args:
 706 |             frame (int): Frame number for the marker.
 707 |             color (str): Marker color (e.g., "Blue", "Red"), defaults to "Blue".
 708 |             name (str): Marker name, defaults to empty string.
 709 |             note (str): Marker note, defaults to empty string.
 710 |         
 711 |         Returns:
 712 |             bool: True if successful, False if no timeline or addition fails.
 713 |         """
 714 |         timeline = self.get_current_timeline()
 715 |         if not timeline:
 716 |             return False
 717 |         try:
 718 |             return timeline.AddMarker(frame, color, name, note, 1)  # Duration of 1 frame
 719 |         except Exception as e:
 720 |             logger.error(f"Failed to add marker: {e}")
 721 |             return False
 722 | 
 723 |     def get_clip_metadata(self, clip) -> Dict[str, Any]:
 724 |         """
 725 |         Get metadata for a specific clip (e.g., frame rate, resolution).
 726 |         
 727 |         Args:
 728 |             clip: Media pool item or timeline item.
 729 |         
 730 |         Returns:
 731 |             Dict[str, Any]: Metadata dictionary, empty if clip is invalid.
 732 |         """
 733 |         if not clip:
 734 |             return {}
 735 |         try:
 736 |             return clip.GetMetadata()
 737 |         except Exception as e:
 738 |             logger.error(f"Failed to get clip metadata: {e}")
 739 |             return {}
 740 | 
 741 |     def get_gallery(self) -> Any:
 742 |         """
 743 |         Get the Gallery object for the current project, used for managing stills and grades.
 744 |         
 745 |         Returns:
 746 |             Any: Gallery object or None if no project is open.
 747 |         """
 748 |         if not self.current_project:
 749 |             logger.warning("No current project available")
 750 |             return None
 751 |         try:
 752 |             return self.current_project.GetGallery()
 753 |         except Exception as e:
 754 |             logger.error(f"Failed to get gallery: {e}")
 755 |             return None
 756 | 
 757 |     def get_gallery_albums(self) -> List[Any]:
 758 |         """
 759 |         Get all albums in the gallery.
 760 |         
 761 |         Returns:
 762 |             List[Any]: List of GalleryAlbum objects, empty if gallery is unavailable.
 763 |         """
 764 |         gallery = self.get_gallery()
 765 |         if not gallery:
 766 |             return []
 767 |         try:
 768 |             return gallery.GetGalleryAlbumList()
 769 |         except Exception as e:
 770 |             logger.error(f"Failed to get gallery albums: {e}")
 771 |             return []
 772 | 
 773 |     def save_still(self, album_name: str = "Stills") -> Optional[Any]:
 774 |         """
 775 |         Save the current clip's grade as a still in the specified gallery album.
 776 |         
 777 |         Args:
 778 |             album_name (str): Name of the album to save the still in, defaults to "Stills".
 779 |         
 780 |         Returns:
 781 |             Optional[Any]: Saved GalleryStill object or None if saving fails.
 782 |         """
 783 |         gallery = self.get_gallery()
 784 |         timeline = self.get_current_timeline()
 785 |         if not gallery or not timeline:
 786 |             return None
 787 |         clip = timeline.GetCurrentVideoItem()
 788 |         if not clip:
 789 |             logger.warning("No current clip to save still from")
 790 |             return None
 791 |         try:
 792 |             album = gallery.GetAlbum(album_name)
 793 |             if not album:
 794 |                 album = gallery.CreateEmptyAlbum(album_name)  # Create album if it doesn't exist
 795 |             return clip.SaveAsStill(album)
 796 |         except Exception as e:
 797 |             logger.error(f"Failed to save still: {e}")
 798 |             return None
 799 | 
 800 |     def apply_still(self, still, clip=None) -> bool:
 801 |         """
 802 |         Apply a still (grade) to a clip, defaulting to the current clip if none specified.
 803 |         
 804 |         Args:
 805 |             still: GalleryStill object to apply.
 806 |             clip: Timeline item to apply the still to (optional).
 807 |         
 808 |         Returns:
 809 |             bool: True if successful, False if still or clip is invalid.
 810 |         """
 811 |         if not still:
 812 |             return False
 813 |         target_clip = clip or self.get_current_timeline().GetCurrentVideoItem() if self.get_current_timeline() else None
 814 |         if not target_clip:
 815 |             logger.warning("No clip to apply still to")
 816 |             return False
 817 |         try:
 818 |             return target_clip.ApplyGradeFromStill(still)
 819 |         except Exception as e:
 820 |             logger.error(f"Failed to apply still: {e}")
 821 |             return False
 822 | 
 823 |     def add_track(self, track_type: str = "video") -> bool:
 824 |         """
 825 |         Add a new track to the current timeline.
 826 |         
 827 |         Args:
 828 |             track_type (str): Type of track to add ("video", "audio", "subtitle"), defaults to "video".
 829 |         
 830 |         Returns:
 831 |             bool: True if successful, False if no timeline or addition fails.
 832 |         """
 833 |         timeline = self.get_current_timeline()
 834 |         if not timeline:
 835 |             return False
 836 |         try:
 837 |             return timeline.AddTrack(track_type)
 838 |         except Exception as e:
 839 |             logger.error(f"Failed to add {track_type} track: {e}")
 840 |             return False
 841 | 
 842 |     def set_track_name(self, track_type: str, track_index: int, name: str) -> bool:
 843 |         """
 844 |         Set the name of a track in the current timeline.
 845 |         
 846 |         Args:
 847 |             track_type (str): Type of track ("video", "audio", "subtitle").
 848 |             track_index (int): 1-based index of the track.
 849 |             name (str): New name for the track.
 850 |         
 851 |         Returns:
 852 |             bool: True if successful, False if no timeline or naming fails.
 853 |         """
 854 |         timeline = self.get_current_timeline()
 855 |         if not timeline:
 856 |             return False
 857 |         try:
 858 |             return timeline.SetTrackName(track_type, track_index, name)
 859 |         except Exception as e:
 860 |             logger.error(f"Failed to set track name: {e}")
 861 |             return False
 862 | 
 863 |     def enable_track(self, track_type: str, track_index: int, enable: bool = True) -> bool:
 864 |         """
 865 |         Enable or disable a track in the current timeline.
 866 |         
 867 |         Args:
 868 |             track_type (str): Type of track ("video", "audio", "subtitle").
 869 |             track_index (int): 1-based index of the track.
 870 |             enable (bool): True to enable, False to disable, defaults to True.
 871 |         
 872 |         Returns:
 873 |             bool: True if successful, False if no timeline or enabling fails.
 874 |         """
 875 |         timeline = self.get_current_timeline()
 876 |         if not timeline:
 877 |             return False
 878 |         try:
 879 |             return timeline.SetTrackEnable(track_type, track_index, enable)
 880 |         except Exception as e:
 881 |             logger.error(f"Failed to set track enable state: {e}")
 882 |             return False
 883 | 
 884 |     def get_audio_volume(self, clip) -> Optional[float]:
 885 |         """
 886 |         Get the audio volume of a timeline clip.
 887 |         
 888 |         Args:
 889 |             clip: Timeline item with audio.
 890 |         
 891 |         Returns:
 892 |             Optional[float]: Volume level (e.g., 0.0 to 1.0) or None if clip is invalid.
 893 |         """
 894 |         if not clip:
 895 |             return None
 896 |         try:
 897 |             return clip.GetAudioVolume()
 898 |         except Exception as e:
 899 |             logger.error(f"Failed to get audio volume: {e}")
 900 |             return None
 901 | 
 902 |     def set_audio_volume(self, clip, volume: float) -> bool:
 903 |         """
 904 |         Set the audio volume of a timeline clip.
 905 |         
 906 |         Args:
 907 |             clip: Timeline item with audio.
 908 |             volume (float): Volume level to set (e.g., 0.0 to 1.0).
 909 |         
 910 |         Returns:
 911 |             bool: True if successful, False if clip is invalid or setting fails.
 912 |         """
 913 |         if not clip:
 914 |             return False
 915 |         try:
 916 |             return clip.SetAudioVolume(volume)
 917 |         except Exception as e:
 918 |             logger.error(f"Failed to set audio volume: {e}")
 919 |             return False
 920 | 
 921 |     def set_track_volume(self, track_index: int, volume: float) -> bool:
 922 |         """
 923 |         Set the volume of an audio track in the current timeline.
 924 |         
 925 |         Args:
 926 |             track_index (int): 1-based index of the audio track.
 927 |             volume (float): Volume level to set (e.g., 0.0 to 1.0).
 928 |         
 929 |         Returns:
 930 |             bool: True if successful, False if no timeline or setting fails.
 931 |         """
 932 |         timeline = self.get_current_timeline()
 933 |         if not timeline:
 934 |             return False
 935 |         try:
 936 |             return timeline.SetTrackVolume("audio", track_index, volume)
 937 |         except Exception as e:
 938 |             logger.error(f"Failed to set track volume: {e}")
 939 |             return False
 940 | 
 941 |     def get_version_count(self, clip, version_type: str = "color") -> int:
 942 |         """
 943 |         Get the number of versions (e.g., color grades) for a clip.
 944 |         
 945 |         Args:
 946 |             clip: Timeline item.
 947 |             version_type (str): Type of version ("color" or "fusion"), defaults to "color".
 948 |         
 949 |         Returns:
 950 |             int: Number of versions, 0 if clip is invalid.
 951 |         """
 952 |         if not clip:
 953 |             return 0
 954 |         try:
 955 |             return clip.GetVersionCount(version_type)
 956 |         except Exception as e:
 957 |             logger.error(f"Failed to get version count: {e}")
 958 |             return 0
 959 | 
 960 |     def set_current_version(self, clip, version_index: int, version_type: str = "color") -> bool:
 961 |         """
 962 |         Set the current version for a clip (e.g., switch between color grades).
 963 |         
 964 |         Args:
 965 |             clip: Timeline item.
 966 |             version_index (int): 0-based index of the version to set.
 967 |             version_type (str): Type of version ("color" or "fusion"), defaults to "color".
 968 |         
 969 |         Returns:
 970 |             bool: True if successful, False if clip is invalid or setting fails.
 971 |         """
 972 |         if not clip:
 973 |             return False
 974 |         try:
 975 |             return clip.SetCurrentVersion(version_index, version_type)
 976 |         except Exception as e:
 977 |             logger.error(f"Failed to set current version: {e}")
 978 |             return False
 979 | 
 980 |     def play(self) -> bool:
 981 |         """
 982 |         Start playback in DaVinci Resolve.
 983 |         
 984 |         Returns:
 985 |             bool: True if successful, False if not connected or playback fails.
 986 |         """
 987 |         if not self.resolve:
 988 |             return False
 989 |         try:
 990 |             self.resolve.Play()
 991 |             return True
 992 |         except Exception as e:
 993 |             logger.error(f"Failed to start playback: {e}")
 994 |             return False
 995 | 
 996 |     def stop(self) -> bool:
 997 |         """
 998 |         Stop playback in DaVinci Resolve.
 999 |         
1000 |         Returns:
1001 |             bool: True if successful, False if not connected or stop fails.
1002 |         """
1003 |         if not self.resolve:
1004 |             return False
1005 |         try:
1006 |             self.resolve.Stop()
1007 |             return True
1008 |         except Exception as e:
1009 |             logger.error(f"Failed to stop playback: {e}")
1010 |             return False
1011 | 
1012 |     def get_current_timecode(self) -> Optional[str]:
1013 |         """
1014 |         Get the current playback timecode in Resolve.
1015 |         
1016 |         Returns:
1017 |             Optional[str]: Timecode string (e.g., "01:00:00:00") or None if not connected.
1018 |         """
1019 |         if not self.resolve:
1020 |             return None
1021 |         try:
1022 |             return self.resolve.GetCurrentTimecode()
1023 |         except Exception as e:
1024 |             logger.error(f"Failed to get current timecode: {e}")
1025 |             return None
1026 | 
1027 |     def set_playhead_position(self, frame: int) -> bool:
1028 |         """
1029 |         Set the playhead position to a specific frame in the current timeline.
1030 |         
1031 |         Args:
1032 |             frame (int): Frame number to set the playhead to.
1033 |         
1034 |         Returns:
1035 |             bool: True if successful, False if no timeline or setting fails.
1036 |         """
1037 |         timeline = self.get_current_timeline()
1038 |         if not timeline:
1039 |             return False
1040 |         try:
1041 |             return timeline.SetCurrentTimecode(timeline.GetTimecodeFromFrame(frame))
1042 |         except Exception as e:
1043 |             logger.error(f"Failed to set playhead position: {e}")
1044 |             return False
1045 | 
1046 |     def export_project(self, project_name: str, file_path: str) -> bool:
1047 |         """
1048 |         Export a project to a file (e.g., .drp file).
1049 |         
1050 |         Args:
1051 |             project_name (str): Name of the project to export.
1052 |             file_path (str): Destination file path for the exported project.
1053 |         
1054 |         Returns:
1055 |             bool: True if successful, False if project manager is unavailable or export fails.
1056 |         """
1057 |         if not self.project_manager:
1058 |             return False
1059 |         try:
1060 |             return self.project_manager.ExportProject(project_name, file_path)
1061 |         except Exception as e:
1062 |             logger.error(f"Failed to export project: {e}")
1063 |             return False
1064 | 
1065 |     def import_project(self, file_path: str) -> bool:
1066 |         """
1067 |         Import a project from a file (e.g., .drp file).
1068 |         
1069 |         Args:
1070 |             file_path (str): Path to the project file to import.
1071 |         
1072 |         Returns:
1073 |             bool: True if successful, False if project manager is unavailable or import fails.
1074 |         """
1075 |         if not self.project_manager:
1076 |             return False
1077 |         try:
1078 |             return self.project_manager.ImportProject(file_path)
1079 |         except Exception as e:
1080 |             logger.error(f"Failed to import project: {e}")
1081 |             return False
```