@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 +65 -100
- package/dist/cleaners.d.ts +13 -0
- package/dist/cleaners.js +145 -0
- package/dist/server.js +85 -66
- package/dist/services/comment.d.ts +17 -0
- package/dist/services/comment.js +47 -0
- package/dist/types.d.ts +10 -0
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# YouTube MCP Server
|
|
2
|
-
[](https://smithery.ai/server/@ZubeidHendricks/youtube)
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
[](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
|
|
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
|
|
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
|
-
"
|
|
47
|
-
"command": "
|
|
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", "
|
|
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
|
-
[](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) [](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
|
-
|
|
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.
|
|
159
|
-
videoId: "
|
|
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.
|
|
170
|
-
query: "
|
|
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.
|
|
180
|
-
channelId: "
|
|
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.
|
|
185
|
-
channelId: "
|
|
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.
|
|
195
|
-
playlistId: "
|
|
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.
|
|
201
|
-
playlistId: "
|
|
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
|
-
|
|
212
|
-
npm test
|
|
162
|
+
# or
|
|
163
|
+
bun install
|
|
213
164
|
|
|
214
165
|
# Build
|
|
215
166
|
npm run build
|
|
216
167
|
|
|
217
|
-
#
|
|
218
|
-
|
|
168
|
+
# Run tests
|
|
169
|
+
bun test
|
|
170
|
+
|
|
171
|
+
# Start server
|
|
172
|
+
npm start
|
|
219
173
|
```
|
|
220
174
|
|
|
221
|
-
##
|
|
222
|
-
|
|
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
|
+
};
|
package/dist/cleaners.js
ADDED
|
@@ -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: '
|
|
11
|
-
version: '1.
|
|
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
|
-
|
|
18
|
-
server.tool('videos_getVideo', 'Get
|
|
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
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
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('
|
|
33
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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', '
|
|
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('
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
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('
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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.
|
|
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.
|
|
4
|
-
"description": "YouTube MCP Server
|
|
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
|
+
}
|