yt-dlp-mcp/src/index.mts
2025-02-11 03:24:39 +08:00

216 lines
5.5 KiB
TypeScript

#!/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 type { CallToolRequest } from "@modelcontextprotocol/sdk/types.js";
import * as os from "os";
import * as fs from "fs";
import * as path from "path";
import { spawnPromise } from "spawn-rx";
import { rimraf } from "rimraf";
const server = new Server(
{
name: "yt-dlp-mcp",
version: "0.6.5",
},
{
capabilities: {
tools: {},
},
}
);
/**
* Returns the list of available tools.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "download_youtube_srt",
description:
"Download YouTube subtitles in SRT format so that LLM can read them.",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL of the YouTube video" },
},
required: ["url"],
},
},
{
name: "download_youtube_video",
description:
"Download YouTube video to the user's default Downloads folder (usually ~/Downloads).",
inputSchema: {
type: "object",
properties: {
url: { type: "string", description: "URL of the YouTube video" },
},
required: ["url"],
},
},
],
};
});
/**
* Downloads YouTube subtitles (SRT format) and returns the concatenated content.
* @param url The URL of the YouTube video.
* @returns Concatenated subtitles text.
*/
async function downloadSubtitles(url: string): Promise<string> {
// Create a temporary directory for subtitle downloads
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "youtube-"));
// Use yt-dlp to download subtitles without downloading the video
await spawnPromise(
"yt-dlp",
[
"--write-sub",
"--write-auto-sub",
"--sub-lang",
"en",
"--skip-download",
"--sub-format",
"srt",
url,
],
{ cwd: tempDirectory, detached: true }
);
let subtitlesContent = "";
try {
const files = fs.readdirSync(tempDirectory);
for (const file of files) {
const filePath = path.join(tempDirectory, file);
const fileData = fs.readFileSync(filePath, "utf8");
subtitlesContent += `${file}\n====================\n${fileData}\n\n`;
}
} finally {
// Clean up the temporary directory after processing
rimraf.sync(tempDirectory);
}
return subtitlesContent;
}
/**
* Downloads a YouTube video to the user's default Downloads folder.
* @param url The URL of the YouTube video.
* @returns A detailed success message including the filename.
*/
async function downloadVideo(url: string): Promise<string> {
const userDownloadsDir = path.join(os.homedir(), "Downloads");
try {
// Get current timestamp for filename
const timestamp = new Date().toISOString()
.replace(/[:.]/g, '-')
.replace('T', '_')
.split('.')[0];
const outputTemplate = path.join(
userDownloadsDir,
// Limit title length to 50 characters to avoid filename too long error
`%(title).50s [%(id)s] ${timestamp}.%(ext)s`
);
// Get expected filename
const infoResult = await spawnPromise("yt-dlp", [
"--get-filename",
"--output", outputTemplate,
url
]);
const expectedFilename = infoResult.trim();
// Download with progress info and use current time
await spawnPromise("yt-dlp", [
"--progress",
"--newline",
"--no-mtime", // Don't use video upload time
"--output", outputTemplate,
url
]);
return `Video successfully downloaded as "${path.basename(expectedFilename)}" to ${userDownloadsDir}`;
} catch (error) {
throw new Error(`Failed to download video: ${error}`);
}
}
/**
* Handles tool execution requests.
*/
server.setRequestHandler(
CallToolRequestSchema,
async (request: CallToolRequest) => {
const toolName = request.params.name;
const args = request.params.arguments as { url: string };
if (toolName === "download_youtube_srt") {
try {
const subtitles = await downloadSubtitles(args.url);
return {
content: [
{
type: "text",
text: subtitles,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error downloading subtitles: ${error}`,
},
],
isError: true,
};
}
} else if (toolName === "download_youtube_video") {
try {
const message = await downloadVideo(args.url);
return {
content: [
{
type: "text",
text: message,
},
],
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error downloading video: ${error}`,
},
],
isError: true,
};
}
} else {
throw new Error(`Unknown tool: ${toolName}`);
}
}
);
/**
* Starts the server using Stdio transport.
*/
async function startServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
// Start the server and handle potential errors
startServer().catch(console.error);