YouTube MCP Server

by Nocodeboy
Verified
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios from "axios"; import dotenv from "dotenv"; // Cargar variables de entorno dotenv.config(); const API_KEY = process.env.YOUTUBE_API_KEY; if (!API_KEY) { throw new Error("YOUTUBE_API_KEY environment variable is required"); } const API_CONFIG = { BASE_URL: "https://www.googleapis.com/youtube/v3", ENDPOINTS: { SEARCH: "search", VIDEOS: "videos", CHANNELS: "channels", }, PART_DEFAULTS: { SEARCH: "snippet", VIDEOS: "snippet,statistics", CHANNELS: "snippet,statistics", }, MAX_RESULTS: 10, }; /** * Servidor MCP para interactuar con la API de YouTube */ class YouTubeMCPServer { constructor() { this.server = new Server( { name: "yt-mcp-server", version: "1.0.0", }, { capabilities: { resources: {}, tools: {}, }, } ); // Configurar instancia de axios this.axiosInstance = axios.create({ baseURL: API_CONFIG.BASE_URL, params: { key: API_KEY, }, }); this.setupHandlers(); this.setupErrorHandling(); } setupErrorHandling() { this.server.onerror = (error) => { console.error("[MCP Error]", error); }; process.on("SIGINT", async () => { await this.server.close(); process.exit(0); }); } setupHandlers() { this.setupResourceHandlers(); this.setupToolHandlers(); } setupResourceHandlers() { // Definimos un recurso que representa videos populares this.server.setRequestHandler( ListResourcesRequestSchema, async () => ({ resources: [ { uri: "youtube://popular/videos", name: "Videos populares en YouTube", mimeType: "application/json", description: "Lista de videos populares actualmente en YouTube", }, ], }) ); // Manejador para leer recursos this.server.setRequestHandler( ReadResourceRequestSchema, async (request) => { if (request.params.uri === "youtube://popular/videos") { try { const response = await this.axiosInstance.get( API_CONFIG.ENDPOINTS.VIDEOS, { params: { part: API_CONFIG.PART_DEFAULTS.VIDEOS, chart: "mostPopular", maxResults: API_CONFIG.MAX_RESULTS, }, } ); const formattedVideos = response.data.items.map((video) => ({ title: video.snippet.title, id: video.id, url: `https://www.youtube.com/watch?v=${video.id}`, channelTitle: video.snippet.channelTitle, viewCount: video.statistics?.viewCount, publishedAt: video.snippet.publishedAt, description: video.snippet.description, })); return { contents: [ { uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(formattedVideos, null, 2), }, ], }; } catch (error) { if (axios.isAxiosError(error)) { throw new McpError( ErrorCode.InternalError, `YouTube API error: ${error.response?.data?.error?.message || error.message}` ); } throw error; } } else { throw new McpError( ErrorCode.InvalidRequest, `Unknown resource: ${request.params.uri}` ); } } ); } setupToolHandlers() { // Configurar herramientas disponibles this.server.setRequestHandler( ListToolsRequestSchema, async () => ({ tools: [ { name: "search_videos", description: "Buscar videos en YouTube", inputSchema: { type: "object", properties: { query: { type: "string", description: "Términos de búsqueda", }, maxResults: { type: "number", description: "Número máximo de resultados (entre 1 y 50)", minimum: 1, maximum: 50, }, pageToken: { type: "string", description: "Token para obtener la siguiente página de resultados", }, }, required: ["query"], }, }, { name: "get_video_details", description: "Obtener detalles de un video específico", inputSchema: { type: "object", properties: { videoId: { type: "string", description: "ID del video de YouTube", }, }, required: ["videoId"], }, }, { name: "get_channel_details", description: "Obtener detalles de un canal específico", inputSchema: { type: "object", properties: { channelId: { type: "string", description: "ID del canal de YouTube", }, }, required: ["channelId"], }, }, { name: "search_channels", description: "Buscar canales en YouTube", inputSchema: { type: "object", properties: { query: { type: "string", description: "Términos de búsqueda", }, maxResults: { type: "number", description: "Número máximo de resultados (entre 1 y 50)", minimum: 1, maximum: 50, }, pageToken: { type: "string", description: "Token para obtener la siguiente página de resultados", }, }, required: ["query"], }, }, ], }) ); // Manejador para llamadas a herramientas this.server.setRequestHandler( CallToolRequestSchema, async (request) => { try { switch (request.params.name) { case "search_videos": return await this.handleSearchVideos(request.params.arguments); case "get_video_details": return await this.handleGetVideoDetails(request.params.arguments); case "get_channel_details": return await this.handleGetChannelDetails(request.params.arguments); case "search_channels": return await this.handleSearchChannels(request.params.arguments); default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } } catch (error) { if (axios.isAxiosError(error)) { return { content: [ { type: "text", text: `YouTube API error: ${error.response?.data?.error?.message || error.message}`, }, ], isError: true, }; } throw error; } } ); } // Manejadores para cada herramienta async handleSearchVideos(args) { if (!args || typeof args !== 'object' || typeof args.query !== 'string') { throw new McpError(ErrorCode.InvalidParams, "Invalid search arguments"); } const { query, maxResults = API_CONFIG.MAX_RESULTS, pageToken } = args; const response = await this.axiosInstance.get( API_CONFIG.ENDPOINTS.SEARCH, { params: { part: API_CONFIG.PART_DEFAULTS.SEARCH, q: query, maxResults: Math.min(maxResults || API_CONFIG.MAX_RESULTS, 50), type: "video", pageToken, }, } ); const formattedResults = { videos: response.data.items.map((item) => ({ title: item.snippet.title, id: item.id.videoId, url: `https://www.youtube.com/watch?v=${item.id.videoId}`, channelTitle: item.snippet.channelTitle, publishedAt: item.snippet.publishedAt, description: item.snippet.description, thumbnail: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url, })), pageInfo: response.data.pageInfo, nextPageToken: response.data.nextPageToken, prevPageToken: response.data.prevPageToken, }; return { content: [ { type: "text", text: JSON.stringify(formattedResults, null, 2), }, ], }; } async handleGetVideoDetails(args) { if (!args || typeof args !== 'object' || typeof args.videoId !== 'string') { throw new McpError(ErrorCode.InvalidParams, "Invalid video details arguments"); } const { videoId } = args; const response = await this.axiosInstance.get( API_CONFIG.ENDPOINTS.VIDEOS, { params: { part: API_CONFIG.PART_DEFAULTS.VIDEOS, id: videoId, }, } ); if (response.data.items.length === 0) { throw new McpError(ErrorCode.NotFound, `Video with ID ${videoId} not found`); } const video = response.data.items[0]; const formattedVideo = { title: video.snippet.title, id: video.id, url: `https://www.youtube.com/watch?v=${video.id}`, channelTitle: video.snippet.channelTitle, channelId: video.snippet.channelId, channelUrl: `https://www.youtube.com/channel/${video.snippet.channelId}`, publishedAt: video.snippet.publishedAt, description: video.snippet.description, tags: video.snippet.tags || [], viewCount: video.statistics?.viewCount, likeCount: video.statistics?.likeCount, commentCount: video.statistics?.commentCount, thumbnail: video.snippet.thumbnails.high?.url || video.snippet.thumbnails.medium?.url || video.snippet.thumbnails.default?.url, }; return { content: [ { type: "text", text: JSON.stringify(formattedVideo, null, 2), }, ], }; } async handleGetChannelDetails(args) { if (!args || typeof args !== 'object' || typeof args.channelId !== 'string') { throw new McpError(ErrorCode.InvalidParams, "Invalid channel details arguments"); } const { channelId } = args; const response = await this.axiosInstance.get( API_CONFIG.ENDPOINTS.CHANNELS, { params: { part: API_CONFIG.PART_DEFAULTS.CHANNELS, id: channelId, }, } ); if (response.data.items.length === 0) { throw new McpError(ErrorCode.NotFound, `Channel with ID ${channelId} not found`); } const channel = response.data.items[0]; const formattedChannel = { title: channel.snippet.title, id: channel.id, url: `https://www.youtube.com/channel/${channel.id}`, customUrl: channel.snippet.customUrl ? `https://www.youtube.com/c/${channel.snippet.customUrl}` : null, publishedAt: channel.snippet.publishedAt, description: channel.snippet.description, country: channel.snippet.country, subscriberCount: channel.statistics?.subscriberCount, viewCount: channel.statistics?.viewCount, videoCount: channel.statistics?.videoCount, thumbnail: channel.snippet.thumbnails.high?.url || channel.snippet.thumbnails.medium?.url || channel.snippet.thumbnails.default?.url, }; return { content: [ { type: "text", text: JSON.stringify(formattedChannel, null, 2), }, ], }; } async handleSearchChannels(args) { if (!args || typeof args !== 'object' || typeof args.query !== 'string') { throw new McpError(ErrorCode.InvalidParams, "Invalid search channels arguments"); } const { query, maxResults = API_CONFIG.MAX_RESULTS, pageToken } = args; const response = await this.axiosInstance.get( API_CONFIG.ENDPOINTS.SEARCH, { params: { part: API_CONFIG.PART_DEFAULTS.SEARCH, q: query, maxResults: Math.min(maxResults || API_CONFIG.MAX_RESULTS, 50), type: "channel", pageToken, }, } ); const formattedResults = { channels: response.data.items.map((item) => ({ title: item.snippet.title, id: item.id.channelId, url: `https://www.youtube.com/channel/${item.id.channelId}`, publishedAt: item.snippet.publishedAt, description: item.snippet.description, thumbnail: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.medium?.url || item.snippet.thumbnails.default?.url, })), pageInfo: response.data.pageInfo, nextPageToken: response.data.nextPageToken, prevPageToken: response.data.prevPageToken, }; return { content: [ { type: "text", text: JSON.stringify(formattedResults, null, 2), }, ], }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("YouTube MCP server running on stdio"); } } // Iniciar servidor const server = new YouTubeMCPServer(); server.run().catch((error) => { console.error("Server error:", error); process.exit(1); });