#
tokens: 10795/50000 13/13 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/icraft2170-youtube-data-mcp-server-badge.png)](https://mseep.ai/app/icraft2170-youtube-data-mcp-server)
  2 | 
  3 | # YouTube MCP Server
  4 | [![smithery badge](https://smithery.ai/badge/@icraft2170/youtube-data-mcp-server)](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 | }); 
```