@yilin-jing/youtube-mcp-server 1.0.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # YouTube MCP Server
2
- [![smithery badge](https://smithery.ai/badge/@ZubeidHendricks/youtube)](https://smithery.ai/server/@ZubeidHendricks/youtube)
3
2
 
4
- A Model Context Protocol (MCP) server implementation for YouTube, enabling AI language models to interact with YouTube content through a standardized interface.
3
+ [![npm version](https://img.shields.io/npm/v/@yilin-jing/youtube-mcp-server.svg)](https://www.npmjs.com/package/@yilin-jing/youtube-mcp-server)
4
+
5
+ A Model Context Protocol (MCP) server implementation for YouTube, enabling AI language models to interact with YouTube content through a standardized interface. Updated to support MCP SDK 1.23.
5
6
 
6
7
  ## Features
7
8
 
@@ -26,8 +27,7 @@ A Model Context Protocol (MCP) server implementation for YouTube, enabling AI la
26
27
  ### Playlist Management
27
28
  * List playlist items
28
29
  * Get playlist details
29
- * Search within playlists
30
- * Get playlist video transcripts
30
+ * Search playlists
31
31
 
32
32
  ## Installation
33
33
 
@@ -35,7 +35,7 @@ A Model Context Protocol (MCP) server implementation for YouTube, enabling AI la
35
35
 
36
36
  1. Install the package:
37
37
  ```bash
38
- npm install -g zubeid-youtube-mcp-server
38
+ npm install -g @yilin-jing/youtube-mcp-server
39
39
  ```
40
40
 
41
41
  2. Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS or `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
@@ -43,8 +43,8 @@ npm install -g zubeid-youtube-mcp-server
43
43
  ```json
44
44
  {
45
45
  "mcpServers": {
46
- "zubeid-youtube-mcp-server": {
47
- "command": "zubeid-youtube-mcp-server",
46
+ "youtube": {
47
+ "command": "youtube-mcp-server",
48
48
  "env": {
49
49
  "YOUTUBE_API_KEY": "your_youtube_api_key_here"
50
50
  }
@@ -62,7 +62,7 @@ Add this to your Claude Desktop configuration:
62
62
  "mcpServers": {
63
63
  "youtube": {
64
64
  "command": "npx",
65
- "args": ["-y", "zubeid-youtube-mcp-server"],
65
+ "args": ["-y", "@yilin-jing/youtube-mcp-server"],
66
66
  "env": {
67
67
  "YOUTUBE_API_KEY": "your_youtube_api_key_here"
68
68
  }
@@ -71,118 +71,70 @@ Add this to your Claude Desktop configuration:
71
71
  }
72
72
  ```
73
73
 
74
- ### Installing via Smithery
75
-
76
- To install YouTube MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@ZubeidHendricks/youtube):
77
-
78
- ```bash
79
- npx -y @smithery/cli install @ZubeidHendricks/youtube --client claude
80
- ```
81
-
82
74
  ## Configuration
75
+
83
76
  Set the following environment variables:
84
77
  * `YOUTUBE_API_KEY`: Your YouTube Data API key (required)
85
78
  * `YOUTUBE_TRANSCRIPT_LANG`: Default language for transcripts (optional, defaults to 'en')
86
- ### Using with VS Code
87
-
88
- For one-click installation, click one of the install buttons below:
89
-
90
- [![Install with NPX in VS Code](https://img.shields.io/badge/VS_Code-NPM-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=youtube&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22zubeid-youtube-mcp-server%22%5D%2C%22env%22%3A%7B%22YOUTUBE_API_KEY%22%3A%22%24%7Binput%3AapiKey%7D%22%7D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apiKey%22%2C%22description%22%3A%22YouTube+API+Key%22%2C%22password%22%3Atrue%7D%5D) [![Install with NPX in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-NPM-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=youtube&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22zubeid-youtube-mcp-server%22%5D%2C%22env%22%3A%7B%22YOUTUBE_API_KEY%22%3A%22%24%7Binput%3AapiKey%7D%22%7D%7D&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22apiKey%22%2C%22description%22%3A%22YouTube+API+Key%22%2C%22password%22%3Atrue%7D%5D&quality=insiders)
91
-
92
- ### Manual Installation
93
-
94
- If you prefer manual installation, first check the install buttons at the top of this section. Otherwise, follow these steps:
95
-
96
- Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
97
-
98
- ```json
99
- {
100
- "mcp": {
101
- "inputs": [
102
- {
103
- "type": "promptString",
104
- "id": "apiKey",
105
- "description": "YouTube API Key",
106
- "password": true
107
- }
108
- ],
109
- "servers": {
110
- "youtube": {
111
- "command": "npx",
112
- "args": ["-y", "zubeid-youtube-mcp-server"],
113
- "env": {
114
- "YOUTUBE_API_KEY": "${input:apiKey}"
115
- }
116
- }
117
- }
118
- }
119
- }
120
- ```
121
-
122
- Optionally, you can add it to a file called `.vscode/mcp.json` in your workspace:
123
79
 
124
- ```json
125
- {
126
- "inputs": [
127
- {
128
- "type": "promptString",
129
- "id": "apiKey",
130
- "description": "YouTube API Key",
131
- "password": true
132
- }
133
- ],
134
- "servers": {
135
- "youtube": {
136
- "command": "npx",
137
- "args": ["-y", "zubeid-youtube-mcp-server"],
138
- "env": {
139
- "YOUTUBE_API_KEY": "${input:apiKey}"
140
- }
141
- }
142
- }
143
- }
144
- ```
145
80
  ## YouTube API Setup
146
- 1. Go to Google Cloud Console
81
+
82
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
147
83
  2. Create a new project or select an existing one
148
84
  3. Enable the YouTube Data API v3
149
85
  4. Create API credentials (API key)
150
86
  5. Copy the API key for configuration
151
87
 
88
+ ## Available Tools
89
+
90
+ | Tool | Description |
91
+ |------|-------------|
92
+ | `videos_getVideo` | Get detailed information about a YouTube video |
93
+ | `videos_searchVideos` | Search for videos on YouTube |
94
+ | `transcripts_getTranscript` | Get the transcript of a YouTube video |
95
+ | `channels_getChannel` | Get information about a YouTube channel |
96
+ | `channels_listVideos` | Get videos from a specific channel |
97
+ | `playlists_getPlaylist` | Get information about a YouTube playlist |
98
+ | `playlists_getPlaylistItems` | Get videos in a YouTube playlist |
99
+
152
100
  ## Examples
153
101
 
154
102
  ### Managing Videos
155
103
 
156
104
  ```javascript
157
105
  // Get video details
158
- const video = await youtube.videos.getVideo({
159
- videoId: "video-id"
160
- });
161
-
162
- // Get video transcript
163
- const transcript = await youtube.transcripts.getTranscript({
164
- videoId: "video-id",
165
- language: "en"
106
+ const video = await youtube.videos_getVideo({
107
+ videoId: "dQw4w9WgXcQ"
166
108
  });
167
109
 
168
110
  // Search videos
169
- const searchResults = await youtube.videos.searchVideos({
170
- query: "search term",
111
+ const searchResults = await youtube.videos_searchVideos({
112
+ query: "TypeScript tutorial",
171
113
  maxResults: 10
172
114
  });
173
115
  ```
174
116
 
117
+ ### Managing Transcripts
118
+
119
+ ```javascript
120
+ // Get video transcript
121
+ const transcript = await youtube.transcripts_getTranscript({
122
+ videoId: "dQw4w9WgXcQ",
123
+ language: "en"
124
+ });
125
+ ```
126
+
175
127
  ### Managing Channels
176
128
 
177
129
  ```javascript
178
130
  // Get channel details
179
- const channel = await youtube.channels.getChannel({
180
- channelId: "channel-id"
131
+ const channel = await youtube.channels_getChannel({
132
+ channelId: "UC_x5XG1OV2P6uZZ5FSM9Ttw"
181
133
  });
182
134
 
183
135
  // List channel videos
184
- const videos = await youtube.channels.listVideos({
185
- channelId: "channel-id",
136
+ const videos = await youtube.channels_listVideos({
137
+ channelId: "UC_x5XG1OV2P6uZZ5FSM9Ttw",
186
138
  maxResults: 50
187
139
  });
188
140
  ```
@@ -191,14 +143,14 @@ const videos = await youtube.channels.listVideos({
191
143
 
192
144
  ```javascript
193
145
  // Get playlist items
194
- const playlistItems = await youtube.playlists.getPlaylistItems({
195
- playlistId: "playlist-id",
146
+ const playlistItems = await youtube.playlists_getPlaylistItems({
147
+ playlistId: "PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf",
196
148
  maxResults: 50
197
149
  });
198
150
 
199
151
  // Get playlist details
200
- const playlist = await youtube.playlists.getPlaylist({
201
- playlistId: "playlist-id"
152
+ const playlist = await youtube.playlists_getPlaylist({
153
+ playlistId: "PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf"
202
154
  });
203
155
  ```
204
156
 
@@ -207,19 +159,32 @@ const playlist = await youtube.playlists.getPlaylist({
207
159
  ```bash
208
160
  # Install dependencies
209
161
  npm install
210
-
211
- # Run tests
212
- npm test
162
+ # or
163
+ bun install
213
164
 
214
165
  # Build
215
166
  npm run build
216
167
 
217
- # Lint
218
- npm run lint
168
+ # Run tests
169
+ bun test
170
+
171
+ # Start server
172
+ npm start
219
173
  ```
220
174
 
221
- ## Contributing
222
- See CONTRIBUTING.md for information about contributing to this repository.
175
+ ## Testing
176
+
177
+ This project includes 50 comprehensive tests covering all services:
178
+
179
+ ```bash
180
+ # Run tests with bun
181
+ YOUTUBE_API_KEY="your_api_key" bun test
182
+ ```
223
183
 
224
184
  ## License
185
+
225
186
  This project is licensed under the MIT License - see the LICENSE file for details.
187
+
188
+ ## Credits
189
+
190
+ Originally forked from [ZubeidHendricks/youtube-mcp-server](https://github.com/ZubeidHendricks/youtube-mcp-server), updated with MCP SDK 1.23 support and comprehensive test coverage.
@@ -0,0 +1,13 @@
1
+ export declare const DataCleaners: {
2
+ cleanVideo: (video: any) => any;
3
+ cleanSearchResult: (item: any) => any;
4
+ cleanChannel: (channel: any) => any;
5
+ cleanPlaylist: (playlist: any) => any;
6
+ cleanPlaylistItem: (item: any) => any;
7
+ cleanTranscript: (result: any) => any;
8
+ cleanVideoList: (videos: any[]) => any[];
9
+ cleanSearchResults: (items: any[]) => any[];
10
+ cleanPlaylistItems: (items: any[]) => any[];
11
+ cleanComment: (item: any) => any;
12
+ cleanComments: (result: any) => any;
13
+ };
@@ -0,0 +1,145 @@
1
+ // Data cleaners for YouTube entities - removes redundant data for token efficiency
2
+ export const DataCleaners = {
3
+ cleanVideo: (video) => {
4
+ if (!video)
5
+ return null;
6
+ const snippet = video.snippet || {};
7
+ const stats = video.statistics || {};
8
+ const content = video.contentDetails || {};
9
+ return {
10
+ id: video.id,
11
+ title: snippet.title,
12
+ channel: snippet.channelTitle,
13
+ channelId: snippet.channelId,
14
+ published: snippet.publishedAt,
15
+ description: snippet.description?.substring(0, 300),
16
+ duration: content.duration,
17
+ views: stats.viewCount ? parseInt(stats.viewCount) : undefined,
18
+ likes: stats.likeCount ? parseInt(stats.likeCount) : undefined,
19
+ comments: stats.commentCount ? parseInt(stats.commentCount) : undefined,
20
+ tags: snippet.tags?.slice(0, 5),
21
+ };
22
+ },
23
+ cleanSearchResult: (item) => {
24
+ if (!item)
25
+ return null;
26
+ const snippet = item.snippet || {};
27
+ return {
28
+ id: item.id?.videoId || item.id,
29
+ title: snippet.title,
30
+ channel: snippet.channelTitle,
31
+ channelId: snippet.channelId,
32
+ published: snippet.publishedAt,
33
+ description: snippet.description?.substring(0, 200),
34
+ };
35
+ },
36
+ cleanChannel: (channel) => {
37
+ if (!channel)
38
+ return null;
39
+ const snippet = channel.snippet || {};
40
+ const stats = channel.statistics || {};
41
+ return {
42
+ id: channel.id,
43
+ title: snippet.title,
44
+ description: snippet.description?.substring(0, 300),
45
+ customUrl: snippet.customUrl,
46
+ published: snippet.publishedAt,
47
+ country: snippet.country,
48
+ subscribers: stats.subscriberCount ? parseInt(stats.subscriberCount) : undefined,
49
+ videos: stats.videoCount ? parseInt(stats.videoCount) : undefined,
50
+ views: stats.viewCount ? parseInt(stats.viewCount) : undefined,
51
+ };
52
+ },
53
+ cleanPlaylist: (playlist) => {
54
+ if (!playlist)
55
+ return null;
56
+ const snippet = playlist.snippet || {};
57
+ const content = playlist.contentDetails || {};
58
+ return {
59
+ id: playlist.id,
60
+ title: snippet.title,
61
+ channel: snippet.channelTitle,
62
+ channelId: snippet.channelId,
63
+ description: snippet.description?.substring(0, 200),
64
+ published: snippet.publishedAt,
65
+ itemCount: content.itemCount,
66
+ };
67
+ },
68
+ cleanPlaylistItem: (item) => {
69
+ if (!item)
70
+ return null;
71
+ const snippet = item.snippet || {};
72
+ return {
73
+ id: snippet.resourceId?.videoId,
74
+ title: snippet.title,
75
+ channel: snippet.videoOwnerChannelTitle,
76
+ position: snippet.position,
77
+ published: snippet.publishedAt,
78
+ };
79
+ },
80
+ cleanTranscript: (result) => {
81
+ if (!result)
82
+ return null;
83
+ const segments = result.transcript || result;
84
+ if (!Array.isArray(segments))
85
+ return { videoId: result.videoId, segments: [] };
86
+ return {
87
+ videoId: result.videoId,
88
+ language: result.language,
89
+ segments: segments.map(s => ({
90
+ text: s.text,
91
+ start: s.offset ?? s.start,
92
+ duration: s.duration,
93
+ })),
94
+ };
95
+ },
96
+ cleanVideoList: (videos) => {
97
+ if (!Array.isArray(videos))
98
+ return [];
99
+ return videos.map(DataCleaners.cleanVideo).filter(Boolean);
100
+ },
101
+ cleanSearchResults: (items) => {
102
+ if (!Array.isArray(items))
103
+ return [];
104
+ return items.map(DataCleaners.cleanSearchResult).filter(Boolean);
105
+ },
106
+ cleanPlaylistItems: (items) => {
107
+ if (!Array.isArray(items))
108
+ return [];
109
+ return items.map(DataCleaners.cleanPlaylistItem).filter(Boolean);
110
+ },
111
+ cleanComment: (item) => {
112
+ if (!item)
113
+ return null;
114
+ const snippet = item.snippet || {};
115
+ const topComment = snippet.topLevelComment?.snippet || {};
116
+ return {
117
+ id: item.id,
118
+ author: topComment.authorDisplayName,
119
+ authorChannelId: topComment.authorChannelId?.value,
120
+ text: topComment.textDisplay,
121
+ likes: topComment.likeCount ? parseInt(topComment.likeCount) : 0,
122
+ published: topComment.publishedAt,
123
+ updated: topComment.updatedAt,
124
+ replyCount: snippet.totalReplyCount || 0,
125
+ replies: item.replies?.comments?.map((reply) => ({
126
+ id: reply.id,
127
+ author: reply.snippet?.authorDisplayName,
128
+ authorChannelId: reply.snippet?.authorChannelId?.value,
129
+ text: reply.snippet?.textDisplay,
130
+ likes: reply.snippet?.likeCount ? parseInt(reply.snippet.likeCount) : 0,
131
+ published: reply.snippet?.publishedAt,
132
+ })) || [],
133
+ };
134
+ },
135
+ cleanComments: (result) => {
136
+ if (!result)
137
+ return { comments: [], nextPageToken: null };
138
+ const items = result.items || [];
139
+ return {
140
+ comments: items.map(DataCleaners.cleanComment).filter(Boolean),
141
+ nextPageToken: result.nextPageToken || null,
142
+ totalResults: result.pageInfo?.totalResults,
143
+ };
144
+ },
145
+ };
package/dist/server.js CHANGED
@@ -1,106 +1,125 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { z } from 'zod';
4
+ import { encode } from '@toon-format/toon';
5
+ import * as fs from 'fs';
6
+ import * as path from 'path';
4
7
  import { VideoService } from './services/video.js';
5
8
  import { TranscriptService } from './services/transcript.js';
6
9
  import { PlaylistService } from './services/playlist.js';
7
10
  import { ChannelService } from './services/channel.js';
11
+ import { CommentService } from './services/comment.js';
12
+ import { DataCleaners } from './cleaners.js';
13
+ function saveData(data, dir, toolName) {
14
+ try {
15
+ if (!fs.existsSync(dir))
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ const filename = `${toolName}_${new Date().toISOString().replace(/[:.]/g, '-')}.json`;
18
+ const filepath = path.join(dir, filename);
19
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
20
+ return filepath;
21
+ }
22
+ catch (e) {
23
+ return `Error saving: ${e}`;
24
+ }
25
+ }
26
+ function formatResponse(cleanedData, options) {
27
+ const output = { data: cleanedData };
28
+ let savedPath = '';
29
+ if (options.saveDir && options.toolName) {
30
+ savedPath = saveData(output, options.saveDir, options.toolName);
31
+ }
32
+ let text = encode(output);
33
+ if (savedPath)
34
+ text += `\n\n[Cleaned data saved to: ${savedPath}]`;
35
+ return { content: [{ type: 'text', text }] };
36
+ }
8
37
  export async function startMcpServer() {
9
38
  const server = new McpServer({
10
- name: 'zubeid-youtube-mcp-server',
11
- version: '1.0.0',
39
+ name: 'youtube-mcp-server',
40
+ version: '1.3.0',
12
41
  });
13
42
  const videoService = new VideoService();
14
43
  const transcriptService = new TranscriptService();
15
44
  const playlistService = new PlaylistService();
16
45
  const channelService = new ChannelService();
17
- // Register tools using the new McpServer API
18
- server.tool('videos_getVideo', 'Get detailed information about a YouTube video', {
46
+ const commentService = new CommentService();
47
+ server.tool('videos_getVideo', 'Get video info. Returns cleaned data in TOON format.', {
19
48
  videoId: z.string().describe('The YouTube video ID'),
20
- parts: z.array(z.string()).optional().describe('Parts of the video to retrieve'),
21
- }, async ({ videoId, parts }) => {
49
+ parts: z.array(z.string()).optional().describe('Parts to retrieve'),
50
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
51
+ }, async ({ videoId, parts, save_dir }) => {
22
52
  const result = await videoService.getVideo({ videoId, parts });
23
- return {
24
- content: [{
25
- type: 'text',
26
- text: JSON.stringify(result, null, 2)
27
- }]
28
- };
53
+ const cleaned = DataCleaners.cleanVideo(result);
54
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'videos_getVideo' });
29
55
  });
30
- server.tool('videos_searchVideos', 'Search for videos on YouTube', {
56
+ server.tool('videos_searchVideos', 'Search videos. Returns cleaned data in TOON format.', {
31
57
  query: z.string().describe('Search query'),
32
- maxResults: z.number().optional().describe('Maximum number of results to return'),
33
- }, async ({ query, maxResults }) => {
58
+ maxResults: z.number().optional().describe('Max results (default: 10)'),
59
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
60
+ }, async ({ query, maxResults, save_dir }) => {
34
61
  const result = await videoService.searchVideos({ query, maxResults });
35
- return {
36
- content: [{
37
- type: 'text',
38
- text: JSON.stringify(result, null, 2)
39
- }]
40
- };
62
+ const cleaned = DataCleaners.cleanSearchResults(result);
63
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'videos_searchVideos' });
41
64
  });
42
- server.tool('transcripts_getTranscript', 'Get the transcript of a YouTube video', {
65
+ server.tool('transcripts_getTranscript', 'Get video transcript. Returns cleaned data in TOON format.', {
43
66
  videoId: z.string().describe('The YouTube video ID'),
44
- language: z.string().optional().describe('Language code for the transcript'),
45
- }, async ({ videoId, language }) => {
67
+ language: z.string().optional().describe('Language code'),
68
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
69
+ }, async ({ videoId, language, save_dir }) => {
46
70
  const result = await transcriptService.getTranscript({ videoId, language });
47
- return {
48
- content: [{
49
- type: 'text',
50
- text: JSON.stringify(result, null, 2)
51
- }]
52
- };
71
+ const cleaned = DataCleaners.cleanTranscript(result);
72
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'transcripts_getTranscript' });
53
73
  });
54
- server.tool('channels_getChannel', 'Get information about a YouTube channel', {
74
+ server.tool('channels_getChannel', 'Get channel info. Returns cleaned data in TOON format.', {
55
75
  channelId: z.string().describe('The YouTube channel ID'),
56
- }, async ({ channelId }) => {
76
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
77
+ }, async ({ channelId, save_dir }) => {
57
78
  const result = await channelService.getChannel({ channelId });
58
- return {
59
- content: [{
60
- type: 'text',
61
- text: JSON.stringify(result, null, 2)
62
- }]
63
- };
79
+ const cleaned = DataCleaners.cleanChannel(result);
80
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'channels_getChannel' });
64
81
  });
65
- server.tool('channels_listVideos', 'Get videos from a specific channel', {
82
+ server.tool('channels_listVideos', 'List channel videos. Returns cleaned data in TOON format.', {
66
83
  channelId: z.string().describe('The YouTube channel ID'),
67
- maxResults: z.number().optional().describe('Maximum number of results to return'),
68
- }, async ({ channelId, maxResults }) => {
84
+ maxResults: z.number().optional().describe('Max results'),
85
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
86
+ }, async ({ channelId, maxResults, save_dir }) => {
69
87
  const result = await channelService.listVideos({ channelId, maxResults });
70
- return {
71
- content: [{
72
- type: 'text',
73
- text: JSON.stringify(result, null, 2)
74
- }]
75
- };
88
+ const cleaned = DataCleaners.cleanSearchResults(result);
89
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'channels_listVideos' });
76
90
  });
77
- server.tool('playlists_getPlaylist', 'Get information about a YouTube playlist', {
91
+ server.tool('playlists_getPlaylist', 'Get playlist info. Returns cleaned data in TOON format.', {
78
92
  playlistId: z.string().describe('The YouTube playlist ID'),
79
- }, async ({ playlistId }) => {
93
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
94
+ }, async ({ playlistId, save_dir }) => {
80
95
  const result = await playlistService.getPlaylist({ playlistId });
81
- return {
82
- content: [{
83
- type: 'text',
84
- text: JSON.stringify(result, null, 2)
85
- }]
86
- };
96
+ const cleaned = DataCleaners.cleanPlaylist(result);
97
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'playlists_getPlaylist' });
87
98
  });
88
- server.tool('playlists_getPlaylistItems', 'Get videos in a YouTube playlist', {
99
+ server.tool('playlists_getPlaylistItems', 'Get playlist videos. Returns cleaned data in TOON format.', {
89
100
  playlistId: z.string().describe('The YouTube playlist ID'),
90
- maxResults: z.number().optional().describe('Maximum number of results to return'),
91
- }, async ({ playlistId, maxResults }) => {
101
+ maxResults: z.number().optional().describe('Max results'),
102
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
103
+ }, async ({ playlistId, maxResults, save_dir }) => {
92
104
  const result = await playlistService.getPlaylistItems({ playlistId, maxResults });
93
- return {
94
- content: [{
95
- type: 'text',
96
- text: JSON.stringify(result, null, 2)
97
- }]
98
- };
105
+ const cleaned = DataCleaners.cleanPlaylistItems(result);
106
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'playlists_getPlaylistItems' });
107
+ });
108
+ server.tool('comments_getComments', 'Get video comments. Returns cleaned data in TOON format.', {
109
+ videoId: z.string().describe('The YouTube video ID'),
110
+ maxResults: z.number().optional().describe('Max results (1-100, default: 100)'),
111
+ order: z.enum(['time', 'relevance']).optional().describe('Sort order (default: relevance)'),
112
+ pageToken: z.string().optional().describe('Page token for pagination'),
113
+ textFormat: z.enum(['html', 'plainText']).optional().describe('Text format (default: plainText)'),
114
+ save_dir: z.string().optional().describe('Directory to save cleaned JSON'),
115
+ }, async ({ videoId, maxResults, order, pageToken, textFormat, save_dir }) => {
116
+ const result = await commentService.getComments({ videoId, maxResults, order, pageToken, textFormat });
117
+ const cleaned = DataCleaners.cleanComments(result);
118
+ return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'comments_getComments' });
99
119
  });
100
- // Create transport and connect
101
120
  const transport = new StdioServerTransport();
102
121
  await server.connect(transport);
103
- console.error(`YouTube MCP Server v1.0.0 started successfully`);
122
+ console.error(`YouTube MCP Server v1.2.0 started successfully`);
104
123
  console.error(`Server will validate YouTube API key when tools are called`);
105
124
  return server;
106
125
  }
@@ -0,0 +1,17 @@
1
+ import { CommentParams } from '../types.js';
2
+ /**
3
+ * Service for interacting with YouTube comments
4
+ */
5
+ export declare class CommentService {
6
+ private youtube;
7
+ private initialized;
8
+ constructor();
9
+ /**
10
+ * Initialize the YouTube client only when needed
11
+ */
12
+ private initialize;
13
+ /**
14
+ * Get comments for a YouTube video
15
+ */
16
+ getComments({ videoId, maxResults, order, pageToken, textFormat }: CommentParams): Promise<any>;
17
+ }
@@ -0,0 +1,47 @@
1
+ import { google } from 'googleapis';
2
+ /**
3
+ * Service for interacting with YouTube comments
4
+ */
5
+ export class CommentService {
6
+ youtube;
7
+ initialized = false;
8
+ constructor() {
9
+ // Don't initialize in constructor
10
+ }
11
+ /**
12
+ * Initialize the YouTube client only when needed
13
+ */
14
+ initialize() {
15
+ if (this.initialized)
16
+ return;
17
+ const apiKey = process.env.YOUTUBE_API_KEY;
18
+ if (!apiKey) {
19
+ throw new Error('YOUTUBE_API_KEY environment variable is not set.');
20
+ }
21
+ this.youtube = google.youtube({
22
+ version: 'v3',
23
+ auth: apiKey
24
+ });
25
+ this.initialized = true;
26
+ }
27
+ /**
28
+ * Get comments for a YouTube video
29
+ */
30
+ async getComments({ videoId, maxResults = 100, order = 'relevance', pageToken, textFormat = 'plainText' }) {
31
+ try {
32
+ this.initialize();
33
+ const response = await this.youtube.commentThreads.list({
34
+ part: ['snippet', 'replies'],
35
+ videoId,
36
+ maxResults,
37
+ order,
38
+ pageToken,
39
+ textFormat
40
+ });
41
+ return response.data;
42
+ }
43
+ catch (error) {
44
+ throw new Error(`Failed to get comments: ${error instanceof Error ? error.message : String(error)}`);
45
+ }
46
+ }
47
+ }
package/dist/types.d.ts CHANGED
@@ -68,3 +68,13 @@ export interface PlaylistItemsParams {
68
68
  playlistId: string;
69
69
  maxResults?: number;
70
70
  }
71
+ /**
72
+ * Comment parameters
73
+ */
74
+ export interface CommentParams {
75
+ videoId: string;
76
+ maxResults?: number;
77
+ order?: 'time' | 'relevance';
78
+ pageToken?: string;
79
+ textFormat?: 'html' | 'plainText';
80
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@yilin-jing/youtube-mcp-server",
3
- "version": "1.0.0",
4
- "description": "YouTube MCP Server Implementation with SDK 1.23 support",
3
+ "version": "1.3.0",
4
+ "description": "YouTube MCP Server with TOON format for 90%+ token savings",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -21,9 +21,10 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@modelcontextprotocol/sdk": "^1.23.0",
24
+ "@toon-format/toon": "^2.0.1",
24
25
  "googleapis": "^129.0.0",
25
- "ytdl-core": "^4.11.5",
26
26
  "youtube-transcript": "^1.0.6",
27
+ "ytdl-core": "^4.11.5",
27
28
  "zod": "^3.23.0"
28
29
  },
29
30
  "devDependencies": {
@@ -39,7 +40,9 @@
39
40
  "model-context-protocol",
40
41
  "ai",
41
42
  "claude",
42
- "anthropic"
43
+ "anthropic",
44
+ "toon",
45
+ "token-efficient"
43
46
  ],
44
47
  "author": "Yilin Jing",
45
48
  "license": "MIT",
@@ -51,4 +54,4 @@
51
54
  "url": "https://github.com/Jing-yilin/youtube-mcp-server/issues"
52
55
  },
53
56
  "homepage": "https://github.com/Jing-yilin/youtube-mcp-server#readme"
54
- }
57
+ }