Skip to content

feat: Live chat, comments, chapters & 18 bug fixes - #27

Open
deafsquad wants to merge 10 commits into
kimtaeyoon83:mainfrom
deafsquad:fix/code-eval-improvements
Open

feat: Live chat, comments, chapters & 18 bug fixes#27
deafsquad wants to merge 10 commits into
kimtaeyoon83:mainfrom
deafsquad:fix/code-eval-improvements

Conversation

@deafsquad

@deafsquad deafsquad commented Feb 7, 2026

Copy link
Copy Markdown
Contributor

New Features

  • Live Chat Support - 3 new tools: get_live_chat (with background streaming mode), stop_live_chat, list_live_streams for monitoring YouTube live streams in real-time
  • Comments Fetching - include_comments parameter to fetch top comments with transcript, plus comments_only mode for fetching all available comments
  • Chapter Extraction - include_chapters parameter returns video chapters with timestamps
  • Auto Live-Stream Detection - get_transcript automatically detects live streams and redirects to live chat with background streaming
  • Extended Metadata - Video duration and comment count added to response metadata

Bug Fixes (18 total)

Critical

  • Fix array mutation bug in comment reply insertion (splicing during iteration caused skipped comments)
  • Eliminate redundant HTTP request for comment count (was re-fetching entire page)

High

  • Fix extractYoutubeId swallowing McpError from URL parsing (specific error messages were lost)
  • Fix visitorData lost on live chat continuation calls (caused potential auth failures)
  • Eliminate redundant checkIfLive page fetch (was fetching page HTML twice for every non-live video)
  • Add SIGTERM handler for container graceful shutdown (Docker/K8s compatibility)

Medium

  • Fix broken seenIds dedup logic in poll logging (new message counts were always 0)
  • Improve chapter regex resilience (handle both simpleText and runs title formats)
  • Move mcp-evals from dependencies to devDependencies
  • Sync server version with package.json (was 0.1.0, now 0.1.1)
  • Remove dead webPayload code (built but never used)
  • Fix Shorts URL parsing to strip trailing path segments
  • Fix live chat messages showing literal \n in structuredContent (use pipe separator)

Low

  • Remove unnecessary as Tool type assertions (SDK 1.x includes outputSchema natively)
  • Add type safety to handleToolCall args (Record<string, unknown> instead of any)
  • Extract shared formatCount helper (was duplicated 4+ times)
  • Remove defensive boolean handling for include_comments number param
  • Exclude src/evals from tsconfig (ESM/CJS incompatibility with Node16)

Upstream Compatibility

Test Plan

  • tsc --noEmit passes clean
  • npm run build succeeds
  • Tested get_transcript with regular video (Rick Astley - confirmed metadata, transcript)
  • Tested get_live_chat with live stream (CS2 IEM Krakow - confirmed 70 messages, streaming)
  • Tested auto live-stream detection via get_transcript on live video

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • YouTube 라이브 채팅: 실시간 조회, 스트리밍 시작/중지 및 메시지 버퍼링/중복 제거 지원
    • YouTube 댓글 조회: 상위 댓글·답글 포함, 댓글 전용 모드 및 제한/정렬 지원
    • 트랜스크립트 개선: 챕터 통합, per-line 타임스탬프, 댓글 수·재생 시간·실시간 여부 등 메타데이터/출력 필드 확장
    • 라이브 스트림 목록 조회 기능 추가 및 실시간 자동 감지
  • Documentation

    • README 업데이트: 라이브 채팅·댓글·챕터 사용법과 예제 추가
  • Chores

    • 패키지 설정 변경: 모듈 타입 필드 제거 및 일부 의존성 이동/업데이트

deafsquad and others added 2 commits February 7, 2026 14:11
- Export/share TranscriptLine, add TranscriptFetchError for live fallback
- Eliminate redundant HTTP requests (comment count, checkIfLive)
- Fix array mutation bug in comment reply insertion
- Fix extractYoutubeId swallowing McpError from URL parsing
- Fix visitorData lost on live chat continuation calls
- Add SIGTERM handler for container graceful shutdown
- Fix broken seenIds dedup logic in poll logging
- Improve chapter regex resilience (handle title.runs format)
- Move mcp-evals to devDependencies
- Sync server version with package.json (0.1.1)
- Remove dead webPayload code
- Fix Shorts URL trailing path segment parsing
- Add type safety to handleToolCall args (Record<string,unknown>)
- Extract shared formatCount helper (DRY)
- Remove defensive boolean handling for include_comments
- Add tool annotations (readOnlyHint, openWorldHint) from upstream
- Upgrade MCP SDK to ^1.11.0, adapt for Node16 module resolution
- Exclude src/evals from tsconfig (ESM/CJS incompatibility)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Newlines in structuredContent fields render as literal \n in Claude Code.
Use ' | ' separator for structuredContent.messages while keeping \n
for content[].text which renders correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

패키지 설정과 TypeScript 제외 경로를 조정하고, YouTube 트랜스크립트에 챕터·댓글·메타데이터를 추가했습니다. 라이브 채팅 조회와 백그라운드 스트리밍 도구를 도입했으며 README와 서버 종료 처리를 갱신했습니다.

