YouTube MCP Server
import { MCPFunction, MCPFunctionGroup } from "@modelcontextprotocol/sdk";
import { createCanvas, loadImage } from 'canvas';
import * as fs from "fs/promises";
import * as path from 'path';
// Utility functions
function safeGet<T>(obj: any, path: string, defaultValue?: T): T | undefined {
return path.split('.').reduce((acc, part) =>
acc && acc[part] !== undefined ? acc[part] : defaultValue, obj);
}
function safeParse(value: string | number | null | undefined, defaultValue = 0): number {
if (value === null || value === undefined) return defaultValue;
const parsed = Number(value);
return isNaN(parsed) ? defaultValue : parsed;
}
function safelyExecute<T>(fn: () => T): T | null {
try {
return fn();
} catch (error: unknown) {
console.error('Execution error:', error instanceof Error ? error.message : 'Unknown error');
return null;
}
}
export class ThumbnailManager implements MCPFunctionGroup {
private youtube: any;
constructor() {
this.youtube = google.youtube({
version: 'v3',
auth: process.env.YOUTUBE_API_KEY
});
}
@MCPFunction({
description: 'Generate custom thumbnail',
parameters: {
type: 'object',
properties: {
title: { type: 'string' },
imageUrl: { type: 'string' },
style: { type: 'string', enum: ['gaming', 'vlog', 'tutorial', 'news'] }
},
required: ['title']
}
})
async generateThumbnail({
title,
imageUrl,
style = 'vlog'
}: {
title: string,
imageUrl?: string,
style?: string
}): Promise<string> {
try {
const canvas = createCanvas(1280, 720);
const ctx = canvas.getContext('2d');
if (imageUrl) {
const image = await loadImage(imageUrl);
ctx.drawImage(image, 0, 0, 1280, 720);
} else {
ctx.fillStyle = this.getStyleBackground(style);
ctx.fillRect(0, 0, 1280, 720);
}
await this.applyStyleEffects(ctx, style);
this.addStyledText(ctx, title, style);
const outputDir = path.join(process.cwd(), 'thumbnails');
await fs.mkdir(outputDir, { recursive: true });
const outputPath = path.join(outputDir, `thumbnail-${Date.now()}.png`);
const buffer = canvas.toBuffer('image/png');
await fs.writeFile(outputPath, buffer);
return outputPath;
} catch (error) {
throw new Error(`Failed to generate thumbnail: ${error instanceof Error ? error.message : String(error)}`);
}
}
@MCPFunction({
description: 'A/B test thumbnails',
parameters: {
type: 'object',
properties: {
thumbnailPaths: { type: 'array', items: { type: 'string' } },
duration: { type: 'number' }
},
required: ['thumbnailPaths']
}
})
async abTestThumbnails({
thumbnailPaths,
duration = 48
}: {
thumbnailPaths: string[],
duration?: number
}): Promise<any> {
try {
const results = [];
const hours = duration || 48;
const interval = hours / thumbnailPaths.length;
for (let i = 0; i < thumbnailPaths.length; i++) {
const startTime = new Date();
startTime.setHours(startTime.getHours() + (i * interval));
const endTime = new Date(startTime);
endTime.setHours(endTime.getHours() + interval);
results.push({
thumbnail: thumbnailPaths[i],
schedule: {
start: startTime.toISOString(),
end: endTime.toISOString()
}
});
}
return results;
} catch (error) {
throw new Error(`Failed to setup A/B test: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Private style-specific methods
private getStyleBackground(style: string): string {
switch (style) {
case 'gaming':
return '#1a1a1a';
case 'vlog':
return '#f5f5f5';
case 'tutorial':
return '#ffffff';
case 'news':
return '#cc0000';
default:
return '#ffffff';
}
}
private async applyStyleEffects(ctx: any, style: string): Promise<void> {
switch (style) {
case 'gaming':
this.addGamingEffects(ctx);
break;
case 'vlog':
this.addVlogEffects(ctx);
break;
case 'tutorial':
this.addTutorialEffects(ctx);
break;
case 'news':
this.addNewsEffects(ctx);
break;
}
}
private addGamingEffects(ctx: any): void {
ctx.shadowColor = '#00ff00';
ctx.shadowBlur = 20;
ctx.fillStyle = '#00ff00';
ctx.fillRect(0, 680, 1280, 40);
}
private addVlogEffects(ctx: any): void {
const gradient = ctx.createLinearGradient(0, 0, 1280, 720);
gradient.addColorStop(0, 'rgba(255,255,255,0.1)');
gradient.addColorStop(1, 'rgba(255,255,255,0.3)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1280, 720);
}
private addTutorialEffects(ctx: any): void {
ctx.fillStyle = '#e0e0e0';
ctx.fillRect(50, 50, 100, 100);
ctx.fillRect(1130, 50, 100, 100);
}
private addNewsEffects(ctx: any): void {
ctx.fillStyle = '#cc0000';
ctx.fillRect(0, 0, 1280, 80);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 40px Arial';
ctx.fillText('BREAKING', 20, 55);
}
private addStyledText(ctx: any, text: string, style: string): void {
ctx.shadowBlur = 0;
switch (style) {
case 'gaming':
this.addGamingText(ctx, text);
break;
case 'vlog':
this.addVlogText(ctx, text);
break;
case 'tutorial':
this.addTutorialText(ctx, text);
break;
case 'news':
this.addNewsText(ctx, text);
break;
}
}
private addGamingText(ctx: any, text: string): void {
ctx.font = 'bold 80px Arial';
ctx.strokeStyle = '#00ff00';
ctx.lineWidth = 4;
ctx.strokeText(text, 50, 650);
ctx.fillStyle = '#ffffff';
ctx.fillText(text, 50, 650);
}
private addVlogText(ctx: any, text: string): void {
ctx.font = '70px Arial';
ctx.fillStyle = '#000000';
ctx.fillText(text, 50, 650);
}
private addTutorialText(ctx: any, text: string): void {
ctx.font = 'bold 60px Arial';
ctx.fillStyle = '#333333';
ctx.fillText(text, 50, 650);
}
private addNewsText(ctx: any, text: string): void {
ctx.font = 'bold 65px Arial';
ctx.fillStyle = '#ffffff';
ctx.fillText(text, 50, 150);
}
}