@iflow-mcp/nvkanirudh-linkedin-post-generator 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.dockerignore ADDED
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ npm-debug.log
3
+ .env
4
+ .git
5
+ .gitignore
6
+ README.md
package/.env.example ADDED
@@ -0,0 +1,5 @@
1
+ # OpenAI API Key
2
+ OPENAI_API_KEY=your_openai_api_key_here
3
+
4
+ # YouTube API Key (optional, used as fallback)
5
+ YOUTUBE_API_KEY=your_youtube_api_key_here
package/Dockerfile ADDED
@@ -0,0 +1,19 @@
1
+ FROM node:18-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files and install dependencies
6
+ COPY package*.json ./
7
+ RUN npm install
8
+
9
+ # Copy application code
10
+ COPY . .
11
+
12
+ # Set executable permissions for the entry point
13
+ RUN chmod +x src/index.js
14
+
15
+ # Expose the port the app runs on
16
+ EXPOSE 8000
17
+
18
+ # Set the entry point
19
+ ENTRYPOINT ["node", "src/index.js"]
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anirudh Nuti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ [![MseeP.ai Security Assessment Badge](https://mseep.net/pr/nvkanirudh-linkedin-post-generator-badge.png)](https://mseep.ai/app/nvkanirudh-linkedin-post-generator)
2
+
3
+ [![Verified on MseeP](https://mseep.ai/badge.svg)](https://mseep.ai/app/a222762a-577e-431d-a7d3-0b5474d7973e)
4
+
5
+ # LinkedIn Post Generator
6
+
7
+ [![smithery badge](https://smithery.ai/badge/@NvkAnirudh/linkedin-post-generator)](https://smithery.ai/server/@NvkAnirudh/linkedin-post-generator)
8
+
9
+ A Model Context Protocol (MCP) server that automates generating professional LinkedIn post drafts from YouTube videos. This tool streamlines content repurposing by extracting transcripts from YouTube videos, summarizing the content, and generating engaging LinkedIn posts tailored to your preferences.
10
+
11
+ <a href="https://glama.ai/mcp/servers/@NvkAnirudh/LinkedIn-Post-Generator">
12
+ <img width="380" height="200" src="https://glama.ai/mcp/servers/@NvkAnirudh/LinkedIn-Post-Generator/badge" />
13
+ </a>
14
+
15
+ ## Table of Contents
16
+ - [Features](#features)
17
+ - [Installation](#installation)
18
+ - [Local Development](#local-development)
19
+ - [Using with Claude Desktop](#using-with-claude-desktop)
20
+ - [Configuration](#configuration)
21
+ - [Usage](#usage)
22
+ - [Available Tools](#available-tools)
23
+ - [Workflow Example](#workflow-example)
24
+ - [Deployment](#deployment)
25
+ - [License](#license)
26
+
27
+ ## Features
28
+
29
+ - **YouTube Transcript Extraction**: Automatically extract transcripts from any YouTube video
30
+ - **Content Summarization**: Generate concise summaries with customizable tone and target audience
31
+ - **LinkedIn Post Generation**: Create professional LinkedIn posts with customizable style and tone
32
+ - **All-in-One Workflow**: Go from YouTube URL to LinkedIn post in a single operation
33
+ - **Customization Options**: Adjust tone, audience, word count, and more to match your personal brand
34
+ - **MCP Integration**: Works seamlessly with AI assistants that support the Model Context Protocol
35
+
36
+ ## Installation
37
+
38
+ ### Local Development
39
+
40
+ 1. Clone the repository:
41
+ ```bash
42
+ git clone https://github.com/NvkAnirudh/LinkedIn-Post-Generator.git
43
+ cd LinkedIn-Post-Generator
44
+ ```
45
+
46
+ 2. Install dependencies:
47
+ ```bash
48
+ npm install
49
+ ```
50
+
51
+ 3. Create a `.env` file based on the example:
52
+ ```bash
53
+ cp .env.example .env
54
+ ```
55
+
56
+ 4. Add your API keys to the `.env` file:
57
+ ```
58
+ OPENAI_API_KEY=your_openai_api_key
59
+ YOUTUBE_API_KEY=your_youtube_api_key
60
+ ```
61
+
62
+ 5. Run the server:
63
+ ```bash
64
+ npm run dev
65
+ ```
66
+
67
+ 6. Test with MCP Inspector:
68
+ ```bash
69
+ npm run inspect
70
+ ```
71
+
72
+ ### Using with Claude Desktop
73
+
74
+ This MCP server is designed to work with Claude Desktop and other AI assistants that support the Model Context Protocol. To use it with Claude Desktop:
75
+
76
+ 1. Configure Claude Desktop by editing the configuration file at `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
77
+
78
+ ```json
79
+ {
80
+ "mcpServers": {
81
+ "linkedin-post-generator": {
82
+ "command": "npx",
83
+ "args": [
84
+ "-y",
85
+ "@smithery/cli@latest",
86
+ "run",
87
+ "@NvkAnirudh/linkedin-post-generator",
88
+ "--key",
89
+ "YOUR_SMITHERY_API_KEY",
90
+ "--config",
91
+ "{\"OPENAI_API_KEY\":\"YOUR_OPENAI_API_KEY\",\"YOUTUBE_API_KEY\":\"YOUR_YOUTUBE_API_KEY\"}",
92
+ "--transport",
93
+ "stdio"
94
+ ]
95
+ }
96
+ }
97
+ }
98
+ ```
99
+
100
+ Replace:
101
+ - `YOUR_SMITHERY_API_KEY` with your Smithery API key
102
+ - `YOUR_OPENAI_API_KEY` with your OpenAI API key
103
+ - `YOUR_YOUTUBE_API_KEY` with your YouTube API key (optional)
104
+
105
+ 2. Restart Claude Desktop
106
+
107
+ 3. In Claude Desktop, you can now access the LinkedIn Post Generator tools without needing to set API keys again
108
+
109
+ ## Configuration
110
+
111
+ The application requires API keys to function properly:
112
+
113
+ 1. **OpenAI API Key** (required): Used for content summarization and post generation
114
+ 2. **YouTube API Key** (optional): Enhances YouTube metadata retrieval
115
+
116
+ You can provide these keys in three ways:
117
+
118
+ ### 1. Via Claude Desktop Configuration (Recommended)
119
+
120
+ When using with Claude Desktop and Smithery, the best approach is to include your API keys in the Claude Desktop configuration file as shown in the [Using with Claude Desktop](#using-with-claude-desktop) section. This way, the keys are automatically passed to the MCP server, and you don't need to set them again.
121
+
122
+ ### 2. As Environment Variables
123
+
124
+ When running locally, you can set API keys as environment variables in a `.env` file:
125
+ ```
126
+ OPENAI_API_KEY=your_openai_api_key
127
+ YOUTUBE_API_KEY=your_youtube_api_key
128
+ ```
129
+
130
+ ### 3. Using the Set API Keys Tool
131
+
132
+ If you haven't provided API keys through the configuration or environment variables, you can set them directly through the MCP interface using the `set_api_keys` tool.
133
+
134
+ ## Usage
135
+
136
+ ### Available Tools
137
+
138
+ #### Set API Keys
139
+ - Tool: `set_api_keys`
140
+ - Purpose: Configure your API keys
141
+ - Parameters:
142
+ - `openaiApiKey`: Your OpenAI API key (required)
143
+ - `youtubeApiKey`: Your YouTube API key (optional)
144
+
145
+ #### Check API Keys
146
+ - Tool: `check_api_keys`
147
+ - Purpose: Verify your API key configuration status
148
+
149
+ #### Extract Transcript
150
+ - Tool: `extract_transcript`
151
+ - Purpose: Get the transcript from a YouTube video
152
+ - Parameters:
153
+ - `youtubeUrl`: URL of the YouTube video
154
+
155
+ #### Summarize Transcript
156
+ - Tool: `summarize_transcript`
157
+ - Purpose: Create a concise summary of the video content
158
+ - Parameters:
159
+ - `transcript`: The video transcript text
160
+ - `tone`: Educational, inspirational, professional, or conversational
161
+ - `audience`: General, technical, business, or academic
162
+ - `wordCount`: Approximate word count for the summary (100-300)
163
+
164
+ #### Generate LinkedIn Post
165
+ - Tool: `generate_linkedin_post`
166
+ - Purpose: Create a LinkedIn post from a summary
167
+ - Parameters:
168
+ - `summary`: Summary of the video content
169
+ - `videoTitle`: Title of the YouTube video
170
+ - `speakerName`: Name of the speaker (optional)
171
+ - `hashtags`: Relevant hashtags (optional)
172
+ - `tone`: First-person, third-person, or thought-leader
173
+ - `includeCallToAction`: Whether to include a call to action
174
+
175
+ #### All-in-One: YouTube to LinkedIn Post
176
+ - Tool: `youtube_to_linkedin_post`
177
+ - Purpose: Complete workflow from YouTube URL to LinkedIn post
178
+ - Parameters:
179
+ - `youtubeUrl`: YouTube video URL
180
+ - `tone`: Desired tone for the post
181
+ - Plus additional customization options
182
+
183
+ ### Workflow Example
184
+
185
+ 1. Set your API keys using the `set_api_keys` tool
186
+ 2. Use the `youtube_to_linkedin_post` tool with a YouTube URL
187
+ 3. Receive a complete LinkedIn post draft ready to publish
188
+
189
+ ## Deployment
190
+
191
+ This server is deployed on [Smithery](https://smithery.ai), a platform for hosting and sharing MCP servers. The deployment configuration is defined in the `smithery.yaml` file.
192
+
193
+ To deploy your own instance:
194
+
195
+ 1. Create an account on Smithery
196
+ 2. Install the Smithery CLI:
197
+ ```bash
198
+ npm install -g @smithery/cli
199
+ ```
200
+ 3. Deploy the server:
201
+ ```bash
202
+ smithery deploy
203
+ ```
204
+
205
+ ## Contributing
206
+
207
+ Contributions are welcome and appreciated! Here's how you can contribute to the LinkedIn Post Generator:
208
+
209
+ ### Reporting Issues
210
+
211
+ - Use the [GitHub issue tracker](https://github.com/NvkAnirudh/LinkedIn-Post-Generator/issues) to report bugs or suggest features
212
+ - Please provide detailed information about the issue, including steps to reproduce, expected behavior, and actual behavior
213
+ - Include your environment details (OS, Node.js version, etc.) when reporting bugs
214
+
215
+ ### Pull Requests
216
+
217
+ 1. Fork the repository
218
+ 2. Create a new branch (`git checkout -b feature/your-feature-name`)
219
+ 3. Make your changes
220
+ 4. Run tests to ensure your changes don't break existing functionality
221
+ 5. Commit your changes (`git commit -m 'Add some feature'`)
222
+ 6. Push to the branch (`git push origin feature/your-feature-name`)
223
+ 7. Open a Pull Request
224
+
225
+ ### Development Guidelines
226
+
227
+ - Follow the existing code style and conventions
228
+ - Write clear, commented code
229
+ - Include tests for new features
230
+ - Update documentation to reflect your changes
231
+
232
+ ### Feature Suggestions
233
+
234
+ If you have ideas for new features or improvements:
235
+
236
+ 1. Check existing issues to see if your suggestion has already been proposed
237
+ 2. If not, open a new issue with the label 'enhancement'
238
+ 3. Clearly describe the feature and its potential benefits
239
+
240
+ ### Documentation
241
+
242
+ Improvements to documentation are always welcome:
243
+
244
+ - Fix typos or clarify existing documentation
245
+ - Add examples or use cases
246
+ - Improve the structure or organization of the documentation
247
+
248
+ By contributing to this project, you agree that your contributions will be licensed under the project's MIT License.
249
+
250
+ ## License
251
+
252
+ [MIT](https://github.com/NvkAnirudh/LinkedIn-Post-Generator/blob/main/LICENSE)
package/language.json ADDED
@@ -0,0 +1 @@
1
+ nodejs
package/package.json ADDED
@@ -0,0 +1 @@
1
+ {"name": "@iflow-mcp/nvkanirudh-linkedin-post-generator", "version": "1.0.0", "type": "module", "bin": {"iflow-mcp-nvkanirudh-linkedin-post-generator": "src/index.js"}, "scripts": {"start": "node src/index.js", "dev": "node --watch src/index.js", "inspect": "npx -y @modelcontextprotocol/inspector node src/index.js"}, "dependencies": {"@modelcontextprotocol/sdk": "^1.0.0", "dotenv": "^16.3.1", "node-fetch": "^3.3.2", "openai": "^4.20.1", "youtube-transcript": "^1.0.6", "zod": "^3.22.4"}}
package/package_name ADDED
@@ -0,0 +1 @@
1
+ @iflow-mcp/nvkanirudh-linkedin-post-generator
package/push_info.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "push_platform": "github",
3
+ "fork_url": "https://github.com/iflow-mcp/nvkanirudh-linkedin-post-generator",
4
+ "fork_branch": "iflow"
5
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "linkedin-post-generator",
3
+ "description": "Generate LinkedIn post drafts from YouTube videos",
4
+ "version": "1.0.0",
5
+ "type": "mcp",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "required": ["OPENAI_API_KEY"],
9
+ "properties": {
10
+ "OPENAI_API_KEY": {
11
+ "type": "string",
12
+ "description": "Your OpenAI API key (required)"
13
+ },
14
+ "YOUTUBE_API_KEY": {
15
+ "type": "string",
16
+ "description": "Your YouTube API key (optional)"
17
+ }
18
+ }
19
+ }
20
+ }
package/smithery.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "linkedin-post-generator",
3
+ "description": "Generate LinkedIn post drafts from YouTube videos",
4
+ "version": "1.0.0",
5
+ "type": "mcp",
6
+ "configSchema": {
7
+ "type": "object",
8
+ "required": ["OPENAI_API_KEY"],
9
+ "properties": {
10
+ "OPENAI_API_KEY": {
11
+ "type": "string",
12
+ "description": "Your OpenAI API key (required)"
13
+ },
14
+ "YOUTUBE_API_KEY": {
15
+ "type": "string",
16
+ "description": "Your YouTube API key (optional)"
17
+ }
18
+ }
19
+ }
20
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,49 @@
1
+ # Smithery.ai configuration
2
+ name: yt-to-linkedin-mcp
3
+ description: Model Context Protocol server that automates generating LinkedIn post drafts from YouTube videos
4
+ version: 1.0.0
5
+ tags:
6
+ - youtube
7
+ - linkedin
8
+ - content-generation
9
+ - mcp
10
+
11
+ startCommand:
12
+ type: stdio
13
+ configSchema:
14
+ type: object
15
+ properties:
16
+ OPENAI_API_KEY:
17
+ type: string
18
+ description: OpenAI API key for summarization and post generation (optional, can be provided in requests)
19
+ YOUTUBE_API_KEY:
20
+ type: string
21
+ description: YouTube Data API key for fetching video metadata (optional, can be provided in requests)
22
+ # PORT:
23
+ # type: string
24
+ # description: Port to run the server on
25
+ # default: "8000"
26
+ required: []
27
+ commandFunction: |-
28
+ (config) => ({
29
+ "command": "node",
30
+ "args": ["src/index.js"],
31
+ "env": {
32
+ "OPENAI_API_KEY": config.OPENAI_API_KEY || "",
33
+ "YOUTUBE_API_KEY": config.YOUTUBE_API_KEY || "",
34
+ "PORT": config.PORT || "8000"
35
+ }
36
+ })
37
+
38
+ # Specify the MCP configuration
39
+ mcp:
40
+ type: "stdio"
41
+
42
+ resources:
43
+ cpu: 1
44
+ memory: 1Gi
45
+
46
+ ports:
47
+ - name: http
48
+ port: 8000
49
+ protocol: TCP
package/src/index.js ADDED
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { server } from './server.js';
4
+ import dotenv from 'dotenv';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+
8
+ /**
9
+ * LinkedIn Post Generator MCP Server
10
+ *
11
+ * Configuration Requirements:
12
+ * - OPENAI_API_KEY: Required for generating content
13
+ * - YOUTUBE_API_KEY: Optional for enhanced YouTube data fetching
14
+ */
15
+
16
+ // Load environment variables (but they're now optional)
17
+ dotenv.config();
18
+
19
+ // Check for Smithery configuration
20
+ const args = process.argv;
21
+ let smitheryConfig = null;
22
+
23
+ // Look for --config argument
24
+ for (let i = 0; i < args.length; i++) {
25
+ if (args[i] === '--config' && i + 1 < args.length) {
26
+ try {
27
+ smitheryConfig = JSON.parse(args[i + 1]);
28
+ console.log('Smithery configuration detected');
29
+
30
+ // Set environment variables from Smithery config
31
+ if (smitheryConfig.OPENAI_API_KEY) {
32
+ process.env.OPENAI_API_KEY = smitheryConfig.OPENAI_API_KEY;
33
+ console.log('OpenAI API key set from Smithery config');
34
+ }
35
+
36
+ if (smitheryConfig.YOUTUBE_API_KEY) {
37
+ process.env.YOUTUBE_API_KEY = smitheryConfig.YOUTUBE_API_KEY;
38
+ console.log('YouTube API key set from Smithery config');
39
+ }
40
+
41
+ break;
42
+ } catch (error) {
43
+ console.error('Error parsing Smithery config:', error.message);
44
+ }
45
+ }
46
+ }
47
+
48
+ console.log('Starting LinkedIn Post Generator MCP server...');
49
+ if (!process.env.OPENAI_API_KEY) {
50
+ console.log('Note: You will need to set your API keys using the set_api_keys tool before using other functionality.');
51
+ }
52
+
53
+ // Start receiving messages on stdin and sending messages on stdout
54
+ const transport = new StdioServerTransport();
55
+ await server.connect(transport);
56
+
57
+ // Keep the process alive
58
+ process.on('SIGINT', () => {
59
+ console.log('Shutting down server...');
60
+ process.exit(0);
61
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Manages API keys for the application
3
+ */
4
+ export class ApiKeyManager {
5
+ constructor() {
6
+ // Initialize API keys from environment variables
7
+ this.openaiApiKey = process.env.OPENAI_API_KEY || null;
8
+ this.youtubeApiKey = process.env.YOUTUBE_API_KEY || null;
9
+
10
+ // Log initialization status
11
+ if (this.openaiApiKey) {
12
+ console.log('OpenAI API key initialized from environment');
13
+ }
14
+ if (this.youtubeApiKey) {
15
+ console.log('YouTube API key initialized from environment');
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Set the OpenAI API key
21
+ * @param {string} key - The OpenAI API key
22
+ */
23
+ setOpenAIKey(key) {
24
+ if (!key || typeof key !== 'string' || key.trim() === '') {
25
+ throw new Error("Invalid OpenAI API key");
26
+ }
27
+ this.openaiApiKey = key.trim();
28
+ console.log("OpenAI API key set successfully");
29
+ }
30
+
31
+ /**
32
+ * Set the YouTube API key
33
+ * @param {string} key - The YouTube API key
34
+ */
35
+ setYouTubeKey(key) {
36
+ if (!key || typeof key !== 'string' || key.trim() === '') {
37
+ throw new Error("Invalid YouTube API key");
38
+ }
39
+ this.youtubeApiKey = key.trim();
40
+ console.log("YouTube API key set successfully");
41
+ }
42
+
43
+ /**
44
+ * Get the OpenAI API key
45
+ * @returns {string|null} - The OpenAI API key or null if not set
46
+ */
47
+ getOpenAIKey() {
48
+ return this.openaiApiKey;
49
+ }
50
+
51
+ /**
52
+ * Get the YouTube API key
53
+ * @returns {string|null} - The YouTube API key or null if not set
54
+ */
55
+ getYouTubeKey() {
56
+ return this.youtubeApiKey;
57
+ }
58
+
59
+ /**
60
+ * Check if OpenAI API key is set
61
+ * @returns {boolean} - True if the key is set
62
+ */
63
+ hasOpenAIKey() {
64
+ return !!this.openaiApiKey;
65
+ }
66
+
67
+ /**
68
+ * Check if YouTube API key is set
69
+ * @returns {boolean} - True if the key is set
70
+ */
71
+ hasYouTubeKey() {
72
+ return !!this.youtubeApiKey;
73
+ }
74
+
75
+ /**
76
+ * Get the status of API keys
77
+ * @returns {Object} - Status object with key information
78
+ */
79
+ getStatus() {
80
+ return {
81
+ openai: {
82
+ set: this.hasOpenAIKey(),
83
+ key: this.hasOpenAIKey() ? "********" + this.openaiApiKey.slice(-4) : null
84
+ },
85
+ youtube: {
86
+ set: this.hasYouTubeKey(),
87
+ key: this.hasYouTubeKey() ? "********" + this.youtubeApiKey.slice(-4) : null
88
+ }
89
+ };
90
+ }
91
+ }
@@ -0,0 +1,99 @@
1
+ import { OpenAI } from 'openai';
2
+
3
+ /**
4
+ * Generate a LinkedIn post from a video summary
5
+ * @param {string} summary - Summary of the video content
6
+ * @param {string} videoTitle - Title of the YouTube video
7
+ * @param {string} speakerName - Name of the speaker (optional)
8
+ * @param {string[]} hashtags - Relevant hashtags (optional)
9
+ * @param {string} tone - Tone for the post (first-person, third-person, thought-leader)
10
+ * @param {boolean} includeCallToAction - Whether to include a call to action
11
+ * @param {string} apiKey - OpenAI API key
12
+ * @returns {Promise<string>} - The generated LinkedIn post
13
+ */
14
+ export async function generateLinkedInPost(
15
+ summary,
16
+ videoTitle,
17
+ speakerName = null,
18
+ hashtags = [],
19
+ tone = "first-person",
20
+ includeCallToAction = true,
21
+ apiKey
22
+ ) {
23
+ if (!apiKey) {
24
+ throw new Error("OpenAI API key not provided");
25
+ }
26
+
27
+ if (!summary || summary.trim().length === 0) {
28
+ throw new Error("Empty summary provided");
29
+ }
30
+
31
+ console.log(`Generating LinkedIn post with tone: ${tone}`);
32
+
33
+ try {
34
+ // Initialize OpenAI client with provided API key
35
+ const openai = new OpenAI({
36
+ apiKey: apiKey,
37
+ });
38
+
39
+ // Prepare hashtag string
40
+ const hashtagString = hashtags && hashtags.length > 0
41
+ ? hashtags.map(tag => tag.startsWith('#') ? tag : `#${tag}`).join(' ')
42
+ : '';
43
+
44
+ // Prepare speaker reference
45
+ const speakerReference = speakerName ? `by ${speakerName}` : '';
46
+
47
+ const response = await openai.chat.completions.create({
48
+ model: "gpt-3.5-turbo",
49
+ messages: [
50
+ {
51
+ role: "system",
52
+ content: `You are a professional LinkedIn content creator.
53
+ Create a compelling LinkedIn post in a ${tone} tone based on the provided video summary.
54
+ The post should be between 500-1200 characters (not including hashtags).
55
+
56
+ Structure the post with:
57
+ 1. An attention-grabbing hook
58
+ 2. 2-3 key insights from the video
59
+ 3. A personal reflection or takeaway
60
+ ${includeCallToAction ? '4. A soft call to action (e.g., asking a question, inviting comments)' : ''}
61
+
62
+ The post should feel authentic, professional, and valuable to LinkedIn readers.
63
+ Avoid clickbait or overly promotional language.`
64
+ },
65
+ {
66
+ role: "user",
67
+ content: `Create a LinkedIn post based on this YouTube video:
68
+
69
+ Title: ${videoTitle} ${speakerReference}
70
+
71
+ Summary:
72
+ ${summary}
73
+
74
+ ${hashtagString ? `Suggested hashtags: ${hashtagString}` : ''}
75
+
76
+ Please format the post ready to copy and paste to LinkedIn.`
77
+ }
78
+ ],
79
+ temperature: 0.7,
80
+ max_tokens: 700
81
+ });
82
+
83
+ if (response.choices && response.choices.length > 0) {
84
+ let post = response.choices[0].message.content.trim();
85
+
86
+ // Ensure hashtags are at the end if they weren't included
87
+ if (hashtagString && !post.includes(hashtagString)) {
88
+ post += `\n\n${hashtagString}`;
89
+ }
90
+
91
+ return post;
92
+ } else {
93
+ throw new Error("No post generated");
94
+ }
95
+ } catch (error) {
96
+ console.error("Post generation error:", error);
97
+ throw new Error(`Failed to generate LinkedIn post: ${error.message}`);
98
+ }
99
+ }
@@ -0,0 +1,104 @@
1
+ import { YoutubeTranscript } from 'youtube-transcript';
2
+ import fetch from 'node-fetch';
3
+
4
+ /**
5
+ * Extract transcript from a YouTube video
6
+ * @param {string} youtubeUrl - The YouTube video URL
7
+ * @param {string} youtubeApiKey - Optional YouTube API key
8
+ * @returns {Promise<string>} - The extracted transcript text
9
+ */
10
+ export async function extractTranscript(youtubeUrl, youtubeApiKey = null) {
11
+ try {
12
+ console.log(`Extracting transcript from: ${youtubeUrl}`);
13
+
14
+ // Extract video ID from URL
15
+ const videoId = extractVideoId(youtubeUrl);
16
+ if (!videoId) {
17
+ throw new Error("Invalid YouTube URL. Could not extract video ID.");
18
+ }
19
+
20
+ // Try to get transcript using youtube-transcript package
21
+ try {
22
+ const transcriptItems = await YoutubeTranscript.fetchTranscript(videoId);
23
+ if (!transcriptItems || transcriptItems.length === 0) {
24
+ throw new Error("No transcript available");
25
+ }
26
+
27
+ // Combine transcript segments into a single text
28
+ const fullTranscript = transcriptItems
29
+ .map(item => item.text)
30
+ .join(' ')
31
+ .replace(/\s+/g, ' '); // Clean up extra spaces
32
+
33
+ return fullTranscript;
34
+ } catch (error) {
35
+ console.error("Error with primary transcript method:", error);
36
+
37
+ // Fallback to YouTube API if available
38
+ if (youtubeApiKey) {
39
+ return await fetchTranscriptWithYouTubeAPI(videoId, youtubeApiKey);
40
+ } else {
41
+ throw new Error("Failed to extract transcript: " + error.message);
42
+ }
43
+ }
44
+ } catch (error) {
45
+ console.error("Transcript extraction error:", error);
46
+ throw new Error(`Failed to extract transcript: ${error.message}`);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Extract video ID from YouTube URL
52
+ * @param {string} url - YouTube URL
53
+ * @returns {string|null} - Video ID or null if not found
54
+ */
55
+ function extractVideoId(url) {
56
+ try {
57
+ const urlObj = new URL(url);
58
+
59
+ // Standard YouTube URL (youtube.com/watch?v=VIDEO_ID)
60
+ if (urlObj.hostname.includes('youtube.com')) {
61
+ return urlObj.searchParams.get('v');
62
+ }
63
+
64
+ // Short YouTube URL (youtu.be/VIDEO_ID)
65
+ if (urlObj.hostname === 'youtu.be') {
66
+ return urlObj.pathname.substring(1);
67
+ }
68
+
69
+ return null;
70
+ } catch (error) {
71
+ console.error("Error extracting video ID:", error);
72
+ return null;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Fallback method to fetch transcript using YouTube API
78
+ * @param {string} videoId - YouTube video ID
79
+ * @param {string} apiKey - YouTube API key
80
+ * @returns {Promise<string>} - Transcript text
81
+ */
82
+ async function fetchTranscriptWithYouTubeAPI(videoId, apiKey) {
83
+ if (!apiKey) {
84
+ throw new Error("YouTube API key not provided");
85
+ }
86
+
87
+ // First, get the caption track
88
+ const captionUrl = `https://www.googleapis.com/youtube/v3/captions?part=snippet&videoId=${videoId}&key=${apiKey}`;
89
+
90
+ const response = await fetch(captionUrl);
91
+ if (!response.ok) {
92
+ throw new Error(`YouTube API error: ${response.statusText}`);
93
+ }
94
+
95
+ const data = await response.json();
96
+
97
+ if (!data.items || data.items.length === 0) {
98
+ throw new Error("No captions available for this video");
99
+ }
100
+
101
+ // Note: Actually downloading the caption track requires OAuth2 authentication
102
+ // which is beyond the scope of this example
103
+ throw new Error("YouTube API fallback requires OAuth2 authentication");
104
+ }
@@ -0,0 +1,79 @@
1
+ import { OpenAI } from 'openai';
2
+
3
+ /**
4
+ * Summarize a transcript using OpenAI
5
+ * @param {string} transcript - The transcript text to summarize
6
+ * @param {string} tone - Desired tone (educational, inspirational, etc.)
7
+ * @param {string} audience - Target audience (general, technical, etc.)
8
+ * @param {number} wordCount - Approximate word count for summary
9
+ * @param {string} apiKey - OpenAI API key
10
+ * @returns {Promise<string>} - The summarized text
11
+ */
12
+ export async function summarizeTranscript(transcript, tone, audience, wordCount, apiKey) {
13
+ if (!apiKey) {
14
+ throw new Error("OpenAI API key not provided");
15
+ }
16
+
17
+ if (!transcript || transcript.trim().length === 0) {
18
+ throw new Error("Empty transcript provided");
19
+ }
20
+
21
+ console.log(`Summarizing transcript (${transcript.length} chars) with tone: ${tone}, audience: ${audience}`);
22
+
23
+ try {
24
+ // Initialize OpenAI client with provided API key
25
+ const openai = new OpenAI({
26
+ apiKey: apiKey,
27
+ });
28
+
29
+ // Truncate transcript if it's too long (to fit within token limits)
30
+ const truncatedTranscript = truncateText(transcript, 15000);
31
+
32
+ const response = await openai.chat.completions.create({
33
+ model: "gpt-3.5-turbo",
34
+ messages: [
35
+ {
36
+ role: "system",
37
+ content: `You are a professional content summarizer. Summarize the provided transcript in a ${tone} tone for a ${audience} audience.
38
+ The summary should be approximately ${wordCount} words and capture the key points, insights, and valuable information from the transcript.
39
+ Focus on making the summary concise, informative, and engaging.`
40
+ },
41
+ {
42
+ role: "user",
43
+ content: `Please summarize the following video transcript:\n\n${truncatedTranscript}`
44
+ }
45
+ ],
46
+ temperature: 0.7,
47
+ max_tokens: 500
48
+ });
49
+
50
+ if (response.choices && response.choices.length > 0) {
51
+ return response.choices[0].message.content.trim();
52
+ } else {
53
+ throw new Error("No summary generated");
54
+ }
55
+ } catch (error) {
56
+ console.error("Summarization error:", error);
57
+ throw new Error(`Failed to summarize transcript: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Truncate text to a maximum character length
63
+ * @param {string} text - Text to truncate
64
+ * @param {number} maxLength - Maximum length in characters
65
+ * @returns {string} - Truncated text
66
+ */
67
+ function truncateText(text, maxLength) {
68
+ if (text.length <= maxLength) return text;
69
+
70
+ // Try to truncate at a sentence boundary
71
+ const truncated = text.substring(0, maxLength);
72
+ const lastPeriod = truncated.lastIndexOf('.');
73
+
74
+ if (lastPeriod > maxLength * 0.8) {
75
+ return truncated.substring(0, lastPeriod + 1);
76
+ }
77
+
78
+ return truncated + "...";
79
+ }
package/src/server.js ADDED
@@ -0,0 +1,319 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { extractTranscript } from './modules/transcript-extractor.js';
4
+ import { summarizeTranscript } from './modules/transcript-summarizer.js';
5
+ import { generateLinkedInPost } from './modules/post-generator.js';
6
+ import { ApiKeyManager } from './modules/api-key-manager.js';
7
+
8
+ // Create API key manager
9
+ const apiKeyManager = new ApiKeyManager();
10
+
11
+ // Create an MCP server for LinkedIn post generation
12
+ const server = new McpServer({
13
+ name: "LinkedIn Post Generator",
14
+ version: "1.0.0",
15
+ description: "Generate LinkedIn post drafts from YouTube videos"
16
+ });
17
+
18
+ // Set API keys tool
19
+ server.tool(
20
+ "set_api_keys",
21
+ {
22
+ openaiApiKey: z.string().min(1).describe("Your OpenAI API key"),
23
+ youtubeApiKey: z.string().optional().describe("Your YouTube API key (optional)")
24
+ },
25
+ async ({ openaiApiKey, youtubeApiKey }) => {
26
+ try {
27
+ apiKeyManager.setOpenAIKey(openaiApiKey);
28
+ if (youtubeApiKey) {
29
+ apiKeyManager.setYouTubeKey(youtubeApiKey);
30
+ }
31
+
32
+ return {
33
+ content: [{
34
+ type: "text",
35
+ text: JSON.stringify({
36
+ success: true,
37
+ message: "API keys set successfully. You can now use the other tools."
38
+ }, null, 2)
39
+ }]
40
+ };
41
+ } catch (error) {
42
+ return {
43
+ content: [{
44
+ type: "text",
45
+ text: JSON.stringify({
46
+ success: false,
47
+ error: error.message
48
+ }, null, 2)
49
+ }],
50
+ isError: true
51
+ };
52
+ }
53
+ },
54
+ { description: "Set your API keys for OpenAI and YouTube (optional)" }
55
+ );
56
+
57
+ // Check API keys status
58
+ server.tool(
59
+ "check_api_keys",
60
+ {},
61
+ async () => {
62
+ const status = apiKeyManager.getStatus();
63
+ return {
64
+ content: [{
65
+ type: "text",
66
+ text: JSON.stringify(status, null, 2)
67
+ }]
68
+ };
69
+ },
70
+ { description: "Check the status of your API keys" }
71
+ );
72
+
73
+ // Extract transcript tool
74
+ server.tool(
75
+ "extract_transcript",
76
+ {
77
+ youtubeUrl: z.string().url().describe("YouTube video URL")
78
+ },
79
+ async ({ youtubeUrl }) => {
80
+ try {
81
+ // Check if YouTube API key is set (if needed)
82
+ if (!apiKeyManager.hasYouTubeKey()) {
83
+ console.log("No YouTube API key set, will try without it");
84
+ }
85
+
86
+ const transcript = await extractTranscript(youtubeUrl, apiKeyManager.getYouTubeKey());
87
+ return {
88
+ content: [{
89
+ type: "text",
90
+ text: JSON.stringify({
91
+ success: true,
92
+ transcript
93
+ }, null, 2)
94
+ }]
95
+ };
96
+ } catch (error) {
97
+ return {
98
+ content: [{
99
+ type: "text",
100
+ text: JSON.stringify({
101
+ success: false,
102
+ error: error.message
103
+ }, null, 2)
104
+ }],
105
+ isError: true
106
+ };
107
+ }
108
+ },
109
+ { description: "Extract transcript from a YouTube video" }
110
+ );
111
+
112
+ // Summarize transcript tool
113
+ server.tool(
114
+ "summarize_transcript",
115
+ {
116
+ transcript: z.string().describe("Video transcript text"),
117
+ tone: z.enum(["educational", "inspirational", "professional", "conversational"])
118
+ .default("professional")
119
+ .describe("Tone of the summary"),
120
+ audience: z.enum(["general", "technical", "business", "academic"])
121
+ .default("general")
122
+ .describe("Target audience for the summary"),
123
+ wordCount: z.number().min(100).max(300).default(200)
124
+ .describe("Approximate word count for the summary")
125
+ },
126
+ async ({ transcript, tone, audience, wordCount }) => {
127
+ try {
128
+ // Check if OpenAI API key is set
129
+ if (!apiKeyManager.hasOpenAIKey()) {
130
+ throw new Error("OpenAI API key not set. Please use the set_api_keys tool first.");
131
+ }
132
+
133
+ const summary = await summarizeTranscript(
134
+ transcript,
135
+ tone,
136
+ audience,
137
+ wordCount,
138
+ apiKeyManager.getOpenAIKey()
139
+ );
140
+
141
+ return {
142
+ content: [{
143
+ type: "text",
144
+ text: JSON.stringify({
145
+ success: true,
146
+ summary
147
+ }, null, 2)
148
+ }]
149
+ };
150
+ } catch (error) {
151
+ return {
152
+ content: [{
153
+ type: "text",
154
+ text: JSON.stringify({
155
+ success: false,
156
+ error: error.message
157
+ }, null, 2)
158
+ }],
159
+ isError: true
160
+ };
161
+ }
162
+ },
163
+ { description: "Summarize a video transcript" }
164
+ );
165
+
166
+ // Generate LinkedIn post tool
167
+ server.tool(
168
+ "generate_linkedin_post",
169
+ {
170
+ summary: z.string().describe("Summary of the video content"),
171
+ videoTitle: z.string().describe("Title of the YouTube video"),
172
+ speakerName: z.string().optional().describe("Name of the speaker in the video (optional)"),
173
+ hashtags: z.array(z.string()).optional().describe("Relevant hashtags (optional)"),
174
+ tone: z.enum(["first-person", "third-person", "thought-leader"])
175
+ .default("first-person")
176
+ .describe("Tone of the LinkedIn post"),
177
+ includeCallToAction: z.boolean().default(true)
178
+ .describe("Whether to include a call to action")
179
+ },
180
+ async ({ summary, videoTitle, speakerName, hashtags, tone, includeCallToAction }) => {
181
+ try {
182
+ // Check if OpenAI API key is set
183
+ if (!apiKeyManager.hasOpenAIKey()) {
184
+ throw new Error("OpenAI API key not set. Please use the set_api_keys tool first.");
185
+ }
186
+
187
+ const post = await generateLinkedInPost(
188
+ summary,
189
+ videoTitle,
190
+ speakerName,
191
+ hashtags,
192
+ tone,
193
+ includeCallToAction,
194
+ apiKeyManager.getOpenAIKey()
195
+ );
196
+
197
+ return {
198
+ content: [{
199
+ type: "text",
200
+ text: JSON.stringify({
201
+ success: true,
202
+ post
203
+ }, null, 2)
204
+ }]
205
+ };
206
+ } catch (error) {
207
+ return {
208
+ content: [{
209
+ type: "text",
210
+ text: JSON.stringify({
211
+ success: false,
212
+ error: error.message
213
+ }, null, 2)
214
+ }],
215
+ isError: true
216
+ };
217
+ }
218
+ },
219
+ { description: "Generate a LinkedIn post draft from a video summary" }
220
+ );
221
+
222
+ // All-in-one tool: YouTube URL to LinkedIn post
223
+ server.tool(
224
+ "youtube_to_linkedin_post",
225
+ {
226
+ youtubeUrl: z.string().url().describe("YouTube video URL"),
227
+ tone: z.enum(["first-person", "third-person", "thought-leader"])
228
+ .default("first-person")
229
+ .describe("Tone of the LinkedIn post"),
230
+ summaryTone: z.enum(["educational", "inspirational", "professional", "conversational"])
231
+ .default("professional")
232
+ .describe("Tone of the summary"),
233
+ audience: z.enum(["general", "technical", "business", "academic"])
234
+ .default("general")
235
+ .describe("Target audience"),
236
+ hashtags: z.array(z.string()).optional().describe("Relevant hashtags (optional)"),
237
+ includeCallToAction: z.boolean().default(true)
238
+ .describe("Whether to include a call to action")
239
+ },
240
+ async ({ youtubeUrl, tone, summaryTone, audience, hashtags, includeCallToAction }) => {
241
+ try {
242
+ // Check if API keys are set
243
+ if (!apiKeyManager.hasOpenAIKey()) {
244
+ throw new Error("OpenAI API key not set. Please use the set_api_keys tool first.");
245
+ }
246
+
247
+ // Step 1: Extract transcript
248
+ const transcript = await extractTranscript(youtubeUrl, apiKeyManager.getYouTubeKey());
249
+
250
+ // Step 2: Get video metadata (title, etc.)
251
+ const videoTitle = await getVideoTitle(youtubeUrl);
252
+
253
+ // Step 3: Summarize transcript
254
+ const summary = await summarizeTranscript(
255
+ transcript,
256
+ summaryTone,
257
+ audience,
258
+ 200,
259
+ apiKeyManager.getOpenAIKey()
260
+ );
261
+
262
+ // Step 4: Generate LinkedIn post
263
+ const post = await generateLinkedInPost(
264
+ summary,
265
+ videoTitle,
266
+ undefined, // speaker name not available without additional API calls
267
+ hashtags,
268
+ tone,
269
+ includeCallToAction,
270
+ apiKeyManager.getOpenAIKey()
271
+ );
272
+
273
+ return {
274
+ content: [{
275
+ type: "text",
276
+ text: JSON.stringify({
277
+ success: true,
278
+ videoTitle,
279
+ transcript: transcript.substring(0, 300) + "...", // Preview only
280
+ summary,
281
+ post
282
+ }, null, 2)
283
+ }]
284
+ };
285
+ } catch (error) {
286
+ return {
287
+ content: [{
288
+ type: "text",
289
+ text: JSON.stringify({
290
+ success: false,
291
+ error: error.message
292
+ }, null, 2)
293
+ }],
294
+ isError: true
295
+ };
296
+ }
297
+ },
298
+ { description: "Generate a LinkedIn post draft directly from a YouTube video URL" }
299
+ );
300
+
301
+ // Helper function to extract video title from URL
302
+ async function getVideoTitle(youtubeUrl) {
303
+ try {
304
+ // Extract video ID from URL
305
+ const videoId = new URL(youtubeUrl).searchParams.get('v');
306
+ if (!videoId) {
307
+ throw new Error("Could not extract video ID from URL");
308
+ }
309
+
310
+ // For now, return a placeholder. In a production environment,
311
+ // you would use the YouTube API to get the actual title
312
+ return `YouTube Video (${videoId})`;
313
+ } catch (error) {
314
+ console.error("Error getting video title:", error);
315
+ return "YouTube Video";
316
+ }
317
+ }
318
+
319
+ export { server };