youtube-data-mcp
Version:
MCP server for YouTube data extraction - transcripts, comments, and search
150 lines • 5.25 kB
JavaScript
import { config } from "../config/index.js";
async function fetchFromSerpApi(params) {
const url = new URL(config.serpApiBaseUrl);
Object.entries({ api_key: config.serpApiKey, ...params }).forEach(([key, value]) => {
url.searchParams.append(key, value);
});
const response = await fetch(url.toString(), {
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`SerpAPI error: ${response.status} - ${errorText}`);
}
return response.json();
}
export async function getVideoInfo(videoId) {
if (!config.serpApiKey) {
throw new Error("SERPAPI_KEY is not set in environment variables");
}
const data = await fetchFromSerpApi({
engine: "youtube_video",
v: videoId,
});
const sortingTokens = data.comments_sorting_token;
return {
videoId,
title: data.title,
viewCount: data.views,
publishDate: data.published_date,
channelName: data.channel?.name,
commentCount: data.extracted_comment_count,
commentsNextPageToken: data.comments_next_page_token,
commentsSortingTokens: sortingTokens,
};
}
export async function getComments(videoId, limit, sort, pageToken) {
if (!config.serpApiKey) {
throw new Error("SERPAPI_KEY is not set in environment variables");
}
let tokenToUse = pageToken;
let extractedVideoId = videoId || "";
if (!pageToken && videoId) {
const videoInfo = await getVideoInfo(videoId);
extractedVideoId = videoInfo.videoId;
tokenToUse = videoInfo.commentsNextPageToken;
if (sort === "time" && videoInfo.commentsSortingTokens) {
const newestFirstToken = videoInfo.commentsSortingTokens.find((token) => token.title.toLowerCase().includes("newest"));
if (newestFirstToken) {
tokenToUse = newestFirstToken.token;
}
}
}
if (!tokenToUse) {
throw new Error("Could not find valid comments token");
}
const data = await fetchFromSerpApi({
engine: "youtube_video",
next_page_token: tokenToUse,
});
const rawComments = data.comments;
if (!rawComments || !Array.isArray(rawComments)) {
return {
videoId: extractedVideoId,
videoTitle: data.title,
comments: [],
commentCount: 0,
nextPageToken: data.comments_next_page_token,
};
}
const comments = rawComments.slice(0, limit).map((comment) => ({
commentId: comment.comment_id || "",
author: comment.channel?.name || "Anonymous",
text: comment.content || "",
time: comment.published_date || "",
likes: comment.extracted_vote_count || 0,
replies: comment.replies_count || 0,
repliesToken: comment.replies_next_page_token || null,
}));
return {
videoId: data.video_id || extractedVideoId,
videoTitle: data.title,
comments,
commentCount: comments.length,
nextPageToken: data.comments_next_page_token,
};
}
export async function getReplies(pageToken) {
if (!config.serpApiKey) {
throw new Error("SERPAPI_KEY is not set in environment variables");
}
const data = await fetchFromSerpApi({
engine: "youtube_video",
next_page_token: pageToken,
});
const rawReplies = data.replies;
if (!rawReplies || !Array.isArray(rawReplies)) {
return {
parentCommentId: data.comment_parent_id || "",
replies: [],
replyCount: 0,
nextPageToken: data.replies_next_page_token,
};
}
const replies = rawReplies.map((reply) => ({
commentId: reply.comment_id || "",
author: reply.channel?.name || "Anonymous",
text: reply.content || "",
time: reply.published_date || "",
likes: reply.extracted_vote_count || 0,
}));
return {
parentCommentId: data.comment_parent_id || "",
replies,
replyCount: replies.length,
nextPageToken: data.replies_next_page_token,
};
}
export async function searchYouTube(query, limit, gl, hl, sp, pageToken) {
if (!config.serpApiKey) {
throw new Error("SERPAPI_KEY is not set in environment variables");
}
const params = {
engine: "youtube",
search_query: query,
};
if (gl)
params.gl = gl;
if (hl)
params.hl = hl;
if (sp)
params.sp = sp;
if (pageToken)
params.sp = pageToken;
const data = await fetchFromSerpApi(params);
const videoResults = data.video_results;
const pagination = data.pagination;
return {
searchMetadata: data.search_metadata,
searchParameters: data.search_parameters,
videoResults: videoResults?.slice(0, limit) || [],
channelResults: data.channel_results,
playlistResults: data.playlist_results,
pagination: pagination ? {
current: pagination.current,
next: pagination.next,
nextPageToken: pagination.next_page_token,
} : undefined,
};
}
//# sourceMappingURL=serpapi.js.map