@mkterswingman/yt-mcp 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/setup.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { createInterface } from "node:readline";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
3
5
  import { loadConfig, saveConfig, PATHS, ensureConfigDir } from "../utils/config.js";
4
6
  import { TokenManager } from "../auth/tokenManager.js";
5
7
  import { runOAuthFlow } from "../auth/oauthFlow.js";
6
8
  import { hasSIDCookies } from "../utils/cookies.js";
9
+ import { getYtDlpVersion } from "../utils/ytdlpPath.js";
7
10
  function detectCli(name) {
8
11
  try {
9
12
  execSync(`${name} --version`, { stdio: "pipe" });
@@ -77,13 +80,31 @@ export async function runSetup() {
77
80
  }
78
81
  // ── Step 1: Check yt-dlp ──
79
82
  console.log("Step 1/5: Checking yt-dlp...");
80
- try {
81
- const ver = execSync("yt-dlp --version", { stdio: "pipe" }).toString().trim();
82
- console.log(` ✅ yt-dlp ${ver}`);
83
+ const ytdlpInfo = getYtDlpVersion();
84
+ if (ytdlpInfo) {
85
+ console.log(` ✅ yt-dlp ${ytdlpInfo.version} (${ytdlpInfo.source})`);
83
86
  }
84
- catch {
85
- console.log(" ⚠️ yt-dlp not found (subtitle features will be unavailable)");
86
- console.log(" 💡 Install later: https://github.com/yt-dlp/yt-dlp#installation");
87
+ else {
88
+ // Try running the postinstall download script
89
+ console.log(" yt-dlp not found, attempting download...");
90
+ try {
91
+ execSync("node scripts/download-ytdlp.mjs", {
92
+ cwd: join(dirname(fileURLToPath(import.meta.url)), "..", ".."),
93
+ stdio: "inherit",
94
+ });
95
+ const retryInfo = getYtDlpVersion();
96
+ if (retryInfo) {
97
+ console.log(` ✅ yt-dlp ${retryInfo.version} (${retryInfo.source})`);
98
+ }
99
+ else {
100
+ console.log(" ⚠️ yt-dlp download failed (subtitle features will be unavailable)");
101
+ console.log(" 💡 Install manually: https://github.com/yt-dlp/yt-dlp#installation");
102
+ }
103
+ }
104
+ catch {
105
+ console.log(" ⚠️ yt-dlp not found (subtitle features will be unavailable)");
106
+ console.log(" 💡 Install: https://github.com/yt-dlp/yt-dlp#installation");
107
+ }
87
108
  }
88
109
  // ── Step 2: Config ──
89
110
  console.log("Step 2/5: Initializing config...");
@@ -157,7 +157,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
157
157
  }
158
158
  // ── get_subtitles ──
159
159
  server.registerTool("get_subtitles", {
160
- description: "Download subtitles for a YouTube video. Accepts video ID or any YouTube URL (watch, shorts, youtu.be). Each language is fetched separately. Supports CSV output (timestamp + text columns).",
160
+ description: "Download subtitles for a YouTube video. Accepts video ID or any YouTube URL (watch, shorts, youtu.be). Each language is fetched separately. Supports CSV output (timestamp + text columns). If languages is omitted, defaults to English + Simplified Chinese (unavailable languages are silently skipped).",
161
161
  inputSchema: {
162
162
  video: z.string().min(1).describe("YouTube video ID or URL"),
163
163
  languages: z.array(z.string().min(1)).optional(),
@@ -170,6 +170,7 @@ export function registerSubtitleTools(server, config, tokenManager) {
170
170
  const videoId = resolveVideoInput(video);
171
171
  if (!videoId)
172
172
  return toolErr("INVALID_INPUT", `无法解析视频 ID: ${video}`);
173
+ const usingDefaults = !languages;
173
174
  const langs = languages ?? config.default_languages;
174
175
  const fmt = format ?? "vtt";
175
176
  const results = [];
@@ -183,11 +184,14 @@ export function registerSubtitleTools(server, config, tokenManager) {
183
184
  return toolErr("COOKIES_EXPIRED", "YouTube cookies have expired. Run: npx @mkterswingman/yt-mcp setup-cookies");
184
185
  }
185
186
  if (!dl.ok) {
186
- results.push({
187
- language: lang,
188
- status: "failed",
189
- error: dl.error,
190
- });
187
+ // When using default languages, silently skip unavailable ones
188
+ if (!usingDefaults) {
189
+ results.push({
190
+ language: lang,
191
+ status: "failed",
192
+ error: dl.error,
193
+ });
194
+ }
191
195
  }
192
196
  else if (dl.text) {
193
197
  results.push({
@@ -13,7 +13,7 @@ export const PATHS = {
13
13
  const DEFAULTS = {
14
14
  auth_url: "https://mkterswingman.com",
15
15
  api_url: "https://mkterswingman.com/mcp/yt",
16
- default_languages: ["en"],
16
+ default_languages: ["en", "zh-Hans"],
17
17
  batch_sleep_min_ms: 3000,
18
18
  batch_sleep_max_ms: 8000,
19
19
  batch_max_size: 10,
@@ -1,6 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { PATHS } from "./config.js";
4
+ import { getYtDlpPath } from "./ytdlpPath.js";
4
5
  export function runYtDlp(args, timeoutMs = 45_000) {
5
6
  return new Promise((resolve, reject) => {
6
7
  const start = Date.now();
@@ -8,7 +9,7 @@ export function runYtDlp(args, timeoutMs = 45_000) {
8
9
  if (existsSync(PATHS.cookiesTxt)) {
9
10
  finalArgs.push("--cookies", PATHS.cookiesTxt);
10
11
  }
11
- const proc = spawn("yt-dlp", finalArgs, {
12
+ const proc = spawn(getYtDlpPath(), finalArgs, {
12
13
  stdio: ["ignore", "pipe", "pipe"],
13
14
  });
14
15
  const stdoutChunks = [];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Resolve the yt-dlp binary path.
3
+ *
4
+ * Priority:
5
+ * 1. YT_DLP_PATH env var (explicit override)
6
+ * 2. Bundled binary at <pkg>/bin/yt-dlp (downloaded by postinstall)
7
+ * 3. "yt-dlp" — fall back to system PATH
8
+ */
9
+ /**
10
+ * Returns the absolute path to the yt-dlp binary, or the bare command name
11
+ * "yt-dlp" if only available on system PATH.
12
+ */
13
+ export declare function getYtDlpPath(): string;
14
+ export interface YtDlpVersionInfo {
15
+ version: string;
16
+ source: "bundled" | "system" | "env";
17
+ }
18
+ /**
19
+ * Get the version string and source of the resolved yt-dlp binary.
20
+ * Returns null if yt-dlp is not available at all.
21
+ */
22
+ export declare function getYtDlpVersion(binPath?: string): YtDlpVersionInfo | null;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Resolve the yt-dlp binary path.
3
+ *
4
+ * Priority:
5
+ * 1. YT_DLP_PATH env var (explicit override)
6
+ * 2. Bundled binary at <pkg>/bin/yt-dlp (downloaded by postinstall)
7
+ * 3. "yt-dlp" — fall back to system PATH
8
+ */
9
+ import { existsSync } from "node:fs";
10
+ import { execFileSync } from "node:child_process";
11
+ import { join, dirname } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ const __dirname = dirname(fileURLToPath(import.meta.url));
14
+ // From dist/utils/ → package root is ../../
15
+ const pkgRoot = join(__dirname, "..", "..");
16
+ function bundledPath() {
17
+ const name = process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
18
+ return join(pkgRoot, "bin", name);
19
+ }
20
+ /**
21
+ * Returns the absolute path to the yt-dlp binary, or the bare command name
22
+ * "yt-dlp" if only available on system PATH.
23
+ */
24
+ export function getYtDlpPath() {
25
+ // 1. Explicit env override
26
+ const envPath = process.env.YT_DLP_PATH;
27
+ if (envPath && existsSync(envPath))
28
+ return envPath;
29
+ // 2. Bundled binary
30
+ const bundled = bundledPath();
31
+ if (existsSync(bundled))
32
+ return bundled;
33
+ // 3. System PATH
34
+ return "yt-dlp";
35
+ }
36
+ /**
37
+ * Get the version string and source of the resolved yt-dlp binary.
38
+ * Returns null if yt-dlp is not available at all.
39
+ */
40
+ export function getYtDlpVersion(binPath) {
41
+ const resolved = binPath ?? getYtDlpPath();
42
+ let source;
43
+ if (process.env.YT_DLP_PATH && resolved === process.env.YT_DLP_PATH) {
44
+ source = "env";
45
+ }
46
+ else if (resolved === bundledPath()) {
47
+ source = "bundled";
48
+ }
49
+ else {
50
+ source = "system";
51
+ }
52
+ try {
53
+ const ver = execFileSync(resolved, ["--version"], {
54
+ stdio: ["ignore", "pipe", "ignore"],
55
+ timeout: 10_000,
56
+ })
57
+ .toString()
58
+ .trim();
59
+ return { version: ver, source };
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/yt-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "YouTube MCP client — local subtitles + remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,8 @@
12
12
  "scripts": {
13
13
  "build": "tsc -p tsconfig.json",
14
14
  "dev": "tsx src/cli/index.ts",
15
- "start": "node dist/cli/index.js"
15
+ "start": "node dist/cli/index.js",
16
+ "postinstall": "node scripts/download-ytdlp.mjs"
16
17
  },
17
18
  "dependencies": {
18
19
  "@modelcontextprotocol/sdk": "^1.27.1",
@@ -34,6 +35,7 @@
34
35
  },
35
36
  "files": [
36
37
  "dist/",
38
+ "scripts/download-ytdlp.mjs",
37
39
  "README.md"
38
40
  ],
39
41
  "devDependencies": {
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * postinstall script — downloads yt-dlp binary for the current platform.
5
+ *
6
+ * Runs automatically on `npm install`. Uses only Node.js built-ins (no deps).
7
+ *
8
+ * Env overrides:
9
+ * YT_MCP_SKIP_YTDLP=1 — skip download entirely (CI / air-gapped)
10
+ * YT_MCP_YTDLP_MIRROR — custom base URL (default: GitHub releases)
11
+ */
12
+
13
+ import { createWriteStream, chmodSync, mkdirSync, existsSync } from "node:fs";
14
+ import { join, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { pipeline } from "node:stream/promises";
17
+
18
+ // ── Constants ────────────────────────────────────────────────────────────────
19
+
20
+ const YT_DLP_VERSION = "2026.03.17";
21
+
22
+ const BASE_URL =
23
+ process.env.YT_MCP_YTDLP_MIRROR ||
24
+ `https://github.com/yt-dlp/yt-dlp/releases/download/${YT_DLP_VERSION}`;
25
+
26
+ /** Map (platform, arch) → binary file name on GitHub Releases. */
27
+ const PLATFORM_MAP = {
28
+ "darwin-arm64": "yt-dlp_macos",
29
+ "darwin-x64": "yt-dlp_macos",
30
+ "linux-x64": "yt-dlp_linux",
31
+ "linux-arm64": "yt-dlp_linux_aarch64",
32
+ "win32-x64": "yt-dlp.exe",
33
+ };
34
+
35
+ // ── Helpers ──────────────────────────────────────────────────────────────────
36
+
37
+ const __dirname = dirname(fileURLToPath(import.meta.url));
38
+ const pkgRoot = join(__dirname, "..");
39
+ const binDir = join(pkgRoot, "bin");
40
+
41
+ function localName() {
42
+ return process.platform === "win32" ? "yt-dlp.exe" : "yt-dlp";
43
+ }
44
+
45
+ /**
46
+ * Follow redirects (GitHub releases → S3) and stream to disk.
47
+ * Uses global fetch() (Node 18+).
48
+ */
49
+ async function download(url, dest) {
50
+ const res = await fetch(url, { redirect: "follow" });
51
+ if (!res.ok) {
52
+ throw new Error(`HTTP ${res.status} ${res.statusText} — ${url}`);
53
+ }
54
+ const fileStream = createWriteStream(dest);
55
+ // res.body is a ReadableStream; convert to Node stream for pipeline
56
+ await pipeline(res.body, fileStream);
57
+ }
58
+
59
+ // ── Main ─────────────────────────────────────────────────────────────────────
60
+
61
+ async function main() {
62
+ // Allow skipping for CI or air-gapped environments
63
+ if (process.env.YT_MCP_SKIP_YTDLP === "1") {
64
+ console.log("[yt-mcp] YT_MCP_SKIP_YTDLP=1 — skipping yt-dlp download");
65
+ return;
66
+ }
67
+
68
+ const key = `${process.platform}-${process.arch}`;
69
+ const remoteName = PLATFORM_MAP[key];
70
+
71
+ if (!remoteName) {
72
+ console.warn(
73
+ `[yt-mcp] ⚠️ No pre-built yt-dlp binary for ${key}. ` +
74
+ `Install yt-dlp manually: https://github.com/yt-dlp/yt-dlp#installation`
75
+ );
76
+ return;
77
+ }
78
+
79
+ const dest = join(binDir, localName());
80
+
81
+ // Already downloaded?
82
+ if (existsSync(dest)) {
83
+ console.log(`[yt-mcp] yt-dlp already present at ${dest}`);
84
+ return;
85
+ }
86
+
87
+ mkdirSync(binDir, { recursive: true });
88
+
89
+ const url = `${BASE_URL}/${remoteName}`;
90
+ console.log(`[yt-mcp] Downloading yt-dlp ${YT_DLP_VERSION} for ${key}...`);
91
+ console.log(`[yt-mcp] ${url}`);
92
+
93
+ try {
94
+ await download(url, dest);
95
+
96
+ // chmod +x on non-Windows
97
+ if (process.platform !== "win32") {
98
+ chmodSync(dest, 0o755);
99
+ }
100
+
101
+ console.log(`[yt-mcp] ✅ yt-dlp installed to ${dest}`);
102
+ } catch (err) {
103
+ console.error(`[yt-mcp] ⚠️ Failed to download yt-dlp: ${err.message}`);
104
+ console.error(
105
+ `[yt-mcp] Install manually: https://github.com/yt-dlp/yt-dlp#installation`
106
+ );
107
+ // Non-fatal — remote tools still work without yt-dlp
108
+ }
109
+ }
110
+
111
+ main();