Changes

YouTube 콘텐츠 확장

Layer / File(s) Summary
패키지 및 컴파일 설정
package.json, tsconfig.json
모듈 타입을 제거하고 MCP SDK 버전을 갱신했으며 mcp-evals를 개발 의존성으로 이동하고 src/evals를 컴파일 제외 목록에 추가했습니다.
자막·챕터·메타데이터 처리
src/youtube-fetcher.ts
InnerTube 캡션과 timedtext XML 기반 자막 추출, 챕터 및 광고 구간 판별, 언어 폴백, duration·commentCount·isLive 메타데이터를 구현했습니다.
댓글 조회 및 트랜스크립트 도구 통합
src/youtube-fetcher.ts, src/index.ts
댓글 페이지네이션과 답글 병합을 추가하고 get_transcript에 챕터·댓글·comments_only 옵션과 결과 필드를 반영했습니다.
라이브 채팅 스트리밍 도구
src/youtube-fetcher.ts, src/index.ts
라이브 채팅 continuation 조회, 백그라운드 버퍼링, 중복 제거, 스트림 중지·목록 조회를 추가하고 get_live_chat, stop_live_chat, list_live_streams 도구를 노출했습니다.
문서 및 서버 수명주기
README.md, src/index.ts
새 도구와 사용 예를 문서화하고 서버 버전을 갱신했으며 SIGTERM 종료 처리를 추가했습니다.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant MCP as MCP Server
    participant Fetcher as YouTube Fetcher
    participant YouTube as YouTube API
    participant Streams as activeStreams

    Client->>MCP: get_transcript(options)
    MCP->>Fetcher: transcript/comments 요청
    Fetcher->>YouTube: 페이지·캡션·댓글 조회
    YouTube-->>Fetcher: 자막·메타데이터·댓글
    Fetcher-->>MCP: 구조화된 트랜스크립트 결과
    MCP-->>Client: transcript, chapters, comments, meta

    Client->>MCP: get_live_chat(url, stream)
    MCP->>Fetcher: 라이브 채팅 시작 또는 조회
    Fetcher->>YouTube: continuation 요청
    YouTube-->>Fetcher: 메시지와 다음 continuation
    Fetcher->>Streams: 스트림 버퍼 갱신
    Streams-->>MCP: 메시지와 스트리밍 상태
    MCP-->>Client: live chat 결과
Loading

Possibly related PRs

Poem

🐰 챕터 따라 자막은 깡충,
댓글 답글 줄줄이 모이고,
라이브 채팅 버퍼는 반짝,
새 도구들이 문을 열었네,
홉홉, 영상 속으로 출발!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 라이브 채팅, 댓글, 챕터 추가와 버그 수정이라는 이번 변경의 핵심을 잘 요약한 제목입니다.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 405-407: The current branch uses comments_only and calls
getComments({ videoID: videoId, limit: 0 }) which requests unlimited comments
and can exhaust resources; change this to enforce a sensible upper bound by
replacing limit: 0 with a capped value (e.g., determine a DEFAULT_COMMENTS_LIMIT
= 1000 or 500 and use Math.min(requestedLimit || DEFAULT_COMMENTS_LIMIT,
DEFAULT_COMMENTS_LIMIT)), or require a CLI/env param and validate it before
calling getComments; update the call site using comments_only and the
getComments invocation to pass the validated/capped limit and document the cap
in the surrounding code.

In `@src/youtube-fetcher.ts`:
- Around line 1448-1461: When the poll() catch block detects a stream has
"ended", it only sets state.isPolling = false but doesn't remove the stream from
activeStreams or clear timers/buffers, causing memory leaks; update the error
branch in poll so that when (err as Error).message?.includes('ended') you also
clear state.pollTimer (if set), remove the entry from activeStreams
(activeStreams.delete(videoId)) or call stopLiveChatStream(videoId) to perform
full cleanup, and ensure any per-stream buffers (seenIds/messages) are released;
keep the existing logging and return behavior but add these cleanup steps to
fully remove the ended stream.
- Around line 1158-1162: The current regex that finds the continuation token
(html.match(/"continuation":"([^"]+)"/)) is too broad and may capture
non-live-chat continuations; narrow the search to the live chat context by
matching the continuation token that appears near liveChatRenderer or
liveChatContinuation (e.g., restrict the regex to look for
liveChatRenderer.*"continuation":"([^"]+)" or
liveChatContinuation.*"continuation":"([^"]+)"). Update the code that sets
continuationMatch to use this context-aware pattern, validate the match exists,
and use the captured group as before (symbols: html.match, continuationMatch,
the continuation token extraction logic).
🧹 Nitpick comments (5)
src/youtube-fetcher.ts (3)

1214-1224: 하드코딩된 API 키 — 환경 변수 또는 페이지 추출로 전환 권장

