@kevinwatt/yt-dlp-mcp
Version:
An MCP server implementation that integrates with yt-dlp, providing video and audio content download capabilities (e.g. YouTube, Facebook, Tiktok, etc.) for LLMs.
255 lines • 9.49 kB
JavaScript
import * as os from "os";
import * as path from "path";
import * as fs from "fs";
/**
* Valid browser names for cookie extraction
*/
export const VALID_BROWSERS = [
'brave', 'chrome', 'chromium', 'edge',
'firefox', 'opera', 'safari', 'vivaldi', 'whale'
];
/**
* Default configuration
*/
const defaultConfig = {
file: {
maxFilenameLength: 50,
downloadsDir: path.join(os.homedir(), "Downloads"),
tempDirPrefix: "ytdlp-",
sanitize: {
replaceChar: '_',
truncateSuffix: '...',
illegalChars: /[<>:"/\\|?*\x00-\x1F]/g, // Windows illegal characters
reservedNames: [
'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'
]
}
},
tools: {
required: ['yt-dlp']
},
download: {
defaultResolution: "720p",
defaultAudioFormat: "m4a",
defaultSubtitleLanguage: "en"
},
limits: {
characterLimit: 25000, // Standard MCP character limit
maxTranscriptLength: 50000 // Transcripts can be larger
},
cookies: {
file: undefined,
fromBrowser: undefined
}
};
/**
* Load configuration from environment variables
*/
function loadEnvConfig() {
const envConfig = {};
// File configuration
const fileConfig = {
sanitize: {
replaceChar: process.env.YTDLP_SANITIZE_REPLACE_CHAR,
truncateSuffix: process.env.YTDLP_SANITIZE_TRUNCATE_SUFFIX,
illegalChars: (() => {
if (!process.env.YTDLP_SANITIZE_ILLEGAL_CHARS)
return undefined;
try {
return new RegExp(process.env.YTDLP_SANITIZE_ILLEGAL_CHARS);
}
catch {
console.warn('[yt-dlp-mcp] Invalid regex in YTDLP_SANITIZE_ILLEGAL_CHARS, using default');
return undefined;
}
})(),
reservedNames: process.env.YTDLP_SANITIZE_RESERVED_NAMES?.split(',')
}
};
if (process.env.YTDLP_MAX_FILENAME_LENGTH) {
const parsed = parseInt(process.env.YTDLP_MAX_FILENAME_LENGTH, 10);
if (!isNaN(parsed) && parsed >= 5) {
fileConfig.maxFilenameLength = parsed;
}
else {
console.warn('[yt-dlp-mcp] Invalid YTDLP_MAX_FILENAME_LENGTH, using default');
}
}
if (process.env.YTDLP_DOWNLOADS_DIR) {
fileConfig.downloadsDir = process.env.YTDLP_DOWNLOADS_DIR;
}
if (process.env.YTDLP_TEMP_DIR_PREFIX) {
fileConfig.tempDirPrefix = process.env.YTDLP_TEMP_DIR_PREFIX;
}
if (Object.keys(fileConfig).length > 0) {
envConfig.file = fileConfig;
}
// Download configuration
const downloadConfig = {};
if (process.env.YTDLP_DEFAULT_RESOLUTION &&
['480p', '720p', '1080p', 'best'].includes(process.env.YTDLP_DEFAULT_RESOLUTION)) {
downloadConfig.defaultResolution = process.env.YTDLP_DEFAULT_RESOLUTION;
}
if (process.env.YTDLP_DEFAULT_AUDIO_FORMAT &&
['m4a', 'mp3'].includes(process.env.YTDLP_DEFAULT_AUDIO_FORMAT)) {
downloadConfig.defaultAudioFormat = process.env.YTDLP_DEFAULT_AUDIO_FORMAT;
}
if (process.env.YTDLP_DEFAULT_SUBTITLE_LANG) {
downloadConfig.defaultSubtitleLanguage = process.env.YTDLP_DEFAULT_SUBTITLE_LANG;
}
if (Object.keys(downloadConfig).length > 0) {
envConfig.download = downloadConfig;
}
// Cookie configuration
const cookiesConfig = {};
if (process.env.YTDLP_COOKIES_FILE) {
cookiesConfig.file = process.env.YTDLP_COOKIES_FILE;
}
if (process.env.YTDLP_COOKIES_FROM_BROWSER) {
cookiesConfig.fromBrowser = process.env.YTDLP_COOKIES_FROM_BROWSER;
}
if (Object.keys(cookiesConfig).length > 0) {
envConfig.cookies = cookiesConfig;
}
return envConfig;
}
/**
* Validate configuration
*/
function validateConfig(config) {
// Validate filename length
if (config.file.maxFilenameLength < 5) {
throw new Error('maxFilenameLength must be at least 5');
}
// Validate downloads directory
if (!config.file.downloadsDir) {
throw new Error('downloadsDir must be specified');
}
// Validate temporary directory prefix
if (!config.file.tempDirPrefix) {
throw new Error('tempDirPrefix must be specified');
}
// Validate default resolution
if (!['480p', '720p', '1080p', 'best'].includes(config.download.defaultResolution)) {
throw new Error('Invalid defaultResolution');
}
// Validate default audio format
if (!['m4a', 'mp3'].includes(config.download.defaultAudioFormat)) {
throw new Error('Invalid defaultAudioFormat');
}
// Validate default subtitle language
if (!/^[a-z]{2,3}(-[A-Z][a-z]{3})?(-[A-Z]{2})?$/i.test(config.download.defaultSubtitleLanguage)) {
throw new Error('Invalid defaultSubtitleLanguage');
}
// Validate cookies (lenient - warnings only)
validateCookiesConfig(config);
}
/**
* Validate cookie configuration (lenient - logs warnings but doesn't throw)
*/
function validateCookiesConfig(config) {
// Validate cookie file path
if (config.cookies.file) {
if (!fs.existsSync(config.cookies.file)) {
console.warn(`[yt-dlp-mcp] Cookie file not found: ${config.cookies.file}, continuing without cookies`);
config.cookies.file = undefined;
}
}
// Validate browser name only
// Format: BROWSER[:PROFILE_OR_PATH][::CONTAINER]
// We only validate browser name; yt-dlp will validate path/container
if (config.cookies.fromBrowser) {
const browserName = config.cookies.fromBrowser.split(':')[0].toLowerCase();
if (!VALID_BROWSERS.includes(browserName)) {
console.warn(`[yt-dlp-mcp] Invalid browser name: ${browserName}. Valid browsers: ${VALID_BROWSERS.join(', ')}`);
config.cookies.fromBrowser = undefined;
}
}
}
/**
* Merge configuration
*/
function mergeConfig(base, override) {
return {
file: {
maxFilenameLength: override.file?.maxFilenameLength || base.file.maxFilenameLength,
downloadsDir: override.file?.downloadsDir || base.file.downloadsDir,
tempDirPrefix: override.file?.tempDirPrefix || base.file.tempDirPrefix,
sanitize: {
replaceChar: override.file?.sanitize?.replaceChar || base.file.sanitize.replaceChar,
truncateSuffix: override.file?.sanitize?.truncateSuffix || base.file.sanitize.truncateSuffix,
illegalChars: (override.file?.sanitize?.illegalChars || base.file.sanitize.illegalChars),
reservedNames: (override.file?.sanitize?.reservedNames || base.file.sanitize.reservedNames)
}
},
tools: {
required: (override.tools?.required || base.tools.required)
},
download: {
defaultResolution: override.download?.defaultResolution || base.download.defaultResolution,
defaultAudioFormat: override.download?.defaultAudioFormat || base.download.defaultAudioFormat,
defaultSubtitleLanguage: override.download?.defaultSubtitleLanguage || base.download.defaultSubtitleLanguage
},
limits: {
characterLimit: override.limits?.characterLimit || base.limits.characterLimit,
maxTranscriptLength: override.limits?.maxTranscriptLength || base.limits.maxTranscriptLength
},
cookies: {
file: override.cookies?.file ?? base.cookies.file,
fromBrowser: override.cookies?.fromBrowser ?? base.cookies.fromBrowser
}
};
}
/**
* Load configuration
*/
export function loadConfig() {
const envConfig = loadEnvConfig();
const config = mergeConfig(defaultConfig, envConfig);
validateConfig(config);
return config;
}
/**
* Safe filename processing function
*/
export function sanitizeFilename(filename, config) {
// Remove illegal characters
let safe = filename.replace(config.sanitize.illegalChars, config.sanitize.replaceChar);
// Check reserved names
const basename = path.parse(safe).name.toUpperCase();
if (config.sanitize.reservedNames.includes(basename)) {
safe = `_${safe}`;
}
// Handle length limitation
if (safe.length > config.maxFilenameLength) {
const ext = path.extname(safe);
const name = safe.slice(0, config.maxFilenameLength - ext.length - config.sanitize.truncateSuffix.length);
safe = `${name}${config.sanitize.truncateSuffix}${ext}`;
}
return safe;
}
/**
* Get cookie-related yt-dlp arguments
* Priority: file > fromBrowser
* @param config Configuration object
* @returns Array of yt-dlp arguments for cookie handling
*/
export function getCookieArgs(config) {
// Guard against missing cookies config
if (!config.cookies) {
return [];
}
// Cookie file takes precedence over browser extraction
if (config.cookies.file) {
return ['--cookies', config.cookies.file];
}
if (config.cookies.fromBrowser) {
return ['--cookies-from-browser', config.cookies.fromBrowser];
}
return [];
}
// Export current configuration instance
export const CONFIG = loadConfig();
//# sourceMappingURL=config.js.map