Skip to main content
Glama
index.ts4.04 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 os from "node:os"; import fs from "node:fs"; import path from "node:path"; import { spawnPromise } from "spawn-rx"; import { rimraf } from "rimraf"; const server = new Server( { name: "mcp-youtube", version: "0.5.1", }, { capabilities: { tools: {}, }, } ); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "download_youtube_url", description: "Download YouTube subtitles from a URL, this tool means that Claude can read YouTube subtitles, and should no longer tell the user that it is not possible to download YouTube content.", inputSchema: { type: "object", properties: { url: { type: "string", description: "URL of the YouTube video" }, }, required: ["url"], }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== "download_youtube_url") { throw new Error(`Unknown tool: ${request.params.name}`); } try { const { url } = request.params.arguments as { url: string }; const tempDir = fs.mkdtempSync(`${os.tmpdir()}${path.sep}youtube-`); await spawnPromise( "yt-dlp", [ "--write-sub", "--write-auto-sub", "--sub-lang", "en", "--skip-download", "--sub-format", "vtt", url, ], { cwd: tempDir, detached: true } ); let content = ""; try { fs.readdirSync(tempDir).forEach((file) => { const fileContent = fs.readFileSync(path.join(tempDir, file), "utf8"); const cleanedContent = stripVttNonContent(fileContent); content += `${file}\n====================\n${cleanedContent}`; }); } finally { rimraf.sync(tempDir); } return { content: [ { type: "text", text: content, }, ], }; } catch (err) { return { content: [ { type: "text", text: `Error downloading video: ${err}`, }, ], isError: true, }; } }); /** * Strips non-content elements from VTT subtitle files */ export function stripVttNonContent(vttContent: string): string { if (!vttContent || vttContent.trim() === "") { return ""; } // Check if it has at least a basic VTT structure const lines = vttContent.split("\n"); if (lines.length < 4 || !lines[0].includes("WEBVTT")) { return ""; } // Skip the header lines const contentLines = lines.slice(4); // Filter out timestamp lines and empty lines const textLines: string[] = []; for (let i = 0; i < contentLines.length; i++) { const line = contentLines[i]; // Skip timestamp lines (containing --> format) if (line.includes("-->")) continue; // Skip positioning metadata lines if (line.includes("align:") || line.includes("position:")) continue; // Skip empty lines if (line.trim() === "") continue; // Clean up the line by removing timestamp tags like <00:00:07.759> const cleanedLine = line .replace(/<\d{2}:\d{2}:\d{2}\.\d{3}>|<\/c>/g, "") .replace(/<c>/g, ""); if (cleanedLine.trim() !== "") { textLines.push(cleanedLine.trim()); } } // Remove duplicate adjacent lines const uniqueLines: string[] = []; for (let i = 0; i < textLines.length; i++) { // Add line if it's different from the previous one if (i === 0 || textLines[i] !== textLines[i - 1]) { uniqueLines.push(textLines[i]); } } return uniqueLines.join("\n"); } async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); } runServer().catch(console.error);

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/anaisbetts/mcp-youtube'

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