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,2251 @@
1
+ """Timeline item property, keyframe, and clip-level tools."""
2
+
3
+ from src.granular.common import * # noqa: F401,F403
4
+
5
+ resolve = ResolveProxy()
6
+
7
+ @mcp.resource("resolve://timeline-item/{timeline_item_id}")
8
+ def get_timeline_item_properties(timeline_item_id: str) -> Dict[str, Any]:
9
+ """Get properties of a specific timeline item by ID.
10
+
11
+ Args:
12
+ timeline_item_id: The ID of the timeline item to get properties for
13
+ """
14
+ pm, current_project = get_current_project()
15
+ if not current_project:
16
+ return {"error": "No project currently open"}
17
+
18
+ current_timeline = current_project.GetCurrentTimeline()
19
+ if not current_timeline:
20
+ return {"error": "No timeline currently active"}
21
+
22
+ try:
23
+ # Find the timeline item by ID
24
+ # We'll need to get all items from all tracks and check their IDs
25
+ video_track_count = current_timeline.GetTrackCount("video")
26
+ audio_track_count = current_timeline.GetTrackCount("audio")
27
+
28
+ timeline_item = None
29
+
30
+ # Search video tracks
31
+ for track_index in range(1, video_track_count + 1):
32
+ items = current_timeline.GetItemListInTrack("video", track_index)
33
+ if items:
34
+ for item in items:
35
+ if str(item.GetUniqueId()) == timeline_item_id:
36
+ timeline_item = item
37
+ break
38
+ if timeline_item:
39
+ break
40
+
41
+ # If not found, search audio tracks
42
+ if not timeline_item:
43
+ for track_index in range(1, audio_track_count + 1):
44
+ items = current_timeline.GetItemListInTrack("audio", track_index)
45
+ if items:
46
+ for item in items:
47
+ if str(item.GetUniqueId()) == timeline_item_id:
48
+ timeline_item = item
49
+ break
50
+ if timeline_item:
51
+ break
52
+
53
+ if not timeline_item:
54
+ return {"error": f"Timeline item with ID '{timeline_item_id}' not found"}
55
+
56
+ # Get basic properties
57
+ properties = {
58
+ "id": timeline_item_id,
59
+ "name": timeline_item.GetName(),
60
+ "type": timeline_item.GetType(),
61
+ "start_frame": timeline_item.GetStart(),
62
+ "end_frame": timeline_item.GetEnd(),
63
+ "duration": timeline_item.GetDuration()
64
+ }
65
+
66
+ # Get additional properties if it's a video item
67
+ if timeline_item.GetType() == "Video":
68
+ # Transform properties
69
+ properties["transform"] = {
70
+ "position": {
71
+ "x": timeline_item.GetProperty("Pan"),
72
+ "y": timeline_item.GetProperty("Tilt")
73
+ },
74
+ "zoom": timeline_item.GetProperty("ZoomX"), # ZoomX/ZoomY can be different for non-uniform scaling
75
+ "zoom_x": timeline_item.GetProperty("ZoomX"),
76
+ "zoom_y": timeline_item.GetProperty("ZoomY"),
77
+ "rotation": timeline_item.GetProperty("Rotation"),
78
+ "anchor_point": {
79
+ "x": timeline_item.GetProperty("AnchorPointX"),
80
+ "y": timeline_item.GetProperty("AnchorPointY")
81
+ },
82
+ "pitch": timeline_item.GetProperty("Pitch"),
83
+ "yaw": timeline_item.GetProperty("Yaw")
84
+ }
85
+
86
+ # Crop properties
87
+ properties["crop"] = {
88
+ "left": timeline_item.GetProperty("CropLeft"),
89
+ "right": timeline_item.GetProperty("CropRight"),
90
+ "top": timeline_item.GetProperty("CropTop"),
91
+ "bottom": timeline_item.GetProperty("CropBottom")
92
+ }
93
+
94
+ # Composite properties
95
+ properties["composite"] = {
96
+ "mode": timeline_item.GetProperty("CompositeMode"),
97
+ "opacity": timeline_item.GetProperty("Opacity")
98
+ }
99
+
100
+ # Dynamic zoom properties
101
+ properties["dynamic_zoom"] = {
102
+ "enabled": timeline_item.GetProperty("DynamicZoomEnable"),
103
+ "mode": timeline_item.GetProperty("DynamicZoomMode")
104
+ }
105
+
106
+ # Retime properties
107
+ properties["retime"] = {
108
+ "speed": timeline_item.GetProperty("Speed"),
109
+ "process": timeline_item.GetProperty("RetimeProcess")
110
+ }
111
+
112
+ # Stabilization properties
113
+ properties["stabilization"] = {
114
+ "enabled": timeline_item.GetProperty("StabilizationEnable"),
115
+ "method": timeline_item.GetProperty("StabilizationMethod"),
116
+ "strength": timeline_item.GetProperty("StabilizationStrength")
117
+ }
118
+
119
+ # Audio-specific properties
120
+ if timeline_item.GetType() == "Audio" or timeline_item.GetMediaType() == "Audio":
121
+ properties["audio"] = {
122
+ "volume": timeline_item.GetProperty("Volume"),
123
+ "pan": timeline_item.GetProperty("Pan"),
124
+ "eq_enabled": timeline_item.GetProperty("EQEnable"),
125
+ "normalize_enabled": timeline_item.GetProperty("NormalizeEnable"),
126
+ "normalize_level": timeline_item.GetProperty("NormalizeLevel")
127
+ }
128
+
129
+ return properties
130
+
131
+ except Exception as e:
132
+ return {"error": f"Error getting timeline item properties: {str(e)}"}
133
+
134
+
135
+ @mcp.resource("resolve://timeline-items")
136
+ def get_timeline_items() -> List[Dict[str, Any]]:
137
+ """Get all items in the current timeline with their IDs and basic properties."""
138
+ pm, current_project = get_current_project()
139
+ if not current_project:
140
+ return [{"error": "No project currently open"}]
141
+
142
+ current_timeline = current_project.GetCurrentTimeline()
143
+ if not current_timeline:
144
+ return [{"error": "No timeline currently active"}]
145
+
146
+ try:
147
+ # Get all tracks in the timeline
148
+ video_track_count = current_timeline.GetTrackCount("video")
149
+ audio_track_count = current_timeline.GetTrackCount("audio")
150
+
151
+ items = []
152
+
153
+ # Process video tracks
154
+ for track_index in range(1, video_track_count + 1):
155
+ track_items = current_timeline.GetItemListInTrack("video", track_index)
156
+ if track_items:
157
+ for item in track_items:
158
+ items.append({
159
+ "id": str(item.GetUniqueId()),
160
+ "name": item.GetName(),
161
+ "type": "video",
162
+ "track": track_index,
163
+ "start_frame": item.GetStart(),
164
+ "end_frame": item.GetEnd(),
165
+ "duration": item.GetDuration()
166
+ })
167
+
168
+ # Process audio tracks
169
+ for track_index in range(1, audio_track_count + 1):
170
+ track_items = current_timeline.GetItemListInTrack("audio", track_index)
171
+ if track_items:
172
+ for item in track_items:
173
+ items.append({
174
+ "id": str(item.GetUniqueId()),
175
+ "name": item.GetName(),
176
+ "type": "audio",
177
+ "track": track_index,
178
+ "start_frame": item.GetStart(),
179
+ "end_frame": item.GetEnd(),
180
+ "duration": item.GetDuration()
181
+ })
182
+
183
+ if not items:
184
+ return [{"info": "No items found in the current timeline"}]
185
+
186
+ return items
187
+ except Exception as e:
188
+ return [{"error": f"Error listing timeline items: {str(e)}"}]
189
+
190
+
191
+ @mcp.tool(annotations=DESTRUCTIVE_TOOL)
192
+ def set_timeline_item_transform(timeline_item_id: str,
193
+ property_name: str,
194
+ property_value: float) -> str:
195
+ """Set a transform property for a timeline item.
196
+
197
+ Args:
198
+ timeline_item_id: The ID of the timeline item to modify
199
+ property_name: The name of the property to set. Options include:
200
+ 'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation', 'AnchorPointX',
201
+ 'AnchorPointY', 'Pitch', 'Yaw'
202
+ property_value: The value to set for the property
203
+ """
204
+ pm, current_project = get_current_project()
205
+ if not current_project:
206
+ return "Error: No project currently open"
207
+
208
+ current_timeline = current_project.GetCurrentTimeline()
209
+ if not current_timeline:
210
+ return "Error: No timeline currently active"
211
+
212
+ # Validate property name
213
+ valid_properties = [
214
+ 'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation',
215
+ 'AnchorPointX', 'AnchorPointY', 'Pitch', 'Yaw'
216
+ ]
217
+
218
+ if property_name not in valid_properties:
219
+ return f"Error: Invalid property name. Must be one of: {', '.join(valid_properties)}"
220
+
221
+ try:
222
+ # Find the timeline item by ID
223
+ video_track_count = current_timeline.GetTrackCount("video")
224
+
225
+ timeline_item = None
226
+
227
+ # Search video tracks
228
+ for track_index in range(1, video_track_count + 1):
229
+ items = current_timeline.GetItemListInTrack("video", track_index)
230
+ if items:
231
+ for item in items:
232
+ if str(item.GetUniqueId()) == timeline_item_id:
233
+ timeline_item = item
234
+ break
235
+ if timeline_item:
236
+ break
237
+
238
+ if not timeline_item:
239
+ return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
240
+
241
+ if timeline_item.GetType() != "Video":
242
+ return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
243
+
244
+ # Set the property
245
+ result = timeline_item.SetProperty(property_name, property_value)
246
+ if result:
247
+ return f"Successfully set {property_name} to {property_value} for timeline item '{timeline_item.GetName()}'"
248
+ else:
249
+ return f"Failed to set {property_name} for timeline item '{timeline_item.GetName()}'"
250
+ except Exception as e:
251
+ return f"Error setting timeline item property: {str(e)}"
252
+
253
+
254
+ @mcp.tool()
255
+ def set_timeline_item_crop(timeline_item_id: str,
256
+ crop_type: str,
257
+ crop_value: float) -> str:
258
+ """Set a crop property for a timeline item.
259
+
260
+ Args:
261
+ timeline_item_id: The ID of the timeline item to modify
262
+ crop_type: The type of crop to set. Options: 'Left', 'Right', 'Top', 'Bottom'
263
+ crop_value: The value to set for the crop (typically 0.0 to 1.0)
264
+ """
265
+ pm, current_project = get_current_project()
266
+ if not current_project:
267
+ return "Error: No project currently open"
268
+
269
+ current_timeline = current_project.GetCurrentTimeline()
270
+ if not current_timeline:
271
+ return "Error: No timeline currently active"
272
+
273
+ # Validate crop type
274
+ valid_crop_types = ['Left', 'Right', 'Top', 'Bottom']
275
+
276
+ if crop_type not in valid_crop_types:
277
+ return f"Error: Invalid crop type. Must be one of: {', '.join(valid_crop_types)}"
278
+
279
+ property_name = f"Crop{crop_type}"
280
+
281
+ try:
282
+ # Find the timeline item by ID
283
+ video_track_count = current_timeline.GetTrackCount("video")
284
+
285
+ timeline_item = None
286
+
287
+ # Search video tracks
288
+ for track_index in range(1, video_track_count + 1):
289
+ items = current_timeline.GetItemListInTrack("video", track_index)
290
+ if items:
291
+ for item in items:
292
+ if str(item.GetUniqueId()) == timeline_item_id:
293
+ timeline_item = item
294
+ break
295
+ if timeline_item:
296
+ break
297
+
298
+ if not timeline_item:
299
+ return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
300
+
301
+ if timeline_item.GetType() != "Video":
302
+ return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
303
+
304
+ # Set the property
305
+ result = timeline_item.SetProperty(property_name, crop_value)
306
+ if result:
307
+ return f"Successfully set crop {crop_type.lower()} to {crop_value} for timeline item '{timeline_item.GetName()}'"
308
+ else:
309
+ return f"Failed to set crop {crop_type.lower()} for timeline item '{timeline_item.GetName()}'"
310
+ except Exception as e:
311
+ return f"Error setting timeline item crop: {str(e)}"
312
+
313
+
314
+ @mcp.tool()
315
+ def set_timeline_item_composite(timeline_item_id: str,
316
+ composite_mode: str = None,
317
+ opacity: float = None) -> str:
318
+ """Set composite properties for a timeline item.
319
+
320
+ Args:
321
+ timeline_item_id: The ID of the timeline item to modify
322
+ composite_mode: Optional composite mode to set (e.g., 'Normal', 'Add', 'Multiply')
323
+ opacity: Optional opacity value to set (0.0 to 1.0)
324
+ """
325
+ pm, current_project = get_current_project()
326
+ if not current_project:
327
+ return "Error: No project currently open"
328
+
329
+ current_timeline = current_project.GetCurrentTimeline()
330
+ if not current_timeline:
331
+ return "Error: No timeline currently active"
332
+
333
+ # Validate inputs
334
+ if composite_mode is None and opacity is None:
335
+ return "Error: Must specify at least one of composite_mode or opacity"
336
+
337
+ # Valid composite modes
338
+ valid_composite_modes = [
339
+ 'Normal', 'Add', 'Subtract', 'Difference', 'Multiply', 'Screen',
340
+ 'Overlay', 'Hardlight', 'Softlight', 'Darken', 'Lighten', 'ColorDodge',
341
+ 'ColorBurn', 'Exclusion', 'Hue', 'Saturation', 'Color', 'Luminosity'
342
+ ]
343
+
344
+ if composite_mode and composite_mode not in valid_composite_modes:
345
+ return f"Error: Invalid composite mode. Must be one of: {', '.join(valid_composite_modes)}"
346
+
347
+ if opacity is not None and (opacity < 0.0 or opacity > 1.0):
348
+ return "Error: Opacity must be between 0.0 and 1.0"
349
+
350
+ try:
351
+ # Find the timeline item by ID
352
+ video_track_count = current_timeline.GetTrackCount("video")
353
+
354
+ timeline_item = None
355
+
356
+ # Search video tracks
357
+ for track_index in range(1, video_track_count + 1):
358
+ items = current_timeline.GetItemListInTrack("video", track_index)
359
+ if items:
360
+ for item in items:
361
+ if str(item.GetUniqueId()) == timeline_item_id:
362
+ timeline_item = item
363
+ break
364
+ if timeline_item:
365
+ break
366
+
367
+ if not timeline_item:
368
+ return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
369
+
370
+ if timeline_item.GetType() != "Video":
371
+ return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
372
+
373
+ success = True
374
+
375
+ # Set composite mode if specified
376
+ if composite_mode:
377
+ result = timeline_item.SetProperty("CompositeMode", composite_mode)
378
+ if not result:
379
+ success = False
380
+
381
+ # Set opacity if specified
382
+ if opacity is not None:
383
+ result = timeline_item.SetProperty("Opacity", opacity)
384
+ if not result:
385
+ success = False
386
+
387
+ if success:
388
+ changes = []
389
+ if composite_mode:
390
+ changes.append(f"composite mode to '{composite_mode}'")
391
+ if opacity is not None:
392
+ changes.append(f"opacity to {opacity}")
393
+
394
+ return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
395
+ else:
396
+ return f"Failed to set some composite properties for timeline item '{timeline_item.GetName()}'"
397
+ except Exception as e:
398
+ return f"Error setting timeline item composite properties: {str(e)}"
399
+
400
+
401
+ @mcp.tool()
402
+ def set_timeline_item_retime(timeline_item_id: str,
403
+ speed: float = None,
404
+ process: str = None) -> str:
405
+ """Set retiming properties for a timeline item.
406
+
407
+ Args:
408
+ timeline_item_id: The ID of the timeline item to modify
409
+ speed: Optional speed factor (e.g., 0.5 for 50%, 2.0 for 200%)
410
+ process: Optional retime process. Options: 'NearestFrame', 'FrameBlend', 'OpticalFlow'
411
+ """
412
+ pm, current_project = get_current_project()
413
+ if not current_project:
414
+ return "Error: No project currently open"
415
+
416
+ current_timeline = current_project.GetCurrentTimeline()
417
+ if not current_timeline:
418
+ return "Error: No timeline currently active"
419
+
420
+ # Validate inputs
421
+ if speed is None and process is None:
422
+ return "Error: Must specify at least one of speed or process"
423
+
424
+ if speed is not None and speed <= 0:
425
+ return "Error: Speed must be greater than 0"
426
+
427
+ valid_processes = ['NearestFrame', 'FrameBlend', 'OpticalFlow']
428
+ if process and process not in valid_processes:
429
+ return f"Error: Invalid retime process. Must be one of: {', '.join(valid_processes)}"
430
+
431
+ try:
432
+ # Find the timeline item by ID
433
+ video_track_count = current_timeline.GetTrackCount("video")
434
+
435
+ timeline_item = None
436
+
437
+ # Search video tracks
438
+ for track_index in range(1, video_track_count + 1):
439
+ items = current_timeline.GetItemListInTrack("video", track_index)
440
+ if items:
441
+ for item in items:
442
+ if str(item.GetUniqueId()) == timeline_item_id:
443
+ timeline_item = item
444
+ break
445
+ if timeline_item:
446
+ break
447
+
448
+ if not timeline_item:
449
+ return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
450
+
451
+ success = True
452
+
453
+ # Set speed if specified
454
+ if speed is not None:
455
+ result = timeline_item.SetProperty("Speed", speed)
456
+ if not result:
457
+ success = False
458
+
459
+ # Set retime process if specified
460
+ if process:
461
+ result = timeline_item.SetProperty("RetimeProcess", process)
462
+ if not result:
463
+ success = False
464
+
465
+ if success:
466
+ changes = []
467
+ if speed is not None:
468
+ changes.append(f"speed to {speed}x")
469
+ if process:
470
+ changes.append(f"retime process to '{process}'")
471
+
472
+ return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
473
+ else:
474
+ return f"Failed to set some retime properties for timeline item '{timeline_item.GetName()}'"
475
+ except Exception as e:
476
+ return f"Error setting timeline item retime properties: {str(e)}"
477
+
478
+
479
+ @mcp.tool()
480
+ def set_timeline_item_stabilization(timeline_item_id: str,
481
+ enabled: bool = None,
482
+ method: str = None,
483
+ strength: float = None) -> str:
484
+ """Set stabilization properties for a timeline item.
485
+
486
+ Args:
487
+ timeline_item_id: The ID of the timeline item to modify
488
+ enabled: Optional boolean to enable/disable stabilization
489
+ method: Optional stabilization method. Options: 'Perspective', 'Similarity', 'Translation'
490
+ strength: Optional strength value (0.0 to 1.0)
491
+ """
492
+ pm, current_project = get_current_project()
493
+ if not current_project:
494
+ return "Error: No project currently open"
495
+
496
+ current_timeline = current_project.GetCurrentTimeline()
497
+ if not current_timeline:
498
+ return "Error: No timeline currently active"
499
+
500
+ # Validate inputs
501
+ if enabled is None and method is None and strength is None:
502
+ return "Error: Must specify at least one parameter to modify"
503
+
504
+ valid_methods = ['Perspective', 'Similarity', 'Translation']
505
+ if method and method not in valid_methods:
506
+ return f"Error: Invalid stabilization method. Must be one of: {', '.join(valid_methods)}"
507
+
508
+ if strength is not None and (strength < 0.0 or strength > 1.0):
509
+ return "Error: Strength must be between 0.0 and 1.0"
510
+
511
+ try:
512
+ # Find the timeline item by ID
513
+ video_track_count = current_timeline.GetTrackCount("video")
514
+
515
+ timeline_item = None
516
+
517
+ # Search video tracks
518
+ for track_index in range(1, video_track_count + 1):
519
+ items = current_timeline.GetItemListInTrack("video", track_index)
520
+ if items:
521
+ for item in items:
522
+ if str(item.GetUniqueId()) == timeline_item_id:
523
+ timeline_item = item
524
+ break
525
+ if timeline_item:
526
+ break
527
+
528
+ if not timeline_item:
529
+ return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
530
+
531
+ if timeline_item.GetType() != "Video":
532
+ return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
533
+
534
+ success = True
535
+
536
+ # Set enabled if specified
537
+ if enabled is not None:
538
+ result = timeline_item.SetProperty("StabilizationEnable", 1 if enabled else 0)
539
+ if not result:
540
+ success = False
541
+
542
+ # Set method if specified
543
+ if method:
544
+ result = timeline_item.SetProperty("StabilizationMethod", method)
545
+ if not result:
546
+ success = False
547
+
548
+ # Set strength if specified
549
+ if strength is not None:
550
+ result = timeline_item.SetProperty("StabilizationStrength", strength)
551
+ if not result:
552
+ success = False
553
+
554
+ if success:
555
+ changes = []
556
+ if enabled is not None:
557
+ changes.append(f"stabilization {'enabled' if enabled else 'disabled'}")
558
+ if method:
559
+ changes.append(f"stabilization method to '{method}'")
560
+ if strength is not None:
561
+ changes.append(f"stabilization strength to {strength}")
562
+
563
+ return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
564
+ else:
565
+ return f"Failed to set some stabilization properties for timeline item '{timeline_item.GetName()}'"
566
+ except Exception as e:
567
+ return f"Error setting timeline item stabilization properties: {str(e)}"
568
+
569
+
570
+ @mcp.tool()
571
+ def set_timeline_item_audio(timeline_item_id: str,
572
+ volume: float = None,
573
+ pan: float = None,
574
+ eq_enabled: bool = None) -> str:
575
+ """Set audio properties for a timeline item.
576
+
577
+ Args:
578
+ timeline_item_id: The ID of the timeline item to modify
579
+ volume: Optional volume level (usually 0.0 to 2.0, where 1.0 is unity gain)
580
+ pan: Optional pan value (-1.0 to 1.0, where -1.0 is left, 0 is center, 1.0 is right)
581
+ eq_enabled: Optional boolean to enable/disable EQ
582
+ """
583
+ pm, current_project = get_current_project()
584
+ if not current_project:
585
+ return "Error: No project currently open"
586
+
587
+ current_timeline = current_project.GetCurrentTimeline()
588
+ if not current_timeline:
589
+ return "Error: No timeline currently active"
590
+
591
+ # Validate inputs
592
+ if volume is None and pan is None and eq_enabled is None:
593
+ return "Error: Must specify at least one parameter to modify"
594
+
595
+ if volume is not None and volume < 0.0:
596
+ return "Error: Volume must be greater than or equal to 0.0"
597
+
598
+ if pan is not None and (pan < -1.0 or pan > 1.0):
599
+ return "Error: Pan must be between -1.0 and 1.0"
600
+
601
+ try:
602
+ # Find the timeline item by ID
603
+ video_track_count = current_timeline.GetTrackCount("video")
604
+ audio_track_count = current_timeline.GetTrackCount("audio")
605
+
606
+ timeline_item = None
607
+ is_audio = False
608
+
609
+ # Search audio tracks first
610
+ for track_index in range(1, audio_track_count + 1):
611
+ items = current_timeline.GetItemListInTrack("audio", track_index)
612
+ if items:
613
+ for item in items:
614
+ if str(item.GetUniqueId()) == timeline_item_id:
615
+ timeline_item = item
616
+ is_audio = True
617
+ break
618
+ if timeline_item:
619
+ break
620
+
621
+ # If not found in audio tracks, search video tracks (might be a video clip with audio)
622
+ if not timeline_item:
623
+ for track_index in range(1, video_track_count + 1):
624
+ items = current_timeline.GetItemListInTrack("video", track_index)
625
+ if items:
626
+ for item in items:
627
+ if str(item.GetUniqueId()) == timeline_item_id:
628
+ timeline_item = item
629
+ break
630
+ if timeline_item:
631
+ break
632
+
633
+ if not timeline_item:
634
+ return f"Error: Timeline item with ID '{timeline_item_id}' not found"
635
+
636
+ # Check if the item has audio capabilities
637
+ if not is_audio and timeline_item.GetMediaType() != "Audio":
638
+ return f"Error: Timeline item with ID '{timeline_item_id}' does not have audio properties"
639
+
640
+ success = True
641
+
642
+ # Set volume if specified
643
+ if volume is not None:
644
+ result = timeline_item.SetProperty("Volume", volume)
645
+ if not result:
646
+ success = False
647
+
648
+ # Set pan if specified
649
+ if pan is not None:
650
+ result = timeline_item.SetProperty("Pan", pan)
651
+ if not result:
652
+ success = False
653
+
654
+ # Set EQ enabled if specified
655
+ if eq_enabled is not None:
656
+ result = timeline_item.SetProperty("EQEnable", 1 if eq_enabled else 0)
657
+ if not result:
658
+ success = False
659
+
660
+ if success:
661
+ changes = []
662
+ if volume is not None:
663
+ changes.append(f"volume to {volume}")
664
+ if pan is not None:
665
+ changes.append(f"pan to {pan}")
666
+ if eq_enabled is not None:
667
+ changes.append(f"EQ {'enabled' if eq_enabled else 'disabled'}")
668
+
669
+ return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
670
+ else:
671
+ return f"Failed to set some audio properties for timeline item '{timeline_item.GetName()}'"
672
+ except Exception as e:
673
+ return f"Error setting timeline item audio properties: {str(e)}"
674
+
675
+
676
+ @mcp.resource("resolve://timeline-item/{timeline_item_id}/keyframes/{property_name}")
677
+ def get_timeline_item_keyframes(timeline_item_id: str, property_name: str) -> Dict[str, Any]:
678
+ """Get keyframes for a specific timeline item by ID.
679
+
680
+ Args:
681
+ timeline_item_id: The ID of the timeline item to get keyframes for
682
+ property_name: Optional property name to filter keyframes (e.g., 'Pan', 'ZoomX')
683
+ """
684
+ pm, current_project = get_current_project()
685
+ if not current_project:
686
+ return {"error": "No project currently open"}
687
+
688
+ current_timeline = current_project.GetCurrentTimeline()
689
+ if not current_timeline:
690
+ return {"error": "No timeline currently active"}
691
+
692
+ try:
693
+ # Find the timeline item by ID
694
+ video_track_count = current_timeline.GetTrackCount("video")
695
+ audio_track_count = current_timeline.GetTrackCount("audio")
696
+
697
+ timeline_item = None
698
+
699
+ # Search video tracks
700
+ for track_index in range(1, video_track_count + 1):
701
+ items = current_timeline.GetItemListInTrack("video", track_index)
702
+ if items:
703
+ for item in items:
704
+ if str(item.GetUniqueId()) == timeline_item_id:
705
+ timeline_item = item
706
+ break
707
+ if timeline_item:
708
+ break
709
+
710
+ # If not found, search audio tracks
711
+ if not timeline_item:
712
+ for track_index in range(1, audio_track_count + 1):
713
+ items = current_timeline.GetItemListInTrack("audio", track_index)
714
+ if items:
715
+ for item in items:
716
+ if str(item.GetUniqueId()) == timeline_item_id:
717
+ timeline_item = item
718
+ break
719
+ if timeline_item:
720
+ break
721
+
722
+ if not timeline_item:
723
+ return {"error": f"Timeline item with ID '{timeline_item_id}' not found"}
724
+
725
+ # Get all keyframeable properties for this item
726
+ keyframeable_properties = []
727
+ keyframes = {}
728
+
729
+ # Common keyframeable properties for video items
730
+ video_properties = [
731
+ 'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation', 'AnchorPointX', 'AnchorPointY',
732
+ 'Pitch', 'Yaw', 'Opacity', 'CropLeft', 'CropRight', 'CropTop', 'CropBottom'
733
+ ]
734
+
735
+ # Audio-specific keyframeable properties
736
+ audio_properties = ['Volume', 'Pan']
737
+
738
+ # Check if it's a video item
739
+ if timeline_item.GetType() == "Video":
740
+ # Check each property to see if it has keyframes
741
+ for prop in video_properties:
742
+ if timeline_item.GetKeyframeCount(prop) > 0:
743
+ keyframeable_properties.append(prop)
744
+
745
+ # Get all keyframes for this property
746
+ keyframes[prop] = []
747
+ keyframe_count = timeline_item.GetKeyframeCount(prop)
748
+
749
+ for i in range(keyframe_count):
750
+ # Get the frame position and value of the keyframe
751
+ frame_pos = timeline_item.GetKeyframeAtIndex(prop, i)["frame"]
752
+ value = timeline_item.GetPropertyAtKeyframeIndex(prop, i)
753
+
754
+ keyframes[prop].append({
755
+ "frame": frame_pos,
756
+ "value": value
757
+ })
758
+
759
+ # Check if it has audio properties (could be video with audio or audio-only)
760
+ if timeline_item.GetType() == "Audio" or timeline_item.GetMediaType() == "Audio":
761
+ # Check each audio property for keyframes
762
+ for prop in audio_properties:
763
+ if timeline_item.GetKeyframeCount(prop) > 0:
764
+ keyframeable_properties.append(prop)
765
+
766
+ # Get all keyframes for this property
767
+ keyframes[prop] = []
768
+ keyframe_count = timeline_item.GetKeyframeCount(prop)
769
+
770
+ for i in range(keyframe_count):
771
+ # Get the frame position and value of the keyframe
772
+ frame_pos = timeline_item.GetKeyframeAtIndex(prop, i)["frame"]
773
+ value = timeline_item.GetPropertyAtKeyframeIndex(prop, i)
774
+
775
+ keyframes[prop].append({
776
+ "frame": frame_pos,
777
+ "value": value
778
+ })
779
+
780
+ # Filter by property_name if specified
781
+ if property_name:
782
+ if property_name in keyframes:
783
+ return {
784
+ "item_id": timeline_item_id,
785
+ "item_name": timeline_item.GetName(),
786
+ "properties": [property_name],
787
+ "keyframes": {property_name: keyframes[property_name]}
788
+ }
789
+ else:
790
+ return {
791
+ "item_id": timeline_item_id,
792
+ "item_name": timeline_item.GetName(),
793
+ "properties": [],
794
+ "keyframes": {}
795
+ }
796
+
797
+ # Return all keyframes
798
+ return {
799
+ "item_id": timeline_item_id,
800
+ "item_name": timeline_item.GetName(),
801
+ "properties": keyframeable_properties,
802
+ "keyframes": keyframes
803
+ }
804
+
805
+ except Exception as e:
806
+ return {"error": f"Error getting timeline item keyframes: {str(e)}"}
807
+
808
+
809
+ @mcp.tool()
810
+ def add_keyframe(timeline_item_id: str, property_name: str, frame: int, value: float) -> str:
811
+ """Add a keyframe at the specified frame for a timeline item property.
812
+
813
+ Args:
814
+ timeline_item_id: The ID of the timeline item to add keyframe to
815
+ property_name: The name of the property to keyframe (e.g., 'Pan', 'ZoomX')
816
+ frame: Frame position for the keyframe
817
+ value: Value to set at the keyframe
818
+ """
819
+ pm, current_project = get_current_project()
820
+ if not current_project:
821
+ return "Error: No project currently open"
822
+
823
+ current_timeline = current_project.GetCurrentTimeline()
824
+ if not current_timeline:
825
+ return "Error: No timeline currently active"
826
+
827
+ # Valid keyframeable properties
828
+ video_properties = [
829
+ 'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation', 'AnchorPointX', 'AnchorPointY',
830
+ 'Pitch', 'Yaw', 'Opacity', 'CropLeft', 'CropRight', 'CropTop', 'CropBottom'
831
+ ]
832
+
833
+ audio_properties = ['Volume', 'Pan']
834
+
835
+ valid_properties = video_properties + audio_properties
836
+
837
+ if property_name not in valid_properties:
838
+ return f"Error: Invalid property name. Must be one of: {', '.join(valid_properties)}"
839
+
840
+ try:
841
+ # Find the timeline item by ID
842
+ video_track_count = current_timeline.GetTrackCount("video")
843
+ audio_track_count = current_timeline.GetTrackCount("audio")
844
+
845
+ timeline_item = None
846
+ is_audio = False
847
+
848
+ # Search video tracks
849
+ for track_index in range(1, video_track_count + 1):
850
+ items = current_timeline.GetItemListInTrack("video", track_index)
851
+ if items:
852
+ for item in items:
853
+ if str(item.GetUniqueId()) == timeline_item_id:
854
+ timeline_item = item
855
+ break
856
+ if timeline_item:
857
+ break
858
+
859
+ # If not found, search audio tracks
860
+ if not timeline_item:
861
+ for track_index in range(1, audio_track_count + 1):
862
+ items = current_timeline.GetItemListInTrack("audio", track_index)
863
+ if items:
864
+ for item in items:
865
+ if str(item.GetUniqueId()) == timeline_item_id:
866
+ timeline_item = item
867
+ is_audio = True
868
+ break
869
+ if timeline_item:
870
+ break
871
+
872
+ if not timeline_item:
873
+ return f"Error: Timeline item with ID '{timeline_item_id}' not found"
874
+
875
+ # Check if the specified property is valid for this item type
876
+ if is_audio and property_name not in audio_properties:
877
+ return f"Error: Property '{property_name}' is not available for audio items"
878
+
879
+ if not is_audio and property_name not in video_properties and timeline_item.GetType() != "Video":
880
+ return f"Error: Property '{property_name}' is not available for this item type"
881
+
882
+ # Validate frame is within the item's range
883
+ start_frame = timeline_item.GetStart()
884
+ end_frame = timeline_item.GetEnd()
885
+
886
+ if frame < start_frame or frame > end_frame:
887
+ return f"Error: Frame {frame} is outside the item's range ({start_frame} to {end_frame})"
888
+
889
+ # Add the keyframe
890
+ result = timeline_item.AddKeyframe(property_name, frame, value)
891
+
892
+ if result:
893
+ return f"Successfully added keyframe for {property_name} at frame {frame} with value {value}"
894
+ else:
895
+ return f"Failed to add keyframe for {property_name} at frame {frame}"
896
+
897
+ except Exception as e:
898
+ return f"Error adding keyframe: {str(e)}"
899
+
900
+
901
+ @mcp.tool()
902
+ def modify_keyframe(timeline_item_id: str, property_name: str, frame: int, new_value: float = None, new_frame: int = None) -> str:
903
+ """Modify an existing keyframe by changing its value or frame position.
904
+
905
+ Args:
906
+ timeline_item_id: The ID of the timeline item
907
+ property_name: The name of the property with keyframe
908
+ frame: Current frame position of the keyframe to modify
909
+ new_value: Optional new value for the keyframe
910
+ new_frame: Optional new frame position for the keyframe
911
+ """
912
+ pm, current_project = get_current_project()
913
+ if not current_project:
914
+ return "Error: No project currently open"
915
+
916
+ current_timeline = current_project.GetCurrentTimeline()
917
+ if not current_timeline:
918
+ return "Error: No timeline currently active"
919
+
920
+ if new_value is None and new_frame is None:
921
+ return "Error: Must specify at least one of new_value or new_frame"
922
+
923
+ try:
924
+ # Find the timeline item by ID
925
+ video_track_count = current_timeline.GetTrackCount("video")
926
+ audio_track_count = current_timeline.GetTrackCount("audio")
927
+
928
+ timeline_item = None
929
+
930
+ # Search video tracks
931
+ for track_index in range(1, video_track_count + 1):
932
+ items = current_timeline.GetItemListInTrack("video", track_index)
933
+ if items:
934
+ for item in items:
935
+ if str(item.GetUniqueId()) == timeline_item_id:
936
+ timeline_item = item
937
+ break
938
+ if timeline_item:
939
+ break
940
+
941
+ # If not found, search audio tracks
942
+ if not timeline_item:
943
+ for track_index in range(1, audio_track_count + 1):
944
+ items = current_timeline.GetItemListInTrack("audio", track_index)
945
+ if items:
946
+ for item in items:
947
+ if str(item.GetUniqueId()) == timeline_item_id:
948
+ timeline_item = item
949
+ break
950
+ if timeline_item:
951
+ break
952
+
953
+ if not timeline_item:
954
+ return f"Error: Timeline item with ID '{timeline_item_id}' not found"
955
+
956
+ # Check if the property has keyframes
957
+ keyframe_count = timeline_item.GetKeyframeCount(property_name)
958
+ if keyframe_count == 0:
959
+ return f"Error: No keyframes found for property '{property_name}'"
960
+
961
+ # Find the keyframe at the specified frame
962
+ keyframe_index = -1
963
+ for i in range(keyframe_count):
964
+ kf = timeline_item.GetKeyframeAtIndex(property_name, i)
965
+ if kf["frame"] == frame:
966
+ keyframe_index = i
967
+ break
968
+
969
+ if keyframe_index == -1:
970
+ return f"Error: No keyframe found at frame {frame} for property '{property_name}'"
971
+
972
+ if new_frame is not None:
973
+ # Check if new frame is within the item's range
974
+ start_frame = timeline_item.GetStart()
975
+ end_frame = timeline_item.GetEnd()
976
+
977
+ if new_frame < start_frame or new_frame > end_frame:
978
+ return f"Error: New frame {new_frame} is outside the item's range ({start_frame} to {end_frame})"
979
+
980
+ # Delete the keyframe at the current frame
981
+ current_value = timeline_item.GetPropertyAtKeyframeIndex(property_name, keyframe_index)
982
+ timeline_item.DeleteKeyframe(property_name, frame)
983
+
984
+ # Add a new keyframe at the new frame position with the current value (or new value if specified)
985
+ value = new_value if new_value is not None else current_value
986
+ result = timeline_item.AddKeyframe(property_name, new_frame, value)
987
+
988
+ if result:
989
+ return f"Successfully moved keyframe for {property_name} from frame {frame} to frame {new_frame}"
990
+ else:
991
+ return f"Failed to move keyframe for {property_name}"
992
+ else:
993
+ # Only changing the value, not the frame position
994
+ # We need to delete and re-add the keyframe with the new value
995
+ timeline_item.DeleteKeyframe(property_name, frame)
996
+ result = timeline_item.AddKeyframe(property_name, frame, new_value)
997
+
998
+ if result:
999
+ return f"Successfully updated keyframe value for {property_name} at frame {frame} to {new_value}"
1000
+ else:
1001
+ return f"Failed to update keyframe value for {property_name} at frame {frame}"
1002
+
1003
+ except Exception as e:
1004
+ return f"Error modifying keyframe: {str(e)}"
1005
+
1006
+
1007
+ @mcp.tool()
1008
+ def delete_keyframe(timeline_item_id: str, property_name: str, frame: int) -> str:
1009
+ """Delete a keyframe at the specified frame for a timeline item property.
1010
+
1011
+ Args:
1012
+ timeline_item_id: The ID of the timeline item
1013
+ property_name: The name of the property with keyframe to delete
1014
+ frame: Frame position of the keyframe to delete
1015
+ """
1016
+ pm, current_project = get_current_project()
1017
+ if not current_project:
1018
+ return "Error: No project currently open"
1019
+
1020
+ current_timeline = current_project.GetCurrentTimeline()
1021
+ if not current_timeline:
1022
+ return "Error: No timeline currently active"
1023
+
1024
+ try:
1025
+ # Find the timeline item by ID
1026
+ video_track_count = current_timeline.GetTrackCount("video")
1027
+ audio_track_count = current_timeline.GetTrackCount("audio")
1028
+
1029
+ timeline_item = None
1030
+
1031
+ # Search video tracks
1032
+ for track_index in range(1, video_track_count + 1):
1033
+ items = current_timeline.GetItemListInTrack("video", track_index)
1034
+ if items:
1035
+ for item in items:
1036
+ if str(item.GetUniqueId()) == timeline_item_id:
1037
+ timeline_item = item
1038
+ break
1039
+ if timeline_item:
1040
+ break
1041
+
1042
+ # If not found, search audio tracks
1043
+ if not timeline_item:
1044
+ for track_index in range(1, audio_track_count + 1):
1045
+ items = current_timeline.GetItemListInTrack("audio", track_index)
1046
+ if items:
1047
+ for item in items:
1048
+ if str(item.GetUniqueId()) == timeline_item_id:
1049
+ timeline_item = item
1050
+ break
1051
+ if timeline_item:
1052
+ break
1053
+
1054
+ if not timeline_item:
1055
+ return f"Error: Timeline item with ID '{timeline_item_id}' not found"
1056
+
1057
+ # Check if the property has keyframes
1058
+ keyframe_count = timeline_item.GetKeyframeCount(property_name)
1059
+ if keyframe_count == 0:
1060
+ return f"Error: No keyframes found for property '{property_name}'"
1061
+
1062
+ # Check if there's a keyframe at the specified frame
1063
+ keyframe_exists = False
1064
+ for i in range(keyframe_count):
1065
+ kf = timeline_item.GetKeyframeAtIndex(property_name, i)
1066
+ if kf["frame"] == frame:
1067
+ keyframe_exists = True
1068
+ break
1069
+
1070
+ if not keyframe_exists:
1071
+ return f"Error: No keyframe found at frame {frame} for property '{property_name}'"
1072
+
1073
+ # Delete the keyframe
1074
+ result = timeline_item.DeleteKeyframe(property_name, frame)
1075
+
1076
+ if result:
1077
+ return f"Successfully deleted keyframe for {property_name} at frame {frame}"
1078
+ else:
1079
+ return f"Failed to delete keyframe for {property_name} at frame {frame}"
1080
+
1081
+ except Exception as e:
1082
+ return f"Error deleting keyframe: {str(e)}"
1083
+
1084
+
1085
+ @mcp.tool()
1086
+ def set_keyframe_interpolation(timeline_item_id: str, property_name: str, frame: int, interpolation_type: str) -> str:
1087
+ """Set the interpolation type for a keyframe.
1088
+
1089
+ Args:
1090
+ timeline_item_id: The ID of the timeline item
1091
+ property_name: The name of the property with keyframe
1092
+ frame: Frame position of the keyframe
1093
+ interpolation_type: Type of interpolation. Options: 'Linear', 'Bezier', 'Ease-In', 'Ease-Out'
1094
+ """
1095
+ pm, current_project = get_current_project()
1096
+ if not current_project:
1097
+ return "Error: No project currently open"
1098
+
1099
+ current_timeline = current_project.GetCurrentTimeline()
1100
+ if not current_timeline:
1101
+ return "Error: No timeline currently active"
1102
+
1103
+ # Validate interpolation type
1104
+ valid_interpolation_types = ['Linear', 'Bezier', 'Ease-In', 'Ease-Out']
1105
+ if interpolation_type not in valid_interpolation_types:
1106
+ return f"Error: Invalid interpolation type. Must be one of: {', '.join(valid_interpolation_types)}"
1107
+
1108
+ try:
1109
+ # Find the timeline item by ID
1110
+ video_track_count = current_timeline.GetTrackCount("video")
1111
+ audio_track_count = current_timeline.GetTrackCount("audio")
1112
+
1113
+ timeline_item = None
1114
+
1115
+ # Search video tracks
1116
+ for track_index in range(1, video_track_count + 1):
1117
+ items = current_timeline.GetItemListInTrack("video", track_index)
1118
+ if items:
1119
+ for item in items:
1120
+ if str(item.GetUniqueId()) == timeline_item_id:
1121
+ timeline_item = item
1122
+ break
1123
+ if timeline_item:
1124
+ break
1125
+
1126
+ # If not found, search audio tracks
1127
+ if not timeline_item:
1128
+ for track_index in range(1, audio_track_count + 1):
1129
+ items = current_timeline.GetItemListInTrack("audio", track_index)
1130
+ if items:
1131
+ for item in items:
1132
+ if str(item.GetUniqueId()) == timeline_item_id:
1133
+ timeline_item = item
1134
+ break
1135
+ if timeline_item:
1136
+ break
1137
+
1138
+ if not timeline_item:
1139
+ return f"Error: Timeline item with ID '{timeline_item_id}' not found"
1140
+
1141
+ # Check if the property has keyframes
1142
+ keyframe_count = timeline_item.GetKeyframeCount(property_name)
1143
+ if keyframe_count == 0:
1144
+ return f"Error: No keyframes found for property '{property_name}'"
1145
+
1146
+ # Check if there's a keyframe at the specified frame
1147
+ keyframe_exists = False
1148
+ for i in range(keyframe_count):
1149
+ kf = timeline_item.GetKeyframeAtIndex(property_name, i)
1150
+ if kf["frame"] == frame:
1151
+ keyframe_exists = True
1152
+ break
1153
+
1154
+ if not keyframe_exists:
1155
+ return f"Error: No keyframe found at frame {frame} for property '{property_name}'"
1156
+
1157
+ # Set the interpolation type
1158
+ interpolation_map = {
1159
+ 'Linear': 0,
1160
+ 'Bezier': 1,
1161
+ 'Ease-In': 2,
1162
+ 'Ease-Out': 3
1163
+ }
1164
+
1165
+ # Get current keyframe value
1166
+ value = None
1167
+ for i in range(keyframe_count):
1168
+ kf = timeline_item.GetKeyframeAtIndex(property_name, i)
1169
+ if kf["frame"] == frame:
1170
+ value = timeline_item.GetPropertyAtKeyframeIndex(property_name, i)
1171
+ break
1172
+
1173
+ # Delete the old keyframe
1174
+ timeline_item.DeleteKeyframe(property_name, frame)
1175
+
1176
+ # Add a new keyframe with the same value but different interpolation
1177
+ result = timeline_item.AddKeyframe(property_name, frame, value, interpolation_map[interpolation_type])
1178
+
1179
+ if result:
1180
+ return f"Successfully set interpolation for {property_name} keyframe at frame {frame} to {interpolation_type}"
1181
+ else:
1182
+ return f"Failed to set interpolation for {property_name} keyframe at frame {frame}"
1183
+
1184
+ except Exception as e:
1185
+ return f"Error setting keyframe interpolation: {str(e)}"
1186
+
1187
+
1188
+ @mcp.tool()
1189
+ def enable_keyframes(timeline_item_id: str, keyframe_mode: str = "All") -> str:
1190
+ """Enable keyframe mode for a timeline item.
1191
+
1192
+ Args:
1193
+ timeline_item_id: The ID of the timeline item
1194
+ keyframe_mode: Keyframe mode to enable. Options: 'All', 'Color', 'Sizing'
1195
+ """
1196
+ pm, current_project = get_current_project()
1197
+ if not current_project:
1198
+ return "Error: No project currently open"
1199
+
1200
+ current_timeline = current_project.GetCurrentTimeline()
1201
+ if not current_timeline:
1202
+ return "Error: No timeline currently active"
1203
+
1204
+ # Validate keyframe mode
1205
+ valid_keyframe_modes = ['All', 'Color', 'Sizing']
1206
+ if keyframe_mode not in valid_keyframe_modes:
1207
+ return f"Error: Invalid keyframe mode. Must be one of: {', '.join(valid_keyframe_modes)}"
1208
+
1209
+ try:
1210
+ # Find the timeline item by ID
1211
+ video_track_count = current_timeline.GetTrackCount("video")
1212
+
1213
+ timeline_item = None
1214
+
1215
+ # Search video tracks
1216
+ for track_index in range(1, video_track_count + 1):
1217
+ items = current_timeline.GetItemListInTrack("video", track_index)
1218
+ if items:
1219
+ for item in items:
1220
+ if str(item.GetUniqueId()) == timeline_item_id:
1221
+ timeline_item = item
1222
+ break
1223
+ if timeline_item:
1224
+ break
1225
+
1226
+ if not timeline_item:
1227
+ return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
1228
+
1229
+ if timeline_item.GetType() != "Video":
1230
+ return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
1231
+
1232
+ # Set the keyframe mode
1233
+ keyframe_mode_map = {
1234
+ 'All': 0,
1235
+ 'Color': 1,
1236
+ 'Sizing': 2
1237
+ }
1238
+
1239
+ result = timeline_item.SetProperty("KeyframeMode", keyframe_mode_map[keyframe_mode])
1240
+
1241
+ if result:
1242
+ return f"Successfully enabled {keyframe_mode} keyframe mode for timeline item '{timeline_item.GetName()}'"
1243
+ else:
1244
+ return f"Failed to enable {keyframe_mode} keyframe mode for timeline item '{timeline_item.GetName()}'"
1245
+
1246
+ except Exception as e:
1247
+ return f"Error enabling keyframe mode: {str(e)}"
1248
+
1249
+
1250
+ @mcp.tool()
1251
+ def ti_get_info(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1252
+ """Get comprehensive info about a timeline item.
1253
+
1254
+ Args:
1255
+ item_index: 0-based item index. Default: 0.
1256
+ track_type: 'video' or 'audio'. Default: 'video'.
1257
+ track_index: 1-based track index. Default: 1.
1258
+ """
1259
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1260
+ if err:
1261
+ return err
1262
+ return {
1263
+ "name": item.GetName(), "duration": item.GetDuration(),
1264
+ "start": item.GetStart(), "end": item.GetEnd(),
1265
+ "left_offset": item.GetLeftOffset(), "right_offset": item.GetRightOffset(),
1266
+ "source_start_frame": item.GetSourceStartFrame(), "source_end_frame": item.GetSourceEndFrame(),
1267
+ "unique_id": item.GetUniqueId(), "clip_enabled": item.GetClipEnabled()
1268
+ }
1269
+
1270
+
1271
+ @mcp.tool()
1272
+ def ti_set_name(name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1273
+ """Rename a timeline item.
1274
+
1275
+ Args:
1276
+ name: New timeline item name.
1277
+ item_index: 0-based item index. Default: 0.
1278
+ track_type: 'video' or 'audio'. Default: 'video'.
1279
+ track_index: 1-based track index. Default: 1.
1280
+ """
1281
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1282
+ if err:
1283
+ return err
1284
+ missing = _requires_method(item, "SetName", "20.2")
1285
+ if missing:
1286
+ return missing
1287
+ result = item.SetName(name)
1288
+ return {"success": bool(result), "name": name}
1289
+
1290
+
1291
+ @mcp.tool()
1292
+ def ti_get_source_start_time(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1293
+ """Get source start time of a timeline item.
1294
+
1295
+ Args:
1296
+ item_index: 0-based item index. Default: 0.
1297
+ track_type: 'video' or 'audio'. Default: 'video'.
1298
+ track_index: 1-based track index. Default: 1.
1299
+ """
1300
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1301
+ if err:
1302
+ return err
1303
+ return {"source_start_time": item.GetSourceStartTime(), "source_end_time": item.GetSourceEndTime()}
1304
+
1305
+
1306
+ @mcp.tool()
1307
+ def ti_set_property(property_name: str, property_value: Any, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1308
+ """Set a property on a timeline item.
1309
+
1310
+ Args:
1311
+ property_name: Property name (Pan, Tilt, ZoomX, ZoomY, RotationAngle, Opacity, CropLeft, CropRight, CropTop, CropBottom, etc.).
1312
+ property_value: Value to set.
1313
+ item_index: 0-based item index. Default: 0.
1314
+ track_type: 'video' or 'audio'. Default: 'video'.
1315
+ track_index: 1-based track index. Default: 1.
1316
+ """
1317
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1318
+ if err:
1319
+ return err
1320
+ result = item.SetProperty(property_name, property_value)
1321
+ return {"success": bool(result)}
1322
+
1323
+
1324
+ @mcp.tool()
1325
+ def ti_get_property(property_name: str = "", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1326
+ """Get property of a timeline item.
1327
+
1328
+ Args:
1329
+ property_name: Property name, or empty for all. Default: ''.
1330
+ item_index: 0-based item index. Default: 0.
1331
+ track_type: 'video' or 'audio'. Default: 'video'.
1332
+ track_index: 1-based track index. Default: 1.
1333
+ """
1334
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1335
+ if err:
1336
+ return err
1337
+ if property_name:
1338
+ result = item.GetProperty(property_name)
1339
+ else:
1340
+ result = item.GetProperty()
1341
+ return {"property": result}
1342
+
1343
+
1344
+ @mcp.tool()
1345
+ def ti_add_marker(frame_id: int, color: str, name: str, note: str = "", duration: int = 1, custom_data: str = "", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1346
+ """Add a marker to a timeline item.
1347
+
1348
+ Args:
1349
+ frame_id: Frame offset within the item.
1350
+ color: Marker color.
1351
+ name: Marker name.
1352
+ note: Marker note. Default: ''.
1353
+ duration: Duration in frames. Default: 1.
1354
+ custom_data: Custom data. Default: ''.
1355
+ item_index: 0-based item index. Default: 0.
1356
+ track_type: 'video' or 'audio'. Default: 'video'.
1357
+ track_index: 1-based track index. Default: 1.
1358
+ """
1359
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1360
+ if err:
1361
+ return err
1362
+ result = item.AddMarker(frame_id, color, name, note, duration, custom_data)
1363
+ return {"success": bool(result)}
1364
+
1365
+
1366
+ @mcp.tool()
1367
+ def ti_get_markers(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1368
+ """Get all markers on a timeline item.
1369
+
1370
+ Args:
1371
+ item_index: 0-based item index. Default: 0.
1372
+ track_type: 'video' or 'audio'. Default: 'video'.
1373
+ track_index: 1-based track index. Default: 1.
1374
+ """
1375
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1376
+ if err:
1377
+ return err
1378
+ return {"markers": item.GetMarkers() or {}}
1379
+
1380
+
1381
+ @mcp.tool()
1382
+ def ti_delete_markers_by_color(color: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1383
+ """Delete markers by color on a timeline item.
1384
+
1385
+ Args:
1386
+ color: Color to delete. '' for all.
1387
+ item_index: 0-based item index. Default: 0.
1388
+ """
1389
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1390
+ if err:
1391
+ return err
1392
+ return {"success": bool(item.DeleteMarkersByColor(color))}
1393
+
1394
+
1395
+ @mcp.tool()
1396
+ def ti_delete_marker_at_frame(frame_id: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1397
+ """Delete a marker at a frame on a timeline item.
1398
+
1399
+ Args:
1400
+ frame_id: Frame number.
1401
+ item_index: 0-based item index. Default: 0.
1402
+ """
1403
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1404
+ if err:
1405
+ return err
1406
+ return {"success": bool(item.DeleteMarkerAtFrame(frame_id))}
1407
+
1408
+
1409
+ @mcp.tool()
1410
+ def ti_delete_marker_by_custom_data(custom_data: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1411
+ """Delete a marker by custom data on a timeline item.
1412
+
1413
+ Args:
1414
+ custom_data: Custom data of the marker.
1415
+ item_index: 0-based item index. Default: 0.
1416
+ """
1417
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1418
+ if err:
1419
+ return err
1420
+ return {"success": bool(item.DeleteMarkerByCustomData(custom_data))}
1421
+
1422
+
1423
+ @mcp.tool()
1424
+ def ti_get_marker_by_custom_data(custom_data: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1425
+ """Find marker by custom data.
1426
+
1427
+ Args:
1428
+ custom_data: Custom data to search for.
1429
+ item_index: 0-based item index. Default: 0.
1430
+ """
1431
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1432
+ if err:
1433
+ return err
1434
+ return {"marker": item.GetMarkerByCustomData(custom_data) or {}}
1435
+
1436
+
1437
+ @mcp.tool()
1438
+ def ti_update_marker_custom_data(frame_id: int, custom_data: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1439
+ """Update marker custom data.
1440
+
1441
+ Args:
1442
+ frame_id: Frame number.
1443
+ custom_data: New custom data.
1444
+ item_index: 0-based item index. Default: 0.
1445
+ """
1446
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1447
+ if err:
1448
+ return err
1449
+ return {"success": bool(item.UpdateMarkerCustomData(frame_id, custom_data))}
1450
+
1451
+
1452
+ @mcp.tool()
1453
+ def ti_get_marker_custom_data(frame_id: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1454
+ """Get marker custom data.
1455
+
1456
+ Args:
1457
+ frame_id: Frame number.
1458
+ item_index: 0-based item index. Default: 0.
1459
+ """
1460
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1461
+ if err:
1462
+ return err
1463
+ return {"custom_data": item.GetMarkerCustomData(frame_id) or ""}
1464
+
1465
+
1466
+ @mcp.tool()
1467
+ def ti_add_flag(color: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1468
+ """Add a flag to a timeline item.
1469
+
1470
+ Args:
1471
+ color: Flag color.
1472
+ item_index: 0-based item index. Default: 0.
1473
+ """
1474
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1475
+ if err:
1476
+ return err
1477
+ return {"success": bool(item.AddFlag(color))}
1478
+
1479
+
1480
+ @mcp.tool()
1481
+ def ti_get_flag_list(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1482
+ """Get flags on a timeline item.
1483
+
1484
+ Args:
1485
+ item_index: 0-based item index. Default: 0.
1486
+ """
1487
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1488
+ if err:
1489
+ return err
1490
+ return {"flags": item.GetFlagList() or []}
1491
+
1492
+
1493
+ @mcp.tool()
1494
+ def ti_clear_flags(color: str = "", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1495
+ """Clear flags from a timeline item.
1496
+
1497
+ Args:
1498
+ color: Color to clear, or '' for all.
1499
+ item_index: 0-based item index. Default: 0.
1500
+ """
1501
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1502
+ if err:
1503
+ return err
1504
+ return {"success": bool(item.ClearFlags(color))}
1505
+
1506
+
1507
+ @mcp.tool()
1508
+ def ti_get_clip_color(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1509
+ """Get clip color of a timeline item.
1510
+
1511
+ Args:
1512
+ item_index: 0-based item index. Default: 0.
1513
+ """
1514
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1515
+ if err:
1516
+ return err
1517
+ return {"clip_color": item.GetClipColor() or ""}
1518
+
1519
+
1520
+ @mcp.tool()
1521
+ def ti_set_clip_color(color: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1522
+ """Set clip color of a timeline item.
1523
+
1524
+ Args:
1525
+ color: Color name.
1526
+ item_index: 0-based item index. Default: 0.
1527
+ """
1528
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1529
+ if err:
1530
+ return err
1531
+ return {"success": bool(item.SetClipColor(color))}
1532
+
1533
+
1534
+ @mcp.tool()
1535
+ def ti_clear_clip_color(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1536
+ """Clear clip color from a timeline item.
1537
+
1538
+ Args:
1539
+ item_index: 0-based item index. Default: 0.
1540
+ """
1541
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1542
+ if err:
1543
+ return err
1544
+ return {"success": bool(item.ClearClipColor())}
1545
+
1546
+
1547
+ @mcp.tool()
1548
+ def ti_add_fusion_comp(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1549
+ """Add a new Fusion composition to a timeline item.
1550
+
1551
+ Args:
1552
+ item_index: 0-based item index. Default: 0.
1553
+ """
1554
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1555
+ if err:
1556
+ return err
1557
+ return {"success": bool(item.AddFusionComp())}
1558
+
1559
+
1560
+ @mcp.tool()
1561
+ def ti_import_fusion_comp(file_path: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1562
+ """Import a Fusion composition from file.
1563
+
1564
+ Args:
1565
+ file_path: Path to the .comp file.
1566
+ item_index: 0-based item index. Default: 0.
1567
+ """
1568
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1569
+ if err:
1570
+ return err
1571
+ return {"success": bool(item.ImportFusionComp(file_path))}
1572
+
1573
+
1574
+ @mcp.tool()
1575
+ def ti_export_fusion_comp(file_path: str, comp_index: int = 1, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1576
+ """Export a Fusion composition to file.
1577
+
1578
+ Args:
1579
+ file_path: Output path for the .comp file.
1580
+ comp_index: 1-based Fusion comp index. Default: 1.
1581
+ item_index: 0-based item index. Default: 0.
1582
+ """
1583
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1584
+ if err:
1585
+ return err
1586
+ comp = item.GetFusionCompByIndex(comp_index)
1587
+ if not comp:
1588
+ return {"error": f"No Fusion comp at index {comp_index}"}
1589
+ return {"success": bool(item.ExportFusionComp(file_path, comp_index))}
1590
+
1591
+
1592
+ @mcp.tool()
1593
+ def ti_delete_fusion_comp(comp_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1594
+ """Delete a Fusion composition by name.
1595
+
1596
+ Args:
1597
+ comp_name: Name of the Fusion composition.
1598
+ item_index: 0-based item index. Default: 0.
1599
+ """
1600
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1601
+ if err:
1602
+ return err
1603
+ return {"success": bool(item.DeleteFusionCompByName(comp_name))}
1604
+
1605
+
1606
+ @mcp.tool()
1607
+ def ti_load_fusion_comp(comp_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1608
+ """Load a Fusion composition by name.
1609
+
1610
+ Args:
1611
+ comp_name: Name of the Fusion composition.
1612
+ item_index: 0-based item index. Default: 0.
1613
+ """
1614
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1615
+ if err:
1616
+ return err
1617
+ return {"success": bool(item.LoadFusionCompByName(comp_name))}
1618
+
1619
+
1620
+ @mcp.tool()
1621
+ def ti_rename_fusion_comp(old_name: str, new_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1622
+ """Rename a Fusion composition.
1623
+
1624
+ Args:
1625
+ old_name: Current name.
1626
+ new_name: New name.
1627
+ item_index: 0-based item index. Default: 0.
1628
+ """
1629
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1630
+ if err:
1631
+ return err
1632
+ return {"success": bool(item.RenameFusionCompByName(old_name, new_name))}
1633
+
1634
+
1635
+ @mcp.tool()
1636
+ def ti_get_fusion_comp_info(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1637
+ """Get Fusion composition info for a timeline item.
1638
+
1639
+ Args:
1640
+ item_index: 0-based item index. Default: 0.
1641
+ """
1642
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1643
+ if err:
1644
+ return err
1645
+ return {
1646
+ "comp_count": item.GetFusionCompCount(),
1647
+ "comp_names": item.GetFusionCompNameList() or {}
1648
+ }
1649
+
1650
+
1651
+ @mcp.tool()
1652
+ def ti_add_version(version_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1653
+ """Add a new color version to a timeline item.
1654
+
1655
+ Args:
1656
+ version_name: Name for the new version.
1657
+ version_type: 0=Local, 1=Remote. Default: 0.
1658
+ item_index: 0-based item index. Default: 0.
1659
+ """
1660
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1661
+ if err:
1662
+ return err
1663
+ return {"success": bool(item.AddVersion(version_name, version_type))}
1664
+
1665
+
1666
+ @mcp.tool()
1667
+ def ti_get_current_version(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1668
+ """Get the current color version of a timeline item.
1669
+
1670
+ Args:
1671
+ item_index: 0-based item index. Default: 0.
1672
+ """
1673
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1674
+ if err:
1675
+ return err
1676
+ return {"version": item.GetCurrentVersion() or {}}
1677
+
1678
+
1679
+ @mcp.tool()
1680
+ def ti_delete_version(version_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1681
+ """Delete a color version.
1682
+
1683
+ Args:
1684
+ version_name: Name of the version.
1685
+ version_type: 0=Local, 1=Remote. Default: 0.
1686
+ item_index: 0-based item index. Default: 0.
1687
+ """
1688
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1689
+ if err:
1690
+ return err
1691
+ return {"success": bool(item.DeleteVersionByName(version_name, version_type))}
1692
+
1693
+
1694
+ @mcp.tool()
1695
+ def ti_load_version(version_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1696
+ """Load a color version.
1697
+
1698
+ Args:
1699
+ version_name: Name of the version.
1700
+ version_type: 0=Local, 1=Remote. Default: 0.
1701
+ item_index: 0-based item index. Default: 0.
1702
+ """
1703
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1704
+ if err:
1705
+ return err
1706
+ return {"success": bool(item.LoadVersionByName(version_name, version_type))}
1707
+
1708
+
1709
+ @mcp.tool()
1710
+ def ti_rename_version(old_name: str, new_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1711
+ """Rename a color version.
1712
+
1713
+ Args:
1714
+ old_name: Current version name.
1715
+ new_name: New version name.
1716
+ version_type: 0=Local, 1=Remote. Default: 0.
1717
+ item_index: 0-based item index. Default: 0.
1718
+ """
1719
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1720
+ if err:
1721
+ return err
1722
+ return {"success": bool(item.RenameVersionByName(old_name, new_name, version_type))}
1723
+
1724
+
1725
+ @mcp.tool()
1726
+ def ti_get_version_name_list(version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1727
+ """Get list of version names.
1728
+
1729
+ Args:
1730
+ version_type: 0=Local, 1=Remote. Default: 0.
1731
+ item_index: 0-based item index. Default: 0.
1732
+ """
1733
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1734
+ if err:
1735
+ return err
1736
+ return {"versions": item.GetVersionNameList(version_type) or []}
1737
+
1738
+
1739
+ @mcp.tool()
1740
+ def ti_set_cdl(cdl: Dict[str, Any], item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1741
+ """Set CDL (Color Decision List) values on a timeline item.
1742
+
1743
+ Args:
1744
+ cdl: Dict with CDL values: {'NodeIndex': str, 'Slope': str, 'Offset': str, 'Power': str, 'Saturation': str}.
1745
+ item_index: 0-based item index. Default: 0.
1746
+ """
1747
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1748
+ if err:
1749
+ return err
1750
+ return {"success": bool(item.SetCDL(_normalize_cdl(cdl)))}
1751
+
1752
+
1753
+ @mcp.tool()
1754
+ def ti_add_take(media_pool_item_id: str, start_frame: int = 0, end_frame: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1755
+ """Add a take to a timeline item.
1756
+
1757
+ Args:
1758
+ media_pool_item_id: Unique ID of the MediaPoolItem to use as take.
1759
+ start_frame: Start frame. Default: 0.
1760
+ end_frame: End frame. Default: 0.
1761
+ item_index: 0-based item index. Default: 0.
1762
+ """
1763
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1764
+ if err:
1765
+ return err
1766
+ _, mp, mp_err = _get_mp()
1767
+ if mp_err:
1768
+ return mp_err
1769
+ mpi = _find_clip_by_id(mp.GetRootFolder(), media_pool_item_id)
1770
+ if not mpi:
1771
+ return {"error": f"MediaPoolItem {media_pool_item_id} not found"}
1772
+ return {"success": bool(item.AddTake(mpi, start_frame, end_frame))}
1773
+
1774
+
1775
+ @mcp.tool()
1776
+ def ti_get_takes_info(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1777
+ """Get takes info for a timeline item.
1778
+
1779
+ Args:
1780
+ item_index: 0-based item index. Default: 0.
1781
+ """
1782
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1783
+ if err:
1784
+ return err
1785
+ count = item.GetTakesCount()
1786
+ selected = item.GetSelectedTakeIndex()
1787
+ takes = []
1788
+ for i in range(count):
1789
+ take = item.GetTakeByIndex(i + 1)
1790
+ takes.append(take if take else {})
1791
+ return {"takes_count": count, "selected_take_index": selected, "takes": takes}
1792
+
1793
+
1794
+ @mcp.tool()
1795
+ def ti_select_take(take_index: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1796
+ """Select a take by index.
1797
+
1798
+ Args:
1799
+ take_index: 1-based take index.
1800
+ item_index: 0-based item index. Default: 0.
1801
+ """
1802
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1803
+ if err:
1804
+ return err
1805
+ return {"success": bool(item.SelectTakeByIndex(take_index))}
1806
+
1807
+
1808
+ @mcp.tool()
1809
+ def ti_delete_take(take_index: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1810
+ """Delete a take by index.
1811
+
1812
+ Args:
1813
+ take_index: 1-based take index.
1814
+ item_index: 0-based item index. Default: 0.
1815
+ """
1816
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1817
+ if err:
1818
+ return err
1819
+ return {"success": bool(item.DeleteTakeByIndex(take_index))}
1820
+
1821
+
1822
+ @mcp.tool()
1823
+ def ti_finalize_take(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1824
+ """Finalize the selected take.
1825
+
1826
+ Args:
1827
+ item_index: 0-based item index. Default: 0.
1828
+ """
1829
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1830
+ if err:
1831
+ return err
1832
+ return {"success": bool(item.FinalizeTake())}
1833
+
1834
+
1835
+ @mcp.tool()
1836
+ def ti_copy_grades(target_item_indices: List[int], track_type: str = "video", track_index: int = 1, source_item_index: int = 0) -> Dict[str, Any]:
1837
+ """Copy grades from one timeline item to others.
1838
+
1839
+ Args:
1840
+ target_item_indices: List of 0-based indices of target items.
1841
+ track_type: 'video' or 'audio'. Default: 'video'.
1842
+ track_index: 1-based track index. Default: 1.
1843
+ source_item_index: 0-based source item index. Default: 0.
1844
+ """
1845
+ _, tl, err = _get_timeline()
1846
+ if err:
1847
+ return err
1848
+ items = tl.GetItemListInTrack(track_type, track_index)
1849
+ if not items:
1850
+ return {"error": "No items in track"}
1851
+ source = items[source_item_index] if source_item_index < len(items) else None
1852
+ if not source:
1853
+ return {"error": "Source item not found"}
1854
+ targets = [items[i] for i in target_item_indices if i < len(items)]
1855
+ if not targets:
1856
+ return {"error": "No target items found"}
1857
+ result = source.CopyGrades(targets)
1858
+ return {"success": bool(result)}
1859
+
1860
+
1861
+ @mcp.tool()
1862
+ def ti_set_clip_enabled(enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1863
+ """Enable or disable a timeline item.
1864
+
1865
+ Args:
1866
+ enabled: True to enable, False to disable.
1867
+ item_index: 0-based item index. Default: 0.
1868
+ """
1869
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1870
+ if err:
1871
+ return err
1872
+ return {"success": bool(item.SetClipEnabled(enabled))}
1873
+
1874
+
1875
+ @mcp.tool()
1876
+ def ti_update_sidecar(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1877
+ """Update sidecar file for a timeline item.
1878
+
1879
+ Args:
1880
+ item_index: 0-based item index. Default: 0.
1881
+ """
1882
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1883
+ if err:
1884
+ return err
1885
+ return {"success": bool(item.UpdateSidecar())}
1886
+
1887
+
1888
+ @mcp.tool()
1889
+ def ti_load_burn_in_preset(preset_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1890
+ """Load a burn-in preset for a timeline item.
1891
+
1892
+ Args:
1893
+ preset_name: Burn-in preset name.
1894
+ item_index: 0-based item index. Default: 0.
1895
+ """
1896
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1897
+ if err:
1898
+ return err
1899
+ return {"success": bool(item.LoadBurnInPreset(preset_name))}
1900
+
1901
+
1902
+ @mcp.tool()
1903
+ def ti_create_magic_mask(mode: str = "Forward", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1904
+ """Create a Magic Mask on a timeline item.
1905
+
1906
+ Args:
1907
+ mode: 'Forward' or 'Backward'. Default: 'Forward'.
1908
+ item_index: 0-based item index. Default: 0.
1909
+ """
1910
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1911
+ if err:
1912
+ return err
1913
+ return {"success": bool(item.CreateMagicMask(mode))}
1914
+
1915
+
1916
+ @mcp.tool()
1917
+ def ti_regenerate_magic_mask(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1918
+ """Regenerate Magic Mask on a timeline item.
1919
+
1920
+ Args:
1921
+ item_index: 0-based item index. Default: 0.
1922
+ """
1923
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1924
+ if err:
1925
+ return err
1926
+ return {"success": bool(item.RegenerateMagicMask())}
1927
+
1928
+
1929
+ @mcp.tool()
1930
+ def ti_stabilize(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1931
+ """Stabilize a timeline item.
1932
+
1933
+ Args:
1934
+ item_index: 0-based item index. Default: 0.
1935
+ """
1936
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1937
+ if err:
1938
+ return err
1939
+ return {"success": bool(item.Stabilize())}
1940
+
1941
+
1942
+ @mcp.tool()
1943
+ def ti_smart_reframe(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1944
+ """Apply Smart Reframe to a timeline item.
1945
+
1946
+ Args:
1947
+ item_index: 0-based item index. Default: 0.
1948
+ """
1949
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1950
+ if err:
1951
+ return err
1952
+ return {"success": bool(item.SmartReframe())}
1953
+
1954
+
1955
+ @mcp.tool()
1956
+ def ti_get_voice_isolation_state(item_index: int = 0, track_type: str = "audio", track_index: int = 1) -> Dict[str, Any]:
1957
+ """Get voice isolation state for a timeline item.
1958
+
1959
+ Args:
1960
+ item_index: 0-based item index. Default: 0.
1961
+ track_type: 'audio' or 'video'. Default: 'audio'.
1962
+ track_index: 1-based track index. Default: 1.
1963
+ """
1964
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1965
+ if err:
1966
+ return err
1967
+ missing = _requires_method(item, "GetVoiceIsolationState", "20.1")
1968
+ if missing:
1969
+ return missing
1970
+ state = item.GetVoiceIsolationState()
1971
+ return {"state": state if state else {"isEnabled": False, "amount": 0}}
1972
+
1973
+
1974
+ @mcp.tool()
1975
+ def ti_set_voice_isolation_state(state: Dict[str, Any], item_index: int = 0, track_type: str = "audio", track_index: int = 1) -> Dict[str, Any]:
1976
+ """Set voice isolation state for a timeline item.
1977
+
1978
+ Args:
1979
+ state: Dict with isEnabled (bool) and amount (0-100).
1980
+ item_index: 0-based item index. Default: 0.
1981
+ track_type: 'audio' or 'video'. Default: 'audio'.
1982
+ track_index: 1-based track index. Default: 1.
1983
+ """
1984
+ item, err = _get_timeline_item(track_type, track_index, item_index)
1985
+ if err:
1986
+ return err
1987
+ missing = _requires_method(item, "SetVoiceIsolationState", "20.1")
1988
+ if missing:
1989
+ return missing
1990
+ result = item.SetVoiceIsolationState(state)
1991
+ return {"success": bool(result)}
1992
+
1993
+
1994
+ @mcp.tool()
1995
+ def ti_reset_all_node_colors(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
1996
+ """Reset node colors for all nodes in the active clip version.
1997
+
1998
+ Args:
1999
+ item_index: 0-based item index. Default: 0.
2000
+ track_type: 'video' or 'audio'. Default: 'video'.
2001
+ track_index: 1-based track index. Default: 1.
2002
+ """
2003
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2004
+ if err:
2005
+ return err
2006
+ missing = _requires_method(item, "ResetAllNodeColors", "20.2")
2007
+ if missing:
2008
+ return missing
2009
+ result = item.ResetAllNodeColors()
2010
+ return {"success": bool(result)}
2011
+
2012
+
2013
+ @mcp.tool()
2014
+ def ti_get_node_graph(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2015
+ """Get the color node graph for a timeline item.
2016
+
2017
+ Args:
2018
+ item_index: 0-based item index. Default: 0.
2019
+ """
2020
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2021
+ if err:
2022
+ return err
2023
+ graph = item.GetNodeGraph()
2024
+ if graph:
2025
+ return {"has_graph": True, "num_nodes": graph.GetNumNodes()}
2026
+ return {"has_graph": False}
2027
+
2028
+
2029
+ @mcp.tool()
2030
+ def ti_get_color_group(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2031
+ """Get the color group for a timeline item.
2032
+
2033
+ Args:
2034
+ item_index: 0-based item index. Default: 0.
2035
+ """
2036
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2037
+ if err:
2038
+ return err
2039
+ group = item.GetColorGroup()
2040
+ if group:
2041
+ return {"group_name": group.GetName()}
2042
+ return {"group_name": None}
2043
+
2044
+
2045
+ @mcp.tool()
2046
+ def ti_assign_to_color_group(group_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2047
+ """Assign a timeline item to a color group.
2048
+
2049
+ Args:
2050
+ group_name: Name of the color group.
2051
+ item_index: 0-based item index. Default: 0.
2052
+ """
2053
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2054
+ if err:
2055
+ return err
2056
+ project = resolve.GetProjectManager().GetCurrentProject()
2057
+ groups = project.GetColorGroupsList()
2058
+ target = None
2059
+ if groups:
2060
+ for g in groups:
2061
+ if g.GetName() == group_name:
2062
+ target = g
2063
+ break
2064
+ if not target:
2065
+ return {"error": f"Color group '{group_name}' not found"}
2066
+ return {"success": bool(item.AssignToColorGroup(target))}
2067
+
2068
+
2069
+ @mcp.tool()
2070
+ def ti_remove_from_color_group(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2071
+ """Remove a timeline item from its color group.
2072
+
2073
+ Args:
2074
+ item_index: 0-based item index. Default: 0.
2075
+ """
2076
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2077
+ if err:
2078
+ return err
2079
+ return {"success": bool(item.RemoveFromColorGroup())}
2080
+
2081
+
2082
+ @mcp.tool()
2083
+ def ti_export_lut(export_type: str, path: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2084
+ """Export LUT from a timeline item.
2085
+
2086
+ Args:
2087
+ export_type: LUT type ('EXPORT_LUT_17PTCUBE', 'EXPORT_LUT_33PTCUBE', 'EXPORT_LUT_65PTCUBE', 'EXPORT_LUT_PANASONICVLUT').
2088
+ path: Output file path.
2089
+ item_index: 0-based item index. Default: 0.
2090
+ """
2091
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2092
+ if err:
2093
+ return err
2094
+ try:
2095
+ etype = getattr(resolve, export_type) if hasattr(resolve, export_type) else export_type
2096
+ except Exception:
2097
+ etype = export_type
2098
+ return {"success": bool(item.ExportLUT(etype, path))}
2099
+
2100
+
2101
+ @mcp.tool()
2102
+ def ti_get_linked_items(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2103
+ """Get items linked to a timeline item.
2104
+
2105
+ Args:
2106
+ item_index: 0-based item index. Default: 0.
2107
+ """
2108
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2109
+ if err:
2110
+ return err
2111
+ linked = item.GetLinkedItems()
2112
+ if linked:
2113
+ return {"linked_items": [{"name": li.GetName(), "unique_id": li.GetUniqueId()} for li in linked]}
2114
+ return {"linked_items": []}
2115
+
2116
+
2117
+ @mcp.tool()
2118
+ def ti_get_track_type_and_index(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2119
+ """Get the track type and index for a timeline item.
2120
+
2121
+ Args:
2122
+ item_index: 0-based item index. Default: 0.
2123
+ """
2124
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2125
+ if err:
2126
+ return err
2127
+ result = item.GetTrackTypeAndIndex()
2128
+ return {"track_type": result[0] if result else "", "track_index": result[1] if result and len(result) > 1 else 0}
2129
+
2130
+
2131
+ @mcp.tool()
2132
+ def ti_get_source_audio_channel_mapping(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2133
+ """Get source audio channel mapping for a timeline item.
2134
+
2135
+ Args:
2136
+ item_index: 0-based item index. Default: 0.
2137
+ """
2138
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2139
+ if err:
2140
+ return err
2141
+ mapping = item.GetSourceAudioChannelMapping()
2142
+ return {"audio_channel_mapping": mapping if mapping else ""}
2143
+
2144
+
2145
+ @mcp.tool()
2146
+ def ti_get_stereo_convergence_values(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2147
+ """Get stereo convergence values for a timeline item.
2148
+
2149
+ Args:
2150
+ item_index: 0-based item index. Default: 0.
2151
+ """
2152
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2153
+ if err:
2154
+ return err
2155
+ return {"convergence": item.GetStereoConvergenceValues() or {}}
2156
+
2157
+
2158
+ @mcp.tool()
2159
+ def ti_get_stereo_floating_window_params(eye: str = "left", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2160
+ """Get stereo floating window parameters.
2161
+
2162
+ Args:
2163
+ eye: 'left' or 'right'. Default: 'left'.
2164
+ item_index: 0-based item index. Default: 0.
2165
+ """
2166
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2167
+ if err:
2168
+ return err
2169
+ if eye == "left":
2170
+ return {"params": item.GetStereoLeftFloatingWindowParams() or {}}
2171
+ else:
2172
+ return {"params": item.GetStereoRightFloatingWindowParams() or {}}
2173
+
2174
+
2175
+ @mcp.tool()
2176
+ def ti_get_media_pool_item(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2177
+ """Get the MediaPoolItem associated with a timeline item.
2178
+
2179
+ Args:
2180
+ item_index: 0-based item index. Default: 0.
2181
+ """
2182
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2183
+ if err:
2184
+ return err
2185
+ mpi = item.GetMediaPoolItem()
2186
+ if mpi:
2187
+ return {"name": mpi.GetName(), "unique_id": mpi.GetUniqueId()}
2188
+ return {"media_pool_item": None}
2189
+
2190
+
2191
+ @mcp.tool()
2192
+ def ti_get_cache_status(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2193
+ """Get cache status for a timeline item.
2194
+
2195
+ Args:
2196
+ item_index: 0-based item index. Default: 0.
2197
+ """
2198
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2199
+ if err:
2200
+ return err
2201
+ return {
2202
+ "color_output_cache_enabled": bool(item.GetIsColorOutputCacheEnabled()),
2203
+ "fusion_output_cache_enabled": bool(item.GetIsFusionOutputCacheEnabled())
2204
+ }
2205
+
2206
+
2207
+ @mcp.tool()
2208
+ def ti_set_color_output_cache(enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2209
+ """Enable/disable color output cache for a timeline item.
2210
+
2211
+ Args:
2212
+ enabled: True to enable, False to disable.
2213
+ item_index: 0-based item index. Default: 0.
2214
+ """
2215
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2216
+ if err:
2217
+ return err
2218
+ return {"success": bool(item.SetColorOutputCache(enabled))}
2219
+
2220
+
2221
+ @mcp.tool()
2222
+ def ti_set_fusion_output_cache(enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
2223
+ """Enable/disable Fusion output cache for a timeline item.
2224
+
2225
+ Args:
2226
+ enabled: True to enable, False to disable.
2227
+ item_index: 0-based item index. Default: 0.
2228
+ """
2229
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2230
+ if err:
2231
+ return err
2232
+ return {"success": bool(item.SetFusionOutputCache(enabled))}
2233
+
2234
+
2235
+ @mcp.tool()
2236
+ def get_fusion_comp_by_name(comp_name: str, track_type: str = "video", track_index: int = 1, item_index: int = 0) -> Dict[str, Any]:
2237
+ """Get a Fusion composition from a timeline item by name.
2238
+
2239
+ Args:
2240
+ comp_name: Name of the Fusion composition to retrieve.
2241
+ track_type: Track type ('video', 'audio', 'subtitle').
2242
+ track_index: Track index (1-based).
2243
+ item_index: Item index on the track (0-based).
2244
+ """
2245
+ item, err = _get_timeline_item(track_type, track_index, item_index)
2246
+ if err:
2247
+ return err
2248
+ comp = item.GetFusionCompByName(comp_name)
2249
+ if comp:
2250
+ return {"success": True, "comp_name": comp_name, "comp_available": True}
2251
+ return {"success": False, "error": f"Fusion composition '{comp_name}' not found on this timeline item"}