# Directory Structure
```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│ ├── index.ts
│ ├── tools
│ │ ├── definitions.ts
│ │ └── handlers.ts
│ └── utils
│ ├── ffmpeg.ts
│ └── file.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | *.mp3
6 | *.jpg
7 | *.png
8 | *.mp4
9 | *.avi
10 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/src/utils/file.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { existsSync } from "fs";
2 | import { dirname } from "path";
3 | import { mkdir } from "fs/promises";
4 |
5 | /**
6 | * Helper function to ensure a directory exists
7 | */
8 | export async function ensureDirectoryExists(filePath: string): Promise<void> {
9 | const dir = dirname(filePath);
10 | try {
11 | await mkdir(dir, { recursive: true });
12 | } catch (error) {
13 | // Directory already exists or cannot be created
14 | if ((error as any).code !== 'EEXIST') {
15 | throw error;
16 | }
17 | }
18 | }
19 |
20 | /**
21 | * Helper function to validate file path
22 | */
23 | export function validatePath(path: string, isInput: boolean = false): string {
24 | if (!path) {
25 | throw new Error("File path is required");
26 | }
27 |
28 | if (isInput && !existsSync(path)) {
29 | throw new Error(`Input file does not exist: ${path}`);
30 | }
31 |
32 | return path;
33 | }
34 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@sworddut/mcp-ffmpeg-helper",
3 | "version": "0.1.0",
4 | "description": "A Model Context Protocol (MCP) helper for FFmpeg video processing operations",
5 | "type": "module",
6 | "bin": {
7 | "mcp-ffmpeg-helper": "./build/index.js"
8 | },
9 | "files": [
10 | "build"
11 | ],
12 | "scripts": {
13 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
14 | "prepare": "npm run build",
15 | "watch": "tsc --watch",
16 | "inspector": "npx @modelcontextprotocol/inspector build/index.js"
17 | },
18 | "dependencies": {
19 | "@modelcontextprotocol/sdk": "0.6.0",
20 | "ffmpeg": "^0.0.4"
21 | },
22 | "devDependencies": {
23 | "@types/node": "^20.11.24",
24 | "typescript": "^5.3.3"
25 | },
26 | "keywords": [
27 | "ffmpeg",
28 | "video",
29 | "mcp",
30 | "model-context-protocol",
31 | "video-processing",
32 | "watermark",
33 | "trim",
34 | "convert"
35 | ],
36 | "author": "Your Name",
37 | "license": "MIT",
38 | "repository": {
39 | "type": "git",
40 | "url": "https://github.com/yourusername/mcp-ffmpeg-helper.git"
41 | },
42 | "bugs": {
43 | "url": "https://github.com/yourusername/mcp-ffmpeg-helper/issues"
44 | },
45 | "homepage": "https://github.com/yourusername/mcp-ffmpeg-helper#readme",
46 | "engines": {
47 | "node": ">=14.0.0"
48 | }
49 | }
50 |
```
--------------------------------------------------------------------------------
/src/utils/ffmpeg.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { exec } from "child_process";
2 | import { promisify } from "util";
3 | import { validatePath } from "./file.js";
4 |
5 | const execPromise = promisify(exec);
6 |
7 | /**
8 | * Helper function to run FFmpeg commands with better error handling
9 | */
10 | export async function runFFmpegCommand(command: string): Promise<string> {
11 | try {
12 | console.log(`Running FFmpeg command: ffmpeg ${command}`);
13 | const { stdout, stderr } = await execPromise(`ffmpeg ${command}`);
14 | return stdout || stderr;
15 | } catch (error: any) {
16 | console.error("FFmpeg error:", error.message);
17 | if (error.stderr) {
18 | return error.stderr;
19 | }
20 | throw new Error(`FFmpeg error: ${error.message}`);
21 | }
22 | }
23 |
24 | /**
25 | * Helper function to get information about a video file
26 | */
27 | export async function getVideoInfo(filePath: string): Promise<string> {
28 | try {
29 | validatePath(filePath, true);
30 | console.log(`Getting video info for: ${filePath}`);
31 | const { stdout, stderr } = await execPromise(`ffprobe -v error -show_format -show_streams -print_format json "${filePath}"`);
32 | return stdout || stderr;
33 | } catch (error: any) {
34 | console.error("FFprobe error:", error.message);
35 | if (error.stderr) {
36 | return error.stderr;
37 | }
38 | throw new Error(`FFprobe error: ${error.message}`);
39 | }
40 | }
41 |
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * FFmpeg Helper MCP Server
5 | * A simple MCP server that provides FFmpeg functionality through tools.
6 | * It allows video operations like:
7 | * - Getting video information
8 | * - Converting video formats
9 | * - Extracting audio from video
10 | * - Creating video from image sequences
11 | * - Trimming videos
12 | * - Adding watermarks
13 | * - Applying filters
14 | */
15 |
16 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
17 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
18 | import {
19 | CallToolRequestSchema,
20 | ListToolsRequestSchema,
21 | } from "@modelcontextprotocol/sdk/types.js";
22 |
23 | // Import our modularized code
24 | import { toolDefinitions } from "./tools/definitions.js";
25 | import { handleToolCall } from "./tools/handlers.js";
26 |
27 | /**
28 | * Create an MCP server with capabilities for tools to interact with FFmpeg
29 | */
30 | const server = new Server(
31 | {
32 | name: "mcp-ffmpeg-helper",
33 | version: "0.2.0",
34 | },
35 | {
36 | capabilities: {
37 | tools: {},
38 | },
39 | }
40 | );
41 |
42 | /**
43 | * Handler that lists available tools.
44 | * Exposes FFmpeg-related tools for video operations.
45 | */
46 | server.setRequestHandler(ListToolsRequestSchema, async () => {
47 | return {
48 | tools: toolDefinitions
49 | };
50 | });
51 |
52 | /**
53 | * Handler for FFmpeg tools.
54 | * Implements various video operations using FFmpeg.
55 | */
56 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
57 | try {
58 | return await handleToolCall(request.params.name, request.params.arguments);
59 | } catch (error: any) {
60 | console.error("Tool execution error:", error.message);
61 | return {
62 | content: [{
63 | type: "text",
64 | text: `Error: ${error.message}`
65 | }]
66 | };
67 | }
68 | });
69 |
70 | /**
71 | * Start the server using stdio transport.
72 | * This allows the server to communicate via standard input/output streams.
73 | */
74 | async function main() {
75 | console.log("Starting MCP FFmpeg Helper server...");
76 | const transport = new StdioServerTransport();
77 | await server.connect(transport);
78 | console.log("MCP FFmpeg Helper server connected and ready");
79 | }
80 |
81 | main().catch((error) => {
82 | console.error("Server error:", error);
83 | process.exit(1);
84 | });
85 |
```
--------------------------------------------------------------------------------
/src/tools/definitions.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tool definitions for FFmpeg operations
3 | * Defines the available tools and their input schemas
4 | */
5 | export const toolDefinitions = [
6 | {
7 | name: "get_video_info",
8 | description: "Get detailed information about a video file",
9 | inputSchema: {
10 | type: "object",
11 | properties: {
12 | filePath: {
13 | type: "string",
14 | description: "Path to the video file"
15 | }
16 | },
17 | required: ["filePath"]
18 | }
19 | },
20 | {
21 | name: "convert_video",
22 | description: "Convert a video file to a different format",
23 | inputSchema: {
24 | type: "object",
25 | properties: {
26 | inputPath: {
27 | type: "string",
28 | description: "Path to the input video file"
29 | },
30 | outputPath: {
31 | type: "string",
32 | description: "Path for the output video file"
33 | },
34 | options: {
35 | type: "string",
36 | description: "Additional FFmpeg options (optional)"
37 | }
38 | },
39 | required: ["inputPath", "outputPath"]
40 | }
41 | },
42 | {
43 | name: "extract_audio",
44 | description: "Extract audio from a video file",
45 | inputSchema: {
46 | type: "object",
47 | properties: {
48 | inputPath: {
49 | type: "string",
50 | description: "Path to the input video file"
51 | },
52 | outputPath: {
53 | type: "string",
54 | description: "Path for the output audio file"
55 | },
56 | format: {
57 | type: "string",
58 | description: "Audio format (mp3, aac, etc.)"
59 | }
60 | },
61 | required: ["inputPath", "outputPath", "format"]
62 | }
63 | },
64 | {
65 | name: "create_video_from_images",
66 | description: "Create a video from a sequence of images",
67 | inputSchema: {
68 | type: "object",
69 | properties: {
70 | inputPattern: {
71 | type: "string",
72 | description: "Pattern for input images (e.g., 'img%03d.jpg' or 'folder/*.png')"
73 | },
74 | outputPath: {
75 | type: "string",
76 | description: "Path for the output video file"
77 | },
78 | framerate: {
79 | type: "number",
80 | description: "Frames per second (default: 25)"
81 | },
82 | codec: {
83 | type: "string",
84 | description: "Video codec to use (default: libx264)"
85 | },
86 | pixelFormat: {
87 | type: "string",
88 | description: "Pixel format (default: yuv420p)"
89 | },
90 | extraOptions: {
91 | type: "string",
92 | description: "Additional FFmpeg options"
93 | }
94 | },
95 | required: ["inputPattern", "outputPath"]
96 | }
97 | },
98 | {
99 | name: "trim_video",
100 | description: "Trim a video to a specific duration",
101 | inputSchema: {
102 | type: "object",
103 | properties: {
104 | inputPath: {
105 | type: "string",
106 | description: "Path to the input video file"
107 | },
108 | outputPath: {
109 | type: "string",
110 | description: "Path for the output video file"
111 | },
112 | startTime: {
113 | type: "string",
114 | description: "Start time (format: HH:MM:SS.mmm or seconds)"
115 | },
116 | duration: {
117 | type: "string",
118 | description: "Duration (format: HH:MM:SS.mmm or seconds)"
119 | },
120 | endTime: {
121 | type: "string",
122 | description: "End time (format: HH:MM:SS.mmm or seconds)"
123 | }
124 | },
125 | required: ["inputPath", "outputPath"]
126 | }
127 | },
128 | {
129 | name: "add_watermark",
130 | description: "Add a watermark to a video",
131 | inputSchema: {
132 | type: "object",
133 | properties: {
134 | inputPath: {
135 | type: "string",
136 | description: "Path to the input video file"
137 | },
138 | watermarkPath: {
139 | type: "string",
140 | description: "Path to the watermark image"
141 | },
142 | outputPath: {
143 | type: "string",
144 | description: "Path for the output video file"
145 | },
146 | position: {
147 | type: "string",
148 | description: "Position of watermark (topleft, topright, bottomleft, bottomright, center)"
149 | },
150 | opacity: {
151 | type: "number",
152 | description: "Opacity of watermark (0.0-1.0)"
153 | }
154 | },
155 | required: ["inputPath", "watermarkPath", "outputPath"]
156 | }
157 | },
158 | {
159 | name: "trim_audio",
160 | description: "Trim an audio file to a specific duration",
161 | inputSchema: {
162 | type: "object",
163 | properties: {
164 | inputPath: {
165 | type: "string",
166 | description: "Path to the input audio file"
167 | },
168 | outputPath: {
169 | type: "string",
170 | description: "Path for the output audio file"
171 | },
172 | startTime: {
173 | type: "string",
174 | description: "Start time (format: HH:MM:SS.mmm or seconds)"
175 | },
176 | duration: {
177 | type: "string",
178 | description: "Duration (format: HH:MM:SS.mmm or seconds)"
179 | },
180 | endTime: {
181 | type: "string",
182 | description: "End time (format: HH:MM:SS.mmm or seconds)"
183 | },
184 | format: {
185 | type: "string",
186 | description: "Audio format for output (mp3, aac, etc.)"
187 | }
188 | },
189 | required: ["inputPath", "outputPath"]
190 | }
191 | },
192 | {
193 | name: "extract_frames",
194 | description: "Extract frames from a video as sequential image files",
195 | inputSchema: {
196 | type: "object",
197 | properties: {
198 | inputPath: {
199 | type: "string",
200 | description: "Path to the input video file"
201 | },
202 | outputDir: {
203 | type: "string",
204 | description: "Directory to save the extracted frames (default: 'output')"
205 | },
206 | frameRate: {
207 | type: "string",
208 | description: "Frame extraction rate (e.g., '1' for every frame, '0.5' for every 2nd frame, '1/30' for 1 frame per 30 seconds)"
209 | },
210 | format: {
211 | type: "string",
212 | description: "Output image format (jpg, png, etc., default: jpg)"
213 | },
214 | quality: {
215 | type: "number",
216 | description: "Image quality for jpg format (1-100, default: 95)"
217 | },
218 | startTime: {
219 | type: "string",
220 | description: "Start time to begin extraction (format: HH:MM:SS.mmm or seconds)"
221 | },
222 | duration: {
223 | type: "string",
224 | description: "Duration to extract frames (format: HH:MM:SS.mmm or seconds)"
225 | }
226 | },
227 | required: ["inputPath"]
228 | }
229 | }
230 | ];
231 |
```
--------------------------------------------------------------------------------
/src/tools/handlers.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { validatePath } from "../utils/file.js";
2 | import { getVideoInfo, runFFmpegCommand } from "../utils/ffmpeg.js";
3 | import { ensureDirectoryExists } from "../utils/file.js";
4 | import { join } from "path";
5 |
6 | /**
7 | * Handles all FFmpeg tool requests
8 | */
9 | export async function handleToolCall(toolName: string, args: any) {
10 | switch (toolName) {
11 | case "get_video_info": {
12 | const filePath = validatePath(String(args?.filePath), true);
13 | const info = await getVideoInfo(filePath);
14 | return {
15 | content: [{
16 | type: "text",
17 | text: info
18 | }]
19 | };
20 | }
21 |
22 | case "convert_video": {
23 | const inputPath = validatePath(String(args?.inputPath), true);
24 | const outputPath = validatePath(String(args?.outputPath));
25 | const options = String(args?.options || "");
26 |
27 | await ensureDirectoryExists(outputPath);
28 | const command = `-i "${inputPath}" ${options} "${outputPath}" -y`;
29 | const result = await runFFmpegCommand(command);
30 |
31 | return {
32 | content: [{
33 | type: "text",
34 | text: `Video conversion completed: ${inputPath} → ${outputPath}\n\n${result}`
35 | }]
36 | };
37 | }
38 |
39 | case "extract_audio": {
40 | const inputPath = validatePath(String(args?.inputPath), true);
41 | const outputPath = validatePath(String(args?.outputPath));
42 | const format = String(args?.format || "mp3");
43 |
44 | await ensureDirectoryExists(outputPath);
45 | const command = `-i "${inputPath}" -vn -acodec ${format} "${outputPath}" -y`;
46 | const result = await runFFmpegCommand(command);
47 |
48 | return {
49 | content: [{
50 | type: "text",
51 | text: `Audio extraction completed: ${inputPath} → ${outputPath}\n\n${result}`
52 | }]
53 | };
54 | }
55 |
56 | case "create_video_from_images": {
57 | const inputPattern = String(args?.inputPattern);
58 | const outputPath = validatePath(String(args?.outputPath));
59 | const framerate = Number(args?.framerate || 25);
60 | const codec = String(args?.codec || "libx264");
61 | const pixelFormat = String(args?.pixelFormat || "yuv420p");
62 | const extraOptions = String(args?.extraOptions || "");
63 |
64 | if (!inputPattern) {
65 | throw new Error("Input pattern is required");
66 | }
67 |
68 | await ensureDirectoryExists(outputPath);
69 | const command = `-framerate ${framerate} -i "${inputPattern}" -c:v ${codec} -pix_fmt ${pixelFormat} ${extraOptions} "${outputPath}" -y`;
70 | const result = await runFFmpegCommand(command);
71 |
72 | return {
73 | content: [{
74 | type: "text",
75 | text: `Video creation completed: ${inputPattern} → ${outputPath}\n\n${result}`
76 | }]
77 | };
78 | }
79 |
80 | case "trim_video": {
81 | const inputPath = validatePath(String(args?.inputPath), true);
82 | const outputPath = validatePath(String(args?.outputPath));
83 | const startTime = String(args?.startTime || "0");
84 | const duration = String(args?.duration || "");
85 | const endTime = String(args?.endTime || "");
86 |
87 | await ensureDirectoryExists(outputPath);
88 |
89 | let command = `-i "${inputPath}" -ss ${startTime}`;
90 | if (duration) {
91 | command += ` -t ${duration}`;
92 | } else if (endTime) {
93 | command += ` -to ${endTime}`;
94 | }
95 | command += ` -c copy "${outputPath}" -y`;
96 |
97 | const result = await runFFmpegCommand(command);
98 |
99 | return {
100 | content: [{
101 | type: "text",
102 | text: `Video trimming completed: ${inputPath} → ${outputPath}\n\n${result}`
103 | }]
104 | };
105 | }
106 |
107 | case "add_watermark": {
108 | const inputPath = validatePath(String(args?.inputPath), true);
109 | const watermarkPath = validatePath(String(args?.watermarkPath), true);
110 | const outputPath = validatePath(String(args?.outputPath));
111 | const position = String(args?.position || "bottomright");
112 | const opacity = Number(args?.opacity || 0.5);
113 |
114 | await ensureDirectoryExists(outputPath);
115 |
116 | // Determine overlay position
117 | let overlayPosition = "";
118 | switch (position.toLowerCase()) {
119 | case "topleft":
120 | overlayPosition = "10:10";
121 | break;
122 | case "topright":
123 | overlayPosition = "W-w-10:10";
124 | break;
125 | case "bottomleft":
126 | overlayPosition = "10:H-h-10";
127 | break;
128 | case "center":
129 | overlayPosition = "(W-w)/2:(H-h)/2";
130 | break;
131 | case "bottomright":
132 | default:
133 | overlayPosition = "W-w-10:H-h-10";
134 | break;
135 | }
136 |
137 | // Improved command with better handling of watermark opacity and format
138 | const command = `-i "${inputPath}" -i "${watermarkPath}" -filter_complex "[1:v]format=rgba,colorchannelmixer=aa=${opacity}[watermark];[0:v][watermark]overlay=${overlayPosition}:format=auto,format=yuv420p" -codec:a copy "${outputPath}" -y`;
139 | const result = await runFFmpegCommand(command);
140 |
141 | return {
142 | content: [{
143 | type: "text",
144 | text: `Watermark added: ${inputPath} → ${outputPath}\n\n${result}`
145 | }]
146 | };
147 | }
148 |
149 | case "trim_audio": {
150 | const inputPath = validatePath(String(args?.inputPath), true);
151 | const outputPath = validatePath(String(args?.outputPath));
152 | const startTime = String(args?.startTime || "0");
153 | const duration = String(args?.duration || "");
154 | const endTime = String(args?.endTime || "");
155 | const format = String(args?.format || "");
156 |
157 | await ensureDirectoryExists(outputPath);
158 |
159 | // Build the FFmpeg command
160 | let command = `-i "${inputPath}" -ss ${startTime}`;
161 |
162 | // Add duration or end time if provided
163 | if (duration) {
164 | command += ` -t ${duration}`;
165 | } else if (endTime) {
166 | command += ` -to ${endTime}`;
167 | }
168 |
169 | // Add format if specified, otherwise use copy codec
170 | if (format) {
171 | command += ` -acodec ${format}`;
172 | } else {
173 | command += ` -acodec copy`;
174 | }
175 |
176 | command += ` "${outputPath}" -y`;
177 |
178 | const result = await runFFmpegCommand(command);
179 |
180 | return {
181 | content: [{
182 | type: "text",
183 | text: `Audio trimming completed: ${inputPath} → ${outputPath}\n\n${result}`
184 | }]
185 | };
186 | }
187 |
188 | case "extract_frames": {
189 | const inputPath = validatePath(String(args?.inputPath), true);
190 | const outputDir = String(args?.outputDir || "output");
191 | const frameRate = String(args?.frameRate || "1");
192 | const format = String(args?.format || "jpg");
193 | const quality = Number(args?.quality || 95);
194 | const startTime = args?.startTime ? String(args?.startTime) : "";
195 | const duration = args?.duration ? String(args?.duration) : "";
196 |
197 | // Create output directory if it doesn't exist
198 | await ensureDirectoryExists(join(outputDir, "dummy.txt"));
199 |
200 | // Build the FFmpeg command
201 | let command = `-i "${inputPath}"`;
202 |
203 | // Add start time if provided
204 | if (startTime) {
205 | command += ` -ss ${startTime}`;
206 | }
207 |
208 | // Add duration if provided
209 | if (duration) {
210 | command += ` -t ${duration}`;
211 | }
212 |
213 | // Set frame extraction rate
214 | command += ` -vf "fps=${frameRate}"`;
215 |
216 | // Set quality based on format
217 | if (format.toLowerCase() === "jpg" || format.toLowerCase() === "jpeg") {
218 | // For JPEG, use a better quality setting (lower values = higher quality in FFmpeg's scale)
219 | // Convert 1-100 scale to FFmpeg's 1-31 scale (inverted, where 1 is best quality)
220 | const ffmpegQuality = Math.max(1, Math.min(31, Math.round(31 - ((quality / 100) * 30))));
221 | command += ` -q:v ${ffmpegQuality}`;
222 | } else if (format.toLowerCase() === "png") {
223 | // For PNG, use compression level (0-9, where 0 is no compression)
224 | const compressionLevel = Math.min(9, Math.max(0, Math.round(9 - ((quality / 100) * 9))));
225 | command += ` -compression_level ${compressionLevel}`;
226 | }
227 |
228 | // Set output pattern with 5-digit numbering
229 | const outputPattern = join(outputDir, `%05d.${format}`);
230 | command += ` "${outputPattern}" -y`;
231 |
232 | const result = await runFFmpegCommand(command);
233 |
234 | return {
235 | content: [{
236 | type: "text",
237 | text: `Frames extracted from video: ${inputPath} → ${outputDir}/*.${format}\n\n${result}`
238 | }]
239 | };
240 | }
241 |
242 | default:
243 | throw new Error(`Unknown tool: ${toolName}`);
244 | }
245 | }
246 |
```