#
tokens: 5879/50000 8/8 files
lines: off (toggle) GitHub
raw markdown copy
# 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:
--------------------------------------------------------------------------------

```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# MCP FFmpeg Video Processor
[![smithery badge](https://smithery.ai/badge/@bitscorp-mcp/mcp-ffmpeg)](https://smithery.ai/server/@bitscorp-mcp/mcp-ffmpeg)

A Node.js server that uses FFmpeg to manipulate video files. This server provides APIs to:

- Resize videos to different resolutions (360p, 480p, 720p, 1080p)
- Extract audio from videos in various formats (MP3, AAC, WAV, OGG)

## Prerequisites

Before running this application, you need to have the following installed:

1. **Node.js** (v14 or higher)
2. **FFmpeg** - This is required for video processing

### Installing FFmpeg

#### On macOS:
```bash
brew install ffmpeg
```

#### On Ubuntu/Debian:
```bash
sudo apt update
sudo apt install ffmpeg
```

#### On Windows:
1. Download FFmpeg from the [official website](https://ffmpeg.org/download.html)
2. Extract the files to a folder (e.g., `C:\ffmpeg`)
3. Add the `bin` folder to your PATH environment variable

## Installation

1. Clone this repository:
```bash
git clone https://github.com/bitscorp-mcp/mcp-ffmpeg.git
cd mcp-ffmpeg
```

2. Install dependencies:
```bash
npm install
```

### Installing via Smithery

To install mcp-ffmpeg for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@bitscorp-mcp/mcp-ffmpeg):

```bash
npx -y @smithery/cli install @bitscorp-mcp/mcp-ffmpeg --client claude
```

## Running the Server

Start the server with:

```bash
npm start
```

For development with auto-restart on file changes:

```bash
npm run dev
```

### Installing via Smithery

To install mcp-ffmpeg for Claude Desktop automatically via [Smithery](https://smithery.ai/server/bitscorp-mcp/mcp-ffmpeg):

```bash
npx -y @smithery/cli install @bitscorp-mcp/mcp-ffmpeg --client claude
```

To install mcp-ffmpeg for Cursor, go to Settings -> Cursor Settings -> Features -> MCP Servers -> + Add

Select Type: command and paste the below, using your API key from Adjust
```
npx -y @smithery/cli@latest run @bitscorp/mcp-ffmpeg
```

## Using with Claude Desktop

This MCP FFmpeg server can be integrated with Claude Desktop to process videos through natural language requests.

### Running with npx

You can run the server directly with npx:

```bash
npx /path/to/mcp-ffmpeg
```

Or if you've published the package to npm:

```bash
npx mcp-ffmpeg
```

### Configuring Claude Desktop

To add this server to Claude Desktop, update your Claude Desktop configuration file:

1. Locate your Claude Desktop config file:
   - macOS: `~/.config/claude-desktop/config.json` or `~/Library/Application Support/Claude Desktop/config.json`
   - Windows: `%APPDATA%\Claude Desktop\config.json`
   - Linux: `~/.config/claude-desktop/config.json`

2. Add the FFmpeg MCP server to the `mcpServers` section:

```json
{
    "mcpServers": {
        "ffmpeg": {
            "command": "npx",
            "args": [
                "--yes",
                "/absolute/path/to/mcp-ffmpeg"
            ]
        }
    }
}
```

If you've published the package to npm:

```json
{
    "mcpServers": {
        "ffmpeg": {
            "command": "npx",
            "args": [
                "--yes",
                "mcp-ffmpeg"
            ]
        }
    }
}
```

3. Restart Claude Desktop for the changes to take effect.

### Example Prompts for Claude

Once configured, you can use prompts like:

```
Using the ffmpeg MCP server, please resize the video at /path/to/video.mp4 to 720p resolution.
```

## Notes

- Uploaded videos are stored temporarily in the `uploads` directory
- Processed videos and audio files are stored in the `output` directory
- The server has a file size limit of 500MB for uploads

## License

MIT

```

