Skip to main content
Glama
index.ts13 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, Tool, Resource, } from "@modelcontextprotocol/sdk/types.js"; import { readFileSync, readdirSync, statSync, mkdirSync, existsSync } from "fs"; import { join } from "path"; import { homedir } from "os"; import { transcribeVideo, checkDependencies, listSupportedSites, WhisperModel } from "./transcriber.js"; const OUTPUT_DIR = join(homedir(), "Downloads", "video-transcripts"); // Ensure output directory exists if (!existsSync(OUTPUT_DIR)) { mkdirSync(OUTPUT_DIR, { recursive: true }); } class VideoTranscriberServer { private server: Server; constructor() { this.server = new Server( { name: "video-transcriber", version: "1.1.1", }, { capabilities: { tools: {}, resources: {}, }, } ); this.setupHandlers(); this.server.onerror = (error) => console.error("[MCP Error]", error); process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: "transcribe_video", description: "Transcribe videos from 1000+ platforms (YouTube, Vimeo, TikTok, Twitter, etc.) or local video files using OpenAI Whisper. Downloads/extracts audio and generates transcript in TXT, JSON, and Markdown formats. Requires yt-dlp, whisper, and ffmpeg to be installed.", inputSchema: { type: "object", properties: { url: { type: "string", description: "Video URL from any supported platform (YouTube, Vimeo, TikTok, Twitter, Facebook, Instagram, Twitch, conference sites, and 1000+ more) OR absolute/relative path to a local video file (mp4, avi, mov, mkv, etc.)", }, output_dir: { type: "string", description: `Optional output directory path. Defaults to ${OUTPUT_DIR}`, }, model: { type: "string", enum: ["tiny", "base", "small", "medium", "large"], description: "Whisper model to use. Larger models are more accurate but slower. Default: 'base'", }, language: { type: "string", description: "Language code (ISO 639-1: en, es, fr, de, etc.) or 'auto' for automatic detection. Default: 'auto'", }, }, required: ["url"], }, }, { name: "list_transcripts", description: "List all available transcripts in the output directory", inputSchema: { type: "object", properties: { output_dir: { type: "string", description: `Optional directory path to list. Defaults to ${OUTPUT_DIR}`, }, }, }, }, { name: "check_dependencies", description: "Check if all required dependencies (yt-dlp, whisper, ffmpeg) are installed", inputSchema: { type: "object", properties: {}, }, }, { name: "list_supported_sites", description: "List all video platforms supported by yt-dlp (1000+ sites including YouTube, Vimeo, TikTok, Twitter, Facebook, Instagram, educational platforms, and more)", inputSchema: { type: "object", properties: {}, }, }, ] as Tool[], })); // List available resources (transcripts) this.server.setRequestHandler(ListResourcesRequestSchema, async () => { try { if (!existsSync(OUTPUT_DIR)) { return { resources: [] }; } const files = readdirSync(OUTPUT_DIR); const transcripts = files .filter(f => f.endsWith('.txt') || f.endsWith('.md') || f.endsWith('.json')) .map(f => { const fullPath = join(OUTPUT_DIR, f); const stats = statSync(fullPath); return { uri: `file://${fullPath}`, name: f, description: `Transcript file (${(stats.size / 1024).toFixed(2)} KB, modified ${stats.mtime.toLocaleDateString()})`, mimeType: f.endsWith('.json') ? 'application/json' : f.endsWith('.md') ? 'text/markdown' : 'text/plain', } as Resource; }); return { resources: transcripts }; } catch (error) { console.error("[MCP] Error listing resources:", error); return { resources: [] }; } }); // Read a specific transcript resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const filePath = request.params.uri.replace('file://', ''); try { const content = readFileSync(filePath, 'utf-8'); return { contents: [ { uri: request.params.uri, mimeType: filePath.endsWith('.json') ? 'application/json' : filePath.endsWith('.md') ? 'text/markdown' : 'text/plain', text: content, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to read file: ${errorMessage}`); } }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { if (name === "transcribe_video") { return await this.handleTranscribeVideo( args?.url as string, args?.output_dir as string | undefined, args?.model as WhisperModel | undefined, args?.language as string | undefined ); } else if (name === "list_transcripts") { return await this.handleListTranscripts(args?.output_dir as string | undefined); } else if (name === "check_dependencies") { return await this.handleCheckDependencies(); } else if (name === "list_supported_sites") { return await this.handleListSupportedSites(); } else { throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`[MCP] Tool error (${name}):`, error); const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `Error: ${errorMessage}`, }, ], isError: true, }; } }); } private async handleCheckDependencies() { try { checkDependencies(); return { content: [ { type: "text", text: "✅ All dependencies are installed:\n - yt-dlp\n - whisper\n - ffmpeg", }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `❌ ${errorMessage}`, }, ], isError: true, }; } } private async handleTranscribeVideo( url: string, outputDir?: string, model?: WhisperModel, language?: string ) { const dir = outputDir || OUTPUT_DIR; // Ensure output directory exists if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } console.error(`[MCP] Starting transcription for: ${url}`); const progressLog: string[] = []; const onProgress = (message: string) => { console.error(`[MCP] ${message}`); progressLog.push(message); }; try { const result = await transcribeVideo({ url, outputDir: dir, model: model || "base", language: language || "auto", onProgress, }); console.error(`[MCP] Transcription complete!`); return { content: [ { type: "text", text: `✅ Video transcribed successfully! **Video Details:** - Title: ${result.metadata.title} - Platform: ${result.metadata.platform} - Channel: ${result.metadata.channel} - Video ID: ${result.metadata.videoId} - Duration: ${Math.floor(result.metadata.duration / 60)}:${String(result.metadata.duration % 60).padStart(2, '0')} **Transcription Settings:** - Model: ${model || 'base'} - Language: ${language || 'auto'} **Output Files:** - Text: ${result.files.txt} - JSON: ${result.files.json} - Markdown: ${result.files.md} **Transcript Preview:** ${result.transcriptPreview}${result.transcript.length > 500 ? '...' : ''} **Full transcript has ${result.transcript.split(/\s+/).length} words.** You can now read the full transcript using the file paths above.`, }, ], }; } catch (error) { console.error(`[MCP] Transcription failed:`, error); throw error; } } private async handleListSupportedSites() { try { const sites = await listSupportedSites(); const siteCount = sites.length; const preview = sites.slice(0, 50).join(', '); return { content: [ { type: "text", text: `📺 Supported Video Platforms (${siteCount} total) **Popular platforms include:** - YouTube - Vimeo - TikTok - Twitter/X - Facebook - Instagram - Twitch - Dailymotion - Reddit - LinkedIn - Many educational and conference platforms **First 50 extractors:** ${preview}${siteCount > 50 ? '...' : ''} **Total: ${siteCount} supported extractors** You can transcribe videos from any of these platforms by providing the video URL!`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: `❌ ${errorMessage}`, }, ], isError: true, }; } } private async handleListTranscripts(outputDir?: string) { const dir = outputDir || OUTPUT_DIR; try { if (!existsSync(dir)) { return { content: [ { type: "text", text: `📂 No transcripts directory found at: ${dir}\n\nTranscribe your first video to create it!`, }, ], }; } const files = readdirSync(dir); const transcripts = files.filter(f => f.endsWith('.txt') || f.endsWith('.md')); if (transcripts.length === 0) { return { content: [ { type: "text", text: `📂 No transcripts found in ${dir}\n\nTranscribe a video to get started!`, }, ], }; } // Group by video ID const videoGroups = new Map<string, string[]>(); transcripts.forEach(f => { const videoId = f.split('-')[0]; if (!videoGroups.has(videoId)) { videoGroups.set(videoId, []); } videoGroups.get(videoId)!.push(f); }); const list = Array.from(videoGroups.entries()).map(([videoId, files], i) => { const mainFile = files.find(f => f.endsWith('.txt')) || files[0]; const fullPath = join(dir, mainFile); const stats = statSync(fullPath); const title = mainFile.replace(`${videoId}-`, '').replace(/\.(txt|md|json)$/, '').replace(/-/g, ' '); return `${i + 1}. **${title}** Video ID: ${videoId} Files: ${files.length} (${files.map(f => f.split('.').pop()).join(', ')}) Size: ${(stats.size / 1024).toFixed(2)} KB Modified: ${stats.mtime.toLocaleDateString()} Path: ${fullPath}`; }).join('\n\n'); return { content: [ { type: "text", text: `📚 Available transcripts (${videoGroups.size} videos):\n\n${list}\n\n💡 Tip: You can read any transcript by asking me to read the file path shown above.`, }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to list transcripts: ${errorMessage}`); } } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Video Transcriber MCP Server v1.1.1 running on stdio"); console.error(`✨ Supports 1000+ video platforms via yt-dlp`); console.error(`📂 Output directory: ${OUTPUT_DIR}`); } } const server = new VideoTranscriberServer(); server.run().catch(console.error);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nhatvu148/video-transcriber-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server