davinci-resolve-mcp 2.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/AGENTS.md +85 -0
  2. package/CHANGELOG.md +802 -0
  3. package/CLAUDE.md +15 -0
  4. package/LICENSE +21 -0
  5. package/README.md +159 -0
  6. package/SECURITY.md +53 -0
  7. package/bin/davinci-resolve-mcp.mjs +376 -0
  8. package/docs/README.md +56 -0
  9. package/docs/SKILL.md +1145 -0
  10. package/docs/authoring/fuse-dctl-authoring.md +242 -0
  11. package/docs/authoring/script-plugin-authoring.md +195 -0
  12. package/docs/contributing.md +82 -0
  13. package/docs/guides/color-decision-guide.md +387 -0
  14. package/docs/guides/editorial-decision-guide.md +136 -0
  15. package/docs/guides/media-analysis-guide.md +615 -0
  16. package/docs/guides/multicam-setup-guide.md +138 -0
  17. package/docs/install.md +198 -0
  18. package/docs/integrations/workflow-integrations.md +120 -0
  19. package/docs/kernels/README.md +28 -0
  20. package/docs/kernels/audio-fairlight-kernel.md +86 -0
  21. package/docs/kernels/color-grade-kernel.md +103 -0
  22. package/docs/kernels/extension-authoring-kernel.md +101 -0
  23. package/docs/kernels/fusion-composition-kernel.md +91 -0
  24. package/docs/kernels/media-pool-ingest-kernel.md +147 -0
  25. package/docs/kernels/project-lifecycle-kernel.md +120 -0
  26. package/docs/kernels/render-deliver-kernel.md +92 -0
  27. package/docs/kernels/review-annotation-kernel.md +110 -0
  28. package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
  29. package/docs/kernels/timeline-edit-kernel.md +189 -0
  30. package/docs/notes/codec-plugin-notes.md +136 -0
  31. package/docs/notes/dctl-notes.md +234 -0
  32. package/docs/notes/fusion-template-notes.md +136 -0
  33. package/docs/notes/lut-notes.md +136 -0
  34. package/docs/notes/openfx-notes.md +120 -0
  35. package/docs/process/release-process.md +152 -0
  36. package/docs/reference/api-coverage.md +488 -0
  37. package/docs/reference/resolve_scripting_api.txt +1012 -0
  38. package/examples/README.md +53 -0
  39. package/examples/markers/README.md +81 -0
  40. package/examples/media/README.md +94 -0
  41. package/examples/timeline/README.md +98 -0
  42. package/install.py +1196 -0
  43. package/package.json +52 -0
  44. package/scripts/audit_api_parity.py +275 -0
  45. package/scripts/live_media_analysis_polish_probe.py +65 -0
  46. package/src/__init__.py +3 -0
  47. package/src/analysis_dashboard.py +4936 -0
  48. package/src/control_panel.py +13 -0
  49. package/src/granular/__init__.py +17 -0
  50. package/src/granular/common.py +727 -0
  51. package/src/granular/folder.py +287 -0
  52. package/src/granular/gallery.py +306 -0
  53. package/src/granular/graph.py +309 -0
  54. package/src/granular/media_pool.py +679 -0
  55. package/src/granular/media_pool_item.py +852 -0
  56. package/src/granular/media_storage.py +179 -0
  57. package/src/granular/project.py +1594 -0
  58. package/src/granular/resolve_control.py +521 -0
  59. package/src/granular/timeline.py +1074 -0
  60. package/src/granular/timeline_item.py +2251 -0
  61. package/src/resolve_mcp_server.py +43 -0
  62. package/src/server.py +15691 -0
  63. package/src/utils/__init__.py +3 -0
  64. package/src/utils/app_control.py +319 -0
  65. package/src/utils/audio_fairlight_live_probe.py +263 -0
  66. package/src/utils/cdl.py +20 -0
  67. package/src/utils/cloud_operations.py +192 -0
  68. package/src/utils/color_grade_live_probe.py +444 -0
  69. package/src/utils/dctl_templates.py +368 -0
  70. package/src/utils/extension_authoring_live_probe.py +292 -0
  71. package/src/utils/fuse_templates.py +1968 -0
  72. package/src/utils/fusion_composition_live_probe.py +284 -0
  73. package/src/utils/layout_presets.py +333 -0
  74. package/src/utils/mcp_stdio.py +32 -0
  75. package/src/utils/media_analysis.py +3618 -0
  76. package/src/utils/media_analysis_jobs.py +796 -0
  77. package/src/utils/media_pool_ingest_live_probe.py +592 -0
  78. package/src/utils/multicam.py +393 -0
  79. package/src/utils/object_inspection.py +287 -0
  80. package/src/utils/platform.py +157 -0
  81. package/src/utils/project_lifecycle_live_probe.py +376 -0
  82. package/src/utils/project_properties.py +601 -0
  83. package/src/utils/render_deliver_live_probe.py +384 -0
  84. package/src/utils/resolve_connection.py +77 -0
  85. package/src/utils/review_annotation_live_probe.py +352 -0
  86. package/src/utils/script_templates.py +1193 -0
  87. package/src/utils/sync_detection.py +887 -0
  88. package/src/utils/timeline_conform_live_probe.py +280 -0
  89. package/src/utils/timeline_kernel_live_probe.py +1091 -0
  90. package/src/utils/timeline_kernel_probe.py +185 -0
  91. package/src/utils/timeline_title_text.py +87 -0
  92. package/src/utils/update_check.py +610 -0
