#!/usr/bin/env node
/**
* YouTube MCP Server
* Provides tools for YouTube content browsing and summarization
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import {
initializeYouTubeClient,
searchYouTube,
getVideoDetails as getVideoDetailsAPI,
getChannelInfo as getChannelInfoAPI,
getChannelVideos,
getPlaylistInfo as getPlaylistInfoAPI,
getPlaylistVideos as getPlaylistVideosAPI,
} from './youtube-api.js';
import { getVideoTranscript, formatTranscript } from './transcript.js';
// Initialize YouTube API client
const apiKey = process.env.YOUTUBE_API_KEY;
if (!apiKey) {
console.error(
'ERROR: YOUTUBE_API_KEY environment variable is not set.\n' +
'Please get an API key from https://console.cloud.google.com:\n' +
'1. Create a new project or select existing\n' +
'2. Enable "YouTube Data API v3"\n' +
'3. Create credentials (API key)\n' +
'4. Set YOUTUBE_API_KEY in your MCP configuration'
);
process.exit(1);
}
try {
initializeYouTubeClient(apiKey);
} catch (error) {
console.error('Failed to initialize YouTube client:', error);
process.exit(1);
}
// Define available tools
const tools: Tool[] = [
{
name: 'search_youtube',
description:
'Search YouTube for videos by keyword. Returns video metadata including title, channel, views, and thumbnails.',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search term or keyword',
},
maxResults: {
type: 'number',
description: 'Number of results to return (default 10, max 50)',
default: 10,
},
order: {
type: 'string',
enum: ['relevance', 'date', 'viewCount', 'rating'],
description: 'Sort order for results (default: relevance)',
default: 'relevance',
},
},
required: ['query'],
},
},
{
name: 'get_video_details',
description:
'Get comprehensive metadata about a specific YouTube video including title, description, duration, views, likes, tags, and thumbnails.',
inputSchema: {
type: 'object',
properties: {
videoId: {
type: 'string',
description: 'YouTube video ID (e.g., "dQw4w9WgXcQ")',
},
},
required: ['videoId'],
},
},
{
name: 'get_video_transcript',
description:
'Fetch the transcript/captions for a YouTube video. Returns the full text with timestamps. Useful for video summarization and content analysis.',
inputSchema: {
type: 'object',
properties: {
videoId: {
type: 'string',
description: 'YouTube video ID',
},
language: {
type: 'string',
description: 'Language code for transcript (default: "en")',
default: 'en',
},
includeTimestamps: {
type: 'boolean',
description: 'Include timestamps in the output (default: true)',
default: true,
},
},
required: ['videoId'],
},
},
{
name: 'get_channel_info',
description:
'Get detailed information about a YouTube channel including subscriber count, video count, view count, and optionally recent videos.',
inputSchema: {
type: 'object',
properties: {
channelId: {
type: 'string',
description: 'YouTube channel ID (e.g., "UC_x5XG1OV2P6uZZ5FSM9Ttw")',
},
includeVideos: {
type: 'boolean',
description: 'Include recent videos from the channel (default: false)',
default: false,
},
},
required: ['channelId'],
},
},
{
name: 'get_channel_videos',
description:
'List videos from a specific YouTube channel. Returns video metadata sorted by date, view count, or title.',
inputSchema: {
type: 'object',
properties: {
channelId: {
type: 'string',
description: 'YouTube channel ID',
},
maxResults: {
type: 'number',
description: 'Number of videos to return (default 25, max 50)',
default: 25,
},
order: {
type: 'string',
enum: ['date', 'viewCount', 'title'],
description: 'Sort order for videos (default: date)',
default: 'date',
},
},
required: ['channelId'],
},
},
{
name: 'get_playlist_info',
description:
'Get information about a YouTube playlist including title, description, video count, and channel details.',
inputSchema: {
type: 'object',
properties: {
playlistId: {
type: 'string',
description: 'YouTube playlist ID (e.g., "PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf")',
},
},
required: ['playlistId'],
},
},
{
name: 'get_playlist_videos',
description:
'List all videos in a YouTube playlist with their metadata and position in the playlist.',
inputSchema: {
type: 'object',
properties: {
playlistId: {
type: 'string',
description: 'YouTube playlist ID',
},
maxResults: {
type: 'number',
description: 'Number of videos to return (default 50)',
default: 50,
},
},
required: ['playlistId'],
},
},
];
// Create MCP server
const server = new Server(
{
name: 'youtube-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'search_youtube': {
const { query, maxResults = 10, order = 'relevance' } = args as {
query: string;
maxResults?: number;
order?: 'relevance' | 'date' | 'viewCount' | 'rating';
};
const results = await searchYouTube(query, maxResults, order);
return {
content: [
{
type: 'text',
text: JSON.stringify(results, null, 2),
},
],
};
}
case 'get_video_details': {
const { videoId } = args as { videoId: string };
const details = await getVideoDetailsAPI(videoId);
return {
content: [
{
type: 'text',
text: JSON.stringify(details, null, 2),
},
],
};
}
case 'get_video_transcript': {
const {
videoId,
language = 'en',
includeTimestamps = true,
} = args as {
videoId: string;
language?: string;
includeTimestamps?: boolean;
};
const transcript = await getVideoTranscript(videoId, language);
const formattedTranscript = formatTranscript(transcript, includeTimestamps);
return {
content: [
{
type: 'text',
text: formattedTranscript,
},
],
};
}
case 'get_channel_info': {
const { channelId, includeVideos = false } = args as {
channelId: string;
includeVideos?: boolean;
};
const channelInfo = await getChannelInfoAPI(channelId, includeVideos);
return {
content: [
{
type: 'text',
text: JSON.stringify(channelInfo, null, 2),
},
],
};
}
case 'get_channel_videos': {
const { channelId, maxResults = 25, order = 'date' } = args as {
channelId: string;
maxResults?: number;
order?: 'date' | 'viewCount' | 'title';
};
const videos = await getChannelVideos(channelId, maxResults, order);
return {
content: [
{
type: 'text',
text: JSON.stringify(videos, null, 2),
},
],
};
}
case 'get_playlist_info': {
const { playlistId } = args as { playlistId: string };
const playlistInfo = await getPlaylistInfoAPI(playlistId);
return {
content: [
{
type: 'text',
text: JSON.stringify(playlistInfo, null, 2),
},
],
};
}
case 'get_playlist_videos': {
const { playlistId, maxResults = 50 } = args as {
playlistId: string;
maxResults?: number;
};
const videos = await getPlaylistVideosAPI(playlistId, maxResults);
return {
content: [
{
type: 'text',
text: JSON.stringify(videos, null, 2),
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
error: errorMessage,
tool: name,
},
null,
2
),
},
],
isError: true,
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('YouTube MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});