youtube_to_linkedin_post
Convert YouTube video content into LinkedIn post drafts by analyzing transcripts and generating professional content tailored to your audience and tone preferences.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| youtubeUrl | Yes | YouTube video URL | |
| tone | No | Tone of the LinkedIn post | first-person |
| summaryTone | No | Tone of the summary | professional |
| audience | No | Target audience | general |
| hashtags | No | Relevant hashtags (optional) | |
| includeCallToAction | No | Whether to include a call to action |
Implementation Reference
- src/server.js:240-297 (handler)Inline handler for the youtube_to_linkedin_post tool. Orchestrates the full pipeline: extracts transcript, fetches title, summarizes content, generates LinkedIn post.async ({ youtubeUrl, tone, summaryTone, audience, hashtags, includeCallToAction }) => { try { // Check if API keys are set if (!apiKeyManager.hasOpenAIKey()) { throw new Error("OpenAI API key not set. Please use the set_api_keys tool first."); } // Step 1: Extract transcript const transcript = await extractTranscript(youtubeUrl, apiKeyManager.getYouTubeKey()); // Step 2: Get video metadata (title, etc.) const videoTitle = await getVideoTitle(youtubeUrl); // Step 3: Summarize transcript const summary = await summarizeTranscript( transcript, summaryTone, audience, 200, apiKeyManager.getOpenAIKey() ); // Step 4: Generate LinkedIn post const post = await generateLinkedInPost( summary, videoTitle, undefined, // speaker name not available without additional API calls hashtags, tone, includeCallToAction, apiKeyManager.getOpenAIKey() ); return { content: [{ type: "text", text: JSON.stringify({ success: true, videoTitle, transcript: transcript.substring(0, 300) + "...", // Preview only summary, post }, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }], isError: true }; } },
- src/server.js:225-239 (schema)Zod input schema defining parameters for the youtube_to_linkedin_post tool.{ youtubeUrl: z.string().url().describe("YouTube video URL"), tone: z.enum(["first-person", "third-person", "thought-leader"]) .default("first-person") .describe("Tone of the LinkedIn post"), summaryTone: z.enum(["educational", "inspirational", "professional", "conversational"]) .default("professional") .describe("Tone of the summary"), audience: z.enum(["general", "technical", "business", "academic"]) .default("general") .describe("Target audience"), hashtags: z.array(z.string()).optional().describe("Relevant hashtags (optional)"), includeCallToAction: z.boolean().default(true) .describe("Whether to include a call to action") },
- src/server.js:223-299 (registration)MCP server tool registration for youtube_to_linkedin_post.server.tool( "youtube_to_linkedin_post", { youtubeUrl: z.string().url().describe("YouTube video URL"), tone: z.enum(["first-person", "third-person", "thought-leader"]) .default("first-person") .describe("Tone of the LinkedIn post"), summaryTone: z.enum(["educational", "inspirational", "professional", "conversational"]) .default("professional") .describe("Tone of the summary"), audience: z.enum(["general", "technical", "business", "academic"]) .default("general") .describe("Target audience"), hashtags: z.array(z.string()).optional().describe("Relevant hashtags (optional)"), includeCallToAction: z.boolean().default(true) .describe("Whether to include a call to action") }, async ({ youtubeUrl, tone, summaryTone, audience, hashtags, includeCallToAction }) => { try { // Check if API keys are set if (!apiKeyManager.hasOpenAIKey()) { throw new Error("OpenAI API key not set. Please use the set_api_keys tool first."); } // Step 1: Extract transcript const transcript = await extractTranscript(youtubeUrl, apiKeyManager.getYouTubeKey()); // Step 2: Get video metadata (title, etc.) const videoTitle = await getVideoTitle(youtubeUrl); // Step 3: Summarize transcript const summary = await summarizeTranscript( transcript, summaryTone, audience, 200, apiKeyManager.getOpenAIKey() ); // Step 4: Generate LinkedIn post const post = await generateLinkedInPost( summary, videoTitle, undefined, // speaker name not available without additional API calls hashtags, tone, includeCallToAction, apiKeyManager.getOpenAIKey() ); return { content: [{ type: "text", text: JSON.stringify({ success: true, videoTitle, transcript: transcript.substring(0, 300) + "...", // Preview only summary, post }, null, 2) }] }; } catch (error) { return { content: [{ type: "text", text: JSON.stringify({ success: false, error: error.message }, null, 2) }], isError: true }; } }, { description: "Generate a LinkedIn post draft directly from a YouTube video URL" } );
- src/modules/post-generator.js:14-99 (helper)Core helper that generates the LinkedIn post text from summary and parameters using OpenAI GPT.export async function generateLinkedInPost( summary, videoTitle, speakerName = null, hashtags = [], tone = "first-person", includeCallToAction = true, apiKey ) { if (!apiKey) { throw new Error("OpenAI API key not provided"); } if (!summary || summary.trim().length === 0) { throw new Error("Empty summary provided"); } console.log(`Generating LinkedIn post with tone: ${tone}`); try { // Initialize OpenAI client with provided API key const openai = new OpenAI({ apiKey: apiKey, }); // Prepare hashtag string const hashtagString = hashtags && hashtags.length > 0 ? hashtags.map(tag => tag.startsWith('#') ? tag : `#${tag}`).join(' ') : ''; // Prepare speaker reference const speakerReference = speakerName ? `by ${speakerName}` : ''; const response = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: `You are a professional LinkedIn content creator. Create a compelling LinkedIn post in a ${tone} tone based on the provided video summary. The post should be between 500-1200 characters (not including hashtags). Structure the post with: 1. An attention-grabbing hook 2. 2-3 key insights from the video 3. A personal reflection or takeaway ${includeCallToAction ? '4. A soft call to action (e.g., asking a question, inviting comments)' : ''} The post should feel authentic, professional, and valuable to LinkedIn readers. Avoid clickbait or overly promotional language.` }, { role: "user", content: `Create a LinkedIn post based on this YouTube video: Title: ${videoTitle} ${speakerReference} Summary: ${summary} ${hashtagString ? `Suggested hashtags: ${hashtagString}` : ''} Please format the post ready to copy and paste to LinkedIn.` } ], temperature: 0.7, max_tokens: 700 }); if (response.choices && response.choices.length > 0) { let post = response.choices[0].message.content.trim(); // Ensure hashtags are at the end if they weren't included if (hashtagString && !post.includes(hashtagString)) { post += `\n\n${hashtagString}`; } return post; } else { throw new Error("No post generated"); } } catch (error) { console.error("Post generation error:", error); throw new Error(`Failed to generate LinkedIn post: ${error.message}`); } }
- Helper to extract full transcript text from YouTube video URL.export async function extractTranscript(youtubeUrl, youtubeApiKey = null) { try { console.log(`Extracting transcript from: ${youtubeUrl}`); // Extract video ID from URL const videoId = extractVideoId(youtubeUrl); if (!videoId) { throw new Error("Invalid YouTube URL. Could not extract video ID."); } // Try to get transcript using youtube-transcript package try { const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId); if (!transcriptItems || transcriptItems.length === 0) { throw new Error("No transcript available"); } // Combine transcript segments into a single text const fullTranscript = transcriptItems .map(item => item.text) .join(' ') .replace(/\s+/g, ' '); // Clean up extra spaces return fullTranscript; } catch (error) { console.error("Error with primary transcript method:", error); // Fallback to YouTube API if available if (youtubeApiKey) { return await fetchTranscriptWithYouTubeAPI(videoId, youtubeApiKey); } else { throw new Error("Failed to extract transcript: " + error.message); } } } catch (error) { console.error("Transcript extraction error:", error); throw new Error(`Failed to extract transcript: ${error.message}`); } }
- Helper to summarize transcript with specified tone and audience using OpenAI.export async function summarizeTranscript(transcript, tone, audience, wordCount, apiKey) { if (!apiKey) { throw new Error("OpenAI API key not provided"); } if (!transcript || transcript.trim().length === 0) { throw new Error("Empty transcript provided"); } console.log(`Summarizing transcript (${transcript.length} chars) with tone: ${tone}, audience: ${audience}`); try { // Initialize OpenAI client with provided API key const openai = new OpenAI({ apiKey: apiKey, }); // Truncate transcript if it's too long (to fit within token limits) const truncatedTranscript = truncateText(transcript, 15000); const response = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: `You are a professional content summarizer. Summarize the provided transcript in a ${tone} tone for a ${audience} audience. The summary should be approximately ${wordCount} words and capture the key points, insights, and valuable information from the transcript. Focus on making the summary concise, informative, and engaging.` }, { role: "user", content: `Please summarize the following video transcript:\n\n${truncatedTranscript}` } ], temperature: 0.7, max_tokens: 500 }); if (response.choices && response.choices.length > 0) { return response.choices[0].message.content.trim(); } else { throw new Error("No summary generated"); } } catch (error) { console.error("Summarization error:", error); throw new Error(`Failed to summarize transcript: ${error.message}`); } }