@yilin-jing/youtube-mcp-server 1.2.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/dist/cleaners.d.ts +2 -0
- package/dist/cleaners.js +34 -0
- package/dist/server.js +15 -1
- 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 +1 -1
package/dist/cleaners.d.ts
CHANGED
package/dist/cleaners.js
CHANGED
|
@@ -108,4 +108,38 @@ export const DataCleaners = {
|
|
|
108
108
|
return [];
|
|
109
109
|
return items.map(DataCleaners.cleanPlaylistItem).filter(Boolean);
|
|
110
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
|
+
},
|
|
111
145
|
};
|
package/dist/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import { VideoService } from './services/video.js';
|
|
|
8
8
|
import { TranscriptService } from './services/transcript.js';
|
|
9
9
|
import { PlaylistService } from './services/playlist.js';
|
|
10
10
|
import { ChannelService } from './services/channel.js';
|
|
11
|
+
import { CommentService } from './services/comment.js';
|
|
11
12
|
import { DataCleaners } from './cleaners.js';
|
|
12
13
|
function saveData(data, dir, toolName) {
|
|
13
14
|
try {
|
|
@@ -36,12 +37,13 @@ function formatResponse(cleanedData, options) {
|
|
|
36
37
|
export async function startMcpServer() {
|
|
37
38
|
const server = new McpServer({
|
|
38
39
|
name: 'youtube-mcp-server',
|
|
39
|
-
version: '1.
|
|
40
|
+
version: '1.3.0',
|
|
40
41
|
});
|
|
41
42
|
const videoService = new VideoService();
|
|
42
43
|
const transcriptService = new TranscriptService();
|
|
43
44
|
const playlistService = new PlaylistService();
|
|
44
45
|
const channelService = new ChannelService();
|
|
46
|
+
const commentService = new CommentService();
|
|
45
47
|
server.tool('videos_getVideo', 'Get video info. Returns cleaned data in TOON format.', {
|
|
46
48
|
videoId: z.string().describe('The YouTube video ID'),
|
|
47
49
|
parts: z.array(z.string()).optional().describe('Parts to retrieve'),
|
|
@@ -103,6 +105,18 @@ export async function startMcpServer() {
|
|
|
103
105
|
const cleaned = DataCleaners.cleanPlaylistItems(result);
|
|
104
106
|
return formatResponse(cleaned, { ...(save_dir && { saveDir: save_dir }), toolName: 'playlists_getPlaylistItems' });
|
|
105
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' });
|
|
119
|
+
});
|
|
106
120
|
const transport = new StdioServerTransport();
|
|
107
121
|
await server.connect(transport);
|
|
108
122
|
console.error(`YouTube MCP Server v1.2.0 started successfully`);
|
|
@@ -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
|
+
}
|