@@ -0,0 +1,1074 @@
1
+ """Timeline resources and operations."""
2
+
3
+ from src.granular.common import * # noqa: F401,F403
4
+
5
+ resolve = ResolveProxy()
6
+
7
+ @mcp.resource("resolve://timelines")
8
+ def list_timelines() -> List[str]:
9
+ """List all timelines in the current project."""
10
+ logger.info("Received request to list timelines")
11
+
12
+ if resolve is None:
13
+ logger.error("Not connected to DaVinci Resolve")
14
+ return ["Error: Not connected to DaVinci Resolve"]
15
+
16
+ project_manager = resolve.GetProjectManager()
17
+ if not project_manager:
18
+ logger.error("Failed to get Project Manager")
19
+ return ["Error: Failed to get Project Manager"]
20
+
21
+ current_project = project_manager.GetCurrentProject()
22
+ if not current_project:
23
+ logger.error("No project currently open")
24
+ return ["Error: No project currently open"]
25
+
26
+ timeline_count = current_project.GetTimelineCount()
27
+ logger.info(f"Timeline count: {timeline_count}")
28
+
29
+ timelines = []
30
+
31
+ for i in range(1, timeline_count + 1):
32
+ timeline = current_project.GetTimelineByIndex(i)
33
+ if timeline:
34
+ timeline_name = timeline.GetName()
35
+ timelines.append(timeline_name)
36
+ logger.info(f"Found timeline {i}: {timeline_name}")
37
+
38
+ if not timelines:
39
+ logger.info("No timelines found in the current project")
40
+ return ["No timelines found in the current project"]
41
+
42
+ logger.info(f"Returning {len(timelines)} timelines: {', '.join(timelines)}")
43
+ return timelines
44
+
45
+
46
+ @mcp.resource("resolve://current-timeline")
47
+ def get_current_timeline() -> Dict[str, Any]:
48
+ """Get information about the current timeline."""
49
+ pm, current_project = get_current_project()
50
+ if not current_project:
51
+ return {"error": "No project currently open"}
52
+
53
+ current_timeline = current_project.GetCurrentTimeline()
54
+ if not current_timeline:
55
+ return {"error": "No timeline currently active"}
56
+
57
+ # Get basic timeline information
58
+ result = {
59
+ "name": current_timeline.GetName(),
60
+ "fps": current_timeline.GetSetting("timelineFrameRate"),
61
+ "resolution": {
62
+ "width": current_timeline.GetSetting("timelineResolutionWidth"),
63
+ "height": current_timeline.GetSetting("timelineResolutionHeight")
64
+ },
65
+ "duration": current_timeline.GetEndFrame() - current_timeline.GetStartFrame() + 1
66
+ }
67
+
68
+ return result
69
+
70
+
71
+ @mcp.tool()
72
+ def create_timeline(name: str) -> str:
73
+ """Create a new timeline with the given name.
74
+
75
+ Args:
76
+ name: The name for the new timeline
77
+ """
78
+ resolve = get_resolve()
79
+ if resolve is None:
80
+ return "Error: Not connected to DaVinci Resolve"
81
+
82
+ if not name:
83
+ return "Error: Timeline name cannot be empty"
84
+
85
+ project_manager = resolve.GetProjectManager()
86
+ if not project_manager:
87
+ return "Error: Failed to get Project Manager"
88
+
89
+ current_project = project_manager.GetCurrentProject()
90
+ if not current_project:
91
+ return "Error: No project currently open"
92
+
93
+ media_pool = current_project.GetMediaPool()
94
+ if not media_pool:
95
+ return "Error: Failed to get Media Pool"
96
+
97
+ timeline = media_pool.CreateEmptyTimeline(name)
98
+ if timeline:
99
+ return f"Successfully created timeline '{name}'"
100
+ else:
101
+ return f"Failed to create timeline '{name}'"
102
+
103
+
104
+ @mcp.tool()
105
+ def set_current_timeline(name: str) -> str:
106
+ """Switch to a timeline by name.
107
+
108
+ Args:
109
+ name: The name of the timeline to set as current
110
+ """
111
+ resolve = get_resolve()
112
+ if resolve is None:
113
+ return "Error: Not connected to DaVinci Resolve"
114
+
115
+ if not name:
116
+ return "Error: Timeline name cannot be empty"
117
+
118
+ project_manager = resolve.GetProjectManager()
119
+ if not project_manager:
120
+ return "Error: Failed to get Project Manager"
121
+
122
+ current_project = project_manager.GetCurrentProject()
123
+ if not current_project:
124
+ return "Error: No project currently open"
125
+
126
+ # Find the timeline by name
127
+ timeline_count = current_project.GetTimelineCount()
128
+ for i in range(1, timeline_count + 1):
129
+ timeline = current_project.GetTimelineByIndex(i)
130
+ if timeline and timeline.GetName() == name:
131
+ result = current_project.SetCurrentTimeline(timeline)
132
+ if result:
133
+ return f"Successfully switched to timeline '{name}'"
134
+ else:
135
+ return f"Failed to switch to timeline '{name}'"
136
+
137
+ return f"Error: Timeline '{name}' not found"
138
+
139
+
140
+ @mcp.resource("resolve://timeline-clips")
141
+ def list_timeline_clips() -> List[Dict[str, Any]]:
142
+ """List all clips in the current timeline."""
143
+ pm, current_project = get_current_project()
144
+ if not current_project:
145
+ return [{"error": "No project currently open"}]
146
+
147
+ current_timeline = current_project.GetCurrentTimeline()
148
+ if not current_timeline:
149
+ return [{"error": "No timeline currently active"}]
150
+
151
+ try:
152
+ # Get all tracks in the timeline
153
+ # Video tracks are 1-based index (1 is first track)
154
+ video_track_count = current_timeline.GetTrackCount("video")
155
+ audio_track_count = current_timeline.GetTrackCount("audio")
156
+
157
+ clips = []
158
+
159
+ # Process video tracks
160
+ for track_index in range(1, video_track_count + 1):
161
+ track_items = current_timeline.GetItemListInTrack("video", track_index)
162
+ if track_items:
163
+ for item in track_items:
164
+ clips.append({
165
+ "name": item.GetName(),
166
+ "type": "video",
167
+ "track": track_index,
168
+ "start_frame": item.GetStart(),
169
+ "end_frame": item.GetEnd(),
170
+ "duration": item.GetDuration()
171
+ })
172
+
173
+ # Process audio tracks
174
+ for track_index in range(1, audio_track_count + 1):
175
+ track_items = current_timeline.GetItemListInTrack("audio", track_index)
176
+ if track_items:
177
+ for item in track_items:
178
+ clips.append({
179
+ "name": item.GetName(),
180
+ "type": "audio",
181
+ "track": track_index,
182
+ "start_frame": item.GetStart(),
183
+ "end_frame": item.GetEnd(),
184
+ "duration": item.GetDuration()
185
+ })
186
+
187
+ if not clips:
188
+ return [{"info": "No clips found in the current timeline"}]
189
+
190
+ return clips
191
+ except Exception as e:
192
+ return [{"error": f"Error listing timeline clips: {str(e)}"}]
193
+
194
+
195
+ @mcp.tool()
196
+ def list_timelines_tool() -> List[str]:
197
+ """List all timelines in the current project as a tool."""
198
+ logger.info("Received request to list timelines via tool")
199
+ return list_timelines()
200
+
201
+
202
+ @mcp.tool()
203
+ def timeline_set_name(name: str) -> Dict[str, Any]:
204
+ """Rename the current timeline.
205
+
206
+ Args:
207
+ name: New name for the timeline.
208
+ """
209
+ _, tl, err = _get_timeline()
210
+ if err:
211
+ return err
212
+ result = tl.SetName(name)
213
+ return {"success": bool(result), "name": name}
214
+
215
+
216
+ @mcp.tool()
217
+ def timeline_set_start_timecode(timecode: str) -> Dict[str, Any]:
218
+ """Set the start timecode of the current timeline.
219
+
220
+ Args:
221
+ timecode: Timecode string (e.g. '01:00:00:00').
222
+ """
223
+ _, tl, err = _get_timeline()
224
+ if err:
225
+ return err
226
+ result = tl.SetStartTimecode(timecode)
227
+ return {"success": bool(result), "timecode": timecode}
228
+
229
+
230
+ @mcp.tool()
231
+ def timeline_get_current_timecode() -> Dict[str, Any]:
232
+ """Get the current playhead timecode."""
233
+ _, tl, err = _get_timeline()
234
+ if err:
235
+ return err
236
+ return {"timecode": tl.GetCurrentTimecode()}
237
+
238
+
239
+ @mcp.tool()
240
+ def timeline_set_current_timecode(timecode: str) -> Dict[str, Any]:
241
+ """Set the playhead to a specific timecode.
242
+
243
+ Args:
244
+ timecode: Timecode string (e.g. '01:00:05:00').
245
+ """
246
+ _, tl, err = _get_timeline()
247
+ if err:
248
+ return err
249
+ result = tl.SetCurrentTimecode(timecode)
250
+ return {"success": bool(result), "timecode": timecode}
251
+
252
+
253
+ @mcp.tool()
254
+ def timeline_add_track(
255
+ track_type: str,
256
+ audio_type: Optional[str] = None,
257
+ index: Optional[int] = None,
258
+ ) -> Dict[str, Any]:
259
+ """Add a new track to the timeline.
260
+
261
+ Mirrors Timeline.AddTrack(trackType, {newTrackOptions}) per docs line 327.
262
+
263
+ Args:
264
+ track_type: 'video', 'audio', or 'subtitle'.
265
+ audio_type: For audio tracks: 'mono', 'stereo', 'lrc', 'lcr', 'lrcs', 'lcrs',
266
+ 'quad', '5.0', '5.0film', '5.1', '5.1film', '7.0', '7.0film', '7.1',
267
+ '7.1film', 'adaptive1' through 'adaptive36'. Defaults to 'mono' for audio
268
+ tracks if omitted.
269
+ index: 1-based track index where 1 <= index <= GetTrackCount(track_type) + 1.
270
+ If omitted or out of bounds, the track is appended.
271
+ """
272
+ _, tl, err = _get_timeline()
273
+ if err:
274
+ return err
275
+ options: Dict[str, Any] = {}
276
+ if audio_type is not None:
277
+ options["audioType"] = audio_type
278
+ if index is not None:
279
+ options["index"] = index
280
+ if options:
281
+ result = tl.AddTrack(track_type, options)
282
+ else:
283
+ result = tl.AddTrack(track_type)
284
+ return {"success": bool(result), "track_type": track_type}
285
+
286
+
287
+ @mcp.tool()
288
+ def timeline_delete_track(track_type: str, track_index: int) -> Dict[str, Any]:
289
+ """Delete a track from the timeline.
290
+
291
+ Args:
292
+ track_type: 'video', 'audio', or 'subtitle'.
293
+ track_index: 1-based track index to delete.
294
+ """
295
+ _, tl, err = _get_timeline()
296
+ if err:
297
+ return err
298
+ result = tl.DeleteTrack(track_type, track_index)
299
+ return {"success": bool(result)}
300
+
301
+
302
+ @mcp.tool()
303
+ def timeline_get_track_sub_type(track_type: str, track_index: int) -> Dict[str, Any]:
304
+ """Get the sub-type of a track (e.g. mono, stereo for audio).
305
+
306
+ Args:
307
+ track_type: 'video' or 'audio'.
308
+ track_index: 1-based track index.
309
+ """
310
+ _, tl, err = _get_timeline()
311
+ if err:
312
+ return err
313
+ sub = tl.GetTrackSubType(track_type, track_index)
314
+ return {"track_type": track_type, "track_index": track_index, "sub_type": sub if sub else ""}
315
+
316
+
317
+ @mcp.tool()
318
+ def timeline_set_track_enable(track_type: str, track_index: int, enabled: bool) -> Dict[str, Any]:
319
+ """Enable or disable a track.
320
+
321
+ Args:
322
+ track_type: 'video' or 'audio'.
323
+ track_index: 1-based track index.
324
+ enabled: True to enable, False to disable.
325
+ """
326
+ _, tl, err = _get_timeline()
327
+ if err:
328
+ return err
329
+ result = tl.SetTrackEnable(track_type, track_index, enabled)
330
+ return {"success": bool(result)}
331
+
332
+
333
+ @mcp.tool()
334
+ def timeline_get_is_track_enabled(track_type: str, track_index: int) -> Dict[str, Any]:
335
+ """Check if a track is enabled.
336
+
337
+ Args:
338
+ track_type: 'video' or 'audio'.
339
+ track_index: 1-based track index.
340
+ """
341
+ _, tl, err = _get_timeline()
342
+ if err:
343
+ return err
344
+ enabled = tl.GetIsTrackEnabled(track_type, track_index)
345
+ return {"enabled": bool(enabled)}
346
+
347
+
348
+ @mcp.tool()
349
+ def timeline_set_track_lock(track_type: str, track_index: int, locked: bool) -> Dict[str, Any]:
350
+ """Lock or unlock a track.
351
+
352
+ Args:
353
+ track_type: 'video' or 'audio'.
354
+ track_index: 1-based track index.
355
+ locked: True to lock, False to unlock.
356
+ """
357
+ _, tl, err = _get_timeline()
358
+ if err:
359
+ return err
360
+ result = tl.SetTrackLock(track_type, track_index, locked)
361
+ return {"success": bool(result)}
362
+
363
+
364
+ @mcp.tool()
365
+ def timeline_get_is_track_locked(track_type: str, track_index: int) -> Dict[str, Any]:
366
+ """Check if a track is locked.
367
+
368
+ Args:
369
+ track_type: 'video' or 'audio'.
370
+ track_index: 1-based track index.
371
+ """
372
+ _, tl, err = _get_timeline()
373
+ if err:
374
+ return err
375
+ locked = tl.GetIsTrackLocked(track_type, track_index)
376
+ return {"locked": bool(locked)}
377
+
378
+
379
+ @mcp.tool()
380
+ def timeline_get_voice_isolation_state(track_index: int) -> Dict[str, Any]:
381
+ """Get voice isolation state for an audio track.
382
+
383
+ Args:
384
+ track_index: 1-based audio track index.
385
+ """
386
+ _, tl, err = _get_timeline()
387
+ if err:
388
+ return err
389
+ missing = _requires_method(tl, "GetVoiceIsolationState", "20.1")
390
+ if missing:
391
+ return missing
392
+ state = tl.GetVoiceIsolationState(track_index)
393
+ return {"state": state if state else {"isEnabled": False, "amount": 0}}
394
+
395
+
396
+ @mcp.tool()
397
+ def timeline_set_voice_isolation_state(track_index: int, state: Dict[str, Any]) -> Dict[str, Any]:
398
+ """Set voice isolation state for an audio track.
399
+
400
+ Args:
401
+ track_index: 1-based audio track index.
402
+ state: Dict with isEnabled (bool) and amount (0-100).
403
+ """
404
+ _, tl, err = _get_timeline()
405
+ if err:
406
+ return err
407
+ missing = _requires_method(tl, "SetVoiceIsolationState", "20.1")
408
+ if missing:
409
+ return missing
410
+ result = tl.SetVoiceIsolationState(track_index, state)
411
+ return {"success": bool(result)}
412
+
413
+
414
+ @mcp.tool()
415
+ def timeline_delete_clips(clip_ids: List[str], track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
416
+ """Delete clips from the timeline.
417
+
418
+ Args:
419
+ clip_ids: List of clip unique IDs to delete.
420
+ track_type: 'video' or 'audio'. Default: 'video'.
421
+ track_index: 1-based track index. Default: 1.
422
+ """
423
+ _, tl, err = _get_timeline()
424
+ if err:
425
+ return err
426
+ items = tl.GetItemListInTrack(track_type, track_index)
427
+ if not items:
428
+ return {"error": "No items in track"}
429
+ to_delete = [i for i in items if i.GetUniqueId() in clip_ids]
430
+ if not to_delete:
431
+ return {"error": "No matching clips found"}
432
+ result = tl.DeleteClips(to_delete)
433
+ return {"success": bool(result), "deleted": len(to_delete)}
434
+
435
+
436
+ @mcp.tool()
437
+ def timeline_set_clips_linked(clip_ids: List[str], linked: bool, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
438
+ """Link or unlink clips in the timeline.
439
+
440
+ Args:
441
+ clip_ids: List of clip unique IDs.
442
+ linked: True to link, False to unlink.
443
+ track_type: 'video' or 'audio'. Default: 'video'.
444
+ track_index: 1-based track index. Default: 1.
445
+ """
446
+ _, tl, err = _get_timeline()
447
+ if err:
448
+ return err
449
+ items = tl.GetItemListInTrack(track_type, track_index)
450
+ if not items:
451
+ return {"error": "No items in track"}
452
+ targets = [i for i in items if i.GetUniqueId() in clip_ids]
453
+ if not targets:
454
+ return {"error": "No matching clips found"}
455
+ result = tl.SetClipsLinked(targets, linked)
456
+ return {"success": bool(result)}
457
+
458
+
459
+ @mcp.tool()
460
+ def timeline_add_marker(frame_id: int, color: str, name: str, note: str = "", duration: int = 1, custom_data: str = "") -> Dict[str, Any]:
461
+ """Add a marker to the current timeline.
462
+
463
+ Args:
464
+ frame_id: Frame number for the marker.
465
+ color: Marker color (Blue, Cyan, Green, Yellow, Red, Pink, Purple, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa, Cream).
466
+ name: Marker name.
467
+ note: Marker note. Default: empty.
468
+ duration: Duration in frames. Default: 1.
469
+ custom_data: Custom data string. Default: empty.
470
+ """
471
+ _, tl, err = _get_timeline()
472
+ if err:
473
+ return err
474
+ result = tl.AddMarker(frame_id, color, name, note, duration, custom_data)
475
+ return {"success": bool(result)}
476
+
477
+
478
+ @mcp.tool()
479
+ def timeline_get_markers() -> Dict[str, Any]:
480
+ """Get all markers on the current timeline."""
481
+ _, tl, err = _get_timeline()
482
+ if err:
483
+ return err
484
+ markers = tl.GetMarkers()
485
+ return {"markers": markers if markers else {}}
486
+
487
+
488
+ @mcp.tool()
489
+ def timeline_get_marker_by_custom_data(custom_data: str) -> Dict[str, Any]:
490
+ """Find a timeline marker by its custom data.
491
+
492
+ Args:
493
+ custom_data: Custom data string to search for.
494
+ """
495
+ _, tl, err = _get_timeline()
496
+ if err:
497
+ return err
498
+ marker = tl.GetMarkerByCustomData(custom_data)
499
+ return {"marker": marker if marker else {}}
500
+
501
+
502
+ @mcp.tool()
503
+ def timeline_update_marker_custom_data(frame_id: int, custom_data: str) -> Dict[str, Any]:
504
+ """Update the custom data of a timeline marker.
505
+
506
+ Args:
507
+ frame_id: Frame number of the marker.
508
+ custom_data: New custom data string.
509
+ """
510
+ _, tl, err = _get_timeline()
511
+ if err:
512
+ return err
513
+ result = tl.UpdateMarkerCustomData(frame_id, custom_data)
514
+ return {"success": bool(result)}
515
+
516
+
517
+ @mcp.tool()
518
+ def timeline_get_marker_custom_data(frame_id: int) -> Dict[str, Any]:
519
+ """Get the custom data of a timeline marker.
520
+
521
+ Args:
522
+ frame_id: Frame number of the marker.
523
+ """
524
+ _, tl, err = _get_timeline()
525
+ if err:
526
+ return err
527
+ data = tl.GetMarkerCustomData(frame_id)
528
+ return {"frame_id": frame_id, "custom_data": data if data else ""}
529
+
530
+
531
+ @mcp.tool()
532
+ def timeline_delete_markers_by_color(color: str) -> Dict[str, Any]:
533
+ """Delete all timeline markers of a specific color.
534
+
535
+ Args:
536
+ color: Color of markers to delete. Use '' to delete all.
537
+ """
538
+ _, tl, err = _get_timeline()
539
+ if err:
540
+ return err
541
+ result = tl.DeleteMarkersByColor(color)
542
+ return {"success": bool(result)}
543
+
544
+
545
+ @mcp.tool()
546
+ def timeline_delete_marker_at_frame(frame_id: int) -> Dict[str, Any]:
547
+ """Delete a timeline marker at a specific frame.
548
+
549
+ Args:
550
+ frame_id: Frame number of the marker.
551
+ """
552
+ _, tl, err = _get_timeline()
553
+ if err:
554
+ return err
555
+ result = tl.DeleteMarkerAtFrame(frame_id)
556
+ return {"success": bool(result)}
557
+
558
+
559
+ @mcp.tool()
560
+ def timeline_delete_marker_by_custom_data(custom_data: str) -> Dict[str, Any]:
561
+ """Delete a timeline marker by custom data.
562
+
563
+ Args:
564
+ custom_data: Custom data of the marker to delete.
565
+ """
566
+ _, tl, err = _get_timeline()
567
+ if err:
568
+ return err
569
+ result = tl.DeleteMarkerByCustomData(custom_data)
570
+ return {"success": bool(result)}
571
+
572
+
573
+ @mcp.tool()
574
+ def timeline_get_track_name(track_type: str, track_index: int) -> Dict[str, Any]:
575
+ """Get the name of a track.
576
+
577
+ Args:
578
+ track_type: 'video' or 'audio'.
579
+ track_index: 1-based track index.
580
+ """
581
+ _, tl, err = _get_timeline()
582
+ if err:
583
+ return err
584
+ name = tl.GetTrackName(track_type, track_index)
585
+ return {"track_name": name if name else ""}
586
+
587
+
588
+ @mcp.tool()
589
+ def timeline_set_track_name(track_type: str, track_index: int, name: str) -> Dict[str, Any]:
590
+ """Set the name of a track.
591
+
592
+ Args:
593
+ track_type: 'video' or 'audio'.
594
+ track_index: 1-based track index.
595
+ name: New track name.
596
+ """
597
+ _, tl, err = _get_timeline()
598
+ if err:
599
+ return err
600
+ result = tl.SetTrackName(track_type, track_index, name)
601
+ return {"success": bool(result)}
602
+
603
+
604
+ @mcp.tool()
605
+ def timeline_duplicate() -> Dict[str, Any]:
606
+ """Duplicate the current timeline."""
607
+ _, tl, err = _get_timeline()
608
+ if err:
609
+ return err
610
+ new_tl = tl.DuplicateTimeline()
611
+ if new_tl:
612
+ return {"success": True, "name": new_tl.GetName(), "unique_id": new_tl.GetUniqueId()}
613
+ return {"success": False, "error": "Failed to duplicate timeline"}
614
+
615
+
616
+ @mcp.tool()
617
+ def timeline_create_compound_clip(
618
+ clip_ids: List[str],
619
+ track_type: str = "video",
620
+ track_index: int = 1,
621
+ name: Optional[str] = None,
622
+ start_timecode: Optional[str] = None,
623
+ ) -> Dict[str, Any]:
624
+ """Create a compound clip from selected items.
625
+
626
+ Mirrors Timeline.CreateCompoundClip([timelineItems], {clipInfo}) per docs line 369.
627
+
628
+ Args:
629
+ clip_ids: List of timeline item unique IDs.
630
+ track_type: 'video' or 'audio'. Default: 'video'.
631
+ track_index: 1-based track index. Default: 1.
632
+ name: Optional name for the compound clip; maps to clipInfo.name.
633
+ start_timecode: Optional start timecode (e.g. "01:00:00:00"); maps to clipInfo.startTimecode.
634
+ """
635
+ _, tl, err = _get_timeline()
636
+ if err:
637
+ return err
638
+ items = tl.GetItemListInTrack(track_type, track_index)
639
+ targets = [i for i in (items or []) if i.GetUniqueId() in clip_ids]
640
+ if not targets:
641
+ return {"error": "No matching items found"}
642
+ clip_info: Dict[str, Any] = {}
643
+ if name is not None:
644
+ clip_info["name"] = name
645
+ if start_timecode is not None:
646
+ clip_info["startTimecode"] = start_timecode
647
+ if clip_info:
648
+ result = tl.CreateCompoundClip(targets, clip_info)
649
+ else:
650
+ result = tl.CreateCompoundClip(targets)
651
+ if not result:
652
+ return {"success": False, "error": "Failed to create compound clip"}
653
+ return {
654
+ "success": True,
655
+ "name": result.GetName() if hasattr(result, "GetName") else None,
656
+ "unique_id": result.GetUniqueId() if hasattr(result, "GetUniqueId") else None,
657
+ }
658
+
659
+
660
+ @mcp.tool()
661
+ def timeline_create_fusion_clip(clip_ids: List[str], track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
662
+ """Create a Fusion clip from selected items.
663
+
664
+ Args:
665
+ clip_ids: List of timeline item unique IDs.
666
+ track_type: 'video' or 'audio'. Default: 'video'.
667
+ track_index: 1-based track index. Default: 1.
668
+ """
669
+ _, tl, err = _get_timeline()
670
+ if err:
671
+ return err
672
+ items = tl.GetItemListInTrack(track_type, track_index)
673
+ targets = [i for i in (items or []) if i.GetUniqueId() in clip_ids]
674
+ if not targets:
675
+ return {"error": "No matching items found"}
676
+ result = tl.CreateFusionClip(targets)
677
+ if result:
678
+ return {"success": True}
679
+ return {"success": False, "error": "Failed to create Fusion clip"}
680
+
681
+
682
+ @mcp.tool()
683
+ def timeline_import_into(file_path: str, import_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
684
+ """Import content into the current timeline from a file.
685
+
686
+ Args:
687
+ file_path: Path to the file to import (AAF, EDL, XML, etc.).
688
+ import_options: Optional dict of import options.
689
+ """
690
+ _, tl, err = _get_timeline()
691
+ if err:
692
+ return err
693
+ if import_options:
694
+ result = tl.ImportIntoTimeline(file_path, import_options)
695
+ else:
696
+ result = tl.ImportIntoTimeline(file_path)
697
+ return {"success": bool(result)}
698
+
699
+
700
+ @mcp.tool()
701
+ def timeline_export(file_path: str, export_type: str, export_subtype: str = "EXPORT_NONE") -> Dict[str, Any]:
702
+ """Export the current timeline to a file.
703
+
704
+ Args:
705
+ file_path: Output file path.
706
+ export_type: Export type (e.g. 'EXPORT_AAF', 'EXPORT_EDL', 'EXPORT_FCP_7_XML', 'EXPORT_FCPXML_1_10', 'EXPORT_DRT', 'EXPORT_TEXT_CSV', 'EXPORT_TEXT_TAB', 'EXPORT_OTIO', 'EXPORT_ALE').
707
+ export_subtype: Export subtype for AAF/EDL. Default: 'EXPORT_NONE'.
708
+ """
709
+ _, tl, err = _get_timeline()
710
+ if err:
711
+ return err
712
+ # Map string constants to resolve constants
713
+ try:
714
+ etype = getattr(resolve, export_type) if hasattr(resolve, export_type) else export_type
715
+ esub = getattr(resolve, export_subtype) if hasattr(resolve, export_subtype) else export_subtype
716
+ except Exception:
717
+ logger.debug("Could not resolve timeline export constants", exc_info=True)
718
+ etype = export_type
719
+ esub = export_subtype
720
+ result = tl.Export(file_path, etype, esub)
721
+ return {"success": bool(result), "file_path": file_path}
722
+
723
+
724
+ @mcp.tool()
725
+ def timeline_insert_generator(generator_name: str, duration: Optional[int] = None) -> Dict[str, Any]:
726
+ """Insert a generator into the timeline.
727
+
728
+ Args:
729
+ generator_name: Name of the generator to insert.
730
+ duration: Optional duration in frames.
731
+ """
732
+ _, tl, err = _get_timeline()
733
+ if err:
734
+ return err
735
+ if duration:
736
+ result = tl.InsertGeneratorIntoTimeline(generator_name, {"duration": duration})
737
+ else:
738
+ result = tl.InsertGeneratorIntoTimeline(generator_name)
739
+ return {"success": result is not None}
740
+
741
+
742
+ @mcp.tool()
743
+ def timeline_insert_fusion_generator(generator_name: str) -> Dict[str, Any]:
744
+ """Insert a Fusion generator into the timeline.
745
+
746
+ Args:
747
+ generator_name: Name of the Fusion generator.
748
+ """
749
+ _, tl, err = _get_timeline()
750
+ if err:
751
+ return err
752
+ result = tl.InsertFusionGeneratorIntoTimeline(generator_name)
753
+ return {"success": result is not None}
754
+
755
+
756
+ @mcp.tool()
757
+ def timeline_insert_fusion_composition() -> Dict[str, Any]:
758
+ """Insert a Fusion composition into the timeline."""
759
+ _, tl, err = _get_timeline()
760
+ if err:
761
+ return err
762
+ result = tl.InsertFusionCompositionIntoTimeline()
763
+ return {"success": result is not None}
764
+
765
+
766
+ @mcp.tool()
767
+ def timeline_insert_ofx_generator(generator_name: str) -> Dict[str, Any]:
768
+ """Insert an OFX generator into the timeline.
769
+
770
+ Args:
771
+ generator_name: Name of the OFX generator.
772
+ """
773
+ _, tl, err = _get_timeline()
774
+ if err:
775
+ return err
776
+ result = tl.InsertOFXGeneratorIntoTimeline(generator_name)
777
+ return {"success": result is not None}
778
+
779
+
780
+ @mcp.tool()
781
+ def timeline_insert_title(title_name: str) -> Dict[str, Any]:
782
+ """Insert a title into the timeline.
783
+
784
+ Args:
785
+ title_name: Name of the title to insert.
786
+ """
787
+ _, tl, err = _get_timeline()
788
+ if err:
789
+ return err
790
+ result = tl.InsertTitleIntoTimeline(title_name)
791
+ return {"success": result is not None}
792
+
793
+
794
+ @mcp.tool()
795
+ def timeline_insert_fusion_title(title_name: str) -> Dict[str, Any]:
796
+ """Insert a Fusion title into the timeline.
797
+
798
+ Args:
799
+ title_name: Name of the Fusion title.
800
+ """
801
+ _, tl, err = _get_timeline()
802
+ if err:
803
+ return err
804
+ result = tl.InsertFusionTitleIntoTimeline(title_name)
805
+ return {"success": result is not None}
806
+
807
+
808
+ @mcp.tool()
809
+ def timeline_grab_still() -> Dict[str, Any]:
810
+ """Grab a still from the current frame of the timeline."""
811
+ _, tl, err = _get_timeline()
812
+ if err:
813
+ return err
814
+ result = tl.GrabStill()
815
+ return {"success": result is not None}
816
+
817
+
818
+ @mcp.tool()
819
+ def timeline_grab_all_stills() -> Dict[str, Any]:
820
+ """Grab stills from all frames at the current position across all timelines."""
821
+ _, tl, err = _get_timeline()
822
+ if err:
823
+ return err
824
+ result = tl.GrabAllStills()
825
+ return {"success": result is not None}
826
+
827
+
828
+ @mcp.tool()
829
+ def timeline_get_unique_id() -> Dict[str, Any]:
830
+ """Get the unique ID of the current timeline."""
831
+ _, tl, err = _get_timeline()
832
+ if err:
833
+ return err
834
+ return {"unique_id": tl.GetUniqueId()}
835
+
836
+
837
+ @mcp.tool()
838
+ def timeline_create_subtitles_from_audio(
839
+ language: str = "auto",
840
+ preset: str = "default",
841
+ chars_per_line: Optional[int] = None,
842
+ line_break: Optional[str] = None,
843
+ gap: Optional[int] = None,
844
+ ) -> Dict[str, Any]:
845
+ """Create subtitles from audio in the current timeline.
846
+
847
+ Mirrors Timeline.CreateSubtitlesFromAudio({autoCaptionSettings}) per docs lines 718-761.
848
+
849
+ Args:
850
+ language: 'auto', 'danish', 'dutch', 'english', 'french', 'german', 'italian',
851
+ 'japanese', 'korean', 'mandarin_simplified', 'mandarin_traditional',
852
+ 'norwegian', 'portuguese', 'russian', 'spanish', 'swedish'. Default: 'auto'.
853
+ preset: 'default', 'teletext', 'netflix'. Default: 'default'.
854
+ chars_per_line: Integer 1-60. Resolve default is 42 (or 16 for Netflix preset).
855
+ line_break: 'single' or 'double'. Default: 'single' on Resolve side.
856
+ gap: Integer 0-10. Default: 0 on Resolve side.
857
+ """
858
+ r = get_resolve()
859
+ if r is None:
860
+ return {"error": "Not connected to DaVinci Resolve"}
861
+ _, tl, err = _get_timeline()
862
+ if err:
863
+ return err
864
+ settings, settings_err = _build_subtitle_settings(
865
+ r, language=language, preset=preset,
866
+ chars_per_line=chars_per_line, line_break=line_break, gap=gap,
867
+ )
868
+ if settings_err:
869
+ return settings_err
870
+ result = tl.CreateSubtitlesFromAudio(settings)
871
+ return {"success": bool(result)}
872
+
873
+
874
+ @mcp.tool()
875
+ def timeline_detect_scene_cuts() -> Dict[str, Any]:
876
+ """Detect scene cuts in the current timeline."""
877
+ _, tl, err = _get_timeline()
878
+ if err:
879
+ return err
880
+ result = tl.DetectSceneCuts()
881
+ return {"success": bool(result)}
882
+
883
+
884
+ @mcp.tool()
885
+ def timeline_convert_to_stereo() -> Dict[str, Any]:
886
+ """Convert the current timeline to stereo."""
887
+ _, tl, err = _get_timeline()
888
+ if err:
889
+ return err
890
+ result = tl.ConvertTimelineToStereo()
891
+ return {"success": bool(result)}
892
+
893
+
894
+ @mcp.tool()
895
+ def timeline_get_node_graph() -> Dict[str, Any]:
896
+ """Get the node graph for the current timeline."""
897
+ _, tl, err = _get_timeline()
898
+ if err:
899
+ return err
900
+ graph = tl.GetNodeGraph()
901
+ if graph:
902
+ return {"has_graph": True, "num_nodes": graph.GetNumNodes()}
903
+ return {"has_graph": False}
904
+
905
+
906
+ @mcp.tool()
907
+ def timeline_analyze_dolby_vision() -> Dict[str, Any]:
908
+ """Analyze Dolby Vision for the current timeline."""
909
+ _, tl, err = _get_timeline()
910
+ if err:
911
+ return err
912
+ result = tl.AnalyzeDolbyVision()
913
+ return {"success": bool(result)}
914
+
915
+
916
+ @mcp.tool()
917
+ def timeline_get_current_video_item() -> Dict[str, Any]:
918
+ """Get the current video item at the playhead."""
919
+ _, tl, err = _get_timeline()
920
+ if err:
921
+ return err
922
+ item = tl.GetCurrentVideoItem()
923
+ if item:
924
+ return {"name": item.GetName(), "unique_id": item.GetUniqueId(), "start": item.GetStart(), "end": item.GetEnd()}
925
+ return {"item": None}
926
+
927
+
928
+ @mcp.tool()
929
+ def timeline_get_current_clip_thumbnail(width: int = 320, height: int = 180) -> Dict[str, Any]:
930
+ """Get thumbnail image data for the current clip.
931
+
932
+ Args:
933
+ width: Thumbnail width. Default: 320.
934
+ height: Thumbnail height. Default: 180.
935
+ """
936
+ _, tl, err = _get_timeline()
937
+ if err:
938
+ return err
939
+ result = tl.GetCurrentClipThumbnailImage()
940
+ if result:
941
+ return {"success": True, "has_data": bool(result)}
942
+ return {"success": False}
943
+
944
+
945
+ @mcp.tool()
946
+ def timeline_get_media_pool_item() -> Dict[str, Any]:
947
+ """Get the MediaPoolItem for the current timeline."""
948
+ _, tl, err = _get_timeline()
949
+ if err:
950
+ return err
951
+ mpi = tl.GetMediaPoolItem()
952
+ if mpi:
953
+ return {"name": mpi.GetName(), "unique_id": mpi.GetUniqueId()}
954
+ return {"media_pool_item": None}
955
+
956
+
957
+ @mcp.tool()
958
+ def timeline_get_mark_in_out() -> Dict[str, Any]:
959
+ """Get mark in/out points for the current timeline."""
960
+ _, tl, err = _get_timeline()
961
+ if err:
962
+ return err
963
+ result = tl.GetMarkInOut()
964
+ return result if result else {}
965
+
966
+
967
+ @mcp.tool()
968
+ def timeline_set_mark_in_out(mark_in: int, mark_out: int) -> Dict[str, Any]:
969
+ """Set mark in/out points for the current timeline.
970
+
971
+ Args:
972
+ mark_in: Mark in frame number.
973
+ mark_out: Mark out frame number.
974
+ """
975
+ _, tl, err = _get_timeline()
976
+ if err:
977
+ return err
978
+ result = tl.SetMarkInOut(mark_in, mark_out)
979
+ return {"success": bool(result)}
980
+
981
+
982
+ @mcp.tool()
983
+ def timeline_clear_mark_in_out() -> Dict[str, Any]:
984
+ """Clear mark in/out points for the current timeline."""
985
+ _, tl, err = _get_timeline()
986
+ if err:
987
+ return err
988
+ result = tl.ClearMarkInOut()
989
+ return {"success": bool(result)}
990
+
991
+
992
+ @mcp.tool()
993
+ def create_timeline_from_clips(
994
+ name: str,
995
+ clip_ids: Optional[List[str]] = None,
996
+ clip_infos: Optional[List[Dict[str, Any]]] = None,
997
+ ) -> Dict[str, Any]:
998
+ """Create a new timeline from specified media pool clips.
999
+
1000
+ Args:
1001
+ name: Name for the new timeline.
1002
+ clip_ids: Simple form — list of MediaPoolItem unique IDs to append end-to-end.
1003
+ clip_infos: Positioned form — list of dicts with keys clip_id (or
1004
+ media_pool_item_id), start_frame, end_frame, record_frame.
1005
+ record_frame is relative to the created timeline start frame by
1006
+ default; pass record_frame_mode="absolute" for raw Resolve values.
1007
+ If both are None, uses the currently selected media pool clips.
1008
+ """
1009
+ project, mp, err = _get_mp()
1010
+ if err:
1011
+ return err
1012
+ if clip_infos is not None:
1013
+ if not isinstance(clip_infos, list) or not clip_infos:
1014
+ return {"error": "clip_infos must be a non-empty list"}
1015
+ root = mp.GetRootFolder()
1016
+ for i, ci in enumerate(clip_infos):
1017
+ _, row_err = _build_create_clip_info_dict(root, ci, i)
1018
+ if row_err:
1019
+ return row_err
1020
+ tl = mp.CreateEmptyTimeline(name)
1021
+ if not tl:
1022
+ return {"success": False, "error": "Failed to create timeline"}
1023
+ try:
1024
+ if project:
1025
+ project.SetCurrentTimeline(tl)
1026
+ except Exception:
1027
+ logger.debug("Could not set newly created timeline current", exc_info=True)
1028
+ timeline_start = _timeline_start_frame(tl)
1029
+ built = []
1030
+ for i, ci in enumerate(clip_infos):
1031
+ append_ci = dict(ci)
1032
+ append_ci.setdefault("track_index", append_ci.get("trackIndex", 1))
1033
+ row, row_err = _build_append_clip_info_dict(root, append_ci, i, timeline_start)
1034
+ if row_err:
1035
+ return row_err
1036
+ built.append(row)
1037
+ appended = mp.AppendToTimeline(built)
1038
+ if not appended:
1039
+ return {"success": False, "error": "Failed to append clip_infos to created timeline"}
1040
+ elif clip_ids:
1041
+ root = mp.GetRootFolder()
1042
+ clips = []
1043
+ for cid in clip_ids:
1044
+ clip = _find_clip_by_id(root, cid)
1045
+ if clip:
1046
+ clips.append(clip)
1047
+ else:
1048
+ return {"error": f"Clip not found: {cid}"}
1049
+ tl = mp.CreateTimelineFromClips(name, clips)
1050
+ else:
1051
+ selected = mp.GetSelectedClips()
1052
+ if not selected:
1053
+ return {"error": "No clips specified and no clips selected in media pool"}
1054
+ tl = mp.CreateTimelineFromClips(name, selected)
1055
+ if tl:
1056
+ return {"success": True, "timeline_name": tl.GetName(), "timeline_id": tl.GetUniqueId()}
1057
+ return {"success": False, "error": "Failed to create timeline from clips"}
1058
+
1059
+
1060
+ @mcp.tool()
1061
+ def set_timeline_setting(setting_name: str, setting_value: str) -> Dict[str, Any]:
1062
+ """Set a timeline setting value.
1063
+
1064
+ Args:
1065
+ setting_name: Name of the timeline setting to set (e.g. 'useCustomSettings', 'timelineFrameRate',
1066
+ 'timelineResolutionWidth', 'timelineResolutionHeight', 'timelineOutputResolutionWidth',
1067
+ 'timelineOutputResolutionHeight', 'colorSpaceTimeline', 'colorSpaceOutput').
1068
+ setting_value: Value to set for the setting (string).
1069
+ """
1070
+ _, tl, err = _get_timeline()
1071
+ if err:
1072
+ return err
1073
+ result = tl.SetSetting(setting_name, setting_value)
1074
+ return {"success": bool(result), "setting_name": setting_name, "setting_value": setting_value}