정적 분석 도구(Gitleaks)가 Line 1216에서 GCP API 키를 감지했습니다. 이 키는 YouTube 웹 페이지 소스에 포함된 공개 키로 보이지만, 하드코딩하면:

  1. 키 로테이션 시 코드 변경이 필요합니다.
  2. 비밀 스캐닝 도구에서 지속적으로 경고가 발생합니다.

다른 API 호출(/youtubei/v1/next, /youtubei/v1/get_transcript)에서는 키 없이 요청하고 있으므로, 여기서도 키를 제거하거나 페이지에서 동적으로 추출하는 것을 권장합니다.

제안된 수정
-      path: '/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
+      path: '/youtubei/v1/live_chat/get_live_chat',

1321-1322: 프로세스 종료 시 활성 스트림 정리 누락

activeStreams의 모든 타이머가 프로세스 종료 시 정리되지 않습니다. process.exit(0) 호출로 강제 종료되긴 하지만, 명시적 정리를 추가하면 그레이스풀 셧다운이 개선됩니다.

youtube-fetcher.ts에 cleanup 함수 추가 제안
+export function stopAllStreams(): void {
+  for (const [videoId, state] of activeStreams) {
+    state.isPolling = false;
+    if (state.pollTimer) clearTimeout(state.pollTimer);
+  }
+  activeStreams.clear();
+}

index.tsstop() 메서드에서 stopAllStreams()를 호출하면 됩니다.


444-499: getCommentCount는 내부에서 사용되지 않음 — 제거하거나 목적을 문서화하세요

getCommentCountsrc/youtube-fetcher.ts에서 내보내지만 src/index.ts에 import되지 않으며, 정의 위치 외에는 코드 전체에서 참조되지 않습니다. getPageData가 이미 HTML에서 댓글 수를 추출하므로(372-384줄), 이 함수의 별도 API 호출은 중복됩니다. 외부 소비자를 위해 유지하려면 JSDoc에 용도를 명시하거나, 내부 사용이 없다면 제거하세요.

src/index.ts (2)

438-462: 라이브 스트림 감지 응답 포맷팅 코드 중복

Lines 438-462와 544-566은 거의 동일한 라이브 채팅 포맷팅 로직입니다. 헬퍼 함수로 추출하면 유지보수성이 개선됩니다.

헬퍼 함수 제안
private formatLiveChatResponse(
  liveResult: { messages: LiveChatMessage[]; continuation?: string; isLive: boolean; pollIntervalMs: number },
  metadata: { title: string; author: string }
): CallToolResult {
  const messagesFormatted = liveResult.messages.map((m: LiveChatMessage) => {
    const prefix = m.isPaid ? `[${m.paidAmount}] ` : '';
    return `${prefix}${m.author}: ${m.text}`;
  }).join('\n');

  return {
    content: [{
      type: "text" as const,
      text: `[LIVE STREAM DETECTED - Auto-switched to live chat with background streaming]\n\nStream: ${metadata.title || 'Unknown'}\nChannel: ${metadata.author || 'Unknown'}\n\nRecent chat:\n${messagesFormatted || '[No messages yet]'}\n\n[Use get_live_chat with stream:true to check for new messages, or stop_live_chat to stop]`
    }],
    structuredContent: {
      meta: `🔴 LIVE | ${metadata.title || 'Live Stream'} | ${metadata.author || 'Unknown'} | ${liveResult.messages.length} messages | STREAMING STARTED`,
      messages: messagesFormatted.replace(/\n/g, ' | '),
      isLive: true,
      streaming: true,
      note: 'Auto-detected live stream. Background streaming started. Use get_live_chat(stream:true) for updates.'
    }
  };
}

Also applies to: 544-566


515-519: structuredResultany 타입 사용

structuredResultany로 타입이 지정되어 있어 후속 프로퍼티 추가(chapters, comments)에 대한 타입 안전성이 없습니다. 인터페이스를 정의하면 유지보수성이 개선됩니다.

