"""Export / relink — tool schemas and handlers. Extracted from server.py; see Engine/docs/03_SERVER_TOOLS.md for the tool catalog. """ from __future__ import annotations from typing import Sequence from mcp.types import TextContent, Tool from fcpxml.export import DaVinciExporter from fcpxml.writer import FCPXMLModifier from server_tools._shared import ( _require_timeline, _resolve_io_paths, _setup_modifier, _text_result, _validate_filepath, format_timecode, ) TOOLS = [ Tool( name="export_edl", description="Generate EDL (Edit Decision List) from timeline", inputSchema={ "type": "object", "properties": {"filepath": {"type": "string"}}, "required": ["filepath"] } ), Tool( name="export_csv", description="Export timeline data to CSV format", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string"}, "include": {"type": "array", "items": {"type": "string"}} }, "required": ["filepath"] } ), Tool( name="export_resolve_xml", description="Export timeline as DaVinci Resolve compatible FCPXML (simplified v1.9)", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Path to FCPXML file"}, "flatten_compounds": {"type": "boolean", "default": True, "description": "Flatten compound clips for compatibility"}, "output_path": {"type": "string", "description": "Output path (default: adds _resolve suffix)"}, }, "required": ["filepath"] } ), Tool( name="export_fcp7_xml", description="Export timeline as FCP7 XML (XMEML) for Premiere Pro, DaVinci Resolve, and Avid compatibility", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Path to FCPXML file"}, "output_path": {"type": "string", "description": "Output path (default: adds _fcp7.xml suffix)"}, }, "required": ["filepath"] } ), Tool( name="relink_media", description="Bulk-rewrite media source paths (asset/media-rep src URLs) to relink moved or renamed media folders without opening FCP. Prefix-based: find='/Volumes/OldDrive/Media' replace='/Volumes/NewDrive/Media'. Use dry_run to preview.", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Path to FCPXML file or .fcpxmld bundle"}, "find": {"type": "string", "description": "Old path prefix to match (plain path or file:// URL)"}, "replace": {"type": "string", "description": "New path prefix to substitute"}, "dry_run": {"type": "boolean", "description": "Preview changes without writing", "default": False}, "output_path": {"type": "string", "description": "Output path (default: adds _relinked suffix)"}, }, "required": ["filepath", "find", "replace"] } ), ] async def handle_export_edl(arguments: dict) -> Sequence[TextContent]: project, tl = _require_timeline(arguments["filepath"]) edl = f"TITLE: {tl.name}\nFCM: NON-DROP FRAME\n\n" for i, c in enumerate(tl.clips, 1): edl += f"{i:03d} AX V C {format_timecode(c.source_start)} {format_timecode(c.end)} {format_timecode(c.start)} {format_timecode(c.end)}\n" edl += f"* FROM CLIP NAME: {c.name}\n\n" return _text_result(f"```edl\n{edl}```") async def handle_export_csv(arguments: dict) -> Sequence[TextContent]: project, tl = _require_timeline(arguments["filepath"]) csv = "Name,Start,End,Duration,Keywords\n" for c in tl.clips: kws = "|".join(k.value for k in c.keywords) csv += f'"{c.name}",{format_timecode(c.start)},{format_timecode(c.end)},{c.duration_seconds:.3f},"{kws}"\n' return _text_result(f"```csv\n{csv}```") async def handle_export_resolve_xml(arguments: dict) -> Sequence[TextContent]: filepath, output_path = _resolve_io_paths(arguments, "_resolve") exporter = DaVinciExporter(filepath) exporter.export_simplified_fcpxml( output_path, flatten_compounds=arguments.get("flatten_compounds", True), ) return _text_result(( f"# Exported for DaVinci Resolve\n\n" f"- **Format**: Simplified FCPXML v1.9\n" f"- **Compound clips flattened**: {arguments.get('flatten_compounds', True)}\n\n" f"Saved to: `{output_path}`\n\n" f"**Next step**: In DaVinci Resolve, go to File > Import > Timeline > Import AAF/EDL/XML" )) async def handle_export_fcp7_xml(arguments: dict) -> Sequence[TextContent]: filepath, output_path = _resolve_io_paths(arguments, "_fcp7") exporter = DaVinciExporter(filepath) exporter.export_xmeml(output_path) return _text_result(( f"# Exported as FCP7 XML (XMEML)\n\n" f"- **Format**: XMEML v5\n" f"- **Compatible with**: Premiere Pro, DaVinci Resolve, Avid Media Composer\n\n" f"Saved to: `{output_path}`\n\n" f"**Next step**: Import via File > Import in your target NLE" )) async def handle_relink_media(arguments: dict) -> Sequence[TextContent]: dry_run = arguments.get("dry_run", False) if dry_run: filepath = _validate_filepath(arguments["filepath"], ('.fcpxml', '.fcpxmld')) modifier = FCPXMLModifier(filepath) result = modifier.relink_media( arguments["find"], arguments["replace"], dry_run=True ) footer = "Dry run — no file written." else: filepath, output_path, modifier = _setup_modifier(arguments, "_relinked") result = modifier.relink_media(arguments["find"], arguments["replace"]) saved = modifier.save(output_path) footer = f"Saved to: {saved}" if not result["relinked"]: return _text_result( f"No media paths matched prefix '{arguments['find']}' " f"({result['total_assets']} assets scanned). Nothing to relink." ) lines = [ f"{'Would relink' if dry_run else 'Relinked'} " f"{result['relinked']} media reference(s) " f"across {result['total_assets']} asset(s):", "", ] missing = 0 for change in result["changes"]: mark = "✓" if change["target_exists"] else "⚠ target missing" if not change["target_exists"]: missing += 1 lines.append(f" {change['asset']}: {change['new']} [{mark}]") if missing: lines.append("") lines.append( f"⚠ {missing} new path(s) do not exist on this machine — " f"FCP will show those clips as missing until the media is present." ) lines.append("") lines.append(footer) return _text_result("\n".join(lines)) HANDLERS = { "export_edl": handle_export_edl, "export_csv": handle_export_csv, "export_resolve_xml": handle_export_resolve_xml, "export_fcp7_xml": handle_export_fcp7_xml, "relink_media": handle_relink_media, }