get_video_info
Get YouTube video metadata and transcript by supplying a video ID. Extract captions, timestamps, and video information via web scraping without API keys.
Instructions
Get information and transcript from a YouTube video
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| video_id | Yes | The YouTube video ID (11 characters) |
Implementation Reference
- src/scraper.ts:19-121 (handler)Core implementation: fetchVideoInfo function that fetches YouTube video metadata and caption tracks. Validates video ID, checks cache, fetches HTML, extracts player response via InnerTube API or HTML parsing, extracts metadata and caption tracks, and caches the result.
export async function fetchVideoInfo(videoId: string): Promise<{ metadata: VideoMetadata; captionTracks: CaptionTrack[] }> { // Validate video ID format with Zod try { VideoIdSchema.parse(videoId); } catch (error) { if (error instanceof z.ZodError) { throw new YouTubeError(error.errors[0].message, 'INVALID_ID'); } throw new YouTubeError('Invalid YouTube video ID format', 'INVALID_ID'); } // Check cache first const cached = videoInfoCache.get(videoId); if (cached) { logger.debug(`Cache hit for video ${videoId}`); return cached; } logger.info(`Fetching video info for ${videoId}`); const url = `https://www.youtube.com/watch?v=${videoId}`; try { // Fetch with retry logic const { html, response } = await withRetry(async () => { const response = await fetch(url, { headers: { 'User-Agent': USER_AGENT, 'Accept-Language': 'en-US,en;q=0.9', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', } }); if (!response.ok) { if (response.status === 404) { throw new YouTubeError('Video not found', 'NOT_FOUND'); } const error = new Error(`HTTP error ${response.status}`) as any; error.status = response.status; throw error; } const html = await response.text(); return { html, response }; }, { maxRetries: 3, baseDelay: 1000 }); // First try to extract InnerTube API key and use InnerTube API const apiKey = extractInnerTubeApiKey(html); let playerResponse: YouTubePlayerResponse | null = null; if (apiKey) { logger.debug('Attempting to fetch via InnerTube API'); playerResponse = await fetchViaInnerTube(videoId, apiKey); } // Fall back to extracting from HTML if InnerTube didn't work if (!playerResponse) { logger.debug('Falling back to HTML extraction'); playerResponse = extractPlayerResponse(html); } if (!playerResponse) { throw new YouTubeError('Could not extract video data from page', 'PARSE_ERROR'); } // Check if video is available if (playerResponse.playabilityStatus?.status !== 'OK') { const status = playerResponse.playabilityStatus?.status; const reason = playerResponse.playabilityStatus?.reason || 'Video is not available'; // Determine specific error type let errorCode: YouTubeErrorCode = 'PRIVATE'; if (reason.toLowerCase().includes('age')) { errorCode = 'AGE_RESTRICTED'; } else if (reason.toLowerCase().includes('region') || reason.toLowerCase().includes('country')) { errorCode = 'REGION_BLOCKED'; } throw new YouTubeError(reason, errorCode); } const metadata = extractMetadata(playerResponse); const captionTracks = extractCaptionTracks(playerResponse); const result = { metadata, captionTracks }; // Cache the result videoInfoCache.set(videoId, result); logger.debug(`Cached video info for ${videoId}`); return result; } catch (error) { logger.error(`Failed to fetch video ${videoId}:`, error); if (error instanceof YouTubeError) { throw error; } throw new YouTubeError( `Failed to fetch video: ${error instanceof Error ? error.message : 'Unknown error'}`, 'NETWORK_ERROR', error ); } } - src/index.ts:52-138 (handler)Tool execution handler for 'get_video_info'. Validates arguments via Zod schema, calls fetchVideoInfo and fetchTranscript, formats the response as JSON.
// Handle tool execution server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'get_video_info') { logger.warn(`Unknown tool requested: ${request.params.name}`); throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } logger.debug(`Tool called: ${request.params.name}`); // Validate arguments with Zod let args: z.infer<typeof GetVideoInfoArgsSchema>; try { args = GetVideoInfoArgsSchema.parse(request.params.arguments); } catch (error) { if (error instanceof z.ZodError) { throw new McpError( ErrorCode.InvalidParams, `Invalid parameters: ${error.errors.map(e => e.message).join(', ')}` ); } throw new McpError( ErrorCode.InvalidParams, 'Invalid parameters' ); } try { // Fetch video metadata and caption tracks const { metadata, captionTracks } = await fetchVideoInfo(args.video_id); let transcript = null; let transcriptError: string | undefined; // Try to fetch transcript if available if (captionTracks.length > 0) { // Prefer manually created captions over auto-generated const manualTrack = captionTracks.find(track => track.kind !== 'asr'); const trackToUse = manualTrack || captionTracks[0]; try { transcript = await fetchTranscript(trackToUse); } catch (error) { if (error instanceof YouTubeError) { transcriptError = error.message; } else { transcriptError = 'Failed to fetch transcript'; } } } else { transcriptError = 'No transcript available for this video'; } const response: VideoInfoResponse = { metadata, transcript, ...(transcriptError && !transcript ? { error: transcriptError } : {}) }; return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ] }; } catch (error) { if (error instanceof YouTubeError) { throw new McpError( ErrorCode.InternalError, error.message, { code: error.code, details: error.details } ); } throw new McpError( ErrorCode.InternalError, `Unexpected error: ${error instanceof Error ? error.message : 'Unknown error'}` ); } }); - src/validation.ts:9-11 (schema)GetVideoInfoArgsSchema: Zod schema validating the video_id argument (exactly 11 chars, alphanumeric plus underscore/dash).
export const GetVideoInfoArgsSchema = z.object({ video_id: VideoIdSchema }); - src/index.ts:30-50 (registration)Tool registration: 'get_video_info' is registered with name, description, and inputSchema listing required 'video_id' parameter.
server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'get_video_info', description: 'Get information and transcript from a YouTube video', inputSchema: { type: 'object', properties: { video_id: { type: 'string', description: 'The YouTube video ID (11 characters)', pattern: '^[a-zA-Z0-9_-]{11}$' } }, required: ['video_id'] } } ] }; }); - src/types.ts:19-23 (helper)VideoInfoResponse type: Defines the response shape returned by the 'get_video_info' tool, with metadata, transcript, and optional error fields.
export interface VideoInfoResponse { metadata: VideoMetadata; transcript: Transcript | null; error?: string; }