@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 +6 -0
- package/.env.example +5 -0
- package/Dockerfile +19 -0
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/language.json +1 -0
- package/package.json +1 -0
- package/package_name +1 -0
- package/push_info.json +5 -0
- package/smithery.config.json +20 -0
- package/smithery.json +20 -0
- package/smithery.yaml +49 -0
- package/src/index.js +61 -0
- package/src/modules/api-key-manager.js +91 -0
- package/src/modules/post-generator.js +99 -0
- package/src/modules/transcript-extractor.js +104 -0
- package/src/modules/transcript-summarizer.js +79 -0
- package/src/server.js +319 -0
package/.dockerignore
ADDED
package/.env.example
ADDED
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
|
+
[](https://mseep.ai/app/nvkanirudh-linkedin-post-generator)
|
|
2
|
+
|
|
3
|
+
[](https://mseep.ai/app/a222762a-577e-431d-a7d3-0b5474d7973e)
|
|
4
|
+
|
|
5
|
+
# LinkedIn Post Generator
|
|
6
|
+
|
|
7
|
+
[](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,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 };
|