list-transcript-languages
List available transcript languages for a video by providing its URL. Works across supported platforms to show subtitle options.
Instructions
List all available transcript languages for a video from any supported platform.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| url | Yes | Video URL from any supported platform (YouTube, Bilibili, Vimeo, etc.) |
Implementation Reference
- src/index.ts:125-142 (registration)Tool name 'list-transcript-languages' registered in the ListToolsRequestSchema handler with inputSchema requiring a 'url' string parameter.
{ name: "list-transcript-languages", description: "List all available transcript languages for a video from any supported platform.", inputSchema: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { url: { type: "string", description: "Video URL from any supported platform (YouTube, Bilibili, Vimeo, etc.)", }, }, required: ["url"], additionalProperties: false, }, }, - src/index.ts:586-635 (handler)The handleListLanguages method on TranscriptMCPServer class. Validates URL via validateUrl(), calls this.fetcher.listLanguages(url), formats and returns the list of available transcript languages.
private async handleListLanguages(args: { url: string }): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean; }> { const { url } = args; // Validate URL validateUrl(url); try { const languages = await this.fetcher.listLanguages(url); if (languages.length === 0) { throw new Error("No transcripts are available for this video."); } // Format the output const languageList = languages .map((lang) => { const autoGenLabel = lang.isAutoGenerated ? " (auto-generated)" : ""; return ` - ${lang.code}: ${lang.name}${autoGenLabel}`; }) .join("\n"); const result = [ `Video URL: ${url}`, `\nAvailable transcript languages (${languages.length}):`, languageList, `\nTo get a transcript in a specific language, use the get-transcript tool with the 'lang' parameter.`, languages.length > 0 ? `Example: lang='${languages[0].code}' for ${languages[0].name}` : "", ] .filter(Boolean) .join("\n"); return { content: [ { type: "text", text: result, }, ], }; } catch (error) { throw new Error( `Could not fetch transcript information: ${error instanceof Error ? error.message : String(error)}`, ); } } - src/index.ts:129-141 (schema)Input schema for list-transcript-languages: requires a 'url' string property.
inputSchema: { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { url: { type: "string", description: "Video URL from any supported platform (YouTube, Bilibili, Vimeo, etc.)", }, }, required: ["url"], additionalProperties: false, }, - src/transcript-fetcher.ts:80-150 (helper)The listLanguages method on TranscriptFetcher class. Runs yt-dlp --list-subs --skip-download and parses the output to extract available transcript languages.
async listLanguages(url: string): Promise<TranscriptLanguage[]> { // Ensure yt-dlp is available if (!(await this.checkYtDlpAvailable())) { throw new YtDlpNotFoundError(); } this.log(`Listing languages for: ${url}`); try { // Use --list-subs to get available subtitles const command = `${this.ytDlpPath} --list-subs --skip-download "${url}"`; this.log(`Executing: ${command}`); const { stdout, stderr } = await execAsync(command, { maxBuffer: 10 * 1024 * 1024, // 10MB buffer }); this.log("yt-dlp stdout:", stdout); if (stderr) { this.log("yt-dlp stderr:", stderr); } // Parse the output to extract language information const languages = this.parseLanguageList(stdout, url); if (languages.length === 0) { // Check if video is unavailable if ( stdout.includes("Video unavailable") || stdout.includes("Private video") ) { throw new VideoUnavailableError(url); } throw new TranscriptNotFoundError( url, "No subtitles found in yt-dlp output", ); } return languages; } catch (error) { if ( error instanceof YtDlpNotFoundError || error instanceof VideoUnavailableError || error instanceof TranscriptNotFoundError ) { throw error; } const errorMessage = error instanceof Error ? error.message : String(error); this.log("Error listing languages:", errorMessage); // Check for common error patterns if ( errorMessage.includes("Video unavailable") || errorMessage.includes("Private video") ) { throw new VideoUnavailableError(url); } if ( errorMessage.includes("No subtitles") || errorMessage.includes("Subtitles not available") ) { throw new TranscriptNotFoundError(url); } throw new YtDlpError(url, errorMessage); } } - src/transcript-fetcher.ts:155-240 (helper)The parseLanguageList helper method that parses yt-dlp --list-subs output into TranscriptLanguage objects with code, name, and isAutoGenerated flags.
private parseLanguageList(output: string, url: string): TranscriptLanguage[] { const languages: TranscriptLanguage[] = []; const lines = output.split("\n"); // Look for subtitle language lines // Format is typically: "Language Name (language_code) - subtitle format" // or "Available subtitles for ..." let inSubtitleSection = false; for (const line of lines) { const trimmed = line.trim(); // Check if we're in the subtitle section if ( trimmed.toLowerCase().includes("available subtitles") || trimmed.toLowerCase().includes("subtitles") ) { inSubtitleSection = true; continue; } if (!inSubtitleSection) continue; // Skip empty lines and separators if (!trimmed || trimmed.startsWith("-")) continue; // Try to match language patterns // Pattern 1: "Language Name (code)" or "Language Name - code" const match1 = trimmed.match( /^([^(]+?)\s*[\(-]\s*([a-z]{2}(?:-[A-Z]{2})?)/i, ); if (match1) { const name = match1[1].trim(); const code = match1[2].toLowerCase(); languages.push({ code, name, isAutoGenerated: name.toLowerCase().includes("auto") || name.toLowerCase().includes("generated"), }); continue; } // Pattern 2: Just language code const match2 = trimmed.match(/^([a-z]{2}(?:-[A-Z]{2})?)\s*[-:]/i); if (match2) { const code = match2[1].toLowerCase(); languages.push({ code, name: code, }); continue; } } // If no languages found with patterns, try to extract from yt-dlp JSON output if (languages.length === 0) { // Sometimes yt-dlp outputs JSON when using --dump-json try { const jsonMatch = output.match(/\{[\s\S]*\}/); if (jsonMatch) { const json = JSON.parse(jsonMatch[0]); if (json.subtitles) { for (const [code, subs] of Object.entries( json.subtitles as Record<string, unknown[]>, )) { if (Array.isArray(subs) && subs.length > 0) { languages.push({ code: code.toLowerCase(), name: code, isAutoGenerated: (subs[0] as { ext?: string }).ext === "vtt" || code.includes("auto"), }); } } } } } catch { // Ignore JSON parsing errors } } return languages; }