@yilin-jing/youtube-mcp-server 1.0.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 +225 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +16 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +106 -0
- package/dist/services/channel.d.ts +29 -0
- package/dist/services/channel.js +95 -0
- package/dist/services/playlist.d.ts +25 -0
- package/dist/services/playlist.js +78 -0
- package/dist/services/transcript.d.ts +21 -0
- package/dist/services/transcript.js +85 -0
- package/dist/services/video.d.ts +35 -0
- package/dist/services/video.js +117 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# YouTube MCP Server
|
|
2
|
+
[](https://smithery.ai/server/@ZubeidHendricks/youtube)
|
|
3
|
+
|
|
4
|
+
A Model Context Protocol (MCP) server implementation for YouTube, enabling AI language models to interact with YouTube content through a standardized interface.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
|
|
8
|
+
### Video Information
|
|
9
|
+
* Get video details (title, description, duration, etc.)
|
|
10
|
+
* List channel videos
|
|
11
|
+
* Get video statistics (views, likes, comments)
|
|
12
|
+
* Search videos across YouTube
|
|
13
|
+
|
|
14
|
+
### Transcript Management
|
|
15
|
+
* Retrieve video transcripts
|
|
16
|
+
* Support for multiple languages
|
|
17
|
+
* Get timestamped captions
|
|
18
|
+
* Search within transcripts
|
|
19
|
+
|
|
20
|
+
### Channel Management
|
|
21
|
+
* Get channel details
|
|
22
|
+
* List channel playlists
|
|
23
|
+
* Get channel statistics
|
|
24
|
+
* Search within channel content
|
|
25
|
+
|
|
26
|
+
### Playlist Management
|
|
27
|
+
* List playlist items
|
|
28
|
+
* Get playlist details
|
|
29
|
+
* Search within playlists
|
|
30
|
+
* Get playlist video transcripts
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
### Quick Setup for Claude Desktop
|
|
35
|
+
|
|
36
|
+
1. Install the package:
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g zubeid-youtube-mcp-server
|
|
39
|
+
```
|
|
40
|
+
|
|
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):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"zubeid-youtube-mcp-server": {
|
|
47
|
+
"command": "zubeid-youtube-mcp-server",
|
|
48
|
+
"env": {
|
|
49
|
+
"YOUTUBE_API_KEY": "your_youtube_api_key_here"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Alternative: Using NPX (No Installation Required)
|
|
57
|
+
|
|
58
|
+
Add this to your Claude Desktop configuration:
|
|
59
|
+
|
|
60
|
+
```json
|
|
61
|
+
{
|
|
62
|
+
"mcpServers": {
|
|
63
|
+
"youtube": {
|
|
64
|
+
"command": "npx",
|
|
65
|
+
"args": ["-y", "zubeid-youtube-mcp-server"],
|
|
66
|
+
"env": {
|
|
67
|
+
"YOUTUBE_API_KEY": "your_youtube_api_key_here"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
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
|
+
## Configuration
|
|
83
|
+
Set the following environment variables:
|
|
84
|
+
* `YOUTUBE_API_KEY`: Your YouTube Data API key (required)
|
|
85
|
+
* `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
|
+
|
|
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
|
+
## YouTube API Setup
|
|
146
|
+
1. Go to Google Cloud Console
|
|
147
|
+
2. Create a new project or select an existing one
|
|
148
|
+
3. Enable the YouTube Data API v3
|
|
149
|
+
4. Create API credentials (API key)
|
|
150
|
+
5. Copy the API key for configuration
|
|
151
|
+
|
|
152
|
+
## Examples
|
|
153
|
+
|
|
154
|
+
### Managing Videos
|
|
155
|
+
|
|
156
|
+
```javascript
|
|
157
|
+
// 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"
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Search videos
|
|
169
|
+
const searchResults = await youtube.videos.searchVideos({
|
|
170
|
+
query: "search term",
|
|
171
|
+
maxResults: 10
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Managing Channels
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
// Get channel details
|
|
179
|
+
const channel = await youtube.channels.getChannel({
|
|
180
|
+
channelId: "channel-id"
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// List channel videos
|
|
184
|
+
const videos = await youtube.channels.listVideos({
|
|
185
|
+
channelId: "channel-id",
|
|
186
|
+
maxResults: 50
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Managing Playlists
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Get playlist items
|
|
194
|
+
const playlistItems = await youtube.playlists.getPlaylistItems({
|
|
195
|
+
playlistId: "playlist-id",
|
|
196
|
+
maxResults: 50
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Get playlist details
|
|
200
|
+
const playlist = await youtube.playlists.getPlaylist({
|
|
201
|
+
playlistId: "playlist-id"
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
# Install dependencies
|
|
209
|
+
npm install
|
|
210
|
+
|
|
211
|
+
# Run tests
|
|
212
|
+
npm test
|
|
213
|
+
|
|
214
|
+
# Build
|
|
215
|
+
npm run build
|
|
216
|
+
|
|
217
|
+
# Lint
|
|
218
|
+
npm run lint
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Contributing
|
|
222
|
+
See CONTRIBUTING.md for information about contributing to this repository.
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { startMcpServer } from './server.js';
|
|
3
|
+
// Check for required environment variables
|
|
4
|
+
if (!process.env.YOUTUBE_API_KEY) {
|
|
5
|
+
console.error('Error: YOUTUBE_API_KEY environment variable is required.');
|
|
6
|
+
console.error('Please set it before running this server.');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
// Start the MCP server
|
|
10
|
+
startMcpServer()
|
|
11
|
+
.then(() => {
|
|
12
|
+
console.log('YouTube MCP Server started successfully');
|
|
13
|
+
})
|
|
14
|
+
.catch(error => {
|
|
15
|
+
console.error('Failed to start YouTube MCP Server:', error);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { startMcpServer } from './server.js';
|
|
2
|
+
// Check for required environment variables
|
|
3
|
+
if (!process.env.YOUTUBE_API_KEY) {
|
|
4
|
+
console.error('Error: YOUTUBE_API_KEY environment variable is required.');
|
|
5
|
+
console.error('Please set it before running this server.');
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
// Start the MCP server
|
|
9
|
+
startMcpServer()
|
|
10
|
+
.then(() => {
|
|
11
|
+
console.log('YouTube MCP Server started successfully');
|
|
12
|
+
})
|
|
13
|
+
.catch(error => {
|
|
14
|
+
console.error('Failed to start YouTube MCP Server:', error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { VideoService } from './services/video.js';
|
|
5
|
+
import { TranscriptService } from './services/transcript.js';
|
|
6
|
+
import { PlaylistService } from './services/playlist.js';
|
|
7
|
+
import { ChannelService } from './services/channel.js';
|
|
8
|
+
export async function startMcpServer() {
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'zubeid-youtube-mcp-server',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
});
|
|
13
|
+
const videoService = new VideoService();
|
|
14
|
+
const transcriptService = new TranscriptService();
|
|
15
|
+
const playlistService = new PlaylistService();
|
|
16
|
+
const channelService = new ChannelService();
|
|
17
|
+
// Register tools using the new McpServer API
|
|
18
|
+
server.tool('videos_getVideo', 'Get detailed information about a YouTube video', {
|
|
19
|
+
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 }) => {
|
|
22
|
+
const result = await videoService.getVideo({ videoId, parts });
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: JSON.stringify(result, null, 2)
|
|
27
|
+
}]
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
server.tool('videos_searchVideos', 'Search for videos on YouTube', {
|
|
31
|
+
query: z.string().describe('Search query'),
|
|
32
|
+
maxResults: z.number().optional().describe('Maximum number of results to return'),
|
|
33
|
+
}, async ({ query, maxResults }) => {
|
|
34
|
+
const result = await videoService.searchVideos({ query, maxResults });
|
|
35
|
+
return {
|
|
36
|
+
content: [{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: JSON.stringify(result, null, 2)
|
|
39
|
+
}]
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
server.tool('transcripts_getTranscript', 'Get the transcript of a YouTube video', {
|
|
43
|
+
videoId: z.string().describe('The YouTube video ID'),
|
|
44
|
+
language: z.string().optional().describe('Language code for the transcript'),
|
|
45
|
+
}, async ({ videoId, language }) => {
|
|
46
|
+
const result = await transcriptService.getTranscript({ videoId, language });
|
|
47
|
+
return {
|
|
48
|
+
content: [{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: JSON.stringify(result, null, 2)
|
|
51
|
+
}]
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
server.tool('channels_getChannel', 'Get information about a YouTube channel', {
|
|
55
|
+
channelId: z.string().describe('The YouTube channel ID'),
|
|
56
|
+
}, async ({ channelId }) => {
|
|
57
|
+
const result = await channelService.getChannel({ channelId });
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: 'text',
|
|
61
|
+
text: JSON.stringify(result, null, 2)
|
|
62
|
+
}]
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
server.tool('channels_listVideos', 'Get videos from a specific channel', {
|
|
66
|
+
channelId: z.string().describe('The YouTube channel ID'),
|
|
67
|
+
maxResults: z.number().optional().describe('Maximum number of results to return'),
|
|
68
|
+
}, async ({ channelId, maxResults }) => {
|
|
69
|
+
const result = await channelService.listVideos({ channelId, maxResults });
|
|
70
|
+
return {
|
|
71
|
+
content: [{
|
|
72
|
+
type: 'text',
|
|
73
|
+
text: JSON.stringify(result, null, 2)
|
|
74
|
+
}]
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
server.tool('playlists_getPlaylist', 'Get information about a YouTube playlist', {
|
|
78
|
+
playlistId: z.string().describe('The YouTube playlist ID'),
|
|
79
|
+
}, async ({ playlistId }) => {
|
|
80
|
+
const result = await playlistService.getPlaylist({ playlistId });
|
|
81
|
+
return {
|
|
82
|
+
content: [{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: JSON.stringify(result, null, 2)
|
|
85
|
+
}]
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
server.tool('playlists_getPlaylistItems', 'Get videos in a YouTube playlist', {
|
|
89
|
+
playlistId: z.string().describe('The YouTube playlist ID'),
|
|
90
|
+
maxResults: z.number().optional().describe('Maximum number of results to return'),
|
|
91
|
+
}, async ({ playlistId, maxResults }) => {
|
|
92
|
+
const result = await playlistService.getPlaylistItems({ playlistId, maxResults });
|
|
93
|
+
return {
|
|
94
|
+
content: [{
|
|
95
|
+
type: 'text',
|
|
96
|
+
text: JSON.stringify(result, null, 2)
|
|
97
|
+
}]
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
// Create transport and connect
|
|
101
|
+
const transport = new StdioServerTransport();
|
|
102
|
+
await server.connect(transport);
|
|
103
|
+
console.error(`YouTube MCP Server v1.0.0 started successfully`);
|
|
104
|
+
console.error(`Server will validate YouTube API key when tools are called`);
|
|
105
|
+
return server;
|
|
106
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ChannelParams, ChannelVideosParams } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube channels
|
|
4
|
+
*/
|
|
5
|
+
export declare class ChannelService {
|
|
6
|
+
private youtube;
|
|
7
|
+
private initialized;
|
|
8
|
+
constructor();
|
|
9
|
+
/**
|
|
10
|
+
* Initialize the YouTube client only when needed
|
|
11
|
+
*/
|
|
12
|
+
private initialize;
|
|
13
|
+
/**
|
|
14
|
+
* Get channel details
|
|
15
|
+
*/
|
|
16
|
+
getChannel({ channelId }: ChannelParams): Promise<any>;
|
|
17
|
+
/**
|
|
18
|
+
* Get channel playlists
|
|
19
|
+
*/
|
|
20
|
+
getPlaylists({ channelId, maxResults }: ChannelVideosParams): Promise<any[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Get channel videos
|
|
23
|
+
*/
|
|
24
|
+
listVideos({ channelId, maxResults }: ChannelVideosParams): Promise<any[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Get channel statistics
|
|
27
|
+
*/
|
|
28
|
+
getStatistics({ channelId }: ChannelParams): Promise<any>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube channels
|
|
4
|
+
*/
|
|
5
|
+
export class ChannelService {
|
|
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 channel details
|
|
29
|
+
*/
|
|
30
|
+
async getChannel({ channelId }) {
|
|
31
|
+
try {
|
|
32
|
+
this.initialize();
|
|
33
|
+
const response = await this.youtube.channels.list({
|
|
34
|
+
part: ['snippet', 'statistics', 'contentDetails'],
|
|
35
|
+
id: [channelId]
|
|
36
|
+
});
|
|
37
|
+
return response.data.items?.[0] || null;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Failed to get channel: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get channel playlists
|
|
45
|
+
*/
|
|
46
|
+
async getPlaylists({ channelId, maxResults = 50 }) {
|
|
47
|
+
try {
|
|
48
|
+
this.initialize();
|
|
49
|
+
const response = await this.youtube.playlists.list({
|
|
50
|
+
part: ['snippet', 'contentDetails'],
|
|
51
|
+
channelId,
|
|
52
|
+
maxResults
|
|
53
|
+
});
|
|
54
|
+
return response.data.items || [];
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw new Error(`Failed to get channel playlists: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get channel videos
|
|
62
|
+
*/
|
|
63
|
+
async listVideos({ channelId, maxResults = 50 }) {
|
|
64
|
+
try {
|
|
65
|
+
this.initialize();
|
|
66
|
+
const response = await this.youtube.search.list({
|
|
67
|
+
part: ['snippet'],
|
|
68
|
+
channelId,
|
|
69
|
+
maxResults,
|
|
70
|
+
order: 'date',
|
|
71
|
+
type: ['video']
|
|
72
|
+
});
|
|
73
|
+
return response.data.items || [];
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new Error(`Failed to list channel videos: ${error instanceof Error ? error.message : String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get channel statistics
|
|
81
|
+
*/
|
|
82
|
+
async getStatistics({ channelId }) {
|
|
83
|
+
try {
|
|
84
|
+
this.initialize();
|
|
85
|
+
const response = await this.youtube.channels.list({
|
|
86
|
+
part: ['statistics'],
|
|
87
|
+
id: [channelId]
|
|
88
|
+
});
|
|
89
|
+
return response.data.items?.[0]?.statistics || null;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
throw new Error(`Failed to get channel statistics: ${error instanceof Error ? error.message : String(error)}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PlaylistParams, PlaylistItemsParams, SearchParams } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube playlists
|
|
4
|
+
*/
|
|
5
|
+
export declare class PlaylistService {
|
|
6
|
+
private youtube;
|
|
7
|
+
private initialized;
|
|
8
|
+
constructor();
|
|
9
|
+
/**
|
|
10
|
+
* Initialize the YouTube client only when needed
|
|
11
|
+
*/
|
|
12
|
+
private initialize;
|
|
13
|
+
/**
|
|
14
|
+
* Get information about a YouTube playlist
|
|
15
|
+
*/
|
|
16
|
+
getPlaylist({ playlistId }: PlaylistParams): Promise<any>;
|
|
17
|
+
/**
|
|
18
|
+
* Get videos in a YouTube playlist
|
|
19
|
+
*/
|
|
20
|
+
getPlaylistItems({ playlistId, maxResults }: PlaylistItemsParams): Promise<any[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Search for playlists on YouTube
|
|
23
|
+
*/
|
|
24
|
+
searchPlaylists({ query, maxResults }: SearchParams): Promise<any[]>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube playlists
|
|
4
|
+
*/
|
|
5
|
+
export class PlaylistService {
|
|
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 information about a YouTube playlist
|
|
29
|
+
*/
|
|
30
|
+
async getPlaylist({ playlistId }) {
|
|
31
|
+
try {
|
|
32
|
+
this.initialize();
|
|
33
|
+
const response = await this.youtube.playlists.list({
|
|
34
|
+
part: ['snippet', 'contentDetails'],
|
|
35
|
+
id: [playlistId]
|
|
36
|
+
});
|
|
37
|
+
return response.data.items?.[0] || null;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Failed to get playlist: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get videos in a YouTube playlist
|
|
45
|
+
*/
|
|
46
|
+
async getPlaylistItems({ playlistId, maxResults = 50 }) {
|
|
47
|
+
try {
|
|
48
|
+
this.initialize();
|
|
49
|
+
const response = await this.youtube.playlistItems.list({
|
|
50
|
+
part: ['snippet', 'contentDetails'],
|
|
51
|
+
playlistId,
|
|
52
|
+
maxResults
|
|
53
|
+
});
|
|
54
|
+
return response.data.items || [];
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw new Error(`Failed to get playlist items: ${error instanceof Error ? error.message : String(error)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Search for playlists on YouTube
|
|
62
|
+
*/
|
|
63
|
+
async searchPlaylists({ query, maxResults = 10 }) {
|
|
64
|
+
try {
|
|
65
|
+
this.initialize();
|
|
66
|
+
const response = await this.youtube.search.list({
|
|
67
|
+
part: ['snippet'],
|
|
68
|
+
q: query,
|
|
69
|
+
maxResults,
|
|
70
|
+
type: ['playlist']
|
|
71
|
+
});
|
|
72
|
+
return response.data.items || [];
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new Error(`Failed to search playlists: ${error instanceof Error ? error.message : String(error)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { TranscriptParams, SearchTranscriptParams } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube video transcripts
|
|
4
|
+
*/
|
|
5
|
+
export declare class TranscriptService {
|
|
6
|
+
private initialized;
|
|
7
|
+
constructor();
|
|
8
|
+
private initialize;
|
|
9
|
+
/**
|
|
10
|
+
* Get the transcript of a YouTube video
|
|
11
|
+
*/
|
|
12
|
+
getTranscript({ videoId, language }: TranscriptParams): Promise<any>;
|
|
13
|
+
/**
|
|
14
|
+
* Search within a transcript
|
|
15
|
+
*/
|
|
16
|
+
searchTranscript({ videoId, query, language }: SearchTranscriptParams): Promise<any>;
|
|
17
|
+
/**
|
|
18
|
+
* Get transcript with timestamps
|
|
19
|
+
*/
|
|
20
|
+
getTimestampedTranscript({ videoId, language }: TranscriptParams): Promise<any>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { YoutubeTranscript } from "youtube-transcript";
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube video transcripts
|
|
4
|
+
*/
|
|
5
|
+
export class TranscriptService {
|
|
6
|
+
// No YouTube API key needed for transcripts, but we'll implement the same pattern
|
|
7
|
+
initialized = false;
|
|
8
|
+
constructor() {
|
|
9
|
+
// No initialization needed
|
|
10
|
+
}
|
|
11
|
+
initialize() {
|
|
12
|
+
if (this.initialized)
|
|
13
|
+
return;
|
|
14
|
+
// No API key needed for transcripts, but we'll check if language is set
|
|
15
|
+
this.initialized = true;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the transcript of a YouTube video
|
|
19
|
+
*/
|
|
20
|
+
async getTranscript({ videoId, language = process.env.YOUTUBE_TRANSCRIPT_LANG || 'en' }) {
|
|
21
|
+
try {
|
|
22
|
+
this.initialize();
|
|
23
|
+
// YoutubeTranscript.fetchTranscript only accepts videoId
|
|
24
|
+
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
|
25
|
+
return {
|
|
26
|
+
videoId,
|
|
27
|
+
language,
|
|
28
|
+
transcript
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Error(`Failed to get transcript: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Search within a transcript
|
|
37
|
+
*/
|
|
38
|
+
async searchTranscript({ videoId, query, language = process.env.YOUTUBE_TRANSCRIPT_LANG || 'en' }) {
|
|
39
|
+
try {
|
|
40
|
+
this.initialize();
|
|
41
|
+
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
|
42
|
+
// Search through transcript for the query
|
|
43
|
+
const matches = transcript.filter(item => item.text.toLowerCase().includes(query.toLowerCase()));
|
|
44
|
+
return {
|
|
45
|
+
videoId,
|
|
46
|
+
query,
|
|
47
|
+
matches,
|
|
48
|
+
totalMatches: matches.length
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Failed to search transcript: ${error instanceof Error ? error.message : String(error)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get transcript with timestamps
|
|
57
|
+
*/
|
|
58
|
+
async getTimestampedTranscript({ videoId, language = process.env.YOUTUBE_TRANSCRIPT_LANG || 'en' }) {
|
|
59
|
+
try {
|
|
60
|
+
this.initialize();
|
|
61
|
+
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
|
62
|
+
// Format timestamps in human-readable format
|
|
63
|
+
const timestampedTranscript = transcript.map(item => {
|
|
64
|
+
const seconds = item.offset / 1000;
|
|
65
|
+
const minutes = Math.floor(seconds / 60);
|
|
66
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
67
|
+
const formattedTime = `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
68
|
+
return {
|
|
69
|
+
timestamp: formattedTime,
|
|
70
|
+
text: item.text,
|
|
71
|
+
startTimeMs: item.offset,
|
|
72
|
+
durationMs: item.duration
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
videoId,
|
|
77
|
+
language,
|
|
78
|
+
timestampedTranscript
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
throw new Error(`Failed to get timestamped transcript: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { VideoParams, SearchParams, TrendingParams, RelatedVideosParams } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube videos
|
|
4
|
+
*/
|
|
5
|
+
export declare class VideoService {
|
|
6
|
+
private youtube;
|
|
7
|
+
private initialized;
|
|
8
|
+
constructor();
|
|
9
|
+
/**
|
|
10
|
+
* Initialize the YouTube client only when needed
|
|
11
|
+
*/
|
|
12
|
+
private initialize;
|
|
13
|
+
/**
|
|
14
|
+
* Get detailed information about a YouTube video
|
|
15
|
+
*/
|
|
16
|
+
getVideo({ videoId, parts }: VideoParams): Promise<any>;
|
|
17
|
+
/**
|
|
18
|
+
* Search for videos on YouTube
|
|
19
|
+
*/
|
|
20
|
+
searchVideos({ query, maxResults }: SearchParams): Promise<any[]>;
|
|
21
|
+
/**
|
|
22
|
+
* Get video statistics like views, likes, and comments
|
|
23
|
+
*/
|
|
24
|
+
getVideoStats({ videoId }: {
|
|
25
|
+
videoId: string;
|
|
26
|
+
}): Promise<any>;
|
|
27
|
+
/**
|
|
28
|
+
* Get trending videos
|
|
29
|
+
*/
|
|
30
|
+
getTrendingVideos({ regionCode, maxResults, videoCategoryId }: TrendingParams): Promise<any[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Get related videos for a specific video
|
|
33
|
+
*/
|
|
34
|
+
getRelatedVideos({ videoId, maxResults }: RelatedVideosParams): Promise<any[]>;
|
|
35
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { google } from 'googleapis';
|
|
2
|
+
/**
|
|
3
|
+
* Service for interacting with YouTube videos
|
|
4
|
+
*/
|
|
5
|
+
export class VideoService {
|
|
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 detailed information about a YouTube video
|
|
29
|
+
*/
|
|
30
|
+
async getVideo({ videoId, parts = ['snippet', 'contentDetails', 'statistics'] }) {
|
|
31
|
+
try {
|
|
32
|
+
this.initialize();
|
|
33
|
+
const response = await this.youtube.videos.list({
|
|
34
|
+
part: parts,
|
|
35
|
+
id: [videoId]
|
|
36
|
+
});
|
|
37
|
+
return response.data.items?.[0] || null;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`Failed to get video: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Search for videos on YouTube
|
|
45
|
+
*/
|
|
46
|
+
async searchVideos({ query, maxResults = 10 }) {
|
|
47
|
+
try {
|
|
48
|
+
this.initialize();
|
|
49
|
+
const response = await this.youtube.search.list({
|
|
50
|
+
part: ['snippet'],
|
|
51
|
+
q: query,
|
|
52
|
+
maxResults,
|
|
53
|
+
type: ['video']
|
|
54
|
+
});
|
|
55
|
+
return response.data.items || [];
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
throw new Error(`Failed to search videos: ${error instanceof Error ? error.message : String(error)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get video statistics like views, likes, and comments
|
|
63
|
+
*/
|
|
64
|
+
async getVideoStats({ videoId }) {
|
|
65
|
+
try {
|
|
66
|
+
this.initialize();
|
|
67
|
+
const response = await this.youtube.videos.list({
|
|
68
|
+
part: ['statistics'],
|
|
69
|
+
id: [videoId]
|
|
70
|
+
});
|
|
71
|
+
return response.data.items?.[0]?.statistics || null;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw new Error(`Failed to get video stats: ${error instanceof Error ? error.message : String(error)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get trending videos
|
|
79
|
+
*/
|
|
80
|
+
async getTrendingVideos({ regionCode = 'US', maxResults = 10, videoCategoryId = '' }) {
|
|
81
|
+
try {
|
|
82
|
+
this.initialize();
|
|
83
|
+
const params = {
|
|
84
|
+
part: ['snippet', 'contentDetails', 'statistics'],
|
|
85
|
+
chart: 'mostPopular',
|
|
86
|
+
regionCode,
|
|
87
|
+
maxResults
|
|
88
|
+
};
|
|
89
|
+
if (videoCategoryId) {
|
|
90
|
+
params.videoCategoryId = videoCategoryId;
|
|
91
|
+
}
|
|
92
|
+
const response = await this.youtube.videos.list(params);
|
|
93
|
+
return response.data.items || [];
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw new Error(`Failed to get trending videos: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get related videos for a specific video
|
|
101
|
+
*/
|
|
102
|
+
async getRelatedVideos({ videoId, maxResults = 10 }) {
|
|
103
|
+
try {
|
|
104
|
+
this.initialize();
|
|
105
|
+
const response = await this.youtube.search.list({
|
|
106
|
+
part: ['snippet'],
|
|
107
|
+
relatedToVideoId: videoId,
|
|
108
|
+
maxResults,
|
|
109
|
+
type: ['video']
|
|
110
|
+
});
|
|
111
|
+
return response.data.items || [];
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new Error(`Failed to get related videos: ${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video details parameters
|
|
3
|
+
*/
|
|
4
|
+
export interface VideoParams {
|
|
5
|
+
videoId: string;
|
|
6
|
+
parts?: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Search videos parameters
|
|
10
|
+
*/
|
|
11
|
+
export interface SearchParams {
|
|
12
|
+
query: string;
|
|
13
|
+
maxResults?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Trending videos parameters
|
|
17
|
+
*/
|
|
18
|
+
export interface TrendingParams {
|
|
19
|
+
regionCode?: string;
|
|
20
|
+
maxResults?: number;
|
|
21
|
+
videoCategoryId?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Related videos parameters
|
|
25
|
+
*/
|
|
26
|
+
export interface RelatedVideosParams {
|
|
27
|
+
videoId: string;
|
|
28
|
+
maxResults?: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Transcript parameters
|
|
32
|
+
*/
|
|
33
|
+
export interface TranscriptParams {
|
|
34
|
+
videoId: string;
|
|
35
|
+
language?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Search transcript parameters
|
|
39
|
+
*/
|
|
40
|
+
export interface SearchTranscriptParams {
|
|
41
|
+
videoId: string;
|
|
42
|
+
query: string;
|
|
43
|
+
language?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Channel parameters
|
|
47
|
+
*/
|
|
48
|
+
export interface ChannelParams {
|
|
49
|
+
channelId: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Channel videos parameters
|
|
53
|
+
*/
|
|
54
|
+
export interface ChannelVideosParams {
|
|
55
|
+
channelId: string;
|
|
56
|
+
maxResults?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Playlist parameters
|
|
60
|
+
*/
|
|
61
|
+
export interface PlaylistParams {
|
|
62
|
+
playlistId: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Playlist items parameters
|
|
66
|
+
*/
|
|
67
|
+
export interface PlaylistItemsParams {
|
|
68
|
+
playlistId: string;
|
|
69
|
+
maxResults?: number;
|
|
70
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yilin-jing/youtube-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "YouTube MCP Server Implementation with SDK 1.23 support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"youtube-mcp-server": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node ./dist/index.js",
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"dev": "nodemon --exec \"npm run build && npm start\" --ext ts",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
24
|
+
"googleapis": "^129.0.0",
|
|
25
|
+
"ytdl-core": "^4.11.5",
|
|
26
|
+
"youtube-transcript": "^1.0.6",
|
|
27
|
+
"zod": "^3.23.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^18.0.0",
|
|
31
|
+
"bun-types": "^1.3.3",
|
|
32
|
+
"nodemon": "^3.0.0",
|
|
33
|
+
"ts-node": "^10.9.1",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"youtube",
|
|
38
|
+
"mcp",
|
|
39
|
+
"model-context-protocol",
|
|
40
|
+
"ai",
|
|
41
|
+
"claude",
|
|
42
|
+
"anthropic"
|
|
43
|
+
],
|
|
44
|
+
"author": "Yilin Jing",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/Jing-yilin/youtube-mcp-server.git"
|
|
49
|
+
},
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/Jing-yilin/youtube-mcp-server/issues"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/Jing-yilin/youtube-mcp-server#readme"
|
|
54
|
+
}
|