# Directory Structure
```
├── .gitignore
├── dist
│ └── mcp-ffmpeg.js
├── Dockerfile
├── eslint.config.js
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│ └── mcp-ffmpeg.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 |
93 | # Gatsby files
94 | .cache/
95 | # Comment in the public line in if your project uses Gatsby and not Next.js
96 | # https://nextjs.org/blog/next-9-1#public-directory-support
97 | # public
98 |
99 | # vuepress build output
100 | .vuepress/dist
101 |
102 | # vuepress v2.x temp and cache directory
103 | .temp
104 | .cache
105 |
106 | # Docusaurus cache and generated files
107 | .docusaurus
108 |
109 | # Serverless directories
110 | .serverless/
111 |
112 | # FuseBox cache
113 | .fusebox/
114 |
115 | # DynamoDB Local files
116 | .dynamodb/
117 |
118 | # TernJS port file
119 | .tern-port
120 |
121 | # Stores VSCode versions used for testing VSCode extensions
122 | .vscode-test
123 |
124 | # yarn v2
125 | .yarn/cache
126 | .yarn/unplugged
127 | .yarn/build-state.yml
128 | .yarn/install-state.gz
129 | .pnp.*
130 |
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP FFmpeg Video Processor
2 | [](https://smithery.ai/server/@bitscorp-mcp/mcp-ffmpeg)
3 |
4 | A Node.js server that uses FFmpeg to manipulate video files. This server provides APIs to:
5 |
6 | - Resize videos to different resolutions (360p, 480p, 720p, 1080p)
7 | - Extract audio from videos in various formats (MP3, AAC, WAV, OGG)
8 |
9 | ## Prerequisites
10 |
11 | Before running this application, you need to have the following installed:
12 |
13 | 1. **Node.js** (v14 or higher)
14 | 2. **FFmpeg** - This is required for video processing
15 |
16 | ### Installing FFmpeg
17 |
18 | #### On macOS:
19 | ```bash
20 | brew install ffmpeg
21 | ```
22 |
23 | #### On Ubuntu/Debian:
24 | ```bash
25 | sudo apt update
26 | sudo apt install ffmpeg
27 | ```
28 |
29 | #### On Windows:
30 | 1. Download FFmpeg from the [official website](https://ffmpeg.org/download.html)
31 | 2. Extract the files to a folder (e.g., `C:\ffmpeg`)
32 | 3. Add the `bin` folder to your PATH environment variable
33 |
34 | ## Installation
35 |
36 | 1. Clone this repository:
37 | ```bash
38 | git clone https://github.com/bitscorp-mcp/mcp-ffmpeg.git
39 | cd mcp-ffmpeg
40 | ```
41 |
42 | 2. Install dependencies:
43 | ```bash
44 | npm install
45 | ```
46 |
47 | ### Installing via Smithery
48 |
49 | To install mcp-ffmpeg for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@bitscorp-mcp/mcp-ffmpeg):
50 |
51 | ```bash
52 | npx -y @smithery/cli install @bitscorp-mcp/mcp-ffmpeg --client claude
53 | ```
54 |
55 | ## Running the Server
56 |
57 | Start the server with:
58 |
59 | ```bash
60 | npm start
61 | ```
62 |
63 | For development with auto-restart on file changes:
64 |
65 | ```bash
66 | npm run dev
67 | ```
68 |
69 | ### Installing via Smithery
70 |
71 | To install mcp-ffmpeg for Claude Desktop automatically via [Smithery](https://smithery.ai/server/bitscorp-mcp/mcp-ffmpeg):
72 |
73 | ```bash
74 | npx -y @smithery/cli install @bitscorp-mcp/mcp-ffmpeg --client claude
75 | ```
76 |
77 | To install mcp-ffmpeg for Cursor, go to Settings -> Cursor Settings -> Features -> MCP Servers -> + Add
78 |
79 | Select Type: command and paste the below, using your API key from Adjust
80 | ```
81 | npx -y @smithery/cli@latest run @bitscorp/mcp-ffmpeg
82 | ```
83 |
84 | ## Using with Claude Desktop
85 |
86 | This MCP FFmpeg server can be integrated with Claude Desktop to process videos through natural language requests.
87 |
88 | ### Running with npx
89 |
90 | You can run the server directly with npx:
91 |
92 | ```bash
93 | npx /path/to/mcp-ffmpeg
94 | ```
95 |
96 | Or if you've published the package to npm:
97 |
98 | ```bash
99 | npx mcp-ffmpeg
100 | ```
101 |
102 | ### Configuring Claude Desktop
103 |
104 | To add this server to Claude Desktop, update your Claude Desktop configuration file:
105 |
106 | 1. Locate your Claude Desktop config file:
107 | - macOS: `~/.config/claude-desktop/config.json` or `~/Library/Application Support/Claude Desktop/config.json`
108 | - Windows: `%APPDATA%\Claude Desktop\config.json`
109 | - Linux: `~/.config/claude-desktop/config.json`
110 |
111 | 2. Add the FFmpeg MCP server to the `mcpServers` section:
112 |
113 | ```json
114 | {
115 | "mcpServers": {
116 | "ffmpeg": {
117 | "command": "npx",
118 | "args": [
119 | "--yes",
120 | "/absolute/path/to/mcp-ffmpeg"
121 | ]
122 | }
123 | }
124 | }
125 | ```
126 |
127 | If you've published the package to npm:
128 |
129 | ```json
130 | {
131 | "mcpServers": {
132 | "ffmpeg": {
133 | "command": "npx",
134 | "args": [
135 | "--yes",
136 | "mcp-ffmpeg"
137 | ]
138 | }
139 | }
140 | }
141 | ```
142 |
143 | 3. Restart Claude Desktop for the changes to take effect.
144 |
145 | ### Example Prompts for Claude
146 |
147 | Once configured, you can use prompts like:
148 |
149 | ```
150 | Using the ffmpeg MCP server, please resize the video at /path/to/video.mp4 to 720p resolution.
151 | ```
152 |
153 | ## Notes
154 |
155 | - Uploaded videos are stored temporarily in the `uploads` directory
156 | - Processed videos and audio files are stored in the `output` directory
157 | - The server has a file size limit of 500MB for uploads
158 |
159 | ## License
160 |
161 | MIT
162 |
```
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
```javascript
1 | import neostandard from 'neostandard'
2 |
3 | export default neostandard({
4 | ignores: ['node_modules', 'dist']
5 | })
6 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "esModuleInterop": true,
7 | "outDir": "./dist",
8 | "rootDir": "./src",
9 | "strict": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": ["src/**/*"],
14 | "exclude": ["node_modules"]
15 | }
16 |
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2 |
3 | startCommand:
4 | type: stdio
5 | configSchema:
6 | # JSON Schema defining the configuration options for the MCP.
7 | type: object
8 | properties: {}
9 | commandFunction:
10 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
11 | |-
12 | (config) => ({ command: 'node', args: ['dist/mcp-ffmpeg.js'] })
13 | exampleConfig: {}
14 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 |
2 | ### Multi-stage Dockerfile for MCP FFmpeg Server
3 |
4 | # Stage 1: Builder
5 | FROM node:lts-alpine AS builder
6 |
7 | # Install ffmpeg and any build dependencies
8 | RUN apk add --no-cache ffmpeg
9 |
10 | # Set working directory
11 | WORKDIR /app
12 |
13 | # Copy package files
14 | COPY package*.json ./
15 |
16 | # Install all dependencies including dev dependencies (needed for building)
17 | RUN npm install
18 |
19 | # Copy the rest of the application source code
20 | COPY . .
21 |
22 | # Build the TypeScript code
23 | RUN npm run build
24 |
25 | # Stage 2: Production
26 | FROM node:lts-alpine
27 |
28 | # Install ffmpeg in production image
29 | RUN apk add --no-cache ffmpeg
30 |
31 | # Set working directory
32 | WORKDIR /app
33 |
34 | # Copy only the necessary files from builder
35 | COPY package*.json ./
36 |
37 | # Install production dependencies only
38 | RUN npm install --production --ignore-scripts
39 |
40 | # Copy built files from the builder stage
41 | COPY --from=builder /app/dist ./dist
42 |
43 | # Expose port if needed (not needed for stdio-based MCP servers)
44 |
45 | # Start the MCP server
46 | CMD [ "node", "dist/mcp-ffmpeg.js" ]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "mcp-ffmpeg",
3 | "version": "1.0.5",
4 | "description": "FFmpeg MCP server",
5 | "main": "dist/mcp-ffmpeg.js",
6 | "type": "module",
7 | "bin": {
8 | "mcp-ffmpeg": "./dist/mcp-ffmpeg.js"
9 | },
10 | "scripts": {
11 | "build": "tsc",
12 | "lint": "eslint .",
13 | "lint:fix": "eslint --fix .",
14 | "dev": "npm run build && npx @modelcontextprotocol/inspector node dist/mcp-ffmpeg.js"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/platformatic/mcp-node.git"
19 | },
20 | "license": "MIT",
21 | "author": "Platformatic Inc. <[email protected]> (https://platformatic.dev)",
22 | "contributors": [
23 | {
24 | "name": "Alexandr Korsak",
25 | "email": "[email protected]"
26 | }
27 | ],
28 | "bugs": {
29 | "url": "https://github.com/bitscorp-mcp/mcp-ffmpeg/issues"
30 | },
31 | "homepage": "https://github.com/bitscorp-mcp/mcp-ffmpeg#readme",
32 | "dependencies": {
33 | "@modelcontextprotocol/sdk": "^1.6.0",
34 | "node-notifier": "^10.0.1",
35 | "zod": "^3.24.2"
36 | },
37 | "devDependencies": {
38 | "@types/node": "^22.13.5",
39 | "@types/node-notifier": "^8.0.5",
40 | "eslint": "^9.21.0",
41 | "neostandard": "^0.12.1",
42 | "typescript": "^5.7.3"
43 | },
44 | "keywords": [
45 | "mcp",
46 | "model-context-protocol",
47 | "ai",
48 | "nodejs",
49 | "javascript-runtime"
50 | ],
51 | "engines": {
52 | "node": ">=22.0.0"
53 | }
54 | }
55 |
```
--------------------------------------------------------------------------------
/src/mcp-ffmpeg.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import { z } from "zod";
4 | import { exec, ExecOptions as ChildProcessExecOptions } from "child_process";
5 | import { promisify } from "util";
6 | import * as path from "path";
7 | import * as fs from "fs/promises";
8 | import * as os from "node:os";
9 | import notifier from "node-notifier";
10 |
11 | const execAsync = promisify(exec);
12 |
13 | // Create an MCP server
14 | const server = new McpServer({
15 | name: "FFmpegProcessor",
16 | version: "1.0.0"
17 | });
18 |
19 | // Define available resolutions
20 | const RESOLUTIONS = {
21 | "360p": { width: 640, height: 360 },
22 | "480p": { width: 854, height: 480 },
23 | "720p": { width: 1280, height: 720 },
24 | "1080p": { width: 1920, height: 1080 }
25 | };
26 |
27 | /**
28 | * Helper function to ask for permission using node-notifier
29 | */
30 | async function askPermission(action: string): Promise<boolean> {
31 | // Skip notification if DISABLE_NOTIFICATIONS is set
32 | if (process.env.DISABLE_NOTIFICATIONS === 'true') {
33 | console.log(`Auto-allowing action (notifications disabled): ${action}`);
34 | return true;
35 | }
36 |
37 | return new Promise((resolve) => {
38 | notifier.notify({
39 | title: 'FFmpeg Processor Permission Request',
40 | message: `${action}`,
41 | wait: true,
42 | timeout: 60,
43 | actions: 'Allow',
44 | closeLabel: 'Deny'
45 | }, (err, response, metadata) => {
46 | if (err) {
47 | console.error('Error showing notification:', err);
48 | resolve(false);
49 | return;
50 | }
51 |
52 | const buttonPressed = metadata?.activationValue || response;
53 | resolve(buttonPressed !== 'Deny');
54 | });
55 | });
56 | }
57 |
58 | /**
59 | * Helper function to ensure output directories exist
60 | */
61 | async function ensureDirectoriesExist() {
62 | const outputDir = path.join(os.tmpdir(), 'ffmpeg-output');
63 | try {
64 | await fs.mkdir(outputDir, { recursive: true });
65 | return outputDir;
66 | } catch (error) {
67 | console.error('Error creating output directory:', error);
68 | return os.tmpdir();
69 | }
70 | }
71 |
72 | // Tool to check FFmpeg version
73 | server.tool(
74 | "get-ffmpeg-version",
75 | "Get the version of FFmpeg installed on the system",
76 | {},
77 | async () => {
78 | try {
79 | const { stdout, stderr } = await execAsync('ffmpeg -version');
80 |
81 | // Extract the version from the output
82 | const versionMatch = stdout.match(/ffmpeg version (\S+)/);
83 | const version = versionMatch ? versionMatch[1] : 'Unknown';
84 |
85 | return {
86 | content: [{
87 | type: "text" as const,
88 | text: `FFmpeg Version: ${version}\n\nFull version info:\n${stdout}`
89 | }]
90 | };
91 | } catch (error) {
92 | const errorMessage = error instanceof Error ? error.message : String(error);
93 | return {
94 | isError: true,
95 | content: [{
96 | type: "text" as const,
97 | text: `Error getting FFmpeg version: ${errorMessage}\n\nMake sure FFmpeg is installed and in your PATH.`
98 | }]
99 | };
100 | }
101 | }
102 | );
103 |
104 | // Tool to resize video
105 | server.tool(
106 | "resize-video",
107 | "Resize a video to one or more standard resolutions",
108 | {
109 | videoPath: z.string().describe("Path to the video file to resize"),
110 | resolutions: z.array(z.enum(["360p", "480p", "720p", "1080p"])).describe("Resolutions to convert the video to"),
111 | outputDir: z.string().optional().describe("Optional directory to save the output files (defaults to a temporary directory)")
112 | },
113 | async ({ videoPath, resolutions, outputDir }) => {
114 | try {
115 | // Resolve the absolute path
116 | const absVideoPath = path.resolve(videoPath);
117 |
118 | // Check if file exists
119 | try {
120 | await fs.access(absVideoPath);
121 | } catch (error) {
122 | return {
123 | isError: true,
124 | content: [{
125 | type: "text" as const,
126 | text: `Error: Video file not found at ${absVideoPath}`
127 | }]
128 | };
129 | }
130 |
131 | // Determine output directory
132 | let outputDirectory = outputDir ? path.resolve(outputDir) : await ensureDirectoriesExist();
133 |
134 | // Check if output directory exists and is writable
135 | try {
136 | await fs.access(outputDirectory, fs.constants.W_OK);
137 | } catch (error) {
138 | return {
139 | isError: true,
140 | content: [{
141 | type: "text" as const,
142 | text: `Error: Output directory ${outputDirectory} does not exist or is not writable`
143 | }]
144 | };
145 | }
146 |
147 | // Format command for permission request
148 | const resolutionsStr = resolutions.join(', ');
149 | const permissionMessage = `Resize video ${path.basename(absVideoPath)} to ${resolutionsStr}`;
150 |
151 | // Ask for permission
152 | const permitted = await askPermission(permissionMessage);
153 |
154 | if (!permitted) {
155 | return {
156 | isError: true,
157 | content: [{
158 | type: "text" as const,
159 | text: "Permission denied by user"
160 | }]
161 | };
162 | }
163 |
164 | // Get video filename without extension
165 | const videoFilename = path.basename(absVideoPath, path.extname(absVideoPath));
166 |
167 | // Define the type for our results
168 | type ResizeResult = {
169 | resolution: "360p" | "480p" | "720p" | "1080p";
170 | outputPath: string;
171 | success: boolean;
172 | error?: string;
173 | };
174 |
175 | // Process each resolution
176 | const results: ResizeResult[] = [];
177 |
178 | for (const resolution of resolutions) {
179 | const { width, height } = RESOLUTIONS[resolution as keyof typeof RESOLUTIONS];
180 | const outputFilename = `${videoFilename}_${resolution}${path.extname(absVideoPath)}`;
181 | const outputPath = path.join(outputDirectory, outputFilename);
182 |
183 | // Build FFmpeg command
184 | const command = `ffmpeg -i "${absVideoPath}" -vf "scale=${width}:${height}" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k "${outputPath}"`;
185 |
186 | try {
187 | // Execute FFmpeg command
188 | const { stdout, stderr } = await execAsync(command);
189 |
190 | results.push({
191 | resolution,
192 | outputPath,
193 | success: true
194 | });
195 | } catch (error) {
196 | const errorMessage = error instanceof Error ? error.message : String(error);
197 |
198 | results.push({
199 | resolution,
200 | outputPath,
201 | success: false,
202 | error: errorMessage
203 | });
204 | }
205 | }
206 |
207 | // Format results
208 | const successCount = results.filter(r => r.success).length;
209 | const failCount = results.length - successCount;
210 |
211 | let resultText = `Processed ${results.length} resolutions (${successCount} successful, ${failCount} failed)\n\n`;
212 |
213 | results.forEach(result => {
214 | if (result.success) {
215 | resultText += `✅ ${result.resolution}: ${result.outputPath}\n`;
216 | } else {
217 | resultText += `❌ ${result.resolution}: Failed - ${result.error}\n`;
218 | }
219 | });
220 |
221 | return {
222 | content: [{
223 | type: "text" as const,
224 | text: resultText
225 | }]
226 | };
227 | } catch (error) {
228 | const errorMessage = error instanceof Error ? error.message : String(error);
229 | return {
230 | isError: true,
231 | content: [{
232 | type: "text" as const,
233 | text: `Error resizing video: ${errorMessage}`
234 | }]
235 | };
236 | }
237 | }
238 | );
239 |
240 | // Tool to extract audio from video
241 | server.tool(
242 | "extract-audio",
243 | "Extract audio from a video file",
244 | {
245 | videoPath: z.string().describe("Path to the video file to extract audio from"),
246 | format: z.enum(["mp3", "aac", "wav", "ogg"]).default("mp3").describe("Audio format to extract"),
247 | outputDir: z.string().optional().describe("Optional directory to save the output file (defaults to a temporary directory)")
248 | },
249 | async ({ videoPath, format, outputDir }) => {
250 | try {
251 | // Resolve the absolute path
252 | const absVideoPath = path.resolve(videoPath);
253 |
254 | // Check if file exists
255 | try {
256 | await fs.access(absVideoPath);
257 | } catch (error) {
258 | return {
259 | isError: true,
260 | content: [{
261 | type: "text" as const,
262 | text: `Error: Video file not found at ${absVideoPath}`
263 | }]
264 | };
265 | }
266 |
267 | // Determine output directory
268 | let outputDirectory = outputDir ? path.resolve(outputDir) : await ensureDirectoriesExist();
269 |
270 | // Check if output directory exists and is writable
271 | try {
272 | await fs.access(outputDirectory, fs.constants.W_OK);
273 | } catch (error) {
274 | return {
275 | isError: true,
276 | content: [{
277 | type: "text" as const,
278 | text: `Error: Output directory ${outputDirectory} does not exist or is not writable`
279 | }]
280 | };
281 | }
282 |
283 | // Format command for permission request
284 | const permissionMessage = `Extract ${format} audio from video ${path.basename(absVideoPath)}`;
285 |
286 | // Ask for permission
287 | const permitted = await askPermission(permissionMessage);
288 |
289 | if (!permitted) {
290 | return {
291 | isError: true,
292 | content: [{
293 | type: "text" as const,
294 | text: "Permission denied by user"
295 | }]
296 | };
297 | }
298 |
299 | // Get video filename without extension
300 | const videoFilename = path.basename(absVideoPath, path.extname(absVideoPath));
301 | const outputFilename = `${videoFilename}.${format}`;
302 | const outputPath = path.join(outputDirectory, outputFilename);
303 |
304 | // Determine audio codec based on format
305 | let audioCodec;
306 | switch (format) {
307 | case 'mp3':
308 | audioCodec = 'libmp3lame';
309 | break;
310 | case 'aac':
311 | audioCodec = 'aac';
312 | break;
313 | case 'wav':
314 | audioCodec = 'pcm_s16le';
315 | break;
316 | case 'ogg':
317 | audioCodec = 'libvorbis';
318 | break;
319 | default:
320 | audioCodec = 'libmp3lame';
321 | }
322 |
323 | // Build FFmpeg command
324 | const command = `ffmpeg -i "${absVideoPath}" -vn -acodec ${audioCodec} "${outputPath}"`;
325 |
326 | try {
327 | // Execute FFmpeg command
328 | const { stdout, stderr } = await execAsync(command);
329 |
330 | return {
331 | content: [{
332 | type: "text" as const,
333 | text: `Successfully extracted audio to: ${outputPath}`
334 | }]
335 | };
336 | } catch (error) {
337 | const errorMessage = error instanceof Error ? error.message : String(error);
338 | return {
339 | isError: true,
340 | content: [{
341 | type: "text" as const,
342 | text: `Error extracting audio: ${errorMessage}`
343 | }]
344 | };
345 | }
346 | } catch (error) {
347 | const errorMessage = error instanceof Error ? error.message : String(error);
348 | return {
349 | isError: true,
350 | content: [{
351 | type: "text" as const,
352 | text: `Error extracting audio: ${errorMessage}`
353 | }]
354 | };
355 | }
356 | }
357 | );
358 |
359 | // Tool to get video information
360 | server.tool(
361 | "get-video-info",
362 | "Get detailed information about a video file",
363 | {
364 | videoPath: z.string().describe("Path to the video file to analyze")
365 | },
366 | async ({ videoPath }) => {
367 | try {
368 | // Resolve the absolute path
369 | const absVideoPath = path.resolve(videoPath);
370 |
371 | // Check if file exists
372 | try {
373 | await fs.access(absVideoPath);
374 | } catch (error) {
375 | return {
376 | isError: true,
377 | content: [{
378 | type: "text" as const,
379 | text: `Error: Video file not found at ${absVideoPath}`
380 | }]
381 | };
382 | }
383 |
384 | // Format command for permission request
385 | const permissionMessage = `Analyze video file ${path.basename(absVideoPath)}`;
386 |
387 | // Ask for permission
388 | const permitted = await askPermission(permissionMessage);
389 |
390 | if (!permitted) {
391 | return {
392 | isError: true,
393 | content: [{
394 | type: "text" as const,
395 | text: "Permission denied by user"
396 | }]
397 | };
398 | }
399 |
400 | // Build FFprobe command to get video information in JSON format
401 | const command = `ffprobe -v quiet -print_format json -show_format -show_streams "${absVideoPath}"`;
402 |
403 | // Execute FFprobe command
404 | const { stdout, stderr } = await execAsync(command);
405 |
406 | // Parse the JSON output
407 | const videoInfo = JSON.parse(stdout);
408 |
409 | // Format the output in a readable way
410 | let formattedInfo = `Video Information for: ${path.basename(absVideoPath)}\n\n`;
411 |
412 | // Format information
413 | if (videoInfo.format) {
414 | formattedInfo += `Format: ${videoInfo.format.format_name}\n`;
415 | formattedInfo += `Duration: ${videoInfo.format.duration} seconds\n`;
416 | formattedInfo += `Size: ${(parseInt(videoInfo.format.size) / (1024 * 1024)).toFixed(2)} MB\n`;
417 | formattedInfo += `Bitrate: ${(parseInt(videoInfo.format.bit_rate) / 1000).toFixed(2)} kbps\n\n`;
418 | }
419 |
420 | // Stream information
421 | if (videoInfo.streams && videoInfo.streams.length > 0) {
422 | formattedInfo += `Streams:\n`;
423 |
424 | videoInfo.streams.forEach((stream: any, index: number) => {
425 | formattedInfo += `\nStream #${index} (${stream.codec_type}):\n`;
426 |
427 | if (stream.codec_type === 'video') {
428 | formattedInfo += ` Codec: ${stream.codec_name}\n`;
429 | formattedInfo += ` Resolution: ${stream.width}x${stream.height}\n`;
430 | formattedInfo += ` Frame rate: ${stream.r_frame_rate}\n`;
431 | if (stream.bit_rate) {
432 | formattedInfo += ` Bitrate: ${(parseInt(stream.bit_rate) / 1000).toFixed(2)} kbps\n`;
433 | }
434 | } else if (stream.codec_type === 'audio') {
435 | formattedInfo += ` Codec: ${stream.codec_name}\n`;
436 | formattedInfo += ` Sample rate: ${stream.sample_rate} Hz\n`;
437 | formattedInfo += ` Channels: ${stream.channels}\n`;
438 | if (stream.bit_rate) {
439 | formattedInfo += ` Bitrate: ${(parseInt(stream.bit_rate) / 1000).toFixed(2)} kbps\n`;
440 | }
441 | }
442 | });
443 | }
444 |
445 | return {
446 | content: [{
447 | type: "text" as const,
448 | text: formattedInfo
449 | }]
450 | };
451 | } catch (error) {
452 | const errorMessage = error instanceof Error ? error.message : String(error);
453 | return {
454 | isError: true,
455 | content: [{
456 | type: "text" as const,
457 | text: `Error getting video information: ${errorMessage}`
458 | }]
459 | };
460 | }
461 | }
462 | );
463 |
464 | // Start the server
465 | async function main() {
466 | try {
467 | const transport = new StdioServerTransport();
468 | await server.connect(transport);
469 | console.error("FFmpeg MCP Server running");
470 | } catch (error) {
471 | console.error("Error starting server:", error);
472 | process.exit(1);
473 | }
474 | }
475 |
476 | main();
```