mcp-youtube-uploader
Version:
MCP server for uploading videos to YouTube with OAuth2 authentication
204 lines • 7.77 kB
JavaScript
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