# Directory Structure ``` ├── .cursorrules ├── .env.example ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── jest.config.js ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── functions │ │ └── videos.ts │ ├── index.ts │ └── types │ ├── youtube-captions-scraper.d.ts │ ├── youtube-transcript-api.d.ts │ └── youtube.ts ├── test-transcript.js └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2022, 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "@typescript-eslint/explicit-function-return-type": "off", 14 | "@typescript-eslint/no-explicit-any": "off", 15 | "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }] 16 | } 17 | } ``` -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- ``` 1 | You are a Senior TypeScript Developer and an Expert in Model Context Protocol (MCP) server development. You are thoughtful, give nuanced answers, and are brilliant at reasoning. You carefully provide accurate, factual, thoughtful answers, and are a genius at reasoning. 2 | 3 | - Follow the user's requirements carefully & to the letter. 4 | - First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail. 5 | - Confirm, then write code! 6 | - Always write correct, best practice, DRY principle (Don't Repeat Yourself), bug free, fully functional and working code. 7 | - Focus on easy and readable code, over being performant. 8 | - Fully implement all requested functionality. 9 | - Leave NO todo's, placeholders or missing pieces. 10 | - Ensure code is complete! Verify thoroughly finalised. 11 | - Include all required imports, and ensure proper naming of key components. 12 | - Be concise. Minimize any other prose. 13 | - If you think there might not be a correct answer, you say so. 14 | - If you do not know the answer, say so, instead of guessing. 15 | 16 | ### Coding Environment 17 | The user asks questions about the following technologies: 18 | - TypeScript 19 | - Node.js 20 | - Model Context Protocol SDK 21 | - YouTube API 22 | - Google Cloud APIs 23 | - External service integrations 24 | 25 | ### Code Implementation Guidelines 26 | Follow these rules when you write code: 27 | - Use early returns whenever possible to make the code more readable. 28 | - Define explicit types for all functions, parameters, and return values. 29 | - Prefer functional programming patterns with const arrow functions over traditional function declarations. 30 | - Event handler functions should be named with a "handle" prefix, like "handleVideoProcessing". 31 | - Organize related MCP functions into logical function groups. 32 | - Implement thorough error handling for all asynchronous operations and API calls. 33 | - Use environment variables for sensitive information like API keys. 34 | - Include appropriate logging for debugging and monitoring. 35 | - Use correct import syntax for external libraries. 36 | - Properly initialize API clients, especially for Google services. 37 | - Write modular, testable code with clear separation of concerns. 38 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/icraft2170-youtube-data-mcp-server) 2 | 3 | # YouTube MCP Server 4 | [](https://smithery.ai/server/@icraft2170/youtube-data-mcp-server) 5 | 6 | A Model Context Protocol (MCP) server implementation utilizing the YouTube Data API. It allows AI language models to interact with YouTube content through a standardized interface. 7 | 8 | ## Key Features 9 | 10 | ### Video Information 11 | * Retrieve detailed video information (title, description, duration, statistics) 12 | * Search for videos by keywords 13 | * Get related videos based on a specific video 14 | * Calculate and analyze video engagement ratios 15 | 16 | ### Transcript/Caption Management 17 | * Retrieve video captions with multi-language support 18 | * Specify language preferences for transcripts 19 | * Access time-stamped captions for precise content reference 20 | 21 | ### Channel Analysis 22 | * View detailed channel statistics (subscribers, views, video count) 23 | * Get top-performing videos from a channel 24 | * Analyze channel growth and engagement metrics 25 | 26 | ### Trend Analysis 27 | * View trending videos by region and category 28 | * Compare performance metrics across multiple videos 29 | * Discover popular content in specific categories 30 | 31 | ## Available Tools 32 | 33 | The server provides the following MCP tools: 34 | 35 | | Tool Name | Description | Required Parameters | 36 | |-----------|-------------|---------------------| 37 | | `getVideoDetails` | Get detailed information about multiple YouTube videos including metadata, statistics, and content details | `videoIds` (array) | 38 | | `searchVideos` | Search for videos based on a query string | `query`, `maxResults` (optional) | 39 | | `getTranscripts` | Retrieve transcripts for multiple videos | `videoIds` (array), `lang` (optional) | 40 | | `getRelatedVideos` | Get videos related to a specific video based on YouTube's recommendation algorithm | `videoId`, `maxResults` (optional) | 41 | | `getChannelStatistics` | Retrieve detailed metrics for multiple channels including subscriber count, view count, and video count | `channelIds` (array) | 42 | | `getChannelTopVideos` | Get the most viewed videos from a specific channel | `channelId`, `maxResults` (optional) | 43 | | `getVideoEngagementRatio` | Calculate engagement metrics for multiple videos (views, likes, comments, and engagement ratio) | `videoIds` (array) | 44 | | `getTrendingVideos` | Get currently popular videos by region and category | `regionCode` (optional), `categoryId` (optional), `maxResults` (optional) | 45 | | `compareVideos` | Compare statistics across multiple videos | `videoIds` (array) | 46 | 47 | ## Installation 48 | 49 | ### Automatic Installation via Smithery 50 | 51 | Automatically install YouTube MCP Server for Claude Desktop via [Smithery](https://smithery.ai/server/@icraft2170/youtube-data-mcp-server): 52 | 53 | ```bash 54 | npx -y @smithery/cli install @icraft2170/youtube-data-mcp-server --client claude 55 | ``` 56 | 57 | ### Manual Installation 58 | ```bash 59 | # Install from npm 60 | npm install youtube-data-mcp-server 61 | 62 | # Or clone repository 63 | git clone https://github.com/icraft2170/youtube-data-mcp-server.git 64 | cd youtube-data-mcp-server 65 | npm install 66 | ``` 67 | 68 | ## Environment Configuration 69 | Set the following environment variables: 70 | * `YOUTUBE_API_KEY`: YouTube Data API key (required) 71 | * `YOUTUBE_TRANSCRIPT_LANG`: Default caption language (optional, default: 'ko') 72 | 73 | ## MCP Client Configuration 74 | Add the following to your Claude Desktop configuration file: 75 | 76 | ```json 77 | { 78 | "mcpServers": { 79 | "youtube": { 80 | "command": "npx", 81 | "args": ["-y", "youtube-data-mcp-server"], 82 | "env": { 83 | "YOUTUBE_API_KEY": "YOUR_API_KEY_HERE", 84 | "YOUTUBE_TRANSCRIPT_LANG": "ko" 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | ## YouTube API Setup 92 | 1. Access Google Cloud Console 93 | 2. Create a new project or select an existing one 94 | 3. Enable YouTube Data API v3 95 | 4. Create API credentials (API key) 96 | 5. Use the generated API key in your environment configuration 97 | 98 | ## Development 99 | 100 | ```bash 101 | # Install dependencies 102 | npm install 103 | 104 | # Run in development mode 105 | npm run dev 106 | 107 | # Build 108 | npm run build 109 | ``` 110 | 111 | ## Network Configuration 112 | 113 | The server exposes the following ports for communication: 114 | - HTTP: 3000 115 | - gRPC: 3001 116 | 117 | ## System Requirements 118 | - Node.js 18.0.0 or higher 119 | 120 | ## Security Considerations 121 | - Always keep your API key secure and never commit it to version control systems 122 | - Manage your API key through environment variables or configuration files 123 | - Set usage limits for your API key to prevent unauthorized use 124 | 125 | ## License 126 | This project is licensed under the MIT License. See the LICENSE file for details. 127 | ``` -------------------------------------------------------------------------------- /src/types/youtube-transcript-api.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | declare module 'youtube-transcript-api' { 2 | interface TranscriptItem { 3 | text: string; 4 | start: number; 5 | duration: number; 6 | } 7 | 8 | export class YoutubeTranscript { 9 | getTranscript(videoId: string): Promise<TranscriptItem[]>; 10 | } 11 | } ``` -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- ```javascript 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | extensionsToTreatAsEsm: ['.ts'], 5 | moduleNameMapper: { 6 | '^(\\.{1,2}/.*)\\.js$': '$1', 7 | }, 8 | transform: { 9 | '^.+\\.tsx?$': ['ts-jest', { 10 | useESM: true, 11 | }], 12 | }, 13 | }; ``` -------------------------------------------------------------------------------- /src/types/youtube-captions-scraper.d.ts: -------------------------------------------------------------------------------- ```typescript 1 | declare module 'youtube-captions-scraper' { 2 | interface SubtitleOptions { 3 | videoID: string; 4 | lang?: string; 5 | } 6 | 7 | interface SubtitleItem { 8 | start: number; 9 | dur: number; 10 | text: string; 11 | } 12 | 13 | export function getSubtitles(options: SubtitleOptions): Promise<SubtitleItem[]>; 14 | } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "skipLibCheck": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "declaration": true 13 | }, 14 | "include": ["src/**/*"], 15 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 16 | } ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | FROM node:lts-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | 7 | # Install app dependencies 8 | COPY package*.json ./ 9 | 10 | # Use npm ci to install dependencies without running scripts 11 | RUN npm ci --ignore-scripts 12 | 13 | # Copy all project files 14 | COPY . . 15 | 16 | # Build the project 17 | RUN npm run build 18 | 19 | # Ensure the main file is executable (if not already set) 20 | RUN chmod +x dist/index.js 21 | 22 | # Set environment variables for Docker (actual values should be provided at runtime) 23 | ENV YOUTUBE_API_KEY="" 24 | ENV YOUTUBE_TRANSCRIPT_LANG="en" 25 | 26 | # Command to run the MCP server 27 | CMD ["node", "dist/index.js"] 28 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - youtubeApiKey 10 | properties: 11 | youtubeApiKey: 12 | type: string 13 | description: YouTube Data API key for accessing the YouTube API. 14 | youtubeTranscriptLang: 15 | type: string 16 | default: en 17 | description: Default transcript language. Defaults to 'en'. 18 | commandFunction: 19 | # A JS function that produces the CLI command based on the given config to start the MCP on stdio. 20 | |- 21 | (config) => ({ 22 | command: 'node', 23 | args: ['dist/index.js'], 24 | env: { 25 | YOUTUBE_API_KEY: config.youtubeApiKey, 26 | YOUTUBE_TRANSCRIPT_LANG: config.youtubeTranscriptLang || 'en' 27 | } 28 | }) 29 | exampleConfig: 30 | youtubeApiKey: YOUR_YOUTUBE_API_KEY_HERE 31 | youtubeTranscriptLang: en 32 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "youtube-data-mcp-server", 3 | "version": "1.0.15", 4 | "description": "YouTube MCP Server Implementation", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "youtube-data-mcp-server": "./dist/index.js" 9 | }, 10 | "files": [ 11 | "dist", 12 | "README.md" 13 | ], 14 | "scripts": { 15 | "start": "node dist/index.js", 16 | "dev": "nodemon --exec ts-node --esm ./src/index.ts", 17 | "build": "tsc", 18 | "clean": "rimraf dist", 19 | "prebuild": "npm run clean", 20 | "lint": "eslint src/**/*.ts", 21 | "test": "jest", 22 | "prepare": "npm run build", 23 | "postinstall": "chmod +x ./dist/index.js" 24 | }, 25 | "keywords": [ 26 | "mcp", 27 | "youtube", 28 | "claude", 29 | "modelcontextprotocol" 30 | ], 31 | "author": "Hero", 32 | "license": "MIT", 33 | "dependencies": { 34 | "@modelcontextprotocol/sdk": "^1.7.0", 35 | "dotenv": "^16.4.7", 36 | "googleapis": "^129.0.0", 37 | "pnpm": "^10.6.4", 38 | "youtube-captions-scraper": "^2.0.0", 39 | "zod": "^3.24.2" 40 | }, 41 | "devDependencies": { 42 | "@types/jest": "^29.5.12", 43 | "@types/node": "^18.0.0", 44 | "@typescript-eslint/eslint-plugin": "^7.1.0", 45 | "@typescript-eslint/parser": "^7.1.0", 46 | "eslint": "^8.57.0", 47 | "jest": "^29.7.0", 48 | "nodemon": "^3.0.0", 49 | "rimraf": "^5.0.10", 50 | "ts-jest": "^29.1.2", 51 | "ts-node": "^10.9.1", 52 | "typescript": "^5.0.0" 53 | }, 54 | "engines": { 55 | "node": ">=18.0.0" 56 | }, 57 | "publishConfig": { 58 | "access": "public" 59 | } 60 | } 61 | ``` -------------------------------------------------------------------------------- /src/types/youtube.ts: -------------------------------------------------------------------------------- ```typescript 1 | export interface VideoInfo { 2 | id: string; 3 | snippet: { 4 | title: string; 5 | description: string; 6 | thumbnails: { 7 | default: { url: string }; 8 | medium: { url: string }; 9 | high: { url: string }; 10 | }; 11 | channelId: string; 12 | channelTitle: string; 13 | publishedAt: string; 14 | }; 15 | statistics: { 16 | viewCount: string; 17 | likeCount: string; 18 | commentCount: string; 19 | }; 20 | } 21 | 22 | export interface ChannelInfo { 23 | id: string; 24 | snippet: { 25 | title: string; 26 | description: string; 27 | thumbnails: { 28 | default: { url: string }; 29 | medium: { url: string }; 30 | high: { url: string }; 31 | }; 32 | customUrl: string; 33 | }; 34 | statistics: { 35 | viewCount: string; 36 | subscriberCount: string; 37 | videoCount: string; 38 | }; 39 | } 40 | 41 | export interface SearchResult { 42 | id: { 43 | kind: string; 44 | videoId: string | null; 45 | channelId: string | null; 46 | playlistId: string | null; 47 | }; 48 | snippet: { 49 | title: string; 50 | description: string; 51 | thumbnails: { 52 | default: { url: string }; 53 | medium: { url: string }; 54 | high: { url: string }; 55 | }; 56 | channelTitle: string; 57 | publishedAt: string; 58 | }; 59 | } 60 | 61 | export interface CommentInfo { 62 | id: string; 63 | snippet: { 64 | topLevelComment: { 65 | snippet: { 66 | textDisplay: string; 67 | authorDisplayName: string; 68 | authorProfileImageUrl: string; 69 | likeCount: number; 70 | publishedAt: string; 71 | }; 72 | }; 73 | totalReplyCount: number; 74 | }; 75 | } ``` -------------------------------------------------------------------------------- /src/functions/videos.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { google, youtube_v3 } from 'googleapis'; 2 | import { getSubtitles } from 'youtube-captions-scraper'; 3 | 4 | export interface VideoOptions { 5 | videoId: string; 6 | parts?: string[]; 7 | } 8 | 9 | export interface SearchOptions { 10 | query: string; 11 | maxResults?: number; 12 | } 13 | 14 | export interface ChannelOptions { 15 | channelId: string; 16 | maxResults?: number; 17 | } 18 | 19 | export interface TrendingOptions { 20 | regionCode?: string; 21 | categoryId?: string; 22 | maxResults?: number; 23 | } 24 | 25 | export interface CompareVideosOptions { 26 | videoIds: string[]; 27 | } 28 | 29 | export class VideoManagement { 30 | private youtube: youtube_v3.Youtube; 31 | private readonly MAX_RESULTS_PER_PAGE = 50; 32 | private readonly ABSOLUTE_MAX_RESULTS = 500; 33 | 34 | constructor() { 35 | this.youtube = google.youtube({ 36 | version: 'v3', 37 | auth: process.env.YOUTUBE_API_KEY 38 | }); 39 | } 40 | 41 | async getVideo({ videoId, parts = ['snippet'] }: VideoOptions) { 42 | try { 43 | const response = await this.youtube.videos.list({ 44 | part: parts, 45 | id: [videoId] 46 | }); 47 | 48 | if (!response.data.items?.length) { 49 | throw new Error('Video not found.'); 50 | } 51 | 52 | return response.data.items[0]; 53 | } catch (error: any) { 54 | throw new Error(`Failed to retrieve video information: ${error.message}`); 55 | } 56 | } 57 | 58 | async searchVideos({ query, maxResults = 10 }: SearchOptions) { 59 | try { 60 | const results: youtube_v3.Schema$SearchResult[] = []; 61 | let nextPageToken: string | undefined = undefined; 62 | const targetResults = Math.min(maxResults, this.ABSOLUTE_MAX_RESULTS); 63 | 64 | while (results.length < targetResults) { 65 | const response: youtube_v3.Schema$SearchListResponse = (await this.youtube.search.list({ 66 | part: ['snippet'], 67 | q: query, 68 | maxResults: Math.min(this.MAX_RESULTS_PER_PAGE, targetResults - results.length), 69 | type: ['video'], 70 | pageToken: nextPageToken 71 | })).data; 72 | 73 | if (!response.items?.length) { 74 | break; 75 | } 76 | 77 | results.push(...response.items); 78 | nextPageToken = response.nextPageToken || undefined; 79 | 80 | if (!nextPageToken) { 81 | break; 82 | } 83 | } 84 | 85 | return results.slice(0, targetResults); 86 | } catch (error: any) { 87 | throw new Error(`Failed to search videos: ${error.message}`); 88 | } 89 | } 90 | 91 | async getTranscript(videoId: string, lang?: string) { 92 | try { 93 | const transcript = await getSubtitles({ 94 | videoID: videoId, 95 | lang: lang || process.env.YOUTUBE_TRANSCRIPT_LANG || 'en' 96 | }); 97 | return transcript; 98 | } catch (error: any) { 99 | throw new Error(`Failed to retrieve transcript: ${error.message}`); 100 | } 101 | } 102 | 103 | async getRelatedVideos(videoId: string, maxResults: number = 10) { 104 | try { 105 | const response = await this.youtube.search.list({ 106 | part: ['snippet'], 107 | type: ['video'], 108 | maxResults, 109 | relatedToVideoId: videoId 110 | } as youtube_v3.Params$Resource$Search$List); 111 | 112 | return response.data.items || []; 113 | } catch (error: any) { 114 | throw new Error(`Failed to retrieve related videos: ${error.message}`); 115 | } 116 | } 117 | 118 | async getChannelStatistics(channelId: string) { 119 | try { 120 | const response = await this.youtube.channels.list({ 121 | part: ['snippet', 'statistics'], 122 | id: [channelId] 123 | }); 124 | 125 | if (!response.data.items?.length) { 126 | throw new Error('Channel not found.'); 127 | } 128 | 129 | const channel = response.data.items[0]; 130 | return { 131 | title: channel.snippet?.title, 132 | subscriberCount: channel.statistics?.subscriberCount, 133 | viewCount: channel.statistics?.viewCount, 134 | videoCount: channel.statistics?.videoCount 135 | }; 136 | } catch (error: any) { 137 | throw new Error(`Failed to retrieve channel statistics: ${error.message}`); 138 | } 139 | } 140 | 141 | async getChannelTopVideos({ channelId, maxResults = 10 }: ChannelOptions) { 142 | try { 143 | const searchResults: youtube_v3.Schema$SearchResult[] = []; 144 | let nextPageToken: string | undefined = undefined; 145 | const targetResults = Math.min(maxResults, this.ABSOLUTE_MAX_RESULTS); 146 | 147 | while (searchResults.length < targetResults) { 148 | const searchResponse: youtube_v3.Schema$SearchListResponse = (await this.youtube.search.list({ 149 | part: ['id'], 150 | channelId: channelId, 151 | maxResults: Math.min(this.MAX_RESULTS_PER_PAGE, targetResults - searchResults.length), 152 | order: 'viewCount', 153 | type: ['video'], 154 | pageToken: nextPageToken 155 | })).data; 156 | 157 | if (!searchResponse.items?.length) { 158 | break; 159 | } 160 | 161 | searchResults.push(...searchResponse.items); 162 | nextPageToken = searchResponse.nextPageToken || undefined; 163 | 164 | if (!nextPageToken) { 165 | break; 166 | } 167 | } 168 | 169 | if (!searchResults.length) { 170 | throw new Error('No videos found.'); 171 | } 172 | 173 | const videoIds = searchResults 174 | .map(item => item.id?.videoId) 175 | .filter((id): id is string => id !== undefined); 176 | 177 | // Retrieve video details in batches of 50 178 | const videoDetails: youtube_v3.Schema$Video[] = []; 179 | for (let i = 0; i < videoIds.length; i += this.MAX_RESULTS_PER_PAGE) { 180 | const batch = videoIds.slice(i, i + this.MAX_RESULTS_PER_PAGE); 181 | const videosResponse = await this.youtube.videos.list({ 182 | part: ['snippet', 'statistics'], 183 | id: batch 184 | }); 185 | 186 | if (videosResponse.data.items) { 187 | videoDetails.push(...videosResponse.data.items); 188 | } 189 | } 190 | 191 | return videoDetails.slice(0, targetResults).map(video => ({ 192 | id: video.id, 193 | title: video.snippet?.title, 194 | publishedAt: video.snippet?.publishedAt, 195 | viewCount: video.statistics?.viewCount, 196 | likeCount: video.statistics?.likeCount, 197 | commentCount: video.statistics?.commentCount 198 | })); 199 | } catch (error: any) { 200 | throw new Error(`Failed to retrieve channel's top videos: ${error.message}`); 201 | } 202 | } 203 | 204 | async getVideoEngagementRatio(videoId: string) { 205 | try { 206 | const response = await this.youtube.videos.list({ 207 | part: ['statistics'], 208 | id: [videoId] 209 | }); 210 | 211 | if (!response.data.items?.length) { 212 | throw new Error('Video not found.'); 213 | } 214 | 215 | const stats = response.data.items[0].statistics; 216 | const viewCount = parseInt(stats?.viewCount || '0'); 217 | const likeCount = parseInt(stats?.likeCount || '0'); 218 | const commentCount = parseInt(stats?.commentCount || '0'); 219 | 220 | const engagementRatio = viewCount > 0 221 | ? ((likeCount + commentCount) / viewCount * 100).toFixed(2) 222 | : '0'; 223 | 224 | return { 225 | viewCount, 226 | likeCount, 227 | commentCount, 228 | engagementRatio: `${engagementRatio}%` 229 | }; 230 | } catch (error: any) { 231 | throw new Error(`Failed to calculate video engagement ratio: ${error.message}`); 232 | } 233 | } 234 | 235 | async getTrendingVideos({ regionCode = 'US', categoryId, maxResults = 10 }: TrendingOptions) { 236 | try { 237 | const params: youtube_v3.Params$Resource$Videos$List = { 238 | part: ['snippet', 'statistics'], 239 | chart: 'mostPopular', 240 | regionCode: regionCode, 241 | maxResults: maxResults 242 | }; 243 | 244 | if (categoryId) { 245 | params.videoCategoryId = categoryId; 246 | } 247 | 248 | const response = await this.youtube.videos.list(params); 249 | 250 | return response.data.items?.map(video => ({ 251 | id: video.id, 252 | title: video.snippet?.title, 253 | channelTitle: video.snippet?.channelTitle, 254 | publishedAt: video.snippet?.publishedAt, 255 | viewCount: video.statistics?.viewCount, 256 | likeCount: video.statistics?.likeCount 257 | })) || []; 258 | } catch (error: any) { 259 | throw new Error(`Failed to retrieve trending videos: ${error.message}`); 260 | } 261 | } 262 | 263 | async compareVideos({ videoIds }: CompareVideosOptions) { 264 | try { 265 | const response = await this.youtube.videos.list({ 266 | part: ['snippet', 'statistics'], 267 | id: videoIds 268 | }); 269 | 270 | if (!response.data.items?.length) { 271 | throw new Error('No videos found.'); 272 | } 273 | 274 | return response.data.items.map(video => ({ 275 | id: video.id, 276 | title: video.snippet?.title, 277 | viewCount: video.statistics?.viewCount, 278 | likeCount: video.statistics?.likeCount, 279 | commentCount: video.statistics?.commentCount, 280 | publishedAt: video.snippet?.publishedAt 281 | })); 282 | } catch (error: any) { 283 | throw new Error(`Failed to compare videos: ${error.message}`); 284 | } 285 | } 286 | } ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | #!/usr/bin/env node 2 | 3 | import 'dotenv/config'; 4 | import { VideoManagement } from './functions/videos.js'; 5 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 6 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 7 | import { z } from "zod"; 8 | 9 | // Environment variable validation 10 | if (!process.env.YOUTUBE_API_KEY) { 11 | console.error('Error: YOUTUBE_API_KEY environment variable is not set.'); 12 | process.exit(1); 13 | } 14 | 15 | // Default subtitle language setting 16 | const defaultTranscriptLang = process.env.YOUTUBE_TRANSCRIPT_LANG || 'ko'; 17 | 18 | interface VideoDetailsParams { 19 | videoId: string; 20 | } 21 | 22 | interface VideoDetailsListParams { 23 | videoIds: string[]; 24 | } 25 | 26 | interface TranscriptParams { 27 | videoId: string; 28 | lang?: string; 29 | } 30 | 31 | interface TranscriptsParams { 32 | videoIds: string[]; 33 | lang?: string; 34 | } 35 | 36 | interface SearchParams { 37 | query: string; 38 | maxResults?: number; 39 | } 40 | 41 | interface RelatedVideosParams { 42 | videoId: string; 43 | maxResults?: number; 44 | } 45 | 46 | interface ChannelParams { 47 | channelId: string; 48 | maxResults?: number; 49 | } 50 | 51 | interface TrendingParams { 52 | regionCode?: string; 53 | categoryId?: string; 54 | maxResults?: number; 55 | } 56 | 57 | interface CompareVideosParams { 58 | videoIds: string[]; 59 | } 60 | 61 | interface VideoEngagementRatiosParams { 62 | videoIds: string[]; 63 | } 64 | 65 | interface ChannelStatisticsParams { 66 | channelIds: string[]; 67 | } 68 | 69 | async function main() { 70 | const videoManager = new VideoManagement(); 71 | 72 | // Create MCP server 73 | const server = new McpServer({ 74 | name: "YouTube", 75 | version: "1.0.0" 76 | }); 77 | 78 | // Video details retrieval tool 79 | server.tool("getVideoDetails", 80 | "Get detailed information about multiple YouTube videos. Returns comprehensive data including video metadata, statistics, and content details. Use this when you need complete information about specific videos.", 81 | { videoIds: z.array(z.string()) }, 82 | async ({ videoIds }: VideoDetailsListParams) => { 83 | try { 84 | const videoPromises = videoIds.map(videoId => 85 | videoManager.getVideo({ 86 | videoId, 87 | parts: ["snippet", "statistics", "contentDetails"] 88 | }) 89 | ); 90 | const videoDetailsList = await Promise.all(videoPromises); 91 | 92 | // Create a map of videoId to details 93 | const result = videoIds.reduce((acc, videoId, index) => { 94 | acc[videoId] = videoDetailsList[index]; 95 | return acc; 96 | }, {} as Record<string, any>); 97 | 98 | return { 99 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }] 100 | }; 101 | } catch (error: any) { 102 | return { 103 | content: [{ 104 | type: "text", 105 | text: JSON.stringify({ 106 | error: error.message, 107 | details: error.response?.data 108 | }, null, 2) 109 | }] 110 | }; 111 | } 112 | } 113 | ); 114 | 115 | // Video search tool 116 | server.tool("searchVideos", 117 | "Searches for videos based on a query string. Returns a list of videos matching the search criteria, including titles, descriptions, and metadata. Use this when you need to find videos related to specific topics or keywords.", 118 | { 119 | query: z.string(), 120 | maxResults: z.number().optional() 121 | }, 122 | async ({ query, maxResults }: SearchParams) => { 123 | try { 124 | const searchResults = await videoManager.searchVideos({ 125 | query, 126 | maxResults 127 | }); 128 | return { 129 | content: [{ type: "text", text: JSON.stringify(searchResults, null, 2) }] 130 | }; 131 | } catch (error: any) { 132 | return { 133 | content: [{ 134 | type: "text", 135 | text: JSON.stringify({ 136 | error: error.message, 137 | details: error.response?.data 138 | }, null, 2) 139 | }] 140 | }; 141 | } 142 | } 143 | ); 144 | 145 | // Video transcript retrieval tool 146 | server.tool("getTranscripts", 147 | "Retrieves transcripts for multiple videos. Returns the text content of videos' captions, useful for accessibility and content analysis. Use this when you need the spoken content of multiple videos.", 148 | { 149 | videoIds: z.array(z.string()), 150 | lang: z.string().optional() 151 | }, 152 | async ({ videoIds, lang }: TranscriptsParams) => { 153 | try { 154 | const transcriptPromises = videoIds.map(videoId => 155 | videoManager.getTranscript(videoId, lang) 156 | ); 157 | const transcripts = await Promise.all(transcriptPromises); 158 | 159 | // Create a map of videoId to transcript 160 | const result = videoIds.reduce((acc, videoId, index) => { 161 | acc[videoId] = transcripts[index]; 162 | return acc; 163 | }, {} as Record<string, any>); 164 | 165 | return { 166 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }] 167 | }; 168 | } catch (error: any) { 169 | return { 170 | content: [{ 171 | type: "text", 172 | text: JSON.stringify({ 173 | error: error.message 174 | }, null, 2) 175 | }] 176 | }; 177 | } 178 | } 179 | ); 180 | 181 | // Related videos retrieval tool 182 | server.tool("getRelatedVideos", 183 | "Retrieves related videos for a specific video. Returns a list of videos that are similar or related to the specified video, based on YouTube's recommendation algorithm. Use this when you want to discover content similar to a particular video.", 184 | { 185 | videoId: z.string(), 186 | maxResults: z.number().optional() 187 | }, 188 | async ({ videoId, maxResults }: RelatedVideosParams) => { 189 | try { 190 | const relatedVideos = await videoManager.getRelatedVideos(videoId, maxResults); 191 | return { 192 | content: [{ type: "text", text: JSON.stringify(relatedVideos, null, 2) }] 193 | }; 194 | } catch (error: any) { 195 | return { 196 | content: [{ 197 | type: "text", 198 | text: JSON.stringify({ 199 | error: error.message, 200 | details: error.response?.data 201 | }, null, 2) 202 | }] 203 | }; 204 | } 205 | } 206 | ); 207 | 208 | // Channel statistics retrieval tool 209 | server.tool("getChannelStatistics", 210 | "Retrieves statistics for multiple channels. Returns detailed metrics including subscriber count, view count, and video count for each channel. Use this when you need to analyze the performance and reach of multiple YouTube channels.", 211 | { channelIds: z.array(z.string()) }, 212 | async ({ channelIds }: ChannelStatisticsParams) => { 213 | try { 214 | const statisticsPromises = channelIds.map(channelId => 215 | videoManager.getChannelStatistics(channelId) 216 | ); 217 | const statisticsResults = await Promise.all(statisticsPromises); 218 | 219 | // Create a map of channelId to statistics 220 | const result = channelIds.reduce((acc, channelId, index) => { 221 | acc[channelId] = statisticsResults[index]; 222 | return acc; 223 | }, {} as Record<string, any>); 224 | 225 | return { 226 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }] 227 | }; 228 | } catch (error: any) { 229 | return { 230 | content: [{ 231 | type: "text", 232 | text: JSON.stringify({ 233 | error: error.message, 234 | details: error.response?.data 235 | }, null, 2) 236 | }] 237 | }; 238 | } 239 | } 240 | ); 241 | 242 | // Channel top videos retrieval tool 243 | server.tool("getChannelTopVideos", 244 | "Retrieves the top videos from a specific channel. Returns a list of the most viewed or popular videos from the channel, based on view count. Use this when you want to identify the most successful content from a channel.", 245 | { 246 | channelId: z.string(), 247 | maxResults: z.number().optional() 248 | }, 249 | async ({ channelId, maxResults }: ChannelParams) => { 250 | try { 251 | const topVideos = await videoManager.getChannelTopVideos({ channelId, maxResults }); 252 | return { 253 | content: [{ type: "text", text: JSON.stringify(topVideos, null, 2) }] 254 | }; 255 | } catch (error: any) { 256 | return { 257 | content: [{ 258 | type: "text", 259 | text: JSON.stringify({ 260 | error: error.message, 261 | details: error.response?.data 262 | }, null, 2) 263 | }] 264 | }; 265 | } 266 | } 267 | ); 268 | 269 | // Video engagement ratio calculation tool 270 | server.tool("getVideoEngagementRatio", 271 | "Calculates the engagement ratio for multiple videos. Returns metrics such as view count, like count, comment count, and the calculated engagement ratio for each video. Use this when you want to measure the audience interaction with videos.", 272 | { videoIds: z.array(z.string()) }, 273 | async ({ videoIds }: VideoEngagementRatiosParams) => { 274 | try { 275 | const engagementPromises = videoIds.map(videoId => 276 | videoManager.getVideoEngagementRatio(videoId) 277 | ); 278 | const engagementResults = await Promise.all(engagementPromises); 279 | 280 | // Create a map of videoId to engagement data 281 | const result = videoIds.reduce((acc, videoId, index) => { 282 | acc[videoId] = engagementResults[index]; 283 | return acc; 284 | }, {} as Record<string, any>); 285 | 286 | return { 287 | content: [{ type: "text", text: JSON.stringify(result, null, 2) }] 288 | }; 289 | } catch (error: any) { 290 | return { 291 | content: [{ 292 | type: "text", 293 | text: JSON.stringify({ 294 | error: error.message, 295 | details: error.response?.data 296 | }, null, 2) 297 | }] 298 | }; 299 | } 300 | } 301 | ); 302 | 303 | // Trending videos retrieval tool 304 | server.tool("getTrendingVideos", 305 | "Retrieves trending videos based on region and category. Returns a list of videos that are currently popular in the specified region and category. Use this when you want to discover what's trending in specific areas or categories. Available category IDs: 1 (Film & Animation), 2 (Autos & Vehicles), 10 (Music), 15 (Pets & Animals), 17 (Sports), 18 (Short Movies), 19 (Travel & Events), 20 (Gaming), 21 (Videoblogging), 22 (People & Blogs), 23 (Comedy), 24 (Entertainment), 25 (News & Politics), 26 (Howto & Style), 27 (Education), 28 (Science & Technology), 29 (Nonprofits & Activism), 30 (Movies), 31 (Anime/Animation), 32 (Action/Adventure), 33 (Classics), 34 (Comedy), 35 (Documentary), 36 (Drama), 37 (Family), 38 (Foreign), 39 (Horror), 40 (Sci-Fi/Fantasy), 41 (Thriller), 42 (Shorts), 43 (Shows), 44 (Trailers).", 306 | { 307 | regionCode: z.string().optional(), 308 | categoryId: z.string().optional(), 309 | maxResults: z.number().optional() 310 | }, 311 | async ({ regionCode, categoryId, maxResults }: TrendingParams) => { 312 | try { 313 | const trendingVideos = await videoManager.getTrendingVideos({ regionCode, categoryId, maxResults }); 314 | return { 315 | content: [{ type: "text", text: JSON.stringify(trendingVideos, null, 2) }] 316 | }; 317 | } catch (error: any) { 318 | return { 319 | content: [{ 320 | type: "text", 321 | text: JSON.stringify({ 322 | error: error.message, 323 | details: error.response?.data 324 | }, null, 2) 325 | }] 326 | }; 327 | } 328 | } 329 | ); 330 | 331 | // Video comparison tool 332 | server.tool("compareVideos", 333 | "Compares multiple videos based on their statistics. Returns a comparison of view counts, like counts, comment counts, and other metrics for the specified videos. Use this when you want to analyze the performance of multiple videos side by side.", 334 | { videoIds: z.array(z.string()) }, 335 | async ({ videoIds }: CompareVideosParams) => { 336 | try { 337 | const comparison = await videoManager.compareVideos({ videoIds }); 338 | return { 339 | content: [{ type: "text", text: JSON.stringify(comparison, null, 2) }] 340 | }; 341 | } catch (error: any) { 342 | return { 343 | content: [{ 344 | type: "text", 345 | text: JSON.stringify({ 346 | error: error.message, 347 | details: error.response?.data 348 | }, null, 2) 349 | }] 350 | }; 351 | } 352 | } 353 | ); 354 | 355 | // Start sending and receiving messages via stdin/stdout 356 | const transport = new StdioServerTransport(); 357 | await server.connect(transport); 358 | 359 | console.error('YouTube MCP server has started.'); 360 | } 361 | 362 | main().catch((err) => { 363 | console.error('Error occurred during server execution:', err); 364 | process.exit(1); 365 | }); ```