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
This commit is contained in:
parent
47da207c57
commit
0e5a30d10c
13
CHANGELOG.md
13
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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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: .+/);
|
||||
|
||||
@ -109,13 +109,26 @@ function loadEnvConfig(): DeepPartial<Config> {
|
||||
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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -91,21 +91,26 @@ export async function safeCleanup(directory: string): Promise<void> {
|
||||
export function _spawnPromise(command: string, args: string[]): Promise<string> {
|
||||
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}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user