--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------

```javascript
import neostandard from 'neostandard'

export default neostandard({
  ignores: ['node_modules', 'dist']
})

```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "esModuleInterop": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml

startCommand:
  type: stdio
  configSchema:
    # JSON Schema defining the configuration options for the MCP.
    type: object
    properties: {}
  commandFunction:
    # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
    |-
    (config) => ({ command: 'node', args: ['dist/mcp-ffmpeg.js'] })
  exampleConfig: {}

```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile

### Multi-stage Dockerfile for MCP FFmpeg Server

# Stage 1: Builder
FROM node:lts-alpine AS builder

# Install ffmpeg and any build dependencies
RUN apk add --no-cache ffmpeg

# Set working directory
WORKDIR /app

# Copy package files
COPY package*.json ./

# Install all dependencies including dev dependencies (needed for building)
RUN npm install

# Copy the rest of the application source code
COPY . .

# Build the TypeScript code
RUN npm run build

# Stage 2: Production
FROM node:lts-alpine

# Install ffmpeg in production image
RUN apk add --no-cache ffmpeg

# Set working directory
WORKDIR /app

# Copy only the necessary files from builder
COPY package*.json ./

# Install production dependencies only
RUN npm install --production --ignore-scripts

# Copy built files from the builder stage
COPY --from=builder /app/dist ./dist

# Expose port if needed (not needed for stdio-based MCP servers)

# Start the MCP server
CMD [ "node", "dist/mcp-ffmpeg.js" ]
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
	"name": "mcp-ffmpeg",
	"version": "1.0.5",
	"description": "FFmpeg MCP server",
	"main": "dist/mcp-ffmpeg.js",
	"type": "module",
	"bin": {
		"mcp-ffmpeg": "./dist/mcp-ffmpeg.js"
	},
	"scripts": {
		"build": "tsc",
		"lint": "eslint .",
		"lint:fix": "eslint --fix .",
		"dev": "npm run build && npx @modelcontextprotocol/inspector node dist/mcp-ffmpeg.js"
	},
	"repository": {
		"type": "git",
		"url": "git+https://github.com/platformatic/mcp-node.git"
	},
	"license": "MIT",
	"author": "Platformatic Inc. <[email protected]> (https://platformatic.dev)",
	"contributors": [
		{
			"name": "Alexandr Korsak",
			"email": "[email protected]"
		}
	],
	"bugs": {
		"url": "https://github.com/bitscorp-mcp/mcp-ffmpeg/issues"
	},
	"homepage": "https://github.com/bitscorp-mcp/mcp-ffmpeg#readme",
	"dependencies": {
		"@modelcontextprotocol/sdk": "^1.6.0",
		"node-notifier": "^10.0.1",
		"zod": "^3.24.2"
	},
	"devDependencies": {
		"@types/node": "^22.13.5",
		"@types/node-notifier": "^8.0.5",
		"eslint": "^9.21.0",
		"neostandard": "^0.12.1",
		"typescript": "^5.7.3"
	},
	"keywords": [
		"mcp",
		"model-context-protocol",
		"ai",
		"nodejs",
		"javascript-runtime"
	],
	"engines": {
		"node": ">=22.0.0"
	}
}

```

--------------------------------------------------------------------------------
/src/mcp-ffmpeg.ts:
--------------------------------------------------------------------------------

