UNPKG

@kevinwatt/yt-dlp-mcp

Version:

An MCP server implementation that integrates with yt-dlp, providing video and audio content download capabilities (e.g. YouTube, Facebook, Tiktok, etc.) for LLMs.

230 lines 10.2 kB
import { prepareComments } from "./comments-prepare.js"; import { buildFlatSummary, buildThreadedSummary } from "./comments-summary.js"; import { countThreadComments } from "./comments-types.js"; export function formatCommentsOutput(rawComments, sourceInfo, options, characterLimit) { const prepared = prepareComments(rawComments); if (options.responseFormat === "markdown_tree") { return formatMarkdownComments(prepared, sourceInfo, options.maxComments, characterLimit); } const orphanIds = new Set(prepared.orphanCommentIds); if (options.view === "threaded") { const selection = selectThreadedComments(prepared.threadedComments, options.maxComments); return serializeThreadedResponse(selection.comments, selection.hasMore, orphanIds, characterLimit); } const selection = selectFlatComments(prepared.flatComments, options.maxComments); return serializeFlatResponse(selection.comments, selection.hasMore, orphanIds, characterLimit); } export function formatCommentsSummary(rawComments, options) { const prepared = prepareComments(rawComments); if (options.view === "threaded") { const selection = selectThreadedComments(prepared.threadedComments, options.maxComments); return buildThreadedSummary(selection.comments, selection.hasMore); } const selection = selectFlatComments(prepared.flatComments, options.maxComments); return buildFlatSummary(selection.comments, selection.hasMore); } function selectFlatComments(comments, maxComments) { const limitedComments = comments.slice(0, maxComments); return { comments: limitedComments, hasMore: comments.length > limitedComments.length, }; } function selectThreadedComments(comments, maxComments) { const selected = []; let selectedCount = 0; for (const thread of comments) { const threadCount = countThreadComments(thread); if (selected.length > 0 && selectedCount + threadCount > maxComments) { return { comments: selected, hasMore: true, }; } selected.push(thread); selectedCount += threadCount; if (selectedCount >= maxComments) { return { comments: selected, hasMore: selected.length < comments.length, }; } } return { comments: selected, hasMore: selected.length < comments.length, }; } function serializeFlatResponse(comments, hasMore, orphanIds, characterLimit) { let selectedComments = comments; let truncated = false; while (true) { const stats = buildFlatStats(selectedComments, orphanIds); const response = { count: stats.count, has_more: hasMore || truncated, root_threads: stats.rootThreads, reply_comments: stats.replyComments, orphan_comments: stats.orphanComments, comments: selectedComments, ...(truncated ? { _truncated: true, _message: `Response truncated to ${selectedComments.length} comments due to size limits.`, } : {}), }; const result = JSON.stringify(response, null, 2); if (!characterLimit || result.length <= characterLimit || selectedComments.length === 0) { return result; } truncated = true; selectedComments = selectedComments.slice(0, -1); } } function serializeThreadedResponse(comments, hasMore, orphanIds, characterLimit) { let selectedThreads = comments; let truncated = false; while (true) { const stats = buildThreadedStats(selectedThreads, orphanIds); const response = { count: stats.count, has_more: hasMore || truncated, root_threads: stats.rootThreads, reply_comments: stats.replyComments, orphan_comments: stats.orphanComments, comments: selectedThreads, ...(truncated ? { _truncated: true, _message: `Response truncated to ${selectedThreads.length} threads due to size limits.`, } : {}), }; const result = JSON.stringify(response, null, 2); if (!characterLimit || result.length <= characterLimit || selectedThreads.length === 0) { return result; } truncated = true; selectedThreads = selectedThreads.slice(0, -1); } } function formatMarkdownComments(prepared, sourceInfo, maxComments, characterLimit) { const orphanIds = new Set(prepared.orphanCommentIds); const baseSelection = selectThreadedComments(prepared.threadedComments, maxComments); let selectedThreads = baseSelection.comments; let truncated = false; while (true) { const result = renderMarkdownDocument(selectedThreads, prepared, sourceInfo, orphanIds, baseSelection.hasMore || truncated, truncated); if (!characterLimit || result.length <= characterLimit || selectedThreads.length === 0) { return result; } truncated = true; selectedThreads = selectedThreads.slice(0, -1); } } function renderMarkdownDocument(comments, prepared, sourceInfo, orphanIds, hasMore, truncated) { const stats = buildThreadedStats(comments, orphanIds); const generatedAtUtc = sourceInfo.generatedAtUtc ?? new Date().toISOString(); const headerLines = [ "# AI-Ready Comment Threads", "", `source_title: ${formatScalar(sourceInfo.title ?? null)}`, `source_id: ${formatScalar(sourceInfo.sourceId ?? null)}`, `source_url: ${formatScalar(sourceInfo.sourceUrl ?? null)}`, `extractor: ${formatScalar(sourceInfo.extractor ?? null)}`, `generated_at_utc: ${formatScalar(generatedAtUtc)}`, `raw_info_json: ${formatScalar(sourceInfo.rawInfoJsonPath ?? null)}`, `comments_detected: ${prepared.detectedCount}`, `comments_returned: ${stats.count}`, `root_threads: ${stats.rootThreads}`, `reply_comments: ${stats.replyComments}`, `orphan_comments: ${stats.orphanComments}`, `has_threading: ${formatScalar(prepared.hasThreading)}`, `has_more: ${formatScalar(hasMore)}`, "", "## Notes", "", "- Optimized for LLMs with stable keys, explicit `parent_id`, and preserved thread structure.", "- If the extractor does not expose `parent`, every comment is treated as a root thread.", "- `orphan_comments` means a reply arrived without its parent in the fetched payload.", ]; if (truncated) { headerLines.push("- Output was truncated by whole thread blocks to fit MCP size limits."); } headerLines.push("", "## Threads", ""); if (comments.length === 0) { headerLines.push("No comments were available in the extracted payload.", ""); return `${headerLines.join("\n")}`.trimEnd() + "\n"; } const threadBlocks = comments.map((thread, index) => renderThreadBlock(thread, index + 1)); return `${headerLines.join("\n")}\n${threadBlocks.join("")}`.trimEnd() + "\n"; } function renderThreadBlock(thread, index) { const lines = [`### Thread ${index}`, ""]; appendThreadLines(thread, 0, lines); lines.push(""); return `${lines.join("\n")}\n`; } function appendThreadLines(comment, depth, lines) { const indent = " ".repeat(depth); lines.push(`${indent}- comment_id: ${formatScalar(comment.id)}`); lines.push(`${indent} parent_id: ${formatScalar(comment.parent)}`); lines.push(`${indent} depth: ${comment.depth}`); appendOptionalLine(lines, `${indent} author`, comment.author); appendOptionalLine(lines, `${indent} author_id`, comment.author_id); appendOptionalLine(lines, `${indent} author_url`, comment.author_url); appendOptionalLine(lines, `${indent} author_is_uploader`, comment.author_is_uploader); appendOptionalLine(lines, `${indent} author_is_verified`, comment.author_is_verified); appendOptionalLine(lines, `${indent} is_pinned`, comment.is_pinned); appendOptionalLine(lines, `${indent} is_favorited`, comment.is_favorited); appendOptionalLine(lines, `${indent} like_count`, comment.like_count); appendOptionalLine(lines, `${indent} timestamp`, comment.timestamp); appendOptionalLine(lines, `${indent} time_text`, comment.time_text); lines.push(`${indent} reply_count: ${comment.reply_count}`); lines.push(`${indent} text:`); appendTextBlock(lines, `${indent} `, comment.text ?? ""); for (const reply of comment.replies) { appendThreadLines(reply, depth + 1, lines); } } function buildFlatStats(comments, orphanIds) { const rootThreads = comments.filter((comment) => comment.parent === "root").length; const orphanComments = comments.filter((comment) => orphanIds.has(comment.id)).length; return { count: comments.length, rootThreads, replyComments: Math.max(0, comments.length - rootThreads), orphanComments, }; } function buildThreadedStats(comments, orphanIds) { const flatComments = flattenThreads(comments); return { count: flatComments.length, rootThreads: comments.length, replyComments: Math.max(0, flatComments.length - comments.length), orphanComments: flatComments.filter((comment) => orphanIds.has(comment.id)).length, }; } function flattenThreads(comments) { const flattened = []; for (const comment of comments) { flattened.push(comment); flattened.push(...flattenThreads(comment.replies)); } return flattened; } function appendOptionalLine(lines, key, value) { if (value === undefined || value === null) { return; } lines.push(`${key}: ${formatScalar(value)}`); } function appendTextBlock(lines, indent, text) { const textLines = text.split("\n"); const normalizedLines = textLines.length > 0 ? textLines : [""]; for (const line of normalizedLines) { lines.push(line ? `${indent}| ${line}` : `${indent}|`); } } function formatScalar(value) { return JSON.stringify(value); } //# sourceMappingURL=comments-render.js.map