Skip to main content
Glama
misbahsy

Video & Audio Editing MCP Server

by misbahsy

add_basic_transitions

Apply fade-in or fade-out transitions to videos with specified duration. Input a video file, define transition type and duration, and save the output. Enhances video editing with customizable transitions.

Instructions

Adds basic fade transitions to the beginning or end of a video.

Args: video_path: Path to the input video file. output_video_path: Path to save the video with the transition. transition_type: Type of transition. Options: 'fade_in', 'fade_out'. (Note: 'crossfade_from_black' is like 'fade_in', 'crossfade_to_black' is like 'fade_out') duration_seconds: Duration of the fade effect in seconds. Returns: A status message indicating success or failure.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
duration_secondsYes
output_video_pathYes
transition_typeYes
video_pathYes

Implementation Reference

  • The handler function implementing the 'add_basic_transitions' tool. It uses FFmpeg's 'fade' filter to apply fade-in or fade-out transitions to the video, with audio stream copying or re-encoding as fallback. Includes input validation and error handling. Automatically registered via @mcp.tool() decorator, with schema derived from type hints and docstring.
    @mcp.tool()
    def add_basic_transitions(video_path: str, output_video_path: str, transition_type: str, duration_seconds: float) -> str:
        """Adds basic fade transitions to the beginning or end of a video.
    
        Args:
            video_path: Path to the input video file.
            output_video_path: Path to save the video with the transition.
            transition_type: Type of transition. Options: 'fade_in', 'fade_out'.
                             (Note: 'crossfade_from_black' is like 'fade_in', 'crossfade_to_black' is like 'fade_out')
            duration_seconds: Duration of the fade effect in seconds.
        Returns:
            A status message indicating success or failure.
        """
        if not os.path.exists(video_path):
            return f"Error: Input video file not found at {video_path}"
        if duration_seconds <= 0:
            return "Error: Transition duration must be positive."
    
        try:
            props = _get_media_properties(video_path)
            video_total_duration = props['duration']
    
            if duration_seconds > video_total_duration:
                return f"Error: Transition duration ({duration_seconds}s) cannot exceed video duration ({video_total_duration}s)."
    
            input_stream = ffmpeg.input(video_path)
            video_stream = input_stream.video
            audio_stream = input_stream.audio
            
            processed_video = None
    
            if transition_type == 'fade_in' or transition_type == 'crossfade_from_black':
                processed_video = video_stream.filter('fade', type='in', start_time=0, duration=duration_seconds)
            elif transition_type == 'fade_out' or transition_type == 'crossfade_to_black':
                fade_start_time = video_total_duration - duration_seconds
                processed_video = video_stream.filter('fade', type='out', start_time=fade_start_time, duration=duration_seconds)
            else:
                return f"Error: Unsupported transition_type '{transition_type}'. Supported: 'fade_in', 'fade_out'."
    
            # Attempt to copy audio, fallback to re-encoding if necessary
            output_streams = []
            if props['has_video']:
                output_streams.append(processed_video)
            if props['has_audio']:
                output_streams.append(audio_stream) # Audio is passed through without fade
            else: # Video only
                pass
            
            if not output_streams:
                return "Error: No suitable video or audio streams found to apply transition."
    
            try:
                ffmpeg.output(*output_streams, output_video_path, acodec='copy').run(capture_stdout=True, capture_stderr=True)
                return f"Transition '{transition_type}' applied successfully (audio copied). Output: {output_video_path}"
            except ffmpeg.Error as e_acopy:
                # Fallback: re-encode audio (or just output video if no audio originally)
                try:
                    ffmpeg.output(*output_streams, output_video_path).run(capture_stdout=True, capture_stderr=True)
                    return f"Transition '{transition_type}' applied successfully (audio re-encoded/processed). Output: {output_video_path}"
                except ffmpeg.Error as e_recode:
                    err_acopy = e_acopy.stderr.decode('utf8') if e_acopy.stderr else str(e_acopy)
                    err_recode = e_recode.stderr.decode('utf8') if e_recode.stderr else str(e_recode)
                    return f"Error applying transition. Audio copy failed: {err_acopy}. Full re-encode failed: {err_recode}."
    
        except ffmpeg.Error as e:
            error_message = e.stderr.decode('utf8') if e.stderr else str(e)
            return f"Error applying basic transition: {error_message}"
        except ValueError as e: # For _parse_time or duration checks
            return f"Error with input values: {str(e)}"
        except RuntimeError as e: # For _get_media_properties error
            return f"Runtime error during transition processing: {str(e)}"
        except Exception as e:
            return f"An unexpected error occurred in add_basic_transitions: {str(e)}"
  • Helper function called by add_basic_transitions to retrieve media properties such as duration, used to validate that transition duration does not exceed video length.
    def _get_media_properties(media_path: str) -> dict:
        """Probes media file and returns key properties."""
        try:
            probe = ffmpeg.probe(media_path)
            video_stream_info = next((s for s in probe['streams'] if s['codec_type'] == 'video'), None)
            audio_stream_info = next((s for s in probe['streams'] if s['codec_type'] == 'audio'), None)
            
            props = {
                'duration': float(probe['format'].get('duration', 0.0)),
                'has_video': video_stream_info is not None,
                'has_audio': audio_stream_info is not None,
                'width': int(video_stream_info['width']) if video_stream_info and 'width' in video_stream_info else 0,
                'height': int(video_stream_info['height']) if video_stream_info and 'height' in video_stream_info else 0,
                'avg_fps': 0, # Default, will be calculated if possible
                'sample_rate': int(audio_stream_info['sample_rate']) if audio_stream_info and 'sample_rate' in audio_stream_info else 44100,
                'channels': int(audio_stream_info['channels']) if audio_stream_info and 'channels' in audio_stream_info else 2,
                'channel_layout': audio_stream_info.get('channel_layout', 'stereo') if audio_stream_info else 'stereo'
            }
            if video_stream_info and 'avg_frame_rate' in video_stream_info and video_stream_info['avg_frame_rate'] != '0/0':
                num, den = map(int, video_stream_info['avg_frame_rate'].split('/'))
                if den > 0:
                    props['avg_fps'] = num / den
                else:
                    props['avg_fps'] = 30 # Default if denominator is 0
            else: # Fallback if avg_frame_rate is not useful
                props['avg_fps'] = 30 # A common default
    
            return props
        except ffmpeg.Error as e:
            raise RuntimeError(f"Error probing file {media_path}: {e.stderr.decode('utf8') if e.stderr else str(e)}")
        except Exception as e:
            raise RuntimeError(f"Unexpected error probing file {media_path}: {str(e)}")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/misbahsy/video-audio-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server