Comment thread src/index.ts Outdated
Comment thread src/youtube-fetcher.ts Outdated
Comment thread src/youtube-fetcher.ts
- Add TranscriptStructuredResult interface with index signature (replaces `any`)
- Extract formatLiveChatResponse helper to deduplicate live chat formatting
- Remove dead getCommentCount function (getPageData already extracts count)
- Default comments_only to 500 cap; user can override via include_comments
- Clean up activeStreams on stream end to prevent memory leaks
- Narrow live chat continuation regex to liveChatRenderer context
- Add stopAllStreams() called on server shutdown for graceful cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/index.ts`:
- Around line 564-575: The catch block is re-calling
extractor.extractYoutubeId(input) even though videoId from earlier in the try is
in scope, which can mask the original TranscriptFetchError with a new McpError;
update the catch to reuse the existing videoId variable (from the outer scope)
when calling startLiveChatStream and formatLiveChatResponse, and only attempt a
fresh extractYoutubeId(input) as a last-resort guarded step that preserves and
rethrows or logs the original TranscriptFetchError; adjust the
TranscriptFetchError handling so startLiveChatStream(videoId) is invoked with
the original videoId and avoid swallowing the original error when handling
fallback failures.

In `@src/youtube-fetcher.ts`:
- Around line 1171-1175: In getLiveChat, wrap the JSON.parse(response) call in a
try-catch to safely handle malformed/non-JSON responses: catch SyntaxError (or
generic errors), log the raw response and error (including response content)
using the existing logger, and then either throw a new, descriptive Error or
return a safe fallback (e.g., null/empty chat object) so the polling loop
doesn't crash; specifically update the code around JSON.parse(response) and the
subsequent json.error check to use the parsed value from the try block (refer to
getLiveChat, the json variable and the JSON.parse call).
🧹 Nitpick comments (5)
src/youtube-fetcher.ts (4)

1158-1158: 하드코딩된 YouTube API 키 — Gitleaks에서 감지됨

이 키(AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8)는 YouTube의 공개 innertube API 키로 널리 알려져 있지만, static analysis(Gitleaks)에서 GCP API 키로 플래그됩니다. 환경 변수나 상수 파일로 추출하면 false positive를 억제하고 향후 키 교체도 용이해집니다.

제안
+const INNERTUBE_API_KEY = process.env.YOUTUBE_INNERTUBE_KEY || 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
+
 // In getLiveChat:
-      path: '/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
+      path: `/youtubei/v1/live_chat/get_live_chat?key=${INNERTUBE_API_KEY}`,

1097-1101: /s 플래그가 있는 .*? 패턴 — 대형 HTML에서 성능 문제 가능

/s 플래그로 .이 개행도 매칭하므로, .*?가 수십~수백 KB HTML 전체를 탐색하게 됩니다. liveChatContinuation이 HTML 뒷부분에 있으면 상당한 백트래킹이 발생할 수 있습니다. [^"]*이나 indexOf 기반 접근이 더 안전합니다.

대안 예시
-  const liveChatContMatch = html.match(/"liveChatContinuation".*?"continuation":"([^"]+)"/s);
-  const liveChatRendererMatch = html.match(/"liveChatRenderer".*?"continuation":"([^"]+)"/s);
+  // Scope search to relevant section to avoid backtracking across entire HTML
+  const lcIdx = html.indexOf('"liveChatContinuation"');
+  const lrIdx = html.indexOf('"liveChatRenderer"');
+  const liveChatContMatch = lcIdx !== -1
+    ? html.slice(lcIdx, lcIdx + 5000).match(/"continuation":"([^"]+)"/)
+    : null;
+  const liveChatRendererMatch = lrIdx !== -1
+    ? html.slice(lrIdx, lrIdx + 5000).match(/"continuation":"([^"]+)"/)
+    : null;

82-87: formatCount(0)이 빈 문자열 반환 — 의도적인지 확인 필요

n <= 0일 때 빈 문자열을 반환합니다. 댓글 수가 0인 영상에서 commentCount''가 되어, UI에서 "comments" 라벨만 남고 숫자가 없는 상황이 될 수 있습니다. 0을 명시적으로 반환하는 것이 더 일관적일 수 있습니다.


1063-1116: getLiveChatContinuationgetPageData와 동일한 페이지를 중복 요청

getSubtitlesgetPageData로 이미 페이지를 가져오고, 라이브 스트림이면 다시 getLiveChatContinuation에서 같은 URL을 요청합니다. getPageData에서 이미 추출한 isLive, visitorData 등을 전달하면 불필요한 HTTP 요청을 줄일 수 있습니다. 지금 당장 필수는 아니지만 라이브 스트림 감지 시 응답 시간이 2배가 됩니다.

src/index.ts (1)

376-401: formatLiveChatResponse — 반복되는 메시지 포맷팅 로직 추출 고려

이 헬퍼는 좋은 시작이지만, 동일한 메시지 포맷팅 패턴(mapjoin('\n')replace(/\n/g, ' | '))이 get_live_chat 핸들러(Lines 613-616, 640-643, 671-674)와 stop_live_chat(Lines 729-732)에서도 반복됩니다. formatMessages(messages: LiveChatMessage[]): { text: string; inline: string } 같은 헬퍼로 추출하면 중복을 줄일 수 있습니다.

Comment thread src/index.ts
Comment thread src/youtube-fetcher.ts Outdated
deafsquad and others added 2 commits February 7, 2026 15:07
- Hoist videoId extraction before try block so catch can reuse it
  instead of redundantly re-calling extractYoutubeId(input)
- Wrap JSON.parse in getLiveChat with try-catch for malformed responses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Chapter markers appear as --- [mm:ss] Title --- section dividers
  directly in the transcript, replacing separate chapters field
- When chapters exist, per-line timestamps are dropped (chapter
  timestamps provide structural context)
- Strip subtitle line breaks (\n) from individual transcript lines
- Fix false "No chapter markers found" note on chaptered videos
- Shared formatTimestamp helper replaces duplicated timestamp logic
- Update README with all 4 tools, key features, and usage examples

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@README.md`:
- Around line 33-34: 문구가 모호하므로 README의 해당 섹션을 수정해 `comments_only`와
`include_comments`의 상호작용을 명확히 하세요: 명시적으로 `include_comments`는 "0 = disabled"이며,
`comments_only: true`일 때 `include_comments > 0`이면 그 값이 가져올 댓글 수의 제한으로 사용되고, 그렇지
않으면 기본 제한으로 500개가 적용된다고 기술하도록 `comments_only`와 `include_comments` 항목을 업데이트하세요
(참조 심볼: comments_only, include_comments, 기본값 500).
- Around line 36-41: The README entry for get_live_chat is missing the
continuation parameter described in the tool definition (see get_live_chat
signature where continuation is defined); update the docs to list `continuation`
(string, optional) alongside `url` and `stream`, explain it accepts a YouTube
continuation token used for manual polling to resume from a specific chat
position, and note that it should be provided only in non-streaming/manual poll
mode; reference the get_live_chat function and the continuation parameter name
when adding this documentation.