```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { exec, ExecOptions as ChildProcessExecOptions } from "child_process";
import { promisify } from "util";
import * as path from "path";
import * as fs from "fs/promises";
import * as os from "node:os";
import notifier from "node-notifier";

const execAsync = promisify(exec);

// Create an MCP server
const server = new McpServer({
  name: "FFmpegProcessor",
  version: "1.0.0"
});

// Define available resolutions
const RESOLUTIONS = {
  "360p": { width: 640, height: 360 },
  "480p": { width: 854, height: 480 },
  "720p": { width: 1280, height: 720 },
  "1080p": { width: 1920, height: 1080 }
};

/**
 * Helper function to ask for permission using node-notifier
 */
async function askPermission(action: string): Promise<boolean> {
  // Skip notification if DISABLE_NOTIFICATIONS is set
  if (process.env.DISABLE_NOTIFICATIONS === 'true') {
    console.log(`Auto-allowing action (notifications disabled): ${action}`);
    return true;
  }

  return new Promise((resolve) => {
    notifier.notify({
      title: 'FFmpeg Processor Permission Request',
      message: `${action}`,
      wait: true,
      timeout: 60,
      actions: 'Allow',
      closeLabel: 'Deny'
    }, (err, response, metadata) => {
      if (err) {
        console.error('Error showing notification:', err);
        resolve(false);
        return;
      }

      const buttonPressed = metadata?.activationValue || response;
      resolve(buttonPressed !== 'Deny');
    });
  });
}

/**
 * Helper function to ensure output directories exist
 */
async function ensureDirectoriesExist() {
  const outputDir = path.join(os.tmpdir(), 'ffmpeg-output');
  try {
    await fs.mkdir(outputDir, { recursive: true });
    return outputDir;
  } catch (error) {
    console.error('Error creating output directory:', error);
    return os.tmpdir();
  }
}

// Tool to check FFmpeg version
server.tool(
  "get-ffmpeg-version",
  "Get the version of FFmpeg installed on the system",
  {},
  async () => {
    try {
      const { stdout, stderr } = await execAsync('ffmpeg -version');

      // Extract the version from the output
      const versionMatch = stdout.match(/ffmpeg version (\S+)/);
      const version = versionMatch ? versionMatch[1] : 'Unknown';

      return {
        content: [{
          type: "text" as const,
          text: `FFmpeg Version: ${version}\n\nFull version info:\n${stdout}`
        }]
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        isError: true,
        content: [{
          type: "text" as const,
          text: `Error getting FFmpeg version: ${errorMessage}\n\nMake sure FFmpeg is installed and in your PATH.`
        }]
      };
    }
  }
);

// Tool to resize video
server.tool(
  "resize-video",
  "Resize a video to one or more standard resolutions",
  {
    videoPath: z.string().describe("Path to the video file to resize"),
    resolutions: z.array(z.enum(["360p", "480p", "720p", "1080p"])).describe("Resolutions to convert the video to"),
    outputDir: z.string().optional().describe("Optional directory to save the output files (defaults to a temporary directory)")
  },
  async ({ videoPath, resolutions, outputDir }) => {
    try {
      // Resolve the absolute path
      const absVideoPath = path.resolve(videoPath);

      // Check if file exists
      try {
        await fs.access(absVideoPath);
      } catch (error) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: `Error: Video file not found at ${absVideoPath}`
          }]
        };
      }

      // Determine output directory
      let outputDirectory = outputDir ? path.resolve(outputDir) : await ensureDirectoriesExist();

      // Check if output directory exists and is writable
      try {
        await fs.access(outputDirectory, fs.constants.W_OK);
      } catch (error) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: `Error: Output directory ${outputDirectory} does not exist or is not writable`
          }]
        };
      }

      // Format command for permission request
      const resolutionsStr = resolutions.join(', ');
      const permissionMessage = `Resize video ${path.basename(absVideoPath)} to ${resolutionsStr}`;

      // Ask for permission
      const permitted = await askPermission(permissionMessage);

      if (!permitted) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: "Permission denied by user"
          }]
        };
      }

      // Get video filename without extension
      const videoFilename = path.basename(absVideoPath, path.extname(absVideoPath));

      // Define the type for our results
      type ResizeResult = {
        resolution: "360p" | "480p" | "720p" | "1080p";
        outputPath: string;
        success: boolean;
        error?: string;
      };

      // Process each resolution
      const results: ResizeResult[] = [];

      for (const resolution of resolutions) {
        const { width, height } = RESOLUTIONS[resolution as keyof typeof RESOLUTIONS];
        const outputFilename = `${videoFilename}_${resolution}${path.extname(absVideoPath)}`;
        const outputPath = path.join(outputDirectory, outputFilename);

        // Build FFmpeg command
        const command = `ffmpeg -i "${absVideoPath}" -vf "scale=${width}:${height}" -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k "${outputPath}"`;

        try {
          // Execute FFmpeg command
          const { stdout, stderr } = await execAsync(command);

          results.push({
            resolution,
            outputPath,
            success: true
          });
        } catch (error) {
          const errorMessage = error instanceof Error ? error.message : String(error);

          results.push({
            resolution,
            outputPath,
            success: false,
            error: errorMessage
          });
        }
      }

      // Format results
      const successCount = results.filter(r => r.success).length;
      const failCount = results.length - successCount;

      let resultText = `Processed ${results.length} resolutions (${successCount} successful, ${failCount} failed)\n\n`;

      results.forEach(result => {
        if (result.success) {
          resultText += `✅ ${result.resolution}: ${result.outputPath}\n`;
        } else {
          resultText += `❌ ${result.resolution}: Failed - ${result.error}\n`;
        }
      });

      return {
        content: [{
          type: "text" as const,
          text: resultText
        }]
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        isError: true,
        content: [{
          type: "text" as const,
          text: `Error resizing video: ${errorMessage}`
        }]
      };
    }
  }
);

// Tool to extract audio from video
server.tool(
  "extract-audio",
  "Extract audio from a video file",
  {
    videoPath: z.string().describe("Path to the video file to extract audio from"),
    format: z.enum(["mp3", "aac", "wav", "ogg"]).default("mp3").describe("Audio format to extract"),
    outputDir: z.string().optional().describe("Optional directory to save the output file (defaults to a temporary directory)")
  },
  async ({ videoPath, format, outputDir }) => {
    try {
      // Resolve the absolute path
      const absVideoPath = path.resolve(videoPath);

      // Check if file exists
      try {
        await fs.access(absVideoPath);
      } catch (error) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: `Error: Video file not found at ${absVideoPath}`
          }]
        };
      }

      // Determine output directory
      let outputDirectory = outputDir ? path.resolve(outputDir) : await ensureDirectoriesExist();

      // Check if output directory exists and is writable
      try {
        await fs.access(outputDirectory, fs.constants.W_OK);
      } catch (error) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: `Error: Output directory ${outputDirectory} does not exist or is not writable`
          }]
        };
      }

      // Format command for permission request
      const permissionMessage = `Extract ${format} audio from video ${path.basename(absVideoPath)}`;

      // Ask for permission
      const permitted = await askPermission(permissionMessage);

      if (!permitted) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: "Permission denied by user"
          }]
        };
      }

      // Get video filename without extension
      const videoFilename = path.basename(absVideoPath, path.extname(absVideoPath));
      const outputFilename = `${videoFilename}.${format}`;
      const outputPath = path.join(outputDirectory, outputFilename);

      // Determine audio codec based on format
      let audioCodec;
      switch (format) {
        case 'mp3':
          audioCodec = 'libmp3lame';
          break;
        case 'aac':
          audioCodec = 'aac';
          break;
        case 'wav':
          audioCodec = 'pcm_s16le';
          break;
        case 'ogg':
          audioCodec = 'libvorbis';
          break;
        default:
          audioCodec = 'libmp3lame';
      }

      // Build FFmpeg command
      const command = `ffmpeg -i "${absVideoPath}" -vn -acodec ${audioCodec} "${outputPath}"`;

      try {
        // Execute FFmpeg command
        const { stdout, stderr } = await execAsync(command);

        return {
          content: [{
            type: "text" as const,
            text: `Successfully extracted audio to: ${outputPath}`
          }]
        };
      } catch (error) {
        const errorMessage = error instanceof Error ? error.message : String(error);
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: `Error extracting audio: ${errorMessage}`
          }]
        };
      }
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        isError: true,
        content: [{
          type: "text" as const,
          text: `Error extracting audio: ${errorMessage}`
        }]
      };
    }
  }
);

// Tool to get video information
server.tool(
  "get-video-info",
  "Get detailed information about a video file",
  {
    videoPath: z.string().describe("Path to the video file to analyze")
  },
  async ({ videoPath }) => {
    try {
      // Resolve the absolute path
      const absVideoPath = path.resolve(videoPath);

      // Check if file exists
      try {
        await fs.access(absVideoPath);
      } catch (error) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: `Error: Video file not found at ${absVideoPath}`
          }]
        };
      }

      // Format command for permission request
      const permissionMessage = `Analyze video file ${path.basename(absVideoPath)}`;

      // Ask for permission
      const permitted = await askPermission(permissionMessage);

      if (!permitted) {
        return {
          isError: true,
          content: [{
            type: "text" as const,
            text: "Permission denied by user"
          }]
        };
      }

      // Build FFprobe command to get video information in JSON format
      const command = `ffprobe -v quiet -print_format json -show_format -show_streams "${absVideoPath}"`;

      // Execute FFprobe command
      const { stdout, stderr } = await execAsync(command);

      // Parse the JSON output
      const videoInfo = JSON.parse(stdout);

      // Format the output in a readable way
      let formattedInfo = `Video Information for: ${path.basename(absVideoPath)}\n\n`;

      // Format information
      if (videoInfo.format) {
        formattedInfo += `Format: ${videoInfo.format.format_name}\n`;
        formattedInfo += `Duration: ${videoInfo.format.duration} seconds\n`;
        formattedInfo += `Size: ${(parseInt(videoInfo.format.size) / (1024 * 1024)).toFixed(2)} MB\n`;
        formattedInfo += `Bitrate: ${(parseInt(videoInfo.format.bit_rate) / 1000).toFixed(2)} kbps\n\n`;
      }

      // Stream information
      if (videoInfo.streams && videoInfo.streams.length > 0) {
        formattedInfo += `Streams:\n`;

        videoInfo.streams.forEach((stream: any, index: number) => {
          formattedInfo += `\nStream #${index} (${stream.codec_type}):\n`;

          if (stream.codec_type === 'video') {
            formattedInfo += `  Codec: ${stream.codec_name}\n`;
            formattedInfo += `  Resolution: ${stream.width}x${stream.height}\n`;
            formattedInfo += `  Frame rate: ${stream.r_frame_rate}\n`;
            if (stream.bit_rate) {
              formattedInfo += `  Bitrate: ${(parseInt(stream.bit_rate) / 1000).toFixed(2)} kbps\n`;
            }
          } else if (stream.codec_type === 'audio') {
            formattedInfo += `  Codec: ${stream.codec_name}\n`;
            formattedInfo += `  Sample rate: ${stream.sample_rate} Hz\n`;
            formattedInfo += `  Channels: ${stream.channels}\n`;
            if (stream.bit_rate) {
              formattedInfo += `  Bitrate: ${(parseInt(stream.bit_rate) / 1000).toFixed(2)} kbps\n`;
            }
          }
        });
      }

      return {
        content: [{
          type: "text" as const,
          text: formattedInfo
        }]
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      return {
        isError: true,
        content: [{
          type: "text" as const,
          text: `Error getting video information: ${errorMessage}`
        }]
      };
    }
  }
);

// Start the server
async function main() {
  try {
    const transport = new StdioServerTransport();
    await server.connect(transport);
    console.error("FFmpeg MCP Server running");
  } catch (error) {
    console.error("Error starting server:", error);
    process.exit(1);
  }
}

main();
```