{"url_pattern":"^https?://(www\\.)?youtube\\.com(/.*)?$","site_name":"youtube","allowed_domains":["youtube.com"],"tools":[{"name":"youtube_channel","description":"Get YouTube channel info and recent videos","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Channel ID (UCxxxx) or handle (@name). Defaults to current page channel."},"max":{"type":"string","description":"Max recent videos to return (default: 10)"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const cfg = window.ytcfg?.data_ || {};\n      const apiKey = cfg.INNERTUBE_API_KEY;\n      const context = cfg.INNERTUBE_CONTEXT;\n      if (!apiKey || !context) return {error: 'YouTube config not found', hint: 'Make sure you are on youtube.com'};\n\n      const max = Math.min(parseInt(args.max) || 10, 30);\n      let browseId = args.id || '';\n\n      // Detect from current page\n      if (!browseId) {\n        const match = location.href.match(/youtube\\.com\\/(channel\\/|c\\/|@)([^/?]+)/);\n        if (match) {\n          browseId = match[1] === 'channel/' ? match[2] : '@' + match[2].replace(/^@/, '');\n        }\n      }\n      if (!browseId) return {error: 'No channel ID or handle', hint: 'Provide a channel ID (UCxxxx) or handle (@name)'};\n\n      // If it's a handle, need to resolve it\n      let resolvedBrowseId = browseId;\n      if (browseId.startsWith('@')) {\n        const resolveResp = await fetch('/youtubei/v1/navigation/resolve_url?key=' + apiKey + '&prettyPrint=false', {\n          method: 'POST',\n          credentials: 'include',\n          headers: {'Content-Type': 'application/json'},\n          body: JSON.stringify({context, url: 'https://www.youtube.com/' + browseId})\n        });\n        if (resolveResp.ok) {\n          const resolveData = await resolveResp.json();\n          resolvedBrowseId = resolveData.endpoint?.browseEndpoint?.browseId || browseId;\n        }\n      }\n\n      // Fetch channel data\n      const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', {\n        method: 'POST',\n        credentials: 'include',\n        headers: {'Content-Type': 'application/json'},\n        body: JSON.stringify({context, browseId: resolvedBrowseId})\n      });\n\n      if (!resp.ok) return {error: 'Channel API returned HTTP ' + resp.status, hint: resp.status === 404 ? 'Channel not found' : 'API error'};\n      const data = await resp.json();\n\n      // Channel metadata\n      const metadata = data.metadata?.channelMetadataRenderer || {};\n      const header = data.header?.pageHeaderRenderer || data.header?.c4TabbedHeaderRenderer || {};\n\n      // Try to get subscriber count from header\n      let subscriberCount = '';\n      if (header.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows) {\n        const rows = header.content.pageHeaderViewModel.metadata.contentMetadataViewModel.metadataRows;\n        for (const row of rows) {\n          for (const part of (row.metadataParts || [])) {\n            const text = part.text?.content || '';\n            if (text.includes('subscriber')) subscriberCount = text;\n          }\n        }\n      }\n\n      // Get tabs to find Videos tab\n      const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];\n      const tabNames = tabs.map(t => t.tabRenderer?.title || t.expandableTabRenderer?.title).filter(Boolean);\n\n      // Extract recent videos from the Home or Videos tab\n      let recentVideos = [];\n\n      // Try Home tab first\n      const homeTab = tabs.find(t => t.tabRenderer?.selected);\n      if (homeTab) {\n        const sections = homeTab.tabRenderer?.content?.sectionListRenderer?.contents || [];\n        for (const section of sections) {\n          const shelfItems = section.itemSectionRenderer?.contents || [];\n          for (const shelf of shelfItems) {\n            const items = shelf.shelfRenderer?.content?.horizontalListRenderer?.items || [];\n            for (const item of items) {\n              const lvm = item.lockupViewModel;\n              if (lvm && lvm.contentType === 'LOCKUP_CONTENT_TYPE_VIDEO' && recentVideos.length < max) {\n                const meta = lvm.metadata?.lockupMetadataViewModel;\n                const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];\n                let viewsAndTime = (rows[0]?.metadataParts || []).map(p => p.text?.content).filter(Boolean).join(' | ');\n                const overlays = lvm.contentImage?.thumbnailViewModel?.overlays || [];\n                let duration = '';\n                for (const ov of overlays) {\n                  for (const b of (ov.thumbnailBottomOverlayViewModel?.badges || [])) {\n                    if (b.thumbnailBadgeViewModel?.text) duration = b.thumbnailBadgeViewModel.text;\n                  }\n                }\n                recentVideos.push({\n                  videoId: lvm.contentId,\n                  title: meta?.title?.content || '',\n                  duration,\n                  viewsAndTime,\n                  url: 'https://www.youtube.com/watch?v=' + lvm.contentId\n                });\n              }\n              // Also handle gridVideoRenderer (older format)\n              if (item.gridVideoRenderer && recentVideos.length < max) {\n                const v = item.gridVideoRenderer;\n                recentVideos.push({\n                  videoId: v.videoId,\n                  title: v.title?.runs?.[0]?.text || v.title?.simpleText || '',\n                  duration: v.thumbnailOverlays?.[0]?.thumbnailOverlayTimeStatusRenderer?.text?.simpleText || '',\n                  viewsAndTime: (v.shortViewCountText?.simpleText || '') + (v.publishedTimeText?.simpleText ? ' | ' + v.publishedTimeText.simpleText : ''),\n                  url: 'https://www.youtube.com/watch?v=' + v.videoId\n                });\n              }\n            }\n          }\n        }\n      }\n\n      return {\n        channelId: metadata.externalId || resolvedBrowseId,\n        name: metadata.title || '',\n        handle: metadata.vanityChannelUrl?.split('/').pop() || '',\n        description: (metadata.description || '').substring(0, 500),\n        subscriberCount,\n        channelUrl: metadata.channelUrl || 'https://www.youtube.com/channel/' + resolvedBrowseId,\n        keywords: metadata.keywords || '',\n        isFamilySafe: metadata.isFamilySafe,\n        tabs: tabNames,\n        recentVideoCount: recentVideos.length,\n        recentVideos\n      };\n  };\n  return run(params || {});\n}"},{"name":"youtube_comments","description":"Get comments for a YouTube video","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Video ID (defaults to current page video)"},"max":{"type":"string","description":"Max comments to return (default: 20)"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const currentUrl = location.href;\n      let videoId = args.id;\n      const max = Math.min(parseInt(args.max) || 20, 100);\n\n      if (!videoId) {\n        const match = currentUrl.match(/[?&]v=([a-zA-Z0-9_-]{11})/);\n        if (match) videoId = match[1];\n      }\n      if (!videoId) return {error: 'No video ID', hint: 'Provide a video ID or navigate to a YouTube video page'};\n\n      const cfg = window.ytcfg?.data_ || {};\n      const apiKey = cfg.INNERTUBE_API_KEY;\n      const context = cfg.INNERTUBE_CONTEXT;\n      if (!apiKey || !context) return {error: 'YouTube config not found', hint: 'Make sure you are on youtube.com'};\n\n      // Step 1: Get comment continuation token\n      let continuationToken = null;\n\n      // Try from current page ytInitialData first\n      if (currentUrl.includes('watch?v=' + videoId) && window.ytInitialData) {\n        const results = window.ytInitialData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];\n        const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');\n        continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;\n      }\n\n      // If not on the page, fetch via next API\n      if (!continuationToken) {\n        const nextResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {\n          method: 'POST',\n          credentials: 'include',\n          headers: {'Content-Type': 'application/json'},\n          body: JSON.stringify({context, videoId})\n        });\n        if (!nextResp.ok) return {error: 'Failed to get video data: HTTP ' + nextResp.status};\n        const nextData = await nextResp.json();\n        const results = nextData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];\n        const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');\n        continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;\n      }\n\n      if (!continuationToken) return {error: 'No comment section found', hint: 'Comments may be disabled for this video'};\n\n      // Step 2: Fetch comments using the continuation token\n      const commentResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {\n        method: 'POST',\n        credentials: 'include',\n        headers: {'Content-Type': 'application/json'},\n        body: JSON.stringify({context, continuation: continuationToken})\n      });\n\n      if (!commentResp.ok) return {error: 'Failed to fetch comments: HTTP ' + commentResp.status};\n      const commentData = await commentResp.json();\n\n      // Parse comments from frameworkUpdates (new ViewModel format)\n      const mutations = commentData.frameworkUpdates?.entityBatchUpdate?.mutations || [];\n      const commentEntities = mutations.filter(m => m.payload?.commentEntityPayload);\n\n      let headerInfo = null;\n      const actions = commentData.onResponseReceivedEndpoints || [];\n      for (const action of actions) {\n        const items = action.reloadContinuationItemsCommand?.continuationItems || [];\n        for (const item of items) {\n          if (item.commentsHeaderRenderer) {\n            headerInfo = item.commentsHeaderRenderer.countText?.runs?.map(r => r.text).join('') || '';\n          }\n        }\n      }\n\n      const comments = commentEntities.slice(0, max).map((m, i) => {\n        const p = m.payload.commentEntityPayload;\n        const props = p.properties || {};\n        const author = p.author || {};\n        const toolbar = p.toolbar || {};\n        return {\n          rank: i + 1,\n          author: author.displayName || '',\n          authorChannelId: author.channelId || '',\n          text: (props.content?.content || '').substring(0, 500),\n          publishedTime: props.publishedTime || '',\n          likes: toolbar.likeCountNotliked || '0',\n          replyCount: toolbar.replyCount || '0',\n          isPinned: !!(p.pinnedText)\n        };\n      });\n\n      return {\n        videoId,\n        commentCountText: headerInfo || '',\n        fetchedCount: comments.length,\n        comments\n      };\n  };\n  return run(params || {});\n}"},{"name":"youtube_get_video_info","description":"Get current video information","inputSchema":{"type":"object","properties":{},"required":null},"handler":"(params) => {\n  const title = document.querySelector('h1.ytd-video-primary-info-renderer')?.textContent?.trim();\n  const channel = document.querySelector('#channel-name a')?.textContent?.trim();\n  const views = document.querySelector('#info-strings yt-formatted-string')?.textContent;\n  const video = document.querySelector('video');\n  return {\n    success: true,\n    video: {\n      title,\n      channel,\n      views,\n      duration: video ? video.duration : null,\n      currentTime: video ? video.currentTime : null\n    }\n  };\n}"},{"name":"youtube_play_pause","description":"Play/pause the current video","inputSchema":{"type":"object","properties":{},"required":null},"handler":"(params) => {\n  const video = document.querySelector('video');\n  if (video) {\n    if (video.paused) {\n      video.play();\n      return { success: true, message: 'Video playing' };\n    } else {\n      video.pause();\n      return { success: true, message: 'Video paused' };\n    }\n  }\n  return { success: false, message: 'Video not found' };\n}"},{"name":"youtube_search","description":"Search for videos on YouTube","inputSchema":{"type":"object","properties":{"query":{"type":"string","description":"Search keywords"}},"required":["query"]},"handler":"(params) => {\n  const searchBox = document.querySelector('input#search');\n  if (searchBox) {\n    searchBox.value = params.query;\n    searchBox.dispatchEvent(new Event('input', { bubbles: true }));\n    const searchBtn = document.querySelector('#search-icon-legacy');\n    if (searchBtn) searchBtn.click();\n    return { success: true, message: 'Searched: ' + params.query };\n  }\n  return { success: false, message: 'Search box not found' };\n}"},{"name":"youtube_transcript","description":"Get video transcript/captions (must be on the video page)","inputSchema":{"type":"object","properties":{"lang":{"type":"string","description":"Language code (default: first available, e.g. 'en', 'ja')"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const currentUrl = location.href;\n      const match = currentUrl.match(/[?&]v=([a-zA-Z0-9_-]{11})/);\n      if (!match) return {error: 'Not on a video page', hint: 'Navigate to a YouTube video page first (youtube.com/watch?v=...)'};\n\n      const videoId = match[1];\n\n      // Get available tracks from ytInitialPlayerResponse\n      const playerResp = window.ytInitialPlayerResponse;\n      const trackList = playerResp?.captions?.playerCaptionsTracklistRenderer;\n      const tracks = trackList?.captionTracks || [];\n      const availableTracks = tracks.map(t => ({lang: t.languageCode, name: t.name?.simpleText, kind: t.kind}));\n\n      // Find the transcript engagement panel\n      const panel = document.querySelector('ytd-engagement-panel-section-list-renderer[target-id=\"engagement-panel-searchable-transcript\"]');\n      if (!panel) return {\n        error: 'No transcript panel found',\n        hint: 'This video may not have captions/subtitles available.',\n        videoId,\n        availableTracks\n      };\n\n      // If a specific language is requested, try to select it\n      if (args.lang && tracks.length > 1) {\n        const langTrack = tracks.find(t => t.languageCode === args.lang);\n        if (!langTrack) return {\n          error: 'Language \"' + args.lang + '\" not found',\n          hint: 'Available: ' + availableTracks.map(t => t.lang + ' (' + (t.name || t.kind || '') + ')').join(', '),\n          videoId\n        };\n      }\n\n      // Expand the transcript panel if hidden\n      const wasHidden = panel.getAttribute('visibility') !== 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED';\n      if (wasHidden) {\n        panel.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_EXPANDED');\n        await new Promise(r => setTimeout(r, 2000));\n      }\n\n      // If language selection needed, click the language dropdown\n      if (args.lang && tracks.length > 1) {\n        const menuBtn = panel.querySelector('yt-sort-filter-sub-menu-renderer button, #menu button');\n        if (menuBtn) {\n          menuBtn.click();\n          await new Promise(r => setTimeout(r, 500));\n          const menuItems = panel.querySelectorAll('tp-yt-paper-listbox tp-yt-paper-item, yt-dropdown-menu tp-yt-paper-item');\n          for (const item of menuItems) {\n            const txt = item.textContent?.trim().toLowerCase();\n            const trackMatch = tracks.find(t => t.languageCode === args.lang);\n            if (trackMatch && txt.includes(trackMatch.name?.simpleText?.toLowerCase() || args.lang)) {\n              item.click();\n              await new Promise(r => setTimeout(r, 2000));\n              break;\n            }\n          }\n        }\n      }\n\n      // Extract transcript segments from DOM\n      const segmentEls = panel.querySelectorAll('ytd-transcript-segment-renderer');\n\n      if (!segmentEls.length) {\n        // Restore panel state\n        if (wasHidden) panel.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN');\n        return {\n          error: 'No transcript segments loaded',\n          hint: 'The transcript panel is present but empty. Try refreshing the page.',\n          videoId,\n          availableTracks\n        };\n      }\n\n      const segments = Array.from(segmentEls).map(seg => {\n        const timeText = seg.querySelector('.segment-timestamp')?.textContent?.trim() || '';\n        const text = seg.querySelector('.segment-text')?.textContent?.trim() || '';\n\n        // Parse time string (e.g. \"1:23\" or \"1:02:34\") to seconds\n        const parts = timeText.split(':').map(Number);\n        let startSec = 0;\n        if (parts.length === 3) startSec = parts[0] * 3600 + parts[1] * 60 + parts[2];\n        else if (parts.length === 2) startSec = parts[0] * 60 + parts[1];\n        else if (parts.length === 1) startSec = parts[0];\n\n        return {\n          start: startSec,\n          startFormatted: timeText,\n          text\n        };\n      }).filter(s => s.text);\n\n      // Restore panel state if we opened it\n      if (wasHidden) {\n        panel.setAttribute('visibility', 'ENGAGEMENT_PANEL_VISIBILITY_HIDDEN');\n      }\n\n      // Determine language from available info\n      let language = tracks[0]?.languageCode || 'unknown';\n      let languageName = tracks[0]?.name?.simpleText || '';\n      let kind = tracks[0]?.kind || 'manual';\n      if (args.lang) {\n        const langTrack = tracks.find(t => t.languageCode === args.lang);\n        if (langTrack) {\n          language = langTrack.languageCode;\n          languageName = langTrack.name?.simpleText || '';\n          kind = langTrack.kind || 'manual';\n        }\n      }\n\n      // Build full text version\n      const fullText = segments.map(s => s.text).join(' ');\n\n      // Estimate total duration from last segment\n      const lastSeg = segments[segments.length - 1];\n      const totalDuration = lastSeg ? lastSeg.start + 10 : 0;\n\n      return {\n        videoId,\n        language,\n        languageName,\n        kind,\n        segmentCount: segments.length,\n        totalDuration,\n        availableTracks,\n        segments,\n        fullText: fullText.substring(0, 5000)\n      };\n  };\n  return run(params || {});\n}"},{"name":"youtube_video","description":"Get detailed info for a YouTube video (from current page or by video ID)","inputSchema":{"type":"object","properties":{"id":{"type":"string","description":"Video ID (defaults to current page video)"}},"required":null},"handler":"(params) => {\n  const run = async function(args) {\n\n      const currentUrl = location.href;\n      let videoId = args.id;\n\n      // Auto-detect from current page\n      if (!videoId) {\n        const match = currentUrl.match(/[?&]v=([a-zA-Z0-9_-]{11})/);\n        if (match) videoId = match[1];\n      }\n      if (!videoId) return {error: 'No video ID', hint: 'Provide a video ID or navigate to a YouTube video page'};\n\n      const onVideoPage = currentUrl.includes('watch?v=' + videoId);\n\n      // If we're on the video page, use pre-rendered data\n      if (onVideoPage && window.ytInitialPlayerResponse && window.ytInitialData) {\n        const p = window.ytInitialPlayerResponse;\n        const d = window.ytInitialData;\n\n        const vd = p.videoDetails || {};\n        const mf = p.microformat?.playerMicroformatRenderer || {};\n\n        // Extract engagement data from ytInitialData\n        const results = d.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];\n        const primary = results.find(i => i.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer;\n\n        let likeCount = '';\n        const menuRenderer = primary?.videoActions?.menuRenderer;\n        if (menuRenderer?.topLevelButtons) {\n          for (const btn of menuRenderer.topLevelButtons) {\n            const seg = btn.segmentedLikeDislikeButtonViewModel;\n            if (seg) {\n              likeCount = seg.likeButtonViewModel?.likeButtonViewModel?.toggleButtonViewModel?.toggleButtonViewModel?.defaultButtonViewModel?.buttonViewModel?.title || '';\n            }\n          }\n        }\n\n        // Channel info\n        const secondary = results.find(i => i.videoSecondaryInfoRenderer)?.videoSecondaryInfoRenderer;\n        const owner = secondary?.owner?.videoOwnerRenderer;\n\n        // Comment count from section\n        const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');\n        const commentToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;\n\n        // Chapters\n        const chapters = [];\n        const chapterPanel = d.engagementPanels?.find(p => p.engagementPanelSectionListRenderer?.panelIdentifier === 'engagement-panel-macro-markers-description-chapters');\n\n        return {\n          videoId: vd.videoId,\n          title: vd.title,\n          channel: vd.author,\n          channelId: vd.channelId,\n          channelUrl: owner?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl ? 'https://www.youtube.com' + owner.navigationEndpoint.browseEndpoint.canonicalBaseUrl : '',\n          subscriberCount: owner?.subscriberCountText?.simpleText || '',\n          description: (vd.shortDescription || '').substring(0, 1000),\n          duration: parseInt(vd.lengthSeconds) || 0,\n          durationFormatted: (() => {\n            const s = parseInt(vd.lengthSeconds) || 0;\n            const h = Math.floor(s / 3600);\n            const m = Math.floor((s % 3600) / 60);\n            const sec = s % 60;\n            return h > 0 ? h + ':' + String(m).padStart(2,'0') + ':' + String(sec).padStart(2,'0') : m + ':' + String(sec).padStart(2,'0');\n          })(),\n          viewCount: parseInt(vd.viewCount) || 0,\n          viewCountFormatted: primary?.viewCount?.videoViewCountRenderer?.viewCount?.simpleText || '',\n          likes: likeCount,\n          publishDate: mf.publishDate || primary?.dateText?.simpleText || '',\n          category: mf.category || '',\n          isLive: vd.isLiveContent || false,\n          keywords: (vd.keywords || []).slice(0, 20),\n          captionLanguages: (p.captions?.playerCaptionsTracklistRenderer?.captionTracks || []).map(t => ({lang: t.languageCode, name: t.name?.simpleText})),\n          url: 'https://www.youtube.com/watch?v=' + vd.videoId,\n          _commentContinuationToken: commentToken || null\n        };\n      }\n\n      // If not on the video page, use innertube next API for basic info\n      const cfg = window.ytcfg?.data_ || {};\n      const apiKey = cfg.INNERTUBE_API_KEY;\n      const context = cfg.INNERTUBE_CONTEXT;\n      if (!apiKey || !context) return {error: 'YouTube config not found', hint: 'Make sure you are on youtube.com'};\n\n      const resp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {\n        method: 'POST',\n        credentials: 'include',\n        headers: {'Content-Type': 'application/json'},\n        body: JSON.stringify({context, videoId})\n      });\n\n      if (!resp.ok) return {error: 'API returned HTTP ' + resp.status};\n      const data = await resp.json();\n\n      const results = data.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];\n      const primary = results.find(i => i.videoPrimaryInfoRenderer)?.videoPrimaryInfoRenderer;\n      const secondary = results.find(i => i.videoSecondaryInfoRenderer)?.videoSecondaryInfoRenderer;\n      const owner = secondary?.owner?.videoOwnerRenderer;\n\n      let likeCount = '';\n      const menuRenderer = primary?.videoActions?.menuRenderer;\n      if (menuRenderer?.topLevelButtons) {\n        for (const btn of menuRenderer.topLevelButtons) {\n          const seg = btn.segmentedLikeDislikeButtonViewModel;\n          if (seg) {\n            likeCount = seg.likeButtonViewModel?.likeButtonViewModel?.toggleButtonViewModel?.toggleButtonViewModel?.defaultButtonViewModel?.buttonViewModel?.title || '';\n          }\n        }\n      }\n\n      return {\n        videoId,\n        title: primary?.title?.runs?.[0]?.text || '',\n        channel: owner?.title?.runs?.[0]?.text || '',\n        channelId: owner?.navigationEndpoint?.browseEndpoint?.browseId || '',\n        subscriberCount: owner?.subscriberCountText?.simpleText || '',\n        viewCountFormatted: primary?.viewCount?.videoViewCountRenderer?.viewCount?.simpleText || '',\n        likes: likeCount,\n        publishDate: primary?.dateText?.simpleText || '',\n        url: 'https://www.youtube.com/watch?v=' + videoId\n      };\n  };\n  return run(params || {});\n}"}]}