"""Roles — 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 server_tools._shared import ( _require_timeline, _setup_modifier, _text_result, format_duration, ) TOOLS = [ Tool( name="assign_role", description="Set the audio or video role on a clip (dialogue, music, effects, titles, etc.)", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Path to FCPXML file"}, "clip_id": {"type": "string", "description": "Clip name or ID"}, "audio_role": {"type": "string", "description": "Audio role (e.g., dialogue, music, effects)"}, "video_role": {"type": "string", "description": "Video role (e.g., video, titles)"}, "output_path": {"type": "string", "description": "Output path (default: adds _modified suffix)"} }, "required": ["filepath", "clip_id"] } ), Tool( name="filter_by_role", description="List all clips matching a specific audio or video role", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Path to FCPXML file"}, "role": {"type": "string", "description": "Role name to filter by"}, "role_type": {"type": "string", "enum": ["audio", "video", "any"], "default": "any", "description": "Which role type to search"}, }, "required": ["filepath", "role"] } ), Tool( name="export_role_stems", description="Export clip list grouped by role for audio mixing stem planning", inputSchema={ "type": "object", "properties": { "filepath": {"type": "string", "description": "Path to FCPXML file"}, }, "required": ["filepath"] } ), ] async def handle_assign_role(arguments: dict) -> Sequence[TextContent]: filepath, output_path, modifier = _setup_modifier(arguments) modifier.assign_role( clip_id=arguments["clip_id"], audio_role=arguments.get("audio_role"), video_role=arguments.get("video_role"), ) modifier.save(output_path) roles_set = [] if arguments.get("audio_role"): roles_set.append(f"audioRole={arguments['audio_role']}") if arguments.get("video_role"): roles_set.append(f"videoRole={arguments['video_role']}") return _text_result(( f"Set {', '.join(roles_set)} on '{arguments['clip_id']}'\n\n" f"Saved to: `{output_path}`" )) async def handle_filter_by_role(arguments: dict) -> Sequence[TextContent]: project, tl = _require_timeline(arguments["filepath"]) role = arguments["role"].lower() role_type = arguments.get("role_type", "any") matches = [] for clip in tl.clips: if role_type in ("audio", "any") and clip.audio_role.lower() == role: matches.append((clip.name, "audio", clip.audio_role, format_duration(clip.duration_seconds))) if role_type in ("video", "any") and clip.video_role.lower() == role: matches.append((clip.name, "video", clip.video_role, format_duration(clip.duration_seconds))) if not matches: return _text_result(f"No clips found with role '{role}'.") result = f"# Clips with role '{role}'\n\n" result += "| Clip | Type | Role | Duration |\n|------|------|------|----------|\n" for name, rtype, rval, dur in matches: result += f"| {name} | {rtype} | {rval} | {dur} |\n" return _text_result(result) async def handle_export_role_stems(arguments: dict) -> Sequence[TextContent]: project, tl = _require_timeline(arguments["filepath"]) stems: dict[str, list] = {} for clip in tl.clips: role = clip.audio_role or "unassigned" stems.setdefault(role, []).append(clip) for cc in tl.connected_clips: role = cc.role or "unassigned" stems.setdefault(role, []).append(cc) result = f"# Audio Stem Plan for {tl.name}\n\n" for role, clips in sorted(stems.items()): total_dur = sum(c.duration_seconds for c in clips) result += f"## {role.title()} ({len(clips)} clips, {format_duration(total_dur)})\n\n" for c in clips: result += f"- {c.name} ({format_duration(c.duration_seconds)})\n" result += "\n" return _text_result(result) HANDLERS = { "assign_role": handle_assign_role, "filter_by_role": handle_filter_by_role, "export_role_stems": handle_export_role_stems, }