UNPKG

@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.

194 lines 7.15 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import * as os from "os"; import * as fs from "fs"; import * as path from "path"; import { CONFIG } from "./config.js"; import { _spawnPromise, safeCleanup } from "./modules/utils.js"; import { downloadVideo } from "./modules/video.js"; import { downloadAudio } from "./modules/audio.js"; import { listSubtitles, downloadSubtitles } from "./modules/subtitle.js"; const VERSION = '0.6.26'; /** * Validate system configuration * @throws {Error} when configuration is invalid */ async function validateConfig() { // Check downloads directory if (!fs.existsSync(CONFIG.file.downloadsDir)) { throw new Error(`Downloads directory does not exist: ${CONFIG.file.downloadsDir}`); } // Check downloads directory permissions try { const testFile = path.join(CONFIG.file.downloadsDir, '.write-test'); fs.writeFileSync(testFile, ''); fs.unlinkSync(testFile); } catch (error) { throw new Error(`No write permission in downloads directory: ${CONFIG.file.downloadsDir}`); } // Check temporary directory permissions try { const testDir = fs.mkdtempSync(path.join(os.tmpdir(), CONFIG.file.tempDirPrefix)); await safeCleanup(testDir); } catch (error) { throw new Error(`Cannot create temporary directory in: ${os.tmpdir()}`); } } /** * Check required external dependencies * @throws {Error} when dependencies are not satisfied */ async function checkDependencies() { for (const tool of CONFIG.tools.required) { try { await _spawnPromise(tool, ["--version"]); } catch (error) { throw new Error(`Required tool '${tool}' is not installed or not accessible`); } } } /** * Initialize service */ async function initialize() { // 在測試環境中跳過初始化檢查 if (process.env.NODE_ENV === 'test') { return; } try { await validateConfig(); await checkDependencies(); } catch (error) { console.error('Initialization failed:', error); process.exit(1); } } const server = new Server({ name: "yt-dlp-mcp", version: VERSION, }, { capabilities: { tools: {} }, }); /** * Returns the list of available tools. */ server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_subtitle_languages", description: "List all available subtitle languages and their formats for a video (including auto-generated captions)", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL of the video" }, }, required: ["url"], }, }, { name: "download_video_subtitles", description: "Download video subtitles in any available format. Supports both regular and auto-generated subtitles in various languages.", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL of the video" }, language: { type: "string", description: "Language code (e.g., 'en', 'zh-Hant', 'ja'). Will try to get auto-generated subtitles if regular subtitles are not available." }, }, required: ["url"], }, }, { name: "download_video", description: "Download video to the user's default Downloads folder (usually ~/Downloads).", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL of the video" }, resolution: { type: "string", description: "Preferred video resolution. For YouTube: '480p', '720p', '1080p', 'best'. For other platforms: '480p' for low quality, '720p'/'1080p' for HD, 'best' for highest quality. Defaults to '720p'", enum: ["480p", "720p", "1080p", "best"] }, }, required: ["url"], }, }, { name: "download_audio", description: "Download audio in best available quality (usually m4a/mp3 format) to the user's default Downloads folder (usually ~/Downloads).", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL of the video" }, }, required: ["url"], }, }, ], }; }); /** * Handle tool execution with unified error handling * @param action Async operation to execute * @param errorPrefix Error message prefix */ async function handleToolExecution(action, errorPrefix) { try { const result = await action(); return { content: [{ type: "text", text: String(result) }] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `${errorPrefix}: ${errorMessage}` }], isError: true }; } } /** * Handles tool execution requests. */ server.setRequestHandler(CallToolRequestSchema, async (request) => { const toolName = request.params.name; const args = request.params.arguments; if (toolName === "list_subtitle_languages") { return handleToolExecution(() => listSubtitles(args.url), "Error listing subtitle languages"); } else if (toolName === "download_video_subtitles") { return handleToolExecution(() => downloadSubtitles(args.url, args.language || CONFIG.download.defaultSubtitleLanguage, CONFIG), "Error downloading subtitles"); } else if (toolName === "download_video") { return handleToolExecution(() => downloadVideo(args.url, CONFIG, args.resolution), "Error downloading video"); } else if (toolName === "download_audio") { return handleToolExecution(() => downloadAudio(args.url, CONFIG), "Error downloading audio"); } else { return { content: [{ type: "text", text: `Unknown tool: ${toolName}` }], isError: true }; } }); /** * Starts the server using Stdio transport. */ async function startServer() { await initialize(); const transport = new StdioServerTransport(); await server.connect(transport); } // Start the server and handle potential errors startServer().catch(console.error); //# sourceMappingURL=index.mjs.map