UNPKG

mcp-youtube-uploader

Version:

MCP server for uploading videos to YouTube with OAuth2 authentication

204 lines 7.77 kB
import { google } from 'googleapis'; import fs from 'fs'; export class YouTubeClient { oauth2Client; youtube; constructor(credentials) { this.oauth2Client = new google.auth.OAuth2(credentials.clientId, credentials.clientSecret, credentials.redirectUri); this.youtube = google.youtube({ version: 'v3', auth: this.oauth2Client }); if (credentials.refreshToken) { this.oauth2Client.setCredentials({ refresh_token: credentials.refreshToken }); } } async getAuthUrl() { const scopes = [ 'https://www.googleapis.com/auth/youtube', 'https://www.googleapis.com/auth/youtube.force-ssl' ]; return this.oauth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes, prompt: 'consent' }); } async setAuthCode(code) { const { tokens } = await this.oauth2Client.getToken(code); this.oauth2Client.setCredentials(tokens); } async getChannels() { // Ensure we have valid credentials if (!this.oauth2Client.credentials.access_token) { throw new Error('No access token available. Please authenticate first.'); } // Try to refresh token if it's expired try { await this.oauth2Client.getAccessToken(); } catch (error) { throw new Error('Authentication expired. Please re-authenticate.'); } try { const response = await this.youtube.channels.list({ part: ['snippet', 'statistics', 'brandingSettings'], mine: true }); const channels = (response.data.items || []).map(channel => ({ id: channel.id, title: channel.snippet?.title || 'Unknown Channel', description: channel.snippet?.description || '', customUrl: channel.snippet?.customUrl || undefined, subscriberCount: channel.statistics?.subscriberCount || undefined, videoCount: channel.statistics?.videoCount || undefined, thumbnailUrl: channel.snippet?.thumbnails?.default?.url || undefined })); return channels; } catch (error) { if (error.response?.data?.error) { const apiError = error.response.data.error; throw new Error(`YouTube API Error: ${apiError.message} (${apiError.code})`); } throw new Error(`Failed to get channels: ${error.message}`); } } async debugPermissions() { try { // Get detailed information about the authenticated user and permissions const results = {}; // 1. Check current scopes results.scopes = this.oauth2Client.credentials.scope; // 2. Get user info try { const userInfo = await this.youtube.channels.list({ part: ['snippet', 'statistics', 'contentDetails', 'status'], mine: true }); results.myChannels = userInfo.data.items; } catch (error) { results.myChannelsError = error.message; } // 3. Try to get playlists (might show managed channels) try { const playlists = await this.youtube.playlists.list({ part: ['snippet'], mine: true, maxResults: 50 }); results.playlists = playlists.data.items; } catch (error) { results.playlistsError = error.message; } // 4. Try to get channel sections try { const sections = await this.youtube.channelSections.list({ part: ['snippet'], mine: true }); results.sections = sections.data.items; } catch (error) { results.sectionsError = error.message; } // 5. Check if we can list videos (might reveal other channels) try { const videos = await this.youtube.search.list({ part: ['snippet'], forMine: true, type: ['video'], maxResults: 10 }); results.myVideos = videos.data.items; } catch (error) { results.myVideosError = error.message; } return results; } catch (error) { return { error: error.message }; } } async uploadVideo(options) { if (!fs.existsSync(options.filePath)) { throw new Error(`File not found: ${options.filePath}`); } const fileSize = fs.statSync(options.filePath).size; if (fileSize > 128 * 1024 * 1024 * 1024) { // 128GB limit throw new Error('File size exceeds YouTube limit of 128GB'); } // Ensure we have valid credentials if (!this.oauth2Client.credentials.access_token) { throw new Error('No access token available. Please authenticate first.'); } // Try to refresh token if it's expired try { await this.oauth2Client.getAccessToken(); } catch (error) { throw new Error('Authentication expired. Please re-authenticate.'); } const requestBody = { snippet: { title: options.title, description: options.description || '', tags: options.tags || [], categoryId: options.categoryId || '22', // People & Blogs }, status: { privacyStatus: options.privacyStatus || 'private', selfDeclaredMadeForKids: false } }; // Add channel ID if specified if (options.channelId) { requestBody.snippet.channelId = options.channelId; } try { const response = await this.youtube.videos.insert({ part: ['snippet', 'status'], requestBody, media: { body: fs.createReadStream(options.filePath), }, }); const videoId = response.data.id; const videoUrl = `https://www.youtube.com/watch?v=${videoId}`; const channelId = response.data.snippet?.channelId || options.channelId || 'unknown'; const channelTitle = response.data.snippet?.channelTitle || 'Unknown Channel'; return { videoId, videoUrl, title: response.data.snippet?.title || options.title, uploadStatus: response.data.status?.uploadStatus || 'uploaded', channelId, channelTitle }; } catch (error) { if (error.response?.data?.error) { const apiError = error.response.data.error; throw new Error(`YouTube API Error: ${apiError.message} (${apiError.code})`); } throw new Error(`Upload failed: ${error.message}`); } } async refreshAccessToken() { try { await this.oauth2Client.refreshAccessToken(); } catch (error) { throw new Error(`Failed to refresh access token: ${error.message}`); } } getRefreshToken() { return this.oauth2Client.credentials.refresh_token; } } //# sourceMappingURL=youtube-client.js.map