In `@src/index.ts`:
- Around line 512-521: The current logic unconditionally redirects to live chat
when result.isLive is true, discarding any successfully fetched transcript;
change the flow in the block that calls startLiveChatStream so that if a
transcript exists (check result.transcript or result.metadata.transcript) you do
not discard it—either return both transcript and live chat data together (e.g.,
call or add a new formatter like this.formatLiveAndTranscriptResponse or extend
formatLiveChatResponse to accept the transcript) or fall back to returning the
transcript immediately and let the caller choose; update the code paths around
result.isLive, startLiveChatStream, and formatLiveChatResponse to
include/preserve result.metadata (and transcript) instead of throwing it away.

In `@src/youtube-fetcher.ts`:
- Around line 1240-1248: The return value currently hardcodes isLive: true in
getLiveChat which prevents detecting stream end; change isLive to be derived
from the parsed continuation (e.g., set isLive = Boolean(nextContinuation) or
check whether the YouTube response contained a continuation token) instead of
true, and update the return object (messages, continuation: nextContinuation,
isLive, ...) so callers can accurately detect stream termination.
- Around line 760-766: fetchCommentsPage currently calls JSON.parse(response)
without handling parse failures; wrap the parse in a try-catch inside
fetchCommentsPage, catch SyntaxError (or generic Error), and throw a new Error
that includes context ("Failed to parse YouTube response in fetchCommentsPage")
plus the original error message and a truncated/escaped snapshot of response to
aid debugging; ensure the thrown error preserves the original error message so
callers like getComments receive a clear, actionable parse error instead of an
opaque crash.
- Around line 1097-1108: The title/author extraction in getLiveChatContinuation
is too broad (using /"title":"([^"]+)"/ and /"author":"([^"]+)"/) and can return
unrelated matches; instead scope extraction to the "videoDetails" context like
getPageData does: first extract the "videoDetails" JSON block (e.g., match
/"videoDetails"\s*:\s*\{([\s\S]*?)\}/ or parse that JSON if already isolated),
then extract "title" and "author" from within that block (or parse the block as
JSON) so getLiveChatContinuation uses the videoDetails-specific title/author
rather than the first global occurrences.
- Around line 963-965: The initial continuationToken is being
encodeURIComponent'd while subsequent tokens from
item.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token
are raw, causing inconsistent encoding; remove the encodeURIComponent wrapper
around buildCommentsParams(videoID, sortByNum) so continuationToken holds the
raw base64, and instead centralize encoding in fetchCommentsPage (ensure it
consistently encodeURIComponent the token when constructing the JSON body) so
both the initial token from buildCommentsParams and subsequent tokens extracted
from responses are treated identically.
🧹 Nitpick comments (5)
src/youtube-fetcher.ts (3)

79-87: formatCount(0) 및 음수 값에서 빈 문자열 반환

n <= 0일 때 빈 문자열을 반환하는 것은 "카운트 없음" 의미로 사용되고 있어 의도된 동작으로 보이지만, 정확히 0개의 댓글이 있는 경우에도 빈 문자열을 반환합니다. 현재 사용처에서는 parseInt가 NaN을 반환하거나 카운트가 없는 경우에 해당하므로 큰 문제는 아닙니다.


1275-1303: checkIfLive가 별도의 HTTP 요청을 수행 — getPageData와 중복 fetch

checkIfLive는 독립 유틸리티로 설계되었지만, getPageData에서 이미 isLive를 추출합니다. get_transcript 흐름에서 getSubtitlesgetPageDataisLive가 반환되므로, checkIfLive를 추가로 호출하면 불필요한 HTTP 요청이 발생합니다.

현재 코드에서 checkIfLiveindex.ts에서 import되지만 직접 호출하지 않는 것으로 보이므로 즉각적인 문제는 아닙니다. 향후 사용 시 중복 fetch에 주의가 필요합니다.


1063-1091: getLiveChatContinuationgetPageData와 중복 페이지 fetch 수행

get_live_chat 도구가 호출될 때 getLiveChatContinuation이 YouTube 페이지를 다시 fetch합니다. 만약 get_transcript에서 라이브 감지 후 startLiveChatStreamgetLiveChatgetLiveChatContinuation 경로로 흐르면, 동일 영상 페이지를 최소 2회 fetch하게 됩니다.

페이지 데이터를 캐싱하거나 getPageData의 결과를 파이프라인으로 전달하면 불필요한 네트워크 요청을 줄일 수 있습니다.

src/index.ts (2)

38-49: formatTimestamp 함수가 youtube-fetcher.ts의 duration 포맷 로직과 중복

youtube-fetcher.ts lines 394-399의 duration 포맷팅과 동일한 로직입니다. 공통 유틸리티로 추출하면 유지보수성이 향상됩니다.


486-494: comments_onlyinclude_comments 모드의 댓글 포맷이 불일치

comments_only 모드(line 494)에서는 \n으로 구분하고, include_comments 모드(line 540)에서는 |로 구분합니다. structuredContent에서도 comments_only는 원본 줄바꿈을 유지하지만, include_comments는 pipe로 구분합니다.

의도적인 설계일 수 있지만(comments_only는 상세, include_comments는 요약), 일관성이 떨어집니다. 포맷 선택을 문서화하거나 통일하는 것을 권장합니다.

Also applies to: 537-540

Comment thread README.md Outdated
Comment thread README.md
Comment thread src/index.ts Outdated
Comment thread src/youtube-fetcher.ts
Comment thread src/youtube-fetcher.ts Outdated
Comment on lines +963 to +965
// Build initial comments params (0 = TOP_COMMENTS, 1 = NEWEST_FIRST)
const sortByNum = sortBy === 'NEWEST_FIRST' ? 1 : 0;
let continuationToken: string | undefined = encodeURIComponent(buildCommentsParams(videoID, sortByNum));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if encodeURIComponent is used consistently with continuation tokens
rg -n 'encodeURIComponent' --type=ts -C3
echo "---"
# Check how continuation tokens are used in fetchCommentsPage
rg -n 'continuationToken|continuation:' --type=ts -C2

Repository: kimtaeyoon83/mcp-server-youtube-transcript

Length of output: 7654


🏁 Script executed:

#!/bin/bash
# Find where nextContinuation is assigned and extracted
rg -n 'nextContinuation' --type=ts -B2 -A2
echo "---"
# Check how pageResult.continuation is extracted/set
rg -n 'pageResult\|\.continuation' --type=ts -B1 -A1 | head -80

Repository: kimtaeyoon83/mcp-server-youtube-transcript

Length of output: 1878


🏁 Script executed:

#!/bin/bash
# Find buildCommentsParams function implementation
rg -n 'buildCommentsParams' --type=ts -B2 -A10

Repository: kimtaeyoon83/mcp-server-youtube-transcript

Length of output: 2060


🏁 Script executed:

#!/bin/bash
# Get full buildCommentsParams function
sed -n '151,200p' src/youtube-fetcher.ts

Repository: kimtaeyoon83/mcp-server-youtube-transcript

Length of output: 1604


초기 댓글 continuation 토큰과 후속 페이지네이션 토큰의 인코딩 상태 불일치

buildCommentsParams가 반환하는 base64 문자열에 encodeURIComponent를 적용하면 =%3D, +%2B 등으로 변환됩니다. 이 인코딩된 토큰은 fetchCommentsPage의 JSON body에 전송됩니다.

그러나 후속 페이지네이션 토큰은 API 응답에서 직접 추출되므로(item.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token), 인코딩이 적용되지 않습니다. 결과적으로 동일한 fetchCommentsPage 함수가 서로 다른 인코딩 상태의 토큰을 받게 되는 문제가 발생합니다.

YouTube API가 JSON body의 continuation 필드에서 인코딩된 토큰과 인코딩되지 않은 토큰을 모두 수용하는지, 아니면 일관된 형식을 요구하는지 확인하여 초기 토큰과 후속 토큰의 인코딩 방식을 통일하는 것이 필요합니다.

🤖 Prompt for AI Agents
In `@src/youtube-fetcher.ts` around lines 963 - 965, The initial continuationToken
is being encodeURIComponent'd while subsequent tokens from
item.continuationItemRenderer.continuationEndpoint?.continuationCommand?.token
are raw, causing inconsistent encoding; remove the encodeURIComponent wrapper
around buildCommentsParams(videoID, sortByNum) so continuationToken holds the
raw base64, and instead centralize encoding in fetchCommentsPage (ensure it
consistently encodeURIComponent the token when constructing the JSON body) so
both the initial token from buildCommentsParams and subsequent tokens extracted
from responses are treated identically.

Comment thread src/youtube-fetcher.ts Outdated
Comment thread src/youtube-fetcher.ts
- README: Clarify comments_only + include_comments interaction
- README: Add continuation param docs for get_live_chat
- index.ts: Only redirect to live chat when no transcript was fetched
- youtube-fetcher.ts: Derive isLive from continuation token presence
- youtube-fetcher.ts: Wrap fetchCommentsPage JSON.parse in try-catch
- youtube-fetcher.ts: Scope title/author to videoDetails context
- youtube-fetcher.ts: Remove encodeURIComponent from initial comments
  token for consistent encoding with subsequent tokens

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/youtube-fetcher.ts`:
- Around line 640-643: The JSON.parse(contResponse) call in getSubtitles (after
building continuationPayload) lacks a try-catch and can throw on malformed
YouTube responses; wrap the parse in a try-catch, log the parsing error along
with contResponse and continuationPayload (consistent with the existing
try-catch at the earlier block), and return a safe fallback (e.g., empty
subtitles/continuation result) or gracefully propagate a descriptive error
instead of letting a raw SyntaxError crash the whole getSubtitles flow.
🧹 Nitpick comments (4)
src/youtube-fetcher.ts (3)

1165-1165: 하드코딩된 GCP API 키가 소스 코드에 노출됨

이 키(AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8)는 YouTube 웹 클라이언트에서 공개적으로 사용되는 키이지만, 소스 코드에 직접 하드코딩하면 키 로테이션이 필요할 때 코드 변경이 필요합니다. 환경 변수나 상수 설정 파일로 분리하는 것을 권장합니다.

제안된 수정
+const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY || 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8';
+
 // In getLiveChat:
-      path: '/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
+      path: `/youtubei/v1/live_chat/get_live_chat?key=${YOUTUBE_API_KEY}`,

1297-1306: checkIfLive의 title/author 추출이 videoDetails로 스코핑되지 않음

getPageData(Line 364-365)와 getLiveChatContinuation(Line 1113)에서는 "videoDetails" 컨텍스트 내에서 title/author를 추출하지만, checkIfLive에서는 여전히 광범위한 패턴(/"title":"([^"]+)"/))을 사용합니다. 추천 동영상이나 광고의 title이 반환될 수 있습니다.

제안된 수정
-    const titleMatch = html.match(/"title":"([^"]+)"/);
-    const authorMatch = html.match(/"author":"([^"]+)"/);
+    const titleMatch = html.match(/"videoDetails":\{.*?"title":"([^"]+)"/);
+    const authorMatch = html.match(/"videoDetails":\{.*?"author":"([^"]+)"/);

82-87: formatCount에서 n === 0 처리

n <= 0일 때 빈 문자열을 반환하는데, 댓글 수가 실제로 0인 경우에도 빈 문자열이 반환됩니다. 이는 "댓글 없음"과 "파싱 실패"를 구분할 수 없게 합니다. 의도된 동작이라면 괜찮지만, n === 0일 때 '0'을 반환하는 것이 더 명확할 수 있습니다.

src/index.ts (1)

415-437: formatLiveChatResponse에서 structuredContent.messages의 파이프 구분자

messagesFormatted.replace(/\n/g, ' | ')로 모든 줄바꿈을 파이프로 치환하는데, 메시지 텍스트 자체에 | 문자가 포함된 경우 파싱이 모호해질 수 있습니다. structuredContent가 기계적으로 파싱되지 않는 표시용이라면 괜찮지만, 향후 파싱이 필요하면 구조화된 배열로 전달하는 것이 더 안전합니다.

Comment thread src/youtube-fetcher.ts Outdated
deafsquad and others added 2 commits February 7, 2026 16:20
Gracefully handles malformed continuation responses by breaking
out of the fetch loop and falling back to segments already collected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mirrors the same fix applied to getLiveChatContinuation - scopes
title/author regex to videoDetails block to avoid matching recommended
video titles/authors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@deafsquad

deafsquad commented Feb 7, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai

Addressing remaining nitpicks from latest review

1. Hardcoded GCP API Key (youtube-fetcher.ts:1165)

Intentional — no change. This is YouTube's public web client API key (AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8), used by every YouTube web page in the browser. It is not a secret credential — it's publicly embedded in youtube.com's JavaScript bundle. Extracting it to an env var would add configuration complexity with no security benefit, since the key is publicly known and not tied to any private quota. That said, we do support process.env.YOUTUBE_API_KEY override if someone wants to use their own key in the future.

2. checkIfLive title/author not scoped to videoDetails (youtube-fetcher.ts:1297-1306)

Fixed in commit bfa8e2d. Now uses the same videoDetails scoping pattern as getLiveChatContinuation and getPageData for consistency.

3. formatCount(0) returns empty string (youtube-fetcher.ts:82-87)

Intentional — no change. formatCount is used for display metadata (subscriber count, view count, comment count). A 0 from YouTube's API typically means "not available/not parsed" rather than "actually zero". Returning an empty string for n <= 0 correctly signals "no data" in the metadata display, which is the desired UX. An actual zero comment count is indistinguishable from a parsing failure at the API level, so treating both as "no display value" is the pragmatic choice.

4. Pipe separator in structuredContent.messages (index.ts:415-437)

Intentional — no change. structuredContent is an MCP display-only format rendered by Claude Code's UI. It is not designed as a machine-parseable API — the primary machine-consumable output is in content[0].text. The pipe separator provides clean visual formatting for the chat message display. If machine parsing of live chat is needed, consumers should use the text content field which contains the full structured output.

@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@deafsquad Thank you for the detailed explanations! I appreciate the clarifications on these design decisions:

  1. Hardcoded API key - Makes sense. Using YouTube's public web client key that's already publicly available in their JS bundle is pragmatic, especially with the env var override option for flexibility.

  2. checkIfLive scoping - Thank you for fixing this in commit bfa8e2d to ensure consistency across the codebase!

  3. formatCount(0) behavior - Good point about the semantic difference between "not available" and "actually zero" in YouTube's API responses. Returning an empty string correctly signals "no data" for display purposes.

  4. Pipe separator - Clear distinction between display format (structuredContent with pipes for visual clarity) versus machine-parseable format (text content field). Makes sense for the MCP UI rendering use case.

All of these decisions are well-reasoned. Thanks for addressing the feedback!


✏️ Learnings added
Learnt from: deafsquad
URL: https://github.com/kimtaeyoon83/mcp-server-youtube-transcript/pull/27

Timestamp: 2026-02-07T15:46:10.322Z
Learning: In src/youtube-fetcher.ts, the hardcoded YouTube API key (AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8) is YouTube's public web client key embedded in youtube.com's JavaScript bundle. It's intentionally not extracted to an environment variable since it's publicly known and not a secret credential. The code supports process.env.YOUTUBE_API_KEY override for custom keys.

Learnt from: deafsquad
URL: https://github.com/kimtaeyoon83/mcp-server-youtube-transcript/pull/27

Timestamp: 2026-02-07T15:46:10.322Z
Learning: In src/youtube-fetcher.ts, the formatCount helper intentionally returns an empty string for n <= 0 because a 0 from YouTube's API typically means "not available/not parsed" rather than "actually zero". This correctly signals "no data" in metadata displays.

Learnt from: deafsquad
URL: https://github.com/kimtaeyoon83/mcp-server-youtube-transcript/pull/27

Timestamp: 2026-02-07T15:46:10.322Z
Learning: In src/index.ts, the pipe separator used in structuredContent.messages for live chat is intentional. structuredContent is a display-only format for Claude's UI, not machine-parseable. For machine parsing of live chat, consumers should use the content[0].text field which contains the full structured output.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

…+ timedtext

YouTube now returns HTTP 400 'Precondition check failed' on
/youtubei/v1/get_transcript for all client types (WEB, ANDROID, IOS),
which broke transcript fetching entirely.

Switch getSubtitles to the surface youtube-transcript-api uses:
POST /youtubei/v1/player as ANDROID 20.10.38, read
captions.playerCaptionsTracklistRenderer.captionTracks, and download
the timedtext XML from the selected track's baseUrl (no poToken
required there). Language fallback, ASR detection, chapters, ad
markers, metadata and error semantics are preserved; the protobuf
params builder, multi-client retry loop and continuation handling
are removed (-103 net lines).

Verified live: manual captions (61 lines), ASR + 31 chapters (6006
lines), entity decoding, language fallback, strict-language errors,
and comments regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/youtube-fetcher.ts`:
- Around line 79-80: Remove HTTP status 400 from the RETRYABLE_CODES set in
src/youtube-fetcher.ts, leaving the existing retry behavior unchanged for the
other retryable statuses and RETRY_DELAYS.
- Around line 580-586: Update the caption URL handling in the timedtext download
flow to parse track.baseUrl as a URL, check trackUrl.searchParams.get('exp') for
the 'xpe' value regardless of query order, and remove fmt via
searchParams.delete('fmt') before fetching. Preserve the existing
TranscriptFetchError behavior and use the resulting URL for download and
parsing.
- Around line 588-599: Wrap the timed-text `httpsRequest` and
`parseTimedTextXml` flow in a catch path that converts any network or parsing
failure into the public `TranscriptFetchError` contract. Preserve and attach the
existing `isLive` and `metadata` values when constructing or propagating the
error, while leaving successful transcript parsing unchanged.
- Around line 569-577: Update the track selection logic around playerTracks.find
so it tries an exact language match first, then accepts a regional subtag such
as en-US for a requested base language. When falling back, select only the exact
English track or an en-* regional track before considering no match; do not fall
through to an unrelated first track.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86f4c0f7-7023-4121-af8f-d677f5fc2d9c

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0eed2 and 9697ac8.

📒 Files selected for processing (1)
  • src/youtube-fetcher.ts

Comment thread src/youtube-fetcher.ts Outdated
Comment thread src/youtube-fetcher.ts Outdated
Comment thread src/youtube-fetcher.ts Outdated
Comment thread src/youtube-fetcher.ts Outdated
- Match regional language subtags: exact match first, then base-language
  prefix (de -> de-DE); English fallback narrowed to en/en-* before
  first-track. Requesting 'de' now returns de-DE instead of English.
- Handle caption URL query params position-independently via
  URL/searchParams (fmt removal and exp=xpe detection no longer miss
  first-position params).
- Wrap timedtext download/parse in TranscriptFetchError so isLive and
  metadata are preserved on every transcript failure path.
- Remove HTTP 400 from RETRYABLE_CODES: client errors are not
  transient; retrying them just added ~3s of delay before failing.

Verified live: de -> de-DE (60 lines), strict-language error, ASR +
31 chapters (6006 lines), entity decoding, re-serialized caption URL
accepted by YouTube.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant