diff --git a/mcp-youtube/.gitignore b/mcp-youtube/.gitignore new file mode 100644 index 0000000..3c25e1e --- /dev/null +++ b/mcp-youtube/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.log diff --git a/mcp-youtube/README.md b/mcp-youtube/README.md new file mode 100644 index 0000000..e08915f --- /dev/null +++ b/mcp-youtube/README.md @@ -0,0 +1,54 @@ +# mcp-youtube + +A [Model Context Protocol](https://modelcontextprotocol.io) server that lets +Claude read YouTube video subtitles, so it can summarize and answer questions +about video content. + +## How it works + +The server exposes a single tool, `download_youtube_url`, which uses +[`yt-dlp`](https://github.com/yt-dlp/yt-dlp) to fetch the English subtitles +(manual or auto-generated) for a video, cleans out timing/markup noise, and +returns the transcript text. + +## Prerequisites + +- [Node.js](https://nodejs.org) 18+ +- [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) available on your `PATH` + - macOS: `brew install yt-dlp` + - Linux: `pipx install yt-dlp` (or your package manager) + - Windows: `winget install yt-dlp` + +## Installation + +Add it to your MCP client configuration (e.g. `claude_desktop_config.json` +for Claude Desktop): + +```json +{ + "mcpServers": { + "youtube": { + "command": "npx", + "args": ["-y", "@anaisbetts/mcp-youtube"] + } + } +} +``` + +Or with Claude Code: + +```bash +claude mcp add youtube -- npx -y @anaisbetts/mcp-youtube +``` + +## Development + +```bash +bun install # install dependencies +bun test # run tests +bun build --target node src/index.ts --outdir dist # build +``` + +## License + +MIT diff --git a/mcp-youtube/bun.lock b/mcp-youtube/bun.lock new file mode 100644 index 0000000..424b77d --- /dev/null +++ b/mcp-youtube/bun.lock @@ -0,0 +1,127 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@anaisbetts/mcp-youtube", + "dependencies": { + "@modelcontextprotocol/sdk": "^0.6.0", + "rimraf": "^6.0.1", + "spawn-rx": "^4.0.0", + }, + "devDependencies": { + "bun-types": "latest", + "shx": "^0.3.4", + }, + }, + }, + "packages": { + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@0.6.1", "", { "dependencies": { "content-type": "^1.0.5", "raw-body": "^3.0.0", "zod": "^3.23.8" } }, "sha512-OkVXMix3EIbB5Z6yife2XTrSlOnVvCLR1Kg91I4pYFEsV9RbnoyQVScXCuVhGaZHOnTZgso8lMQN1Po2TadGKQ=="], + + "@types/node": ["@types/node@26.2.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg=="], + + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "interpret": ["interpret@1.4.0", "", {}, "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="], + + "is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="], + + "lodash.assign": ["lodash.assign@4.2.0", "", {}, "sha512-hFuH8TY+Yji7Eja3mGiuAxBqLagejScbG8GbG0j6o9vzn0YL14My+ktnqtZgFTosKymC9/44wP6s7xyuLfnClw=="], + + "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], + + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "rechoir": ["rechoir@0.6.2", "", { "dependencies": { "resolve": "^1.1.6" } }, "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + + "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shelljs": ["shelljs@0.8.5", "", { "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", "rechoir": "^0.6.2" }, "bin": { "shjs": "bin/shjs" } }, "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow=="], + + "shx": ["shx@0.3.4", "", { "dependencies": { "minimist": "^1.2.3", "shelljs": "^0.8.5" }, "bin": { "shx": "lib/cli.js" } }, "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g=="], + + "spawn-rx": ["spawn-rx@4.0.0", "", { "dependencies": { "debug": "^4.3.7", "lodash.assign": "^4.2.0", "rxjs": "^7.8.1" } }, "sha512-N4cXZu2xWaIq39IcwcibBhhtArRt43/P3nHI5c1GVmlqbtN4oM6VuumdcZYs3TsH4Z17dsTRpqJls3Y8Foy26w=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "shelljs/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "shelljs/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "shelljs/glob/minimatch/brace-expansion": ["brace-expansion@1.1.18", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw=="], + + "shelljs/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + } +} diff --git a/mcp-youtube/package.json b/mcp-youtube/package.json new file mode 100644 index 0000000..6e487b6 --- /dev/null +++ b/mcp-youtube/package.json @@ -0,0 +1,25 @@ +{ + "name": "@anaisbetts/mcp-youtube", + "version": "0.7.3", + "bin": { + "mcp-youtube": "dist/index.js" + }, + "description": "YouTube downloader for MCP", + "type": "module", + "main": "dist/index.js", + "scripts": { + "prepublish": "git clean -xdf dist && bun build --target node src/index.ts --outdir dist", + "test": "bun test" + }, + "author": "Ani Betts ", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^0.6.0", + "rimraf": "^6.0.1", + "spawn-rx": "^4.0.0" + }, + "devDependencies": { + "shx": "^0.3.4", + "bun-types": "latest" + } +} diff --git a/mcp-youtube/src/index.ts b/mcp-youtube/src/index.ts new file mode 100644 index 0000000..19ced3f --- /dev/null +++ b/mcp-youtube/src/index.ts @@ -0,0 +1,149 @@ +#!/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 { spawnPromise } from "spawn-rx"; +import { rimraf } from "rimraf"; + +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; + +const server = new Server( + { + name: "mcp-youtube", + version: "0.7.3", + }, + { + 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 summarize a YouTube video.", + 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") { + return { + content: [ + { + type: "text", + text: `Unknown tool: ${request.params.name}`, + }, + ], + isError: true, + }; + } + + const { url } = request.params.arguments as { url: string }; + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "youtube-")); + + try { + await spawnPromise( + "yt-dlp", + [ + "--write-sub", + "--write-auto-sub", + "--skip-download", + "--sub-lang", + "en", + "--convert-subs", + "srt", + url, + ], + { cwd: tempDir, detached: true }, + ); + + let content = ""; + for (const file of fs.readdirSync(tempDir)) { + const fileContent = fs.readFileSync(path.join(tempDir, file), "utf8"); + content += `${file}\n====================\n${stripVttNoise(fileContent)}\n`; + } + + return { + content: [{ type: "text", text: content }], + isError: false, + }; + } catch (err) { + return { + content: [ + { + type: "text", + text: `Error downloading video: ${err}`, + }, + ], + isError: true, + }; + } finally { + rimraf.sync(tempDir); + } +}); + +// Auto-generated subtitle files repeat every cue several times and carry +// timing/positioning noise that wastes context; collapse them to plain text. +export function stripVttNoise(subtitles: string): string { + const seen = new Set(); + const lines: string[] = []; + + for (const rawLine of subtitles.split(/\r?\n/)) { + const line = rawLine + .replace(/<[^>]+>/g, "") + .replace(/\[Music\]|\[Applause\]/gi, "") + .trim(); + + if ( + line === "" || + /^\d+$/.test(line) || + /^WEBVTT/.test(line) || + /^(Kind|Language):/.test(line) || + /-->/.test(line) + ) { + continue; + } + + if (!seen.has(line)) { + seen.add(line); + lines.push(line); + } + } + + return lines.join("\n"); +} + +async function runServer() { + const transport = new StdioServerTransport(); + await server.connect(transport); +} + +// Only start the stdio server when executed directly, so tests can import +// helpers without spinning it up. +if (import.meta.url === `file://${process.argv[1]}`) { + runServer().catch((err) => { + console.error(err); + process.exit(1); + }); +} diff --git a/mcp-youtube/test/strip-vtt-noise.test.ts b/mcp-youtube/test/strip-vtt-noise.test.ts new file mode 100644 index 0000000..565a78a --- /dev/null +++ b/mcp-youtube/test/strip-vtt-noise.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from "bun:test"; +import { stripVttNoise } from "../src/index"; + +describe("stripVttNoise", () => { + it("removes WEBVTT headers and timing lines", () => { + const input = [ + "WEBVTT", + "Kind: captions", + "Language: en", + "", + "00:00:00.000 --> 00:00:02.000", + "hello world", + ].join("\n"); + + expect(stripVttNoise(input)).toBe("hello world"); + }); + + it("deduplicates repeated cues from auto-generated subtitles", () => { + const input = [ + "00:00:00.000 --> 00:00:02.000", + "hello world", + "00:00:02.000 --> 00:00:04.000", + "hello world", + "and more", + ].join("\n"); + + expect(stripVttNoise(input)).toBe("hello world\nand more"); + }); + + it("strips inline tags and sound annotations", () => { + const input = [ + "<00:00:01.000>hello there", + "[Music]", + "[Applause]", + ].join("\n"); + + expect(stripVttNoise(input)).toBe("hello there"); + }); + + it("drops SRT sequence numbers", () => { + const input = ["1", "00:00:00,000 --> 00:00:02,000", "first line"].join( + "\n", + ); + + expect(stripVttNoise(input)).toBe("first line"); + }); +}); diff --git a/mcp-youtube/tsconfig.json b/mcp-youtube/tsconfig.json new file mode 100644 index 0000000..10d1ffb --- /dev/null +++ b/mcp-youtube/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["bun-types"] + }, + "include": ["src/**/*"] +}