UNPKG

mcp-youtube-uploader

Version:

MCP server for uploading videos to YouTube with OAuth2 authentication

325 lines • 15.8 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { YouTubeClient } from './youtube-client.js'; // Request schemas for MCP const ToolsListRequestSchema = z.object({ method: z.literal('tools/list') }); const ToolsCallRequestSchema = z.object({ method: z.literal('tools/call'), params: z.object({ name: z.string(), arguments: z.record(z.any()).optional() }) }); // Input schemas for tool arguments const UploadVideoSchema = z.object({ filePath: z.string().describe('Path to the video file to upload'), title: z.string().describe('Title of the video'), description: z.string().optional().describe('Description of the video'), tags: z.array(z.string()).optional().describe('Tags for the video'), categoryId: z.string().optional().describe('YouTube category ID (default: 22 - People & Blogs)'), privacyStatus: z.enum(['private', 'public', 'unlisted']).optional().describe('Privacy status (default: private)'), channelId: z.string().optional().describe('YouTube channel ID to upload to (if managing multiple channels)') }); const GetAuthUrlSchema = z.object({ clientId: z.string().optional().describe('YouTube/Google OAuth2 client ID (optional if YOUTUBE_CLIENT_ID env var is set)'), clientSecret: z.string().optional().describe('YouTube/Google OAuth2 client secret (optional if YOUTUBE_CLIENT_SECRET env var is set)'), redirectUri: z.string().optional().describe('OAuth2 redirect URI (optional if YOUTUBE_REDIRECT_URI env var is set)') }); const SetAuthCodeSchema = z.object({ authCode: z.string().describe('Authorization code from OAuth2 flow') }); const SetRefreshTokenSchema = z.object({ clientId: z.string().optional().describe('YouTube/Google OAuth2 client ID (optional if YOUTUBE_CLIENT_ID env var is set)'), clientSecret: z.string().optional().describe('YouTube/Google OAuth2 client secret (optional if YOUTUBE_CLIENT_SECRET env var is set)'), redirectUri: z.string().optional().describe('OAuth2 redirect URI (optional if YOUTUBE_REDIRECT_URI env var is set)'), refreshToken: z.string().describe('Refresh token from previous authentication') }); class YouTubeMCPServer { server; youtubeClient = null; credentials = null; constructor() { this.server = new Server({ name: 'youtube-uploader', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupTools(); this.setupErrorHandling(); } setupTools() { this.server.setRequestHandler(ToolsListRequestSchema, async () => { return { tools: [ { name: 'get_auth_url', description: 'Get OAuth2 authorization URL for YouTube authentication', inputSchema: { type: 'object', properties: { clientId: { type: 'string', description: 'YouTube/Google OAuth2 client ID (optional if YOUTUBE_CLIENT_ID env var is set)' }, clientSecret: { type: 'string', description: 'YouTube/Google OAuth2 client secret (optional if YOUTUBE_CLIENT_SECRET env var is set)' }, redirectUri: { type: 'string', description: 'OAuth2 redirect URI (optional if YOUTUBE_REDIRECT_URI env var is set)' } }, required: [] } }, { name: 'set_auth_code', description: 'Set authorization code from OAuth2 flow to complete authentication', inputSchema: { type: 'object', properties: { authCode: { type: 'string', description: 'Authorization code from OAuth2 flow' } }, required: ['authCode'] } }, { name: 'set_refresh_token', description: 'Authenticate using a saved refresh token (skip OAuth2 flow)', inputSchema: { type: 'object', properties: { clientId: { type: 'string', description: 'YouTube/Google OAuth2 client ID (optional if YOUTUBE_CLIENT_ID env var is set)' }, clientSecret: { type: 'string', description: 'YouTube/Google OAuth2 client secret (optional if YOUTUBE_CLIENT_SECRET env var is set)' }, redirectUri: { type: 'string', description: 'OAuth2 redirect URI (optional if YOUTUBE_REDIRECT_URI env var is set)' }, refreshToken: { type: 'string', description: 'Refresh token from previous authentication' } }, required: ['refreshToken'] } }, { name: 'list_channels', description: 'List all YouTube channels the authenticated user can manage', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'debug_permissions', description: 'Debug tool to check API permissions and scopes', inputSchema: { type: 'object', properties: {}, required: [] } }, { name: 'upload_video', description: 'Upload a video to YouTube and return the video URL', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Path to the video file to upload' }, title: { type: 'string', description: 'Title of the video' }, description: { type: 'string', description: 'Description of the video' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags for the video' }, categoryId: { type: 'string', description: 'YouTube category ID (default: 22 - People & Blogs)' }, privacyStatus: { type: 'string', enum: ['private', 'public', 'unlisted'], description: 'Privacy status (default: private)' }, channelId: { type: 'string', description: 'YouTube channel ID to upload to (if managing multiple channels)' } }, required: ['filePath', 'title'] } } ] }; }); this.server.setRequestHandler(ToolsCallRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'get_auth_url': return await this.handleGetAuthUrl(args || {}); case 'set_auth_code': return await this.handleSetAuthCode(args || {}); case 'set_refresh_token': return await this.handleSetRefreshToken(args || {}); case 'list_channels': return await this.handleListChannels(args || {}); case 'debug_permissions': return await this.handleDebugPermissions(); case 'upload_video': return await this.handleUploadVideo(args || {}); default: throw new Error(`Tool ${name} not found`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], isError: true, }; } }); } async handleGetAuthUrl(args) { const parsed = GetAuthUrlSchema.parse(args); // Use environment variables as fallbacks const clientId = parsed.clientId || process.env.YOUTUBE_CLIENT_ID; const clientSecret = parsed.clientSecret || process.env.YOUTUBE_CLIENT_SECRET; const redirectUri = parsed.redirectUri || process.env.YOUTUBE_REDIRECT_URI; // Validate that all required credentials are available if (!clientId) { throw new Error('Client ID is required. Provide it as a parameter or set YOUTUBE_CLIENT_ID environment variable.'); } if (!clientSecret) { throw new Error('Client secret is required. Provide it as a parameter or set YOUTUBE_CLIENT_SECRET environment variable.'); } if (!redirectUri) { throw new Error('Redirect URI is required. Provide it as a parameter or set YOUTUBE_REDIRECT_URI environment variable.'); } this.credentials = { clientId, clientSecret, redirectUri }; this.youtubeClient = new YouTubeClient(this.credentials); const authUrl = await this.youtubeClient.getAuthUrl(); return { content: [ { type: 'text', text: `Please visit this URL to authorize the application:\n\n${authUrl}\n\nAfter authorization, copy the authorization code and use the 'set_auth_code' tool.`, }, ], }; } async handleSetAuthCode(args) { const { authCode } = SetAuthCodeSchema.parse(args); if (!this.youtubeClient) { throw new Error('No YouTube client initialized. Call get_auth_url first.'); } await this.youtubeClient.setAuthCode(authCode); const refreshToken = this.youtubeClient.getRefreshToken(); return { content: [ { type: 'text', text: `Authentication successful! You can now upload videos.${refreshToken ? `\n\nRefresh token: ${refreshToken}\n\nSave this refresh token for future use to avoid re-authentication.` : ''}`, }, ], }; } async handleSetRefreshToken(args) { const parsed = SetRefreshTokenSchema.parse(args); // Use environment variables as fallbacks const clientId = parsed.clientId || process.env.YOUTUBE_CLIENT_ID; const clientSecret = parsed.clientSecret || process.env.YOUTUBE_CLIENT_SECRET; const redirectUri = parsed.redirectUri || process.env.YOUTUBE_REDIRECT_URI; const refreshToken = parsed.refreshToken; // Validate that all required credentials are available if (!clientId) { throw new Error('Client ID is required. Provide it as a parameter or set YOUTUBE_CLIENT_ID environment variable.'); } if (!clientSecret) { throw new Error('Client secret is required. Provide it as a parameter or set YOUTUBE_CLIENT_SECRET environment variable.'); } if (!redirectUri) { throw new Error('Redirect URI is required. Provide it as a parameter or set YOUTUBE_REDIRECT_URI environment variable.'); } this.credentials = { clientId, clientSecret, redirectUri, refreshToken }; this.youtubeClient = new YouTubeClient(this.credentials); return { content: [ { type: 'text', text: 'Authentication successful using refresh token! You can now upload videos.', }, ], }; } async handleListChannels(args) { if (!this.youtubeClient) { throw new Error('YouTube client not authenticated. Call get_auth_url and set_auth_code first, or use set_refresh_token.'); } const channels = await this.youtubeClient.getChannels(); if (channels.length === 0) { return { content: [ { type: 'text', text: 'No YouTube channels found. You may need to create a YouTube channel first.', }, ], }; } const channelList = channels.map(channel => { const stats = []; if (channel.subscriberCount) stats.push(`${channel.subscriberCount} subscribers`); if (channel.videoCount) stats.push(`${channel.videoCount} videos`); const statsText = stats.length > 0 ? ` (${stats.join(', ')})` : ''; const customUrlText = channel.customUrl ? ` | @${channel.customUrl}` : ''; return `šŸŽ¬ ${channel.title}${statsText}${customUrlText}\n ID: ${channel.id}\n ${channel.description || 'No description'}`; }).join('\n\n'); return { content: [ { type: 'text', text: `šŸ“ŗ Available YouTube Channels (${channels.length}):\n\n${channelList}\n\nšŸ’” Use the channel ID in the upload_video tool to specify which channel to upload to.`, }, ], }; } async handleDebugPermissions() { if (!this.youtubeClient) { throw new Error('YouTube client not authenticated. Call get_auth_url and set_auth_code first, or use set_refresh_token.'); } const debugInfo = await this.youtubeClient.debugPermissions(); return { content: [ { type: 'text', text: `šŸ” Debug Information:\n\n${JSON.stringify(debugInfo, null, 2)}`, }, ], }; } async handleUploadVideo(args) { const options = UploadVideoSchema.parse(args); if (!this.youtubeClient) { throw new Error('YouTube client not authenticated. Call get_auth_url and set_auth_code first, or use set_refresh_token.'); } const result = await this.youtubeClient.uploadVideo(options); return { content: [ { type: 'text', text: `Video uploaded successfully!\n\nVideo ID: ${result.videoId}\nVideo URL: ${result.videoUrl}\nTitle: ${result.title}\nChannel: ${result.channelTitle} (${result.channelId})\nUpload Status: ${result.uploadStatus}`, }, ], }; } setupErrorHandling() { this.server.onerror = (error) => { console.error('[MCP Error]', error); }; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('YouTube MCP Server running on stdio'); } } const server = new YouTubeMCPServer(); server.run().catch((error) => { console.error('Failed to start server:', error); process.exit(1); }); //# sourceMappingURL=index.js.map