#
tokens: 8261/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![smithery badge](https://smithery.ai/badge/@bitscorp-mcp/mcp-ffmpeg)](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();
```