Skip to main content
Glama
config.ts10.1 kB
import * as os from "os"; import * as path from "path"; import * as fs from "fs"; type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; }; /** * Valid browser names for cookie extraction */ export const VALID_BROWSERS = [ 'brave', 'chrome', 'chromium', 'edge', 'firefox', 'opera', 'safari', 'vivaldi', 'whale' ] as const; export type ValidBrowser = typeof VALID_BROWSERS[number]; /** * Configuration type definitions */ export interface Config { // File-related configuration file: { maxFilenameLength: number; downloadsDir: string; tempDirPrefix: string; // Filename processing configuration sanitize: { // Character to replace illegal characters replaceChar: string; // Suffix when truncating filenames truncateSuffix: string; // Regular expression for illegal characters illegalChars: RegExp; // List of reserved names reservedNames: readonly string[]; }; }; // Tool-related configuration tools: { required: readonly string[]; }; // Download-related configuration download: { defaultResolution: "480p" | "720p" | "1080p" | "best"; defaultAudioFormat: "m4a" | "mp3"; defaultSubtitleLanguage: string; }; // Response limits limits: { characterLimit: number; maxTranscriptLength: number; }; // Cookie configuration for authenticated access cookies: { // Path to Netscape format cookie file file?: string; // Browser name and settings (format: BROWSER[:PROFILE][::CONTAINER]) fromBrowser?: string; }; } /** * Default configuration */ const defaultConfig: Config = { 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(): DeepPartial<Config> { const envConfig: DeepPartial<Config> = {}; // File configuration const fileConfig: DeepPartial<Config['file']> = { sanitize: { replaceChar: process.env.YTDLP_SANITIZE_REPLACE_CHAR, truncateSuffix: process.env.YTDLP_SANITIZE_TRUNCATE_SUFFIX, illegalChars: process.env.YTDLP_SANITIZE_ILLEGAL_CHARS ? new RegExp(process.env.YTDLP_SANITIZE_ILLEGAL_CHARS) : undefined, reservedNames: process.env.YTDLP_SANITIZE_RESERVED_NAMES?.split(',') } }; if (process.env.YTDLP_MAX_FILENAME_LENGTH) { fileConfig.maxFilenameLength = parseInt(process.env.YTDLP_MAX_FILENAME_LENGTH); } 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: Partial<Config['download']> = {}; if (process.env.YTDLP_DEFAULT_RESOLUTION && ['480p', '720p', '1080p', 'best'].includes(process.env.YTDLP_DEFAULT_RESOLUTION)) { downloadConfig.defaultResolution = process.env.YTDLP_DEFAULT_RESOLUTION as Config['download']['defaultResolution']; } if (process.env.YTDLP_DEFAULT_AUDIO_FORMAT && ['m4a', 'mp3'].includes(process.env.YTDLP_DEFAULT_AUDIO_FORMAT)) { downloadConfig.defaultAudioFormat = process.env.YTDLP_DEFAULT_AUDIO_FORMAT as Config['download']['defaultAudioFormat']; } 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: Partial<Config['cookies']> = {}; 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: Config): void { // 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: Config): void { // 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 as ValidBrowser)) { console.warn(`[yt-dlp-mcp] Invalid browser name: ${browserName}. Valid browsers: ${VALID_BROWSERS.join(', ')}`); config.cookies.fromBrowser = undefined; } } } /** * Merge configuration */ function mergeConfig(base: Config, override: DeepPartial<Config>): Config { 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) as RegExp, reservedNames: (override.file?.sanitize?.reservedNames || base.file.sanitize.reservedNames) as readonly string[] } }, tools: { required: (override.tools?.required || base.tools.required) as readonly string[] }, 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(): Config { const envConfig = loadEnvConfig(); const config = mergeConfig(defaultConfig, envConfig); validateConfig(config); return config; } /** * Safe filename processing function */ export function sanitizeFilename(filename: string, config: Config['file']): string { // 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: Config): string[] { // 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();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kevinwatt/yt-dlp-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server