@mkterswingman/yt-mcp 0.3.6 → 0.3.7

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/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # yt-mcp
2
+
3
+ Local MCP server for YouTube workflows.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx @mkterswingman/yt-mcp setup
9
+ ```
10
+
11
+ The setup command configures auth, optional YouTube cookies, and prints MCP config for supported AI clients.
12
+
13
+ ## Commands
14
+
15
+ - `setup` — first-time setup
16
+ - `serve` — start the stdio MCP server
17
+ - `setup-cookies` — refresh YouTube cookies
18
+ - `update` — update to the latest npm version
19
+ - `version` — print the installed version
package/dist/cli/setup.js CHANGED
@@ -8,6 +8,8 @@ import { TokenManager } from "../auth/tokenManager.js";
8
8
  import { runOAuthFlow } from "../auth/oauthFlow.js";
9
9
  import { hasSIDCookies } from "../utils/cookies.js";
10
10
  import { getYtDlpPath, getYtDlpVersion } from "../utils/ytdlpPath.js";
11
+ import { buildLauncherCommand, writeLauncherFile } from "../utils/launcher.js";
12
+ import { MCP_REGISTER_TIMEOUT_MS, classifyRegistrationFailure, } from "../utils/mcpRegistration.js";
11
13
  function detectCli(name) {
12
14
  try {
13
15
  execSync(`${name} --version`, { stdio: "pipe" });
@@ -19,26 +21,36 @@ function detectCli(name) {
19
21
  }
20
22
  function tryRegisterMcp(cmd, label) {
21
23
  try {
22
- execSync(cmd, { stdio: "pipe" });
24
+ execSync(cmd, { stdio: "pipe", timeout: MCP_REGISTER_TIMEOUT_MS });
23
25
  console.log(` ✅ MCP registered in ${label}`);
24
26
  return true;
25
27
  }
26
28
  catch (err) {
27
- // "already exists" is still a success — the MCP is configured
28
- const stderr = err instanceof Error && "stderr" in err
29
- ? String(err.stderr)
30
- : "";
31
- const stdout = err instanceof Error && "stdout" in err
32
- ? String(err.stdout)
33
- : "";
34
- const output = stderr + stdout;
35
- if (output.includes("already exists")) {
29
+ const failure = classifyRegistrationFailure(err);
30
+ if (failure.kind === "already_exists") {
36
31
  console.log(` ✅ MCP already registered in ${label}`);
37
32
  return true;
38
33
  }
34
+ if (failure.kind === "authentication") {
35
+ console.log(` ⚠️ ${label} is waiting for authentication — skipped auto-register.`);
36
+ return false;
37
+ }
38
+ if (failure.kind === "timeout") {
39
+ console.log(` ⚠️ ${label} registration timed out after ${MCP_REGISTER_TIMEOUT_MS}ms — skipped auto-register.`);
40
+ return false;
41
+ }
39
42
  return false;
40
43
  }
41
44
  }
45
+ function quoteForShell(value) {
46
+ return `'${value.replace(/'/g, `'\\''`)}'`;
47
+ }
48
+ function quoteForCmd(value) {
49
+ return `"${value.replace(/"/g, '""')}"`;
50
+ }
51
+ function quoteArg(value) {
52
+ return process.platform === "win32" ? quoteForCmd(value) : quoteForShell(value);
53
+ }
42
54
  /**
43
55
  * Detect if we can open a browser (local machine with display).
44
56
  * Cloud environments (SSH, Docker, cloud IDE) typically can't.
@@ -129,7 +141,9 @@ export async function runSetup() {
129
141
  console.log("Step 2/5: Initializing config...");
130
142
  const config = loadConfig();
131
143
  saveConfig(config);
144
+ writeLauncherFile();
132
145
  console.log(` ✅ Config: ${PATHS.configJson}`);
146
+ console.log(` ✅ Launcher: ${PATHS.launcherJs}`);
133
147
  // ── Step 3: Authentication ──
134
148
  console.log("Step 3/5: Authentication...");
135
149
  const tokenManager = new TokenManager(config.auth_url);
@@ -238,7 +252,8 @@ export async function runSetup() {
238
252
  }
239
253
  // ── Step 5: MCP Registration ──
240
254
  console.log("Step 5/5: Registering MCP in AI clients...");
241
- const mcpArgs = "npx @mkterswingman/yt-mcp@latest serve";
255
+ const launcherCommand = buildLauncherCommand();
256
+ const mcpArgs = [launcherCommand.command, ...launcherCommand.args].map(quoteArg).join(" ");
242
257
  let registered = false;
243
258
  const cliCandidates = [
244
259
  // Claude Code: {bin} mcp add yt-mcp -- npx ... serve
@@ -286,8 +301,8 @@ export async function runSetup() {
286
301
  {
287
302
  "mcpServers": {
288
303
  "yt-mcp": {
289
- "command": "npx",
290
- "args": ["@mkterswingman/yt-mcp@latest", "serve"]
304
+ "command": "node",
305
+ "args": [${JSON.stringify(PATHS.launcherJs)}, "serve"]
291
306
  }
292
307
  }
293
308
  }
@@ -3,6 +3,8 @@ export declare const PATHS: {
3
3
  authJson: string;
4
4
  cookiesTxt: string;
5
5
  browserProfile: string;
6
+ launcherJs: string;
7
+ npmCacheDir: string;
6
8
  subtitlesDir: string;
7
9
  configJson: string;
8
10
  };
@@ -8,12 +8,15 @@ export const PATHS = {
8
8
  authJson: join(CONFIG_DIR, "auth.json"),
9
9
  cookiesTxt: join(CONFIG_DIR, "cookies.txt"),
10
10
  browserProfile: join(CONFIG_DIR, "browser-profile"),
11
+ launcherJs: join(CONFIG_DIR, "launcher.mjs"),
12
+ npmCacheDir: join(CONFIG_DIR, "npm-cache"),
11
13
  subtitlesDir: SUBTITLES_DIR,
12
14
  configJson: join(CONFIG_DIR, "config.json"),
13
15
  };
14
16
  const DEFAULTS = {
15
17
  auth_url: "https://mkterswingman.com",
16
18
  api_url: "https://mkterswingman.com/mcp/yt",
19
+ // Fixed list, not probed: --list-subs costs ~10s extra and YouTube has no "source language" concept
17
20
  default_languages: ["en", "zh-Hans"],
18
21
  batch_sleep_min_ms: 3000,
19
22
  batch_sleep_max_ms: 8000,
@@ -0,0 +1,12 @@
1
+ export interface LauncherSourceOptions {
2
+ packageSpec?: string;
3
+ npmCacheDir?: string;
4
+ }
5
+ export interface LauncherCommand {
6
+ command: string;
7
+ args: string[];
8
+ }
9
+ export declare function buildLauncherCommand(launcherPath?: string): LauncherCommand;
10
+ export declare function isRepairableNpxFailure(stderr: string): boolean;
11
+ export declare function buildLauncherSource(options?: LauncherSourceOptions): string;
12
+ export declare function writeLauncherFile(options?: LauncherSourceOptions): string;
@@ -0,0 +1,82 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { PATHS } from "./config.js";
4
+ const DEFAULT_PACKAGE_SPEC = "@mkterswingman/yt-mcp@latest";
5
+ export function buildLauncherCommand(launcherPath = PATHS.launcherJs) {
6
+ return {
7
+ command: "node",
8
+ args: [launcherPath, "serve"],
9
+ };
10
+ }
11
+ export function isRepairableNpxFailure(stderr) {
12
+ const lower = stderr.toLowerCase();
13
+ const referencesNpxDir = lower.includes("_npx/") || lower.includes("_npx\\");
14
+ return referencesNpxDir && (lower.includes("enotempty") || lower.includes("rename"));
15
+ }
16
+ export function buildLauncherSource(options = {}) {
17
+ const packageSpec = options.packageSpec ?? DEFAULT_PACKAGE_SPEC;
18
+ const npmCacheDir = options.npmCacheDir ?? PATHS.npmCacheDir;
19
+ return `#!/usr/bin/env node
20
+ import { existsSync, mkdirSync, renameSync } from "node:fs";
21
+ import { join } from "node:path";
22
+ import { spawnSync } from "node:child_process";
23
+
24
+ const packageSpec = ${JSON.stringify(packageSpec)};
25
+ const npmCacheDir = ${JSON.stringify(npmCacheDir)};
26
+ const args = process.argv.slice(2);
27
+ const targetArgs = args.length > 0 ? args : ["serve"];
28
+
29
+ function isRepairableNpxFailure(stderr) {
30
+ const lower = stderr.toLowerCase();
31
+ return (lower.includes("_npx/") || lower.includes("_npx\\"))
32
+ && (lower.includes("enotempty") || lower.includes("rename"));
33
+ }
34
+
35
+ function runNpx(subArgs, captureStdErrOnly = false) {
36
+ mkdirSync(npmCacheDir, { recursive: true });
37
+ const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
38
+ return spawnSync(npxBin, ["--yes", packageSpec, ...subArgs], {
39
+ env: { ...process.env, npm_config_cache: npmCacheDir },
40
+ // Why: probe runs before MCP starts; stdout must stay silent or it will corrupt stdio transport.
41
+ stdio: captureStdErrOnly ? ["ignore", "ignore", "pipe"] : "inherit",
42
+ });
43
+ }
44
+
45
+ function rotateNpxDir() {
46
+ const npxDir = join(npmCacheDir, "_npx");
47
+ if (!existsSync(npxDir)) return false;
48
+ const backup = \`\${npxDir}.bad.\${new Date().toISOString().replace(/[^0-9]/g, "").slice(0, 14)}\`;
49
+ renameSync(npxDir, backup);
50
+ process.stderr.write(\`[yt-mcp launcher] repaired corrupted npx cache: \${backup}\\n\`);
51
+ return true;
52
+ }
53
+
54
+ function ensurePackageReady() {
55
+ const first = runNpx(["version"], true);
56
+ if ((first.status ?? 1) === 0) return;
57
+
58
+ const stderr = first.stderr ? String(first.stderr) : "";
59
+ if (!isRepairableNpxFailure(stderr) || !rotateNpxDir()) {
60
+ if (stderr) process.stderr.write(stderr);
61
+ process.exit(first.status ?? 1);
62
+ }
63
+
64
+ const second = runNpx(["version"], true);
65
+ const secondStderr = second.stderr ? String(second.stderr) : "";
66
+ if ((second.status ?? 1) !== 0) {
67
+ if (secondStderr) process.stderr.write(secondStderr);
68
+ process.exit(second.status ?? 1);
69
+ }
70
+ }
71
+
72
+ ensurePackageReady();
73
+ const finalRun = runNpx(targetArgs, false);
74
+ process.exit(finalRun.status ?? 0);
75
+ `;
76
+ }
77
+ export function writeLauncherFile(options = {}) {
78
+ mkdirSync(dirname(PATHS.launcherJs), { recursive: true });
79
+ const source = buildLauncherSource(options);
80
+ writeFileSync(PATHS.launcherJs, source, { encoding: "utf8", mode: 0o755 });
81
+ return PATHS.launcherJs;
82
+ }
@@ -0,0 +1,7 @@
1
+ export declare const MCP_REGISTER_TIMEOUT_MS = 5000;
2
+ export type RegistrationFailureKind = "already_exists" | "authentication" | "timeout" | "other";
3
+ export interface RegistrationFailure {
4
+ kind: RegistrationFailureKind;
5
+ output: string;
6
+ }
7
+ export declare function classifyRegistrationFailure(err: unknown): RegistrationFailure;
@@ -0,0 +1,23 @@
1
+ export const MCP_REGISTER_TIMEOUT_MS = 5000;
2
+ function readOutput(err) {
3
+ if (!(err instanceof Error))
4
+ return "";
5
+ const stderr = "stderr" in err ? String(err.stderr ?? "") : "";
6
+ const stdout = "stdout" in err ? String(err.stdout ?? "") : "";
7
+ return `${stderr}${stdout}`;
8
+ }
9
+ export function classifyRegistrationFailure(err) {
10
+ const output = readOutput(err);
11
+ const lower = output.toLowerCase();
12
+ if (lower.includes("already exists")) {
13
+ return { kind: "already_exists", output };
14
+ }
15
+ if (lower.includes("waiting for authentication")) {
16
+ return { kind: "authentication", output };
17
+ }
18
+ if (err instanceof Error &&
19
+ ("code" in err && err.code === "ETIMEDOUT")) {
20
+ return { kind: "timeout", output };
21
+ }
22
+ return { kind: "other", output };
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mkterswingman/yt-mcp",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "YouTube MCP client — local subtitles + remote API proxy",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,8 @@ import { pipeline } from "node:stream/promises";
18
18
 
19
19
  // ── Constants ────────────────────────────────────────────────────────────────
20
20
 
21
+ // Pinned version: upgrade by changing this constant + publishing new npm version.
22
+ // Avoids auto-update breaking compat. postinstall (not optionalDeps) because yt-dlp is a standalone binary.
21
23
  const YT_DLP_VERSION = "2026.03.17";
22
24
 
23
25
  const BASE_URL =