mcp-youtube-uploader
Version:
MCP server for uploading videos to YouTube with OAuth2 authentication
325 lines ⢠15.8 kB
JavaScript
#!/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