From 0e5a30d10c0062a008cc122feaf93b4d753662d9 Mon Sep 17 00:00:00 2001 From: kevinwatt Date: Mon, 5 Jan 2026 01:47:59 +0800 Subject: [PATCH] fix: prevent server hang and output corruption in spawn handling (#23) - Add process 'error' event handler to catch spawn failures (e.g., yt-dlp not installed) - Separate stdout/stderr to prevent yt-dlp warnings from corrupting parsed output - Add try-catch for RegExp construction from YTDLP_SANITIZE_ILLEGAL_CHARS env var - Add NaN validation for YTDLP_MAX_FILENAME_LENGTH env var - Sync VERSION constant with package.json (0.8.4) - Update tests for new output format and null handling - Add version sync guidance to CLAUDE.md --- CHANGELOG.md | 13 +++++++++++++ CLAUDE.md | 3 +++ package-lock.json | 4 ++-- package.json | 2 +- src/__tests__/metadata.test.ts | 5 +++-- src/__tests__/search.test.ts | 2 +- src/config.ts | 19 ++++++++++++++++--- src/index.mts | 2 +- src/modules/utils.ts | 15 ++++++++++----- 9 files changed, 50 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ce4bab..5775e8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +--- + +## [0.8.4] - 2026-01-04 + +### Fixed +- **Critical**: Added missing process error handler in `_spawnPromise()` to prevent server hang when yt-dlp is not installed or fails to spawn ([#23](https://github.com/kevinwatt/yt-dlp-mcp/issues/23)) +- **Critical**: Fixed stdout/stderr mixing in `_spawnPromise()` that caused yt-dlp warnings to corrupt parsed output +- Fixed VERSION constant mismatch (was `0.7.0`, now synced with package.json) +- Added try-catch for RegExp construction from `YTDLP_SANITIZE_ILLEGAL_CHARS` env var to prevent startup crash on invalid regex +- Added validation for `YTDLP_MAX_FILENAME_LENGTH` env var to handle NaN values gracefully +- Fixed test expectations for search output format and metadata `creators` field null handling + ### Changed - **Documentation**: Added warning about JavaScript runtime (deno) requirement when using cookie authentication - YouTube authenticated API endpoints require JS challenge solving - Without deno, downloads will fail with "n challenge solving failed" error +- **Documentation**: Added version sync guidance to CLAUDE.md (package.json + src/index.mts) --- diff --git a/CLAUDE.md b/CLAUDE.md index 5eb0d15..04d0a38 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,6 +5,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Development Guidelines - **Always update CHANGELOG.md** when making changes to the codebase +- **Version updates require TWO files**: + 1. `package.json` - line 3: `"version": "x.x.x"` + 2. `src/index.mts` - line 24: `const VERSION = 'x.x.x'` ## Development Commands diff --git a/package-lock.json b/package-lock.json index 60f2180..702ad55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@kevinwatt/yt-dlp-mcp", - "version": "0.8.2", + "version": "0.8.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@kevinwatt/yt-dlp-mcp", - "version": "0.8.2", + "version": "0.8.3", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "0.7.0", diff --git a/package.json b/package.json index b2cfbb0..a52d4ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevinwatt/yt-dlp-mcp", - "version": "0.8.3", + "version": "0.8.4", "description": "An MCP server implementation that integrates with yt-dlp, providing video and audio content download capabilities (e.g. YouTube, Facebook, Tiktok, etc.) for LLMs.", "keywords": [ "mcp", diff --git a/src/__tests__/metadata.test.ts b/src/__tests__/metadata.test.ts index 86d4162..4450ba1 100644 --- a/src/__tests__/metadata.test.ts +++ b/src/__tests__/metadata.test.ts @@ -83,8 +83,9 @@ describe('Video Metadata Extraction', () => { if (metadata.description !== undefined) { expect(typeof metadata.description).toBe('string'); } - if (metadata.creators !== undefined) { - expect(Array.isArray(metadata.creators)).toBe(true); + if (metadata.creators !== undefined && metadata.creators !== null) { + // creators can be an array or a string depending on the video + expect(Array.isArray(metadata.creators) || typeof metadata.creators === 'string').toBe(true); } if (metadata.timestamp !== undefined) { expect(typeof metadata.timestamp).toBe('number'); diff --git a/src/__tests__/search.test.ts b/src/__tests__/search.test.ts index 96e6b45..95ab403 100644 --- a/src/__tests__/search.test.ts +++ b/src/__tests__/search.test.ts @@ -48,7 +48,7 @@ describe('Search functionality tests', () => { const result = await searchVideos('react tutorial', 2, 0, 'markdown', CONFIG); // Check for proper formatting - expect(result).toMatch(/Found \d+ videos?:/); + expect(result).toMatch(/Found \d+ videos? \(showing \d+\):/); expect(result).toMatch(/\d+\. \*\*.*\*\*/); // Numbered list with bold titles expect(result).toMatch(/📺 Channel: .+/); expect(result).toMatch(/⏱️ Duration: .+/); diff --git a/src/config.ts b/src/config.ts index fbc675b..56341be 100644 --- a/src/config.ts +++ b/src/config.ts @@ -109,13 +109,26 @@ function loadEnvConfig(): DeepPartial { sanitize: { replaceChar: process.env.YTDLP_SANITIZE_REPLACE_CHAR, truncateSuffix: process.env.YTDLP_SANITIZE_TRUNCATE_SUFFIX, - illegalChars: process.env.YTDLP_SANITIZE_ILLEGAL_CHARS ? new RegExp(process.env.YTDLP_SANITIZE_ILLEGAL_CHARS) : undefined, + illegalChars: (() => { + if (!process.env.YTDLP_SANITIZE_ILLEGAL_CHARS) return undefined; + try { + return new RegExp(process.env.YTDLP_SANITIZE_ILLEGAL_CHARS); + } catch { + console.warn('[yt-dlp-mcp] Invalid regex in YTDLP_SANITIZE_ILLEGAL_CHARS, using default'); + return undefined; + } + })(), reservedNames: process.env.YTDLP_SANITIZE_RESERVED_NAMES?.split(',') } }; - + if (process.env.YTDLP_MAX_FILENAME_LENGTH) { - fileConfig.maxFilenameLength = parseInt(process.env.YTDLP_MAX_FILENAME_LENGTH); + const parsed = parseInt(process.env.YTDLP_MAX_FILENAME_LENGTH, 10); + if (!isNaN(parsed) && parsed >= 5) { + fileConfig.maxFilenameLength = parsed; + } else { + console.warn('[yt-dlp-mcp] Invalid YTDLP_MAX_FILENAME_LENGTH, using default'); + } } if (process.env.YTDLP_DOWNLOADS_DIR) { fileConfig.downloadsDir = process.env.YTDLP_DOWNLOADS_DIR; diff --git a/src/index.mts b/src/index.mts index f403aaf..e336d22 100644 --- a/src/index.mts +++ b/src/index.mts @@ -21,7 +21,7 @@ import { searchVideos } from "./modules/search.js"; import { getVideoMetadata, getVideoMetadataSummary } from "./modules/metadata.js"; import { getVideoComments, getVideoCommentsSummary } from "./modules/comments.js"; -const VERSION = '0.7.0'; +const VERSION = '0.8.4'; // Response format enum enum ResponseFormat { diff --git a/src/modules/utils.ts b/src/modules/utils.ts index a1efb49..e05b5d0 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -91,21 +91,26 @@ export async function safeCleanup(directory: string): Promise { export function _spawnPromise(command: string, args: string[]): Promise { return new Promise((resolve, reject) => { const process = spawn(command, args); - let output = ''; + let stdout = ''; + let stderr = ''; + + process.on('error', (err) => { + reject(new Error(`Failed to spawn ${command}: ${err.message}`)); + }); process.stdout.on('data', (data) => { - output += data.toString(); + stdout += data.toString(); }); process.stderr.on('data', (data) => { - output += data.toString(); + stderr += data.toString(); }); process.on('close', (code) => { if (code === 0) { - resolve(output); + resolve(stdout); } else { - reject(new Error(`Failed with exit code: ${code}\n${output}`)); + reject(new Error(`Failed with exit code: ${code}\n${stderr}\n${stdout}`)); } }); });