@twick/mcp-agent 0.14.15

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/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # MCP Twick Server
2
+
3
+ An MCP (Model Context Protocol) server for Claude Desktop that generates video captions using Google Vertex AI (Gemini) and integrates with Twick Studio.
4
+
5
+ ## šŸ“¦ Installation
6
+
7
+ ### Quick Install (Recommended)
8
+
9
+ ```bash
10
+ git clone <repository-url>
11
+ cd twick-mcp-agent
12
+ npm run install-claude
13
+ ```
14
+
15
+ This will automatically install dependencies, build the project, and configure Claude Desktop.
16
+
17
+ See [SHIPPING.md](./SHIPPING.md) for distribution options and release information.
18
+
19
+ ## Features
20
+
21
+ - šŸŽ¬ **Video Transcription**: Transcribe videos from public URLs using Google Vertex AI
22
+ - šŸŒ **Multi-language Support**: Support for multiple languages and fonts
23
+ - šŸ“ **Subtitle Generation**: Generate timed subtitle files in Twick Studio format
24
+ - šŸ”— **Twick Studio Integration**: Direct upload and link generation for Twick Studio
25
+ - šŸ“¦ **Claude Desktop Extension**: Easy one-click installation
26
+
27
+ ## Prerequisites
28
+
29
+ - Node.js 18+ and npm
30
+ - Google Cloud Platform account with:
31
+ - Vertex AI API enabled
32
+ - Service account key file (`gcp-sa-key.json`)
33
+ - Claude Desktop installed
34
+
35
+ ## Quick Installation (One-Click)
36
+
37
+ ```bash
38
+ npm run install-claude
39
+ ```
40
+
41
+ This will:
42
+ 1. Install all dependencies
43
+ 2. Build the TypeScript project
44
+ 3. Automatically merge the MCP server config into your Claude Desktop configuration
45
+
46
+ ## Manual Installation
47
+
48
+ 1. **Clone and install dependencies:**
49
+ ```bash
50
+ git clone <your-repo-url>
51
+ cd mcp-agent
52
+ npm install
53
+ ```
54
+
55
+ 2. **Build the project:**
56
+ ```bash
57
+ npm run build
58
+ ```
59
+
60
+ 3. **Configure Claude Desktop:**
61
+ - Open Claude Desktop settings
62
+ - Go to "MCP Servers" section
63
+ - Click "Import config from file"
64
+ - Select `claude_desktop_config.json` from this project
65
+ - Edit the environment variables with your values (see Configuration below)
66
+
67
+ 4. **Restart Claude Desktop**
68
+
69
+ ## Configuration
70
+
71
+ Edit the `twick-mcp-agent` section in your Claude Desktop config file:
72
+
73
+ **Windows:**
74
+ ```
75
+ %APPDATA%\Claude\claude_desktop_config.json
76
+ ```
77
+
78
+ **macOS:**
79
+ ```
80
+ ~/Library/Application Support/Claude/claude_desktop_config.json
81
+ ```
82
+
83
+ **Linux:**
84
+ ```
85
+ ~/.config/Claude/claude_desktop_config.json
86
+ ```
87
+
88
+ ### Required Environment Variables
89
+
90
+ - `GOOGLE_CLOUD_PROJECT`: Your GCP project ID
91
+ - `GOOGLE_CLOUD_LOCATION`: GCP location (default: "global")
92
+ - `GOOGLE_APPLICATION_CREDENTIALS`: Absolute path to your `gcp-sa-key.json` file
93
+
94
+ ### Optional Environment Variables
95
+
96
+ - `GOOGLE_VERTEX_MODEL`: Vertex AI model name (default: "gemini-2.5-flash-lite")
97
+ - `UPLOAD_API_URL`: Your upload API endpoint for uploading project files
98
+ - `TWICK_STUDIO_URL`: Twick Studio URL with `$project` placeholder (e.g., `https://studio.example.com?project-file=$project`)
99
+
100
+ ### Example Configuration
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "twick-mcp-agent": {
106
+ "command": "node",
107
+ "args": ["C:\\path\\to\\mcp-agent\\dist\\stdio-server.js"],
108
+ "env": {
109
+ "GOOGLE_CLOUD_PROJECT": "my-gcp-project",
110
+ "GOOGLE_CLOUD_LOCATION": "global",
111
+ "GOOGLE_APPLICATION_CREDENTIALS": "C:\\path\\to\\mcp-agent\\gcp-sa-key.json",
112
+ "GOOGLE_VERTEX_MODEL": "gemini-2.5-flash-lite",
113
+ "UPLOAD_API_URL": "https://api.example.com/upload",
114
+ "TWICK_STUDIO_URL": "https://studio.example.com?project-file=$project"
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ ## Usage
122
+
123
+ Once installed, the `generate-subtitles` tool will be available in Claude Desktop. You can use it like:
124
+
125
+ ```
126
+ Generate subtitless for this video: https://example.com/video.mp4
127
+ ```
128
+
129
+ Or with specific language settings:
130
+
131
+ ```
132
+ Generate subtitless for https://example.com/video.mp4 in Spanish with Spanish font
133
+ ```
134
+
135
+ ### Tool Parameters
136
+
137
+ - `videoUrl` (required): Publicly accessible video URL
138
+ - `language` (optional): Target language for transcription (default: "english")
139
+ - `language_font` (optional): Font/script for subtitles (default: "english")
140
+
141
+ ## Project Structure
142
+
143
+ ```
144
+ mcp-agent/
145
+ ā”œā”€ā”€ dist/ # Compiled JavaScript (generated)
146
+ ā”œā”€ā”€ src/ # TypeScript source files
147
+ ā”œā”€ā”€ stdio-server.ts # Main MCP server entry point
148
+ ā”œā”€ā”€ transcriber.ts # Video transcription logic
149
+ ā”œā”€ā”€ utils.ts # Utility functions
150
+ ā”œā”€ā”€ claude_desktop_config.json # Claude Desktop config template
151
+ ā”œā”€ā”€ install.js # Installation script
152
+ ā”œā”€ā”€ package.json
153
+ └── README.md
154
+ ```
155
+
156
+ ## Development
157
+
158
+ ```bash
159
+ # Install dependencies
160
+ npm install
161
+
162
+ # Build TypeScript
163
+ npm run build
164
+
165
+ # Run in development mode
166
+ npm run dev
167
+ ```
168
+
169
+ ## Troubleshooting
170
+
171
+ ### Server not appearing in Claude Desktop
172
+
173
+ 1. Check that the config file path is correct
174
+ 2. Verify all environment variables are set
175
+ 3. Check Claude Desktop logs for errors
176
+ 4. Ensure `dist/stdio-server.js` exists (run `npm run build`)
177
+
178
+ ### Transcription errors
179
+
180
+ 1. Verify your GCP credentials are valid
181
+ 2. Check that Vertex AI API is enabled in your GCP project
182
+ 3. Ensure the video URL is publicly accessible
183
+ 4. Check that you have sufficient GCP quota
184
+
185
+ ### Upload/Studio link issues
186
+
187
+ - If `UPLOAD_API_URL` is not set, the tool will return the project as a downloadable JSON file
188
+ - If `TWICK_STUDIO_URL` is not set, only the upload result will be returned
189
+ - The `$project` placeholder in `TWICK_STUDIO_URL` will be automatically replaced with the encoded project URL
190
+
191
+ ## License
192
+
193
+ ISC
194
+
@@ -0,0 +1,18 @@
1
+ {
2
+ "mcpServers": {
3
+ "twick-mcp-agent": {
4
+ "command": "node",
5
+ "args": ["dist/stdio-server.js"],
6
+ "env": {
7
+ "GOOGLE_CLOUD_PROJECT": "your-gcp-project-id",
8
+ "GOOGLE_CLOUD_LOCATION": "global",
9
+ "GOOGLE_APPLICATION_CREDENTIALS": "gcp-sa-key.json",
10
+ "GOOGLE_VERTEX_MODEL": "gemini-2.5-flash-lite",
11
+ "UPLOAD_API_URL": "https://your-upload-api.example.com/upload",
12
+ "TWICK_STUDIO_URL": "https://development.d1vtsw7m0lx01h.amplifyapp.com?project-file=$project"
13
+ }
14
+ }
15
+ }
16
+ }
17
+
18
+
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=stdio-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio-server.d.ts","sourceRoot":"","sources":["../stdio-server.ts"],"names":[],"mappings":""}
@@ -0,0 +1,94 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { transcribeVideoUrl } from "./transcriber.js";
5
+ import { processSubtitlesToProject, returnProjectAsFile, returnProjectAsTwickStudioLink, } from "./utils.js";
6
+ import { fileURLToPath } from "node:url";
7
+ import { dirname, resolve } from "node:path";
8
+ import dotenv from "dotenv";
9
+ // Get project root (go up from dist/ to project root)
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+ const PROJECT_ROOT = resolve(__dirname, "..");
13
+ // Load .env.local from project root, not cwd
14
+ dotenv.config({ path: resolve(PROJECT_ROOT, ".env.local") });
15
+ // Initialize MCP server
16
+ const server = new Server({
17
+ name: "twick-mcp-agent",
18
+ version: "1.0.0",
19
+ }, {
20
+ capabilities: {
21
+ tools: {},
22
+ },
23
+ });
24
+ // Advertise tools so MCP clients can discover them
25
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
26
+ return {
27
+ tools: [
28
+ {
29
+ name: "generate-subtitles",
30
+ description: "Generate subtitles for an video URL using an external service",
31
+ inputSchema: {
32
+ type: "object",
33
+ properties: {
34
+ videoUrl: {
35
+ type: "string",
36
+ description: "Publicly accessible media URL (video)",
37
+ },
38
+ language: {
39
+ type: "string",
40
+ description: "Language for transcription (default: english)",
41
+ },
42
+ language_font: {
43
+ type: "string",
44
+ description: "Language font (default: english)",
45
+ },
46
+ },
47
+ required: ["videoUrl"],
48
+ },
49
+ },
50
+ ],
51
+ };
52
+ });
53
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
54
+ try {
55
+ if (request.params.name === "generate-subtitles") {
56
+ const { videoUrl, language, language_font } = request.params
57
+ .arguments;
58
+ if (!videoUrl || typeof videoUrl !== "string") {
59
+ throw new Error("Invalid or missing videoUrl");
60
+ }
61
+ const data = await transcribeVideoUrl({
62
+ videoUrl,
63
+ language: language ?? "english",
64
+ languageFont: language_font ?? "english",
65
+ });
66
+ const project = await processSubtitlesToProject({
67
+ subtitles: data.subtitles,
68
+ videoUrl: data.videoUrl,
69
+ duration: data.duration,
70
+ videoSize: { width: 720, height: 1280 },
71
+ });
72
+ if (process.env.UPLOAD_API_URL && process.env.TWICK_STUDIO_URL) {
73
+ return await returnProjectAsTwickStudioLink(project);
74
+ }
75
+ else {
76
+ return returnProjectAsFile(project);
77
+ }
78
+ }
79
+ else {
80
+ throw new Error(`Unknown tool: ${request.params.name}`);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error(error);
85
+ throw error;
86
+ }
87
+ });
88
+ // Start the server with stdio transport
89
+ async function main() {
90
+ const transport = new StdioServerTransport();
91
+ await server.connect(transport);
92
+ }
93
+ main().catch(console.error);
94
+ //# sourceMappingURL=stdio-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio-server.js","sourceRoot":"","sources":["../stdio-server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Transcribe an audio URL to JSON subtitles using Google GenAI (Vertex AI),
3
+ * mirroring the Python implementation in `playground/vertex/transcript.py`.
4
+ *
5
+ * @param {Object} params
6
+ * @param {string} params.videoUrl - Publicly reachable video URL.
7
+ * @param {string} [params.language="english"] - Target transcription language (human-readable).
8
+ * @param {string} [params.languageFont="english"] - Target font/script for subtitles.
9
+ * @returns {Promise<{ subtitles: Array<{t: string, s: number, e: number}> }>} Subtitles array with text, start time, and end time.
10
+ * @throws {Error} When audioUrl is missing or downstream calls fail.
11
+ */
12
+ export declare const transcribeVideoUrl: (params: {
13
+ videoUrl: string;
14
+ language?: string;
15
+ languageFont?: string;
16
+ }) => Promise<{
17
+ subtitles: any[];
18
+ duration: number;
19
+ videoUrl: string;
20
+ }>;
21
+ //# sourceMappingURL=transcriber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcriber.d.ts","sourceRoot":"","sources":["../transcriber.ts"],"names":[],"mappings":"AAsMA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB,GAAU,QAAQ;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE;;;;EAwG9G,CAAC"}
@@ -0,0 +1,281 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+ import fs from "fs";
3
+ import path, { join } from "path";
4
+ import { mkdtemp, readFile, rm } from "fs/promises";
5
+ import { tmpdir } from "os";
6
+ import { execFile } from "child_process";
7
+ import { promisify } from "util";
8
+ import { Readable, pipeline } from "stream";
9
+ import { fileURLToPath } from "node:url";
10
+ import { dirname } from "node:path";
11
+ // These packages provide prebuilt ffmpeg/ffprobe binaries. Types are not bundled,
12
+ // so we import them as `any` to keep TypeScript satisfied.
13
+ import ffmpeg from "@ffmpeg-installer/ffmpeg";
14
+ import ffprobe from "@ffprobe-installer/ffprobe";
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ // Get project root (go up from dist/ to project root)
18
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
19
+ const execFileAsync = promisify(execFile);
20
+ const pipelineAsync = promisify(pipeline);
21
+ const ffmpegPath = ffmpeg.path;
22
+ const ffprobePath = ffprobe.path;
23
+ /**
24
+ * Read a required environment variable, optionally falling back to a default.
25
+ * Throws if neither value is available, making configuration errors obvious.
26
+ *
27
+ * @param {string} name - Environment variable to read.
28
+ * @param {string | undefined} defaultValue - Optional fallback value.
29
+ * @returns {string} The resolved value.
30
+ * @throws {Error} If no value is found.
31
+ */
32
+ const ensureEnv = (name, defaultValue) => {
33
+ const value = process.env[name] ?? defaultValue;
34
+ if (!value) {
35
+ throw new Error(`Missing required environment variable: ${name}`);
36
+ }
37
+ return value;
38
+ };
39
+ const ensureCredentials = () => {
40
+ // Resolve gcp-sa-key.json relative to project root (not cwd, which may be Claude Desktop's directory)
41
+ // First try an explicit env var, then fall back to project root
42
+ const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS
43
+ || process.env.GCP_SA_KEY_PATH
44
+ || path.join(PROJECT_ROOT, "gcp-sa-key.json");
45
+ if (!fs.existsSync(credentialsPath)) {
46
+ throw new Error(`Credentials file not found at ${credentialsPath}. Set GCP_SA_KEY_PATH environment variable or place gcp-sa-key.json in the project root (${PROJECT_ROOT})`);
47
+ }
48
+ process.env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath;
49
+ };
50
+ /**
51
+ * Initialize a Google GenAI client configured for Vertex AI.
52
+ * Ensures credentials, project, and location are available before instantiating.
53
+ *
54
+ * @returns {Promise<GoogleGenAI>} Configured GenAI client instance.
55
+ * @throws {Error} When required environment variables are missing.
56
+ */
57
+ const createGenAIClient = async () => {
58
+ ensureCredentials();
59
+ const project = ensureEnv("GOOGLE_CLOUD_PROJECT");
60
+ const location = ensureEnv("GOOGLE_CLOUD_LOCATION", "global");
61
+ const client = new GoogleGenAI({
62
+ vertexai: true,
63
+ project: project,
64
+ location: location,
65
+ });
66
+ return client;
67
+ };
68
+ const extractAudioBufferFromVideo = async (videoUrl) => {
69
+ const videoResponse = await fetch(videoUrl);
70
+ if (!videoResponse.ok) {
71
+ throw new Error(`Failed to download video: ${videoResponse.status} ${videoResponse.statusText}`);
72
+ }
73
+ const tmpBase = await mkdtemp(join(tmpdir(), 'mcp-'));
74
+ const inputPath = join(tmpBase, 'input_video');
75
+ const outputPath = join(tmpBase, 'output_audio.mp3');
76
+ // Stream the video response directly to disk to avoid holding the full video in memory
77
+ if (!videoResponse.body) {
78
+ await rm(tmpBase, { recursive: true, force: true });
79
+ throw new Error("Video response has no body");
80
+ }
81
+ const videoStream = Readable.fromWeb(videoResponse.body);
82
+ const fileWriteStream = fs.createWriteStream(inputPath);
83
+ await pipelineAsync(videoStream, fileWriteStream);
84
+ // Get duration using bundled ffprobe
85
+ let duration = 0;
86
+ try {
87
+ const { stdout } = await execFileAsync(ffprobePath, [
88
+ '-v', 'error',
89
+ '-show_entries', 'format=duration',
90
+ '-of', 'default=noprint_wrappers=1:nokey=1',
91
+ inputPath
92
+ ]);
93
+ duration = parseFloat(stdout.toString().trim()) || 0;
94
+ }
95
+ catch (err) {
96
+ console.warn('Failed to get duration using ffprobe, duration will be 0');
97
+ }
98
+ try {
99
+ await execFileAsync(ffmpegPath, [
100
+ '-y',
101
+ '-i', inputPath,
102
+ '-vn',
103
+ '-acodec', 'libmp3lame',
104
+ '-q:a', '2',
105
+ outputPath
106
+ ]);
107
+ }
108
+ catch (err) {
109
+ await rm(tmpBase, { recursive: true, force: true });
110
+ const stderr = err?.stderr?.toString?.().trim?.() || "";
111
+ const msg = stderr || (err instanceof Error ? err.message : String(err));
112
+ throw new Error(`ffmpeg execution failed: ${msg}`);
113
+ }
114
+ const audioBuffer = await readFile(outputPath);
115
+ await rm(tmpBase, { recursive: true, force: true });
116
+ return { audioBuffer, duration };
117
+ };
118
+ /**
119
+ * Build the captioning prompt passed to the Gemini model.
120
+ *
121
+ * @param {string} language - Human-readable target language.
122
+ * @param {string} languageFont - Desired script/font name.
123
+ * @returns {string} Instruction prompt for the model.
124
+ */
125
+ const buildPrompt = (duration, language, languageFont) => {
126
+ const durationMs = Math.round(duration * 1000);
127
+ return `You are a professional subtitle and transcription engine.
128
+
129
+ ## INPUT
130
+ - Audio duration: ${durationMs} milliseconds
131
+ - Target language: ${language}
132
+ - Subtitle font script: ${languageFont}
133
+
134
+ ## OBJECTIVE
135
+ Transcribe the audio into clear, readable subtitles.
136
+
137
+ If the spoken audio is NOT in ${language}, translate it into ${language} before generating subtitles.
138
+
139
+ ## SUBTITLE SEGMENTATION RULES
140
+ - Split speech into short, natural phrases.
141
+ - Each subtitle phrase MUST contain a maximum of 4 words.
142
+ - Do NOT split words across phrases.
143
+ - Avoid breaking phrases mid-sentence unless required by timing constraints.
144
+
145
+ ## TIMING RULES (STRICT — MUST FOLLOW)
146
+ - All timestamps are in **milliseconds**.
147
+ - Each subtitle object MUST include:
148
+ - 's': start timestamp
149
+ - 'e': end timestamp
150
+ - Duration of each phrase = 'e - s'
151
+ - Minimum phrase duration: **100 ms**
152
+ - 'e' MUST be greater than 's'
153
+ - 'e' MUST be **less than or equal to ${durationMs}**
154
+ - Subtitles MUST be sequential:
155
+ - 's' of the next phrase MUST be **greater than or equal to** the previous 'e'
156
+ - NO overlapping timestamps
157
+ - Prefer aligning timestamps with natural speech pauses.
158
+
159
+ ## TEXT RULES
160
+ - 't' MUST be written using ${languageFont} characters.
161
+ - No emojis.
162
+ - No punctuation-only subtitles.
163
+ - Normalize casing according to the target language's writing system.
164
+ - Remove filler sounds (e.g., ā€œumā€, ā€œuhā€) unless semantically important.
165
+
166
+ ## OUTPUT FORMAT (CRITICAL)
167
+ Return ONLY a valid JSON array.
168
+ - No markdown
169
+ - No code blocks
170
+ - No explanations
171
+ - No additional text
172
+ - Output MUST start with '[' and end with ']'
173
+
174
+ ## OUTPUT SCHEMA
175
+ [
176
+ {
177
+ "t": "Subtitle text",
178
+ "s": 0,
179
+ "e": 1200
180
+ }
181
+ ]
182
+ `.trim();
183
+ };
184
+ /**
185
+ * Transcribe an audio URL to JSON subtitles using Google GenAI (Vertex AI),
186
+ * mirroring the Python implementation in `playground/vertex/transcript.py`.
187
+ *
188
+ * @param {Object} params
189
+ * @param {string} params.videoUrl - Publicly reachable video URL.
190
+ * @param {string} [params.language="english"] - Target transcription language (human-readable).
191
+ * @param {string} [params.languageFont="english"] - Target font/script for subtitles.
192
+ * @returns {Promise<{ subtitles: Array<{t: string, s: number, e: number}> }>} Subtitles array with text, start time, and end time.
193
+ * @throws {Error} When audioUrl is missing or downstream calls fail.
194
+ */
195
+ export const transcribeVideoUrl = async (params) => {
196
+ const { videoUrl, language = "english", languageFont = "english", } = params || {};
197
+ if (!videoUrl) {
198
+ throw new Error("Missing required parameter: videoUrl");
199
+ }
200
+ const { audioBuffer, duration } = await extractAudioBufferFromVideo(videoUrl);
201
+ if (!duration) {
202
+ throw new Error("Failed to get duration of video");
203
+ }
204
+ const prompt = buildPrompt(duration, language, languageFont);
205
+ const client = await createGenAIClient();
206
+ const modelName = process.env.GOOGLE_VERTEX_MODEL || "gemini-2.5-flash-lite";
207
+ const generationConfig = {
208
+ maxOutputTokens: 65535,
209
+ temperature: 1,
210
+ topP: 0.95,
211
+ thinkingConfig: {
212
+ thinkingBudget: 0,
213
+ },
214
+ safetySettings: [
215
+ {
216
+ category: "HARM_CATEGORY_HATE_SPEECH",
217
+ threshold: "OFF",
218
+ },
219
+ {
220
+ category: "HARM_CATEGORY_DANGEROUS_CONTENT",
221
+ threshold: "OFF",
222
+ },
223
+ {
224
+ category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
225
+ threshold: "OFF",
226
+ },
227
+ {
228
+ category: "HARM_CATEGORY_HARASSMENT",
229
+ threshold: "OFF",
230
+ },
231
+ ],
232
+ };
233
+ const req = {
234
+ model: modelName,
235
+ contents: [
236
+ {
237
+ role: "user",
238
+ parts: [
239
+ {
240
+ inlineData: {
241
+ data: audioBuffer.toString("base64"),
242
+ mimeType: "audio/mpeg",
243
+ },
244
+ },
245
+ { text: prompt },
246
+ ],
247
+ },
248
+ ],
249
+ config: generationConfig,
250
+ };
251
+ //@ts-ignore
252
+ const response = await client.models.generateContent(req);
253
+ let textPart = response.text || "";
254
+ // Strip markdown code fences if present (```json ... ``` or ``` ... ```)
255
+ textPart = textPart
256
+ .replace(/^```json\s*/i, "") // Remove opening ```json
257
+ .replace(/^```\s*/i, "") // Remove opening ```
258
+ .replace(/\s*```$/i, "") // Remove closing ```
259
+ .trim();
260
+ let subtitles = [];
261
+ try {
262
+ // Try to find JSON array in the text (in case there's extra text)
263
+ const jsonMatch = textPart.match(/\[[\s\S]*\]/);
264
+ const jsonText = jsonMatch ? jsonMatch[0] : textPart;
265
+ subtitles = JSON.parse(jsonText);
266
+ if (!Array.isArray(subtitles)) {
267
+ throw new Error("Parsed subtitles are not an array");
268
+ }
269
+ }
270
+ catch (err) {
271
+ console.warn("Failed to parse model output as JSON subtitles, returning raw text", err);
272
+ console.warn("Raw response text:", textPart.substring(0, 500));
273
+ subtitles = [];
274
+ }
275
+ return {
276
+ subtitles,
277
+ duration,
278
+ videoUrl
279
+ };
280
+ };
281
+ //# sourceMappingURL=transcriber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcriber.js","sourceRoot":"","sources":["../transcriber.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ export declare const uploadDataToFile: (data: any) => Promise<unknown>;
2
+ export declare const processSubtitles: (project: any) => Promise<unknown>;
3
+ export declare const processSubtitlesToProject: ({ subtitles, videoUrl, duration, videoSize, }: {
4
+ subtitles: any;
5
+ videoUrl: string;
6
+ duration: number;
7
+ videoSize: {
8
+ width: number;
9
+ height: number;
10
+ };
11
+ }) => Promise<{
12
+ properties: {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ tracks: ({
17
+ id: string;
18
+ type: string;
19
+ elements: {
20
+ id: string;
21
+ type: string;
22
+ s: number;
23
+ e: number;
24
+ props: {
25
+ src: string;
26
+ width: number;
27
+ height: number;
28
+ };
29
+ }[];
30
+ props?: never;
31
+ } | {
32
+ id: string;
33
+ type: string;
34
+ props: {
35
+ capStyle: string;
36
+ font: {
37
+ size: number;
38
+ weight: number;
39
+ family: string;
40
+ };
41
+ colors: {
42
+ text: string;
43
+ highlight: string;
44
+ bgColor: string;
45
+ };
46
+ lineWidth: number;
47
+ stroke: string;
48
+ fontWeight: number;
49
+ shadowOffset: number[];
50
+ shadowColor: string;
51
+ x: number;
52
+ y: number;
53
+ applyToAll: boolean;
54
+ };
55
+ elements: any;
56
+ })[];
57
+ version: number;
58
+ }>;
59
+ export declare const getTwickStudioLink: (uploadResult: any) => string | undefined;
60
+ export declare const returnProjectAsTwickStudioLink: (project: any) => Promise<{
61
+ content: {
62
+ type: string;
63
+ text: string;
64
+ }[];
65
+ } | {
66
+ content: {
67
+ type: string;
68
+ text: string;
69
+ }[];
70
+ structuredContent: {
71
+ success: boolean;
72
+ result: string;
73
+ };
74
+ }>;
75
+ export declare const returnProjectAsFile: (project: any) => {
76
+ content: {
77
+ type: string;
78
+ text: string;
79
+ }[];
80
+ };
81
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAU,MAAM,GAAG,qBAoB/C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAU,SAAS,GAAG,qBAclD,CAAC;AAEF,eAAO,MAAM,yBAAyB,GAAU,+CAK7C;IACD,SAAS,EAAE,GAAG,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2DA,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,cAAc,GAAG,uBAanD,CAAC;AAEF,eAAO,MAAM,8BAA8B,GAAU,SAAS,GAAG;;;;;;;;;;;;;;EAYhE,CAAC;AAEF,eAAO,MAAM,mBAAmB,GAAI,SAAS,GAAG;;;;;CAuB/C,CAAC"}
package/dist/utils.js ADDED
@@ -0,0 +1,142 @@
1
+ export const uploadDataToFile = async (data) => {
2
+ const formData = new FormData();
3
+ const blob = new Blob([JSON.stringify(data, null, 2)], {
4
+ type: "application/json",
5
+ });
6
+ formData.append("file", blob, "data.json");
7
+ const res = await fetch(process.env.UPLOAD_API_URL ?? "", {
8
+ method: "POST",
9
+ body: formData,
10
+ });
11
+ if (!res.ok) {
12
+ throw new Error(`Failed to upload data to file: ${res.status} ${res.statusText}`);
13
+ }
14
+ return res.json();
15
+ };
16
+ export const processSubtitles = async (project) => {
17
+ try {
18
+ const uploadResult = await uploadDataToFile(project);
19
+ // Use stderr for logs so stdio JSON-RPC traffic on stdout is not corrupted.
20
+ console.error("Upload Result", uploadResult);
21
+ return uploadResult;
22
+ }
23
+ catch (err) {
24
+ console.error("Error processing subtitles", err);
25
+ return {
26
+ success: false,
27
+ error: "Error uploading project to file",
28
+ project: project,
29
+ };
30
+ }
31
+ };
32
+ export const processSubtitlesToProject = async ({ subtitles, videoUrl, duration, videoSize, }) => {
33
+ return {
34
+ properties: {
35
+ width: videoSize.width || 720,
36
+ height: videoSize.height || 1280,
37
+ },
38
+ tracks: [
39
+ {
40
+ id: "video",
41
+ type: "video",
42
+ elements: [
43
+ {
44
+ id: "video",
45
+ type: "video",
46
+ s: 0,
47
+ e: duration,
48
+ props: {
49
+ src: videoUrl,
50
+ width: videoSize.width || 720,
51
+ height: videoSize.height || 1280,
52
+ },
53
+ },
54
+ ],
55
+ },
56
+ {
57
+ id: "subtitle",
58
+ type: "caption",
59
+ props: {
60
+ capStyle: "highlight_bg",
61
+ font: {
62
+ size: 50,
63
+ weight: 700,
64
+ family: "Bangers",
65
+ },
66
+ colors: {
67
+ text: "#ffffff",
68
+ highlight: "#ff4081",
69
+ bgColor: "#444444",
70
+ },
71
+ lineWidth: 0.35,
72
+ stroke: "#000000",
73
+ fontWeight: 700,
74
+ shadowOffset: [-3, 3],
75
+ shadowColor: "#000000",
76
+ x: 0,
77
+ y: 200,
78
+ applyToAll: true,
79
+ },
80
+ elements: subtitles.map((subtitle, index) => ({
81
+ id: `subtitle-${index}`,
82
+ type: "caption",
83
+ s: subtitle.s / 1000,
84
+ e: subtitle.e / 1000,
85
+ t: subtitle.t,
86
+ })),
87
+ },
88
+ ],
89
+ version: 1,
90
+ };
91
+ };
92
+ export const getTwickStudioLink = (uploadResult) => {
93
+ if (!uploadResult?.success)
94
+ return;
95
+ const fileUrl = uploadResult.result?.[0]?.url;
96
+ if (!fileUrl)
97
+ return;
98
+ const template = process.env.TWICK_STUDIO_URL;
99
+ if (!template)
100
+ return;
101
+ // If TWICK_STUDIO_URL contains a $project placeholder, substitute it
102
+ if (template.includes("$project")) {
103
+ return template.replace("$project", encodeURIComponent(fileUrl));
104
+ }
105
+ };
106
+ export const returnProjectAsTwickStudioLink = async (project) => {
107
+ const uploadResult = await uploadDataToFile(project);
108
+ const twickStudioLink = getTwickStudioLink(uploadResult);
109
+ if (twickStudioLink) {
110
+ return {
111
+ content: [
112
+ { type: "text", text: `Open link in browser: ${twickStudioLink}` },
113
+ ],
114
+ structuredContent: { success: true, result: twickStudioLink },
115
+ };
116
+ }
117
+ return returnProjectAsFile(project);
118
+ };
119
+ export const returnProjectAsFile = (project) => {
120
+ const data = Buffer.from(JSON.stringify(project, null, 2)).toString("base64");
121
+ const result = {
122
+ file: {
123
+ name: "project.json",
124
+ content: data,
125
+ encoding: "base64",
126
+ mimeType: "application/json",
127
+ },
128
+ metadata: {
129
+ size: data.length,
130
+ created: new Date().toISOString(),
131
+ },
132
+ };
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: JSON.stringify(result, null, 2),
138
+ },
139
+ ],
140
+ };
141
+ };
142
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../utils.ts"],"names":[],"mappings":"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"}
package/install.js ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Installation script for twick-mcp-agent Claude Desktop extension
5
+ * This script:
6
+ * 1. Installs npm dependencies
7
+ * 2. Builds the TypeScript project
8
+ * 3. Merges the MCP server config into Claude Desktop's config file
9
+ */
10
+
11
+ import { execSync } from "child_process";
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
13
+ import { join, dirname, resolve } from "path";
14
+ import { fileURLToPath } from "url";
15
+ import { homedir } from "os";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const PROJECT_ROOT = resolve(__dirname);
20
+
21
+ // Platform-specific Claude Desktop config paths
22
+ function getClaudeConfigPath() {
23
+ const platform = process.platform;
24
+ if (platform === "win32") {
25
+ // Windows: %APPDATA%\Claude\claude_desktop_config.json
26
+ return join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
27
+ } else if (platform === "darwin") {
28
+ // macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
29
+ return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
30
+ } else {
31
+ // Linux: ~/.config/Claude/claude_desktop_config.json
32
+ return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
33
+ }
34
+ }
35
+
36
+ function mergeConfig(existingConfig, newServerConfig) {
37
+ const config = existingConfig || { mcpServers: {} };
38
+
39
+ if (!config.mcpServers) {
40
+ config.mcpServers = {};
41
+ }
42
+
43
+ // Merge the new server config
44
+ Object.assign(config.mcpServers, newServerConfig.mcpServers);
45
+
46
+ return config;
47
+ }
48
+
49
+ function main() {
50
+ console.log("šŸš€ Installing twick-mcp-agent for Claude Desktop...\n");
51
+
52
+ try {
53
+ // Step 1: Install dependencies
54
+ console.log("šŸ“¦ Installing npm dependencies...");
55
+ execSync("npm install", { cwd: PROJECT_ROOT, stdio: "inherit" });
56
+ console.log("āœ… Dependencies installed\n");
57
+
58
+ // Step 2: Build the project
59
+ console.log("šŸ”Ø Building TypeScript project...");
60
+ execSync("npm run build", { cwd: PROJECT_ROOT, stdio: "inherit" });
61
+ console.log("āœ… Build completed\n");
62
+
63
+ // Step 3: Read the MCP server config template
64
+ const configTemplatePath = join(PROJECT_ROOT, "claude_desktop_config.json");
65
+ if (!existsSync(configTemplatePath)) {
66
+ throw new Error(`Config template not found: ${configTemplatePath}`);
67
+ }
68
+
69
+ const configTemplate = JSON.parse(
70
+ readFileSync(configTemplatePath, "utf-8")
71
+ );
72
+
73
+ // Step 4: Update paths in config to be absolute
74
+ const serverConfig = { ...configTemplate };
75
+ const serverName = Object.keys(serverConfig.mcpServers)[0];
76
+ const serverEntry = serverConfig.mcpServers[serverName];
77
+
78
+ // Make the args path absolute
79
+ if (serverEntry.args && serverEntry.args[0]) {
80
+ serverEntry.args[0] = join(PROJECT_ROOT, "dist", "stdio-server.js");
81
+ }
82
+
83
+ // Make GOOGLE_APPLICATION_CREDENTIALS path absolute if it's a relative path
84
+ if (serverEntry.env?.GOOGLE_APPLICATION_CREDENTIALS) {
85
+ const credsPath = serverEntry.env.GOOGLE_APPLICATION_CREDENTIALS;
86
+ if (!credsPath.startsWith("/") && !credsPath.match(/^[A-Z]:/)) {
87
+ // Relative path, make it absolute
88
+ serverEntry.env.GOOGLE_APPLICATION_CREDENTIALS = join(
89
+ PROJECT_ROOT,
90
+ credsPath
91
+ );
92
+ }
93
+ }
94
+
95
+ // Step 5: Merge into Claude Desktop config
96
+ const claudeConfigPath = getClaudeConfigPath();
97
+ console.log(`šŸ“ Updating Claude Desktop config: ${claudeConfigPath}`);
98
+
99
+ let existingConfig = null;
100
+ if (existsSync(claudeConfigPath)) {
101
+ try {
102
+ existingConfig = JSON.parse(readFileSync(claudeConfigPath, "utf-8"));
103
+ console.log("āœ… Found existing Claude Desktop config");
104
+ } catch (err) {
105
+ console.warn(`āš ļø Could not parse existing config: ${err.message}`);
106
+ console.log("šŸ“ Creating new config file");
107
+ }
108
+ } else {
109
+ console.log("šŸ“ Creating new Claude Desktop config file");
110
+ // Ensure directory exists
111
+ mkdirSync(dirname(claudeConfigPath), { recursive: true });
112
+ }
113
+
114
+ const mergedConfig = mergeConfig(existingConfig, serverConfig);
115
+ writeFileSync(
116
+ claudeConfigPath,
117
+ JSON.stringify(mergedConfig, null, 2) + "\n",
118
+ "utf-8"
119
+ );
120
+
121
+ console.log("āœ… Configuration merged successfully\n");
122
+
123
+ // Step 6: Summary
124
+ console.log("šŸŽ‰ Installation complete!\n");
125
+ console.log("šŸ“‹ Next steps:");
126
+ console.log(" 1. Edit your Claude Desktop config to set your environment variables:");
127
+ console.log(` ${claudeConfigPath}`);
128
+ console.log(" 2. Update the following in the 'twick-mcp-agent' section:");
129
+ console.log(" - GOOGLE_CLOUD_PROJECT: Your GCP project ID");
130
+ console.log(" - GOOGLE_APPLICATION_CREDENTIALS: Path to your GCP service account key");
131
+ console.log(" - UPLOAD_API_URL: Your upload API endpoint (optional)");
132
+ console.log(" - TWICK_STUDIO_URL: Your Twick Studio URL (optional)");
133
+ console.log(" 3. Restart Claude Desktop to load the new MCP server");
134
+ console.log("\n✨ The 'generate-subtitles' tool will be available in Claude Desktop!\n");
135
+ } catch (error) {
136
+ console.error("\nāŒ Installation failed:");
137
+ console.error(error.message);
138
+ if (error.stdout) console.error(error.stdout.toString());
139
+ if (error.stderr) console.error(error.stderr.toString());
140
+ process.exit(1);
141
+ }
142
+ }
143
+
144
+ main();
145
+
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@twick/mcp-agent",
3
+ "version": "0.14.15",
4
+ "description": "MCP server for Claude Desktop that generates video captions using Google Vertex AI and integrates with Twick Studio",
5
+ "main": "dist/stdio-server.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "twick-mcp-agent": "./dist/stdio-server.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*",
12
+ "claude_desktop_config.json",
13
+ "README.md",
14
+ "install.js"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "start": "node dist/stdio-server.js",
22
+ "dev": "tsc && node dist/stdio-server.js",
23
+ "install-claude": "node install.js",
24
+ "prepublishOnly": "pnpm run build",
25
+ "test": "echo \"Error: no test specified\" && exit 1"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "claude-desktop",
31
+ "video-transcription",
32
+ "captions",
33
+ "subtitle",
34
+ "google-vertex-ai",
35
+ "gemini",
36
+ "twick-studio"
37
+ ],
38
+ "author": "",
39
+ "license": "SEE LICENSE IN LICENSE.md",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": ""
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "dependencies": {
48
+ "@modelcontextprotocol/sdk": "^1.20.0",
49
+ "@google/genai": "^1.0.0",
50
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
51
+ "@ffprobe-installer/ffprobe": "^2.0.0",
52
+ "dotenv": "^16.4.5",
53
+ "express": "^5.1.0",
54
+ "zod": "^3.25.76"
55
+ },
56
+ "devDependencies": {
57
+ "@types/express": "^5.0.0",
58
+ "@types/node": "^24.7.2",
59
+ "typescript": "^5.9.3"
60
+ }
61
+ }