@studiomeyer/mcp-video 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/.github/ISSUE_TEMPLATE/bug_report.md +31 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/workflows/ci.yml +34 -0
- package/CHANGELOG.md +24 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/USAGE.md +144 -0
- package/dist/handlers/capcut.d.ts +6 -0
- package/dist/handlers/capcut.js +229 -0
- package/dist/handlers/capcut.js.map +1 -0
- package/dist/handlers/editing.d.ts +6 -0
- package/dist/handlers/editing.js +242 -0
- package/dist/handlers/editing.js.map +1 -0
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +33 -0
- package/dist/handlers/index.js.map +1 -0
- package/dist/handlers/post-production.d.ts +5 -0
- package/dist/handlers/post-production.js +109 -0
- package/dist/handlers/post-production.js.map +1 -0
- package/dist/handlers/smart-screenshot.d.ts +5 -0
- package/dist/handlers/smart-screenshot.js +83 -0
- package/dist/handlers/smart-screenshot.js.map +1 -0
- package/dist/handlers/tts.d.ts +5 -0
- package/dist/handlers/tts.js +83 -0
- package/dist/handlers/tts.js.map +1 -0
- package/dist/handlers/video.d.ts +5 -0
- package/dist/handlers/video.js +127 -0
- package/dist/handlers/video.js.map +1 -0
- package/dist/lib/dual-transport.d.ts +42 -0
- package/dist/lib/dual-transport.js +208 -0
- package/dist/lib/dual-transport.js.map +1 -0
- package/dist/lib/logger.d.ts +12 -0
- package/dist/lib/logger.js +42 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/types.js +15 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/schemas/capcut.d.ts +608 -0
- package/dist/schemas/capcut.js +411 -0
- package/dist/schemas/capcut.js.map +1 -0
- package/dist/schemas/editing.d.ts +822 -0
- package/dist/schemas/editing.js +466 -0
- package/dist/schemas/editing.js.map +1 -0
- package/dist/schemas/index.d.ts +2366 -0
- package/dist/schemas/index.js +15 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/post-production.d.ts +379 -0
- package/dist/schemas/post-production.js +268 -0
- package/dist/schemas/post-production.js.map +1 -0
- package/dist/schemas/smart-screenshot.d.ts +127 -0
- package/dist/schemas/smart-screenshot.js +122 -0
- package/dist/schemas/smart-screenshot.js.map +1 -0
- package/dist/schemas/tts.d.ts +220 -0
- package/dist/schemas/tts.js +194 -0
- package/dist/schemas/tts.js.map +1 -0
- package/dist/schemas/video.d.ts +236 -0
- package/dist/schemas/video.js +210 -0
- package/dist/schemas/video.js.map +1 -0
- package/dist/server.d.ts +11 -0
- package/dist/server.js +239 -0
- package/dist/server.js.map +1 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +87 -0
- package/dist/server.test.js.map +1 -0
- package/dist/tools/engine/audio-mixer.d.ts +40 -0
- package/dist/tools/engine/audio-mixer.js +169 -0
- package/dist/tools/engine/audio-mixer.js.map +1 -0
- package/dist/tools/engine/audio.d.ts +22 -0
- package/dist/tools/engine/audio.js +73 -0
- package/dist/tools/engine/audio.js.map +1 -0
- package/dist/tools/engine/beat-sync.d.ts +31 -0
- package/dist/tools/engine/beat-sync.js +270 -0
- package/dist/tools/engine/beat-sync.js.map +1 -0
- package/dist/tools/engine/capture.d.ts +12 -0
- package/dist/tools/engine/capture.js +290 -0
- package/dist/tools/engine/capture.js.map +1 -0
- package/dist/tools/engine/chroma-key.d.ts +27 -0
- package/dist/tools/engine/chroma-key.js +154 -0
- package/dist/tools/engine/chroma-key.js.map +1 -0
- package/dist/tools/engine/concat.d.ts +49 -0
- package/dist/tools/engine/concat.js +149 -0
- package/dist/tools/engine/concat.js.map +1 -0
- package/dist/tools/engine/cursor.d.ts +26 -0
- package/dist/tools/engine/cursor.js +185 -0
- package/dist/tools/engine/cursor.js.map +1 -0
- package/dist/tools/engine/easing.d.ts +15 -0
- package/dist/tools/engine/easing.js +100 -0
- package/dist/tools/engine/easing.js.map +1 -0
- package/dist/tools/engine/editing.d.ts +158 -0
- package/dist/tools/engine/editing.js +541 -0
- package/dist/tools/engine/editing.js.map +1 -0
- package/dist/tools/engine/encoder.d.ts +31 -0
- package/dist/tools/engine/encoder.js +154 -0
- package/dist/tools/engine/encoder.js.map +1 -0
- package/dist/tools/engine/index.d.ts +30 -0
- package/dist/tools/engine/index.js +23 -0
- package/dist/tools/engine/index.js.map +1 -0
- package/dist/tools/engine/lut-presets.d.ts +25 -0
- package/dist/tools/engine/lut-presets.js +141 -0
- package/dist/tools/engine/lut-presets.js.map +1 -0
- package/dist/tools/engine/narrated-video.d.ts +63 -0
- package/dist/tools/engine/narrated-video.js +163 -0
- package/dist/tools/engine/narrated-video.js.map +1 -0
- package/dist/tools/engine/scenes.d.ts +17 -0
- package/dist/tools/engine/scenes.js +223 -0
- package/dist/tools/engine/scenes.js.map +1 -0
- package/dist/tools/engine/smart-screenshot.d.ts +80 -0
- package/dist/tools/engine/smart-screenshot.js +744 -0
- package/dist/tools/engine/smart-screenshot.js.map +1 -0
- package/dist/tools/engine/social-format.d.ts +66 -0
- package/dist/tools/engine/social-format.js +107 -0
- package/dist/tools/engine/social-format.js.map +1 -0
- package/dist/tools/engine/template-renderer.d.ts +45 -0
- package/dist/tools/engine/template-renderer.js +233 -0
- package/dist/tools/engine/template-renderer.js.map +1 -0
- package/dist/tools/engine/templates.d.ts +87 -0
- package/dist/tools/engine/templates.js +272 -0
- package/dist/tools/engine/templates.js.map +1 -0
- package/dist/tools/engine/text-animations.d.ts +33 -0
- package/dist/tools/engine/text-animations.js +192 -0
- package/dist/tools/engine/text-animations.js.map +1 -0
- package/dist/tools/engine/text-overlay.d.ts +27 -0
- package/dist/tools/engine/text-overlay.js +84 -0
- package/dist/tools/engine/text-overlay.js.map +1 -0
- package/dist/tools/engine/tts.d.ts +54 -0
- package/dist/tools/engine/tts.js +186 -0
- package/dist/tools/engine/tts.js.map +1 -0
- package/dist/tools/engine/types.d.ts +166 -0
- package/dist/tools/engine/types.js +13 -0
- package/dist/tools/engine/types.js.map +1 -0
- package/dist/tools/engine/voice-effects.d.ts +18 -0
- package/dist/tools/engine/voice-effects.js +215 -0
- package/dist/tools/engine/voice-effects.js.map +1 -0
- package/dist/tools/index.d.ts +32 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/package.json +56 -0
- package/scripts/check-deps.js +39 -0
- package/src/handlers/capcut.ts +245 -0
- package/src/handlers/editing.ts +260 -0
- package/src/handlers/index.ts +34 -0
- package/src/handlers/post-production.ts +136 -0
- package/src/handlers/smart-screenshot.ts +86 -0
- package/src/handlers/tts.ts +103 -0
- package/src/handlers/video.ts +137 -0
- package/src/lib/dual-transport.ts +272 -0
- package/src/lib/logger.ts +59 -0
- package/src/lib/types.ts +25 -0
- package/src/schemas/capcut.ts +418 -0
- package/src/schemas/editing.ts +476 -0
- package/src/schemas/index.ts +15 -0
- package/src/schemas/post-production.ts +273 -0
- package/src/schemas/smart-screenshot.ts +122 -0
- package/src/schemas/tts.ts +197 -0
- package/src/schemas/video.ts +211 -0
- package/src/server.test.ts +99 -0
- package/src/server.ts +289 -0
- package/src/tools/engine/audio-mixer.ts +244 -0
- package/src/tools/engine/audio.ts +115 -0
- package/src/tools/engine/beat-sync.ts +356 -0
- package/src/tools/engine/capture.ts +360 -0
- package/src/tools/engine/chroma-key.ts +202 -0
- package/src/tools/engine/concat.ts +242 -0
- package/src/tools/engine/cursor.ts +222 -0
- package/src/tools/engine/easing.ts +120 -0
- package/src/tools/engine/editing.ts +809 -0
- package/src/tools/engine/encoder.ts +208 -0
- package/src/tools/engine/index.ts +33 -0
- package/src/tools/engine/lut-presets.ts +235 -0
- package/src/tools/engine/narrated-video.ts +267 -0
- package/src/tools/engine/scenes.ts +309 -0
- package/src/tools/engine/smart-screenshot.ts +923 -0
- package/src/tools/engine/social-format.ts +146 -0
- package/src/tools/engine/template-renderer.ts +294 -0
- package/src/tools/engine/templates.ts +370 -0
- package/src/tools/engine/text-animations.ts +282 -0
- package/src/tools/engine/text-overlay.ts +143 -0
- package/src/tools/engine/tts.ts +284 -0
- package/src/tools/engine/types.ts +191 -0
- package/src/tools/engine/voice-effects.ts +258 -0
- package/src/tools/index.ts +67 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Schemas for Cinema Video Engine
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const sceneSchema = {
|
|
6
|
+
type: 'object' as const,
|
|
7
|
+
properties: {
|
|
8
|
+
type: {
|
|
9
|
+
type: 'string',
|
|
10
|
+
enum: ['scroll', 'pause', 'hover', 'click', 'type', 'wait'],
|
|
11
|
+
description: 'Scene type',
|
|
12
|
+
},
|
|
13
|
+
to: {
|
|
14
|
+
type: ['string', 'number'],
|
|
15
|
+
description: '[scroll] Target: "bottom", "top", pixel number, or CSS selector',
|
|
16
|
+
},
|
|
17
|
+
duration: {
|
|
18
|
+
type: 'number',
|
|
19
|
+
description: '[scroll/pause/hover] Duration in seconds',
|
|
20
|
+
},
|
|
21
|
+
easing: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: [
|
|
24
|
+
'linear', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
|
|
25
|
+
'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
|
|
26
|
+
'easeInQuart', 'easeOutQuart', 'easeInOutQuart',
|
|
27
|
+
'easeInQuint', 'easeOutQuint', 'easeInOutQuint',
|
|
28
|
+
'easeInOutSine', 'cinematic', 'showcase',
|
|
29
|
+
],
|
|
30
|
+
description: '[scroll] Easing curve (default: easeInOutCubic). "showcase" = dramatic slow start/end, "cinematic" = cruise-style',
|
|
31
|
+
},
|
|
32
|
+
selector: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: '[hover/click/type/wait] CSS selector',
|
|
35
|
+
},
|
|
36
|
+
text: {
|
|
37
|
+
type: 'string',
|
|
38
|
+
description: '[type] Text to type',
|
|
39
|
+
},
|
|
40
|
+
delay: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description: '[type] Delay between keystrokes in ms (default: 80)',
|
|
43
|
+
},
|
|
44
|
+
waitFor: {
|
|
45
|
+
type: ['string', 'number'],
|
|
46
|
+
description: '[click] Wait strategy: "networkidle", "load", or milliseconds',
|
|
47
|
+
},
|
|
48
|
+
pauseAfter: {
|
|
49
|
+
type: 'number',
|
|
50
|
+
description: '[click] Pause after click in seconds (default: 1)',
|
|
51
|
+
},
|
|
52
|
+
animateCursor: {
|
|
53
|
+
type: 'boolean',
|
|
54
|
+
description: '[hover] Animate cursor movement (default: true)',
|
|
55
|
+
},
|
|
56
|
+
timeout: {
|
|
57
|
+
type: 'number',
|
|
58
|
+
description: '[wait] Max wait time in ms (default: 5000)',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
required: ['type'],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const videoSchemas = [
|
|
65
|
+
{
|
|
66
|
+
name: 'record_website_video',
|
|
67
|
+
description: `Record a cinema-quality video of a website with buttery-smooth 60fps scrolling. Uses frame-by-frame capture (not real-time screen recording) for perfect quality with zero frame drops. Supports custom scenes (scroll, hover, click, pause), multiple viewports (desktop/mobile/4K), cinematic easing curves, and visible cursor simulation. Output: MP4 + thumbnail PNG. Output directory: ./output/ (configurable via VIDEO_OUTPUT_DIR env var).`,
|
|
68
|
+
annotations: {
|
|
69
|
+
title: 'Record Website Video',
|
|
70
|
+
readOnlyHint: false,
|
|
71
|
+
destructiveHint: false,
|
|
72
|
+
openWorldHint: true,
|
|
73
|
+
idempotentHint: true,
|
|
74
|
+
},
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object' as const,
|
|
77
|
+
properties: {
|
|
78
|
+
url: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'URL of the website to record',
|
|
81
|
+
},
|
|
82
|
+
outputPath: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
description: 'Output file path without extension (default: ./output/website-video-{timestamp})',
|
|
85
|
+
},
|
|
86
|
+
viewport: {
|
|
87
|
+
type: 'string',
|
|
88
|
+
enum: ['desktop', 'desktop-4k', 'tablet', 'tablet-landscape', 'mobile', 'mobile-landscape'],
|
|
89
|
+
description: 'Viewport preset (default: desktop = 1920x1080)',
|
|
90
|
+
},
|
|
91
|
+
fps: {
|
|
92
|
+
type: 'number',
|
|
93
|
+
description: 'Frames per second (default: 60). Use 30 for smaller files.',
|
|
94
|
+
},
|
|
95
|
+
scenes: {
|
|
96
|
+
type: 'array',
|
|
97
|
+
items: sceneSchema,
|
|
98
|
+
description: 'Array of scene definitions. If empty, does a default full-page scroll with pause at top/bottom.',
|
|
99
|
+
},
|
|
100
|
+
cursor: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
enabled: { type: 'boolean', description: 'Show visible cursor (default: true)' },
|
|
104
|
+
style: { type: 'string', enum: ['dot', 'arrow', 'pointer'], description: 'Cursor style (default: dot)' },
|
|
105
|
+
color: { type: 'string', description: 'Cursor color (default: rgba(255,255,255,0.9))' },
|
|
106
|
+
size: { type: 'number', description: 'Cursor size in px (default: 20)' },
|
|
107
|
+
},
|
|
108
|
+
description: 'Cursor configuration',
|
|
109
|
+
},
|
|
110
|
+
codec: {
|
|
111
|
+
type: 'string',
|
|
112
|
+
enum: ['h264', 'h265', 'vp9'],
|
|
113
|
+
description: 'Video codec (default: h264). h265 = smaller files, vp9 = WebM format.',
|
|
114
|
+
},
|
|
115
|
+
quality: {
|
|
116
|
+
type: 'number',
|
|
117
|
+
description: 'CRF quality (0=lossless, 51=worst, default: 18). Lower = better quality, bigger file.',
|
|
118
|
+
},
|
|
119
|
+
darkMode: {
|
|
120
|
+
type: 'boolean',
|
|
121
|
+
description: 'Enable dark mode (default: false)',
|
|
122
|
+
},
|
|
123
|
+
preloadContent: {
|
|
124
|
+
type: 'boolean',
|
|
125
|
+
description: 'Pre-scroll to trigger lazy loading (default: true)',
|
|
126
|
+
},
|
|
127
|
+
dismissOverlays: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
description: 'Auto-dismiss cookie banners and popups (default: true)',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
required: ['url'],
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'record_website_scroll',
|
|
137
|
+
description: 'Quick scroll-through video of a website. Simplified version of record_website_video — just provide URL and get a smooth 60fps scroll video. Perfect for quick portfolio showcases.',
|
|
138
|
+
annotations: {
|
|
139
|
+
title: 'Record Website Scroll',
|
|
140
|
+
readOnlyHint: false,
|
|
141
|
+
destructiveHint: false,
|
|
142
|
+
openWorldHint: true,
|
|
143
|
+
idempotentHint: true,
|
|
144
|
+
},
|
|
145
|
+
inputSchema: {
|
|
146
|
+
type: 'object' as const,
|
|
147
|
+
properties: {
|
|
148
|
+
url: {
|
|
149
|
+
type: 'string',
|
|
150
|
+
description: 'URL of the website to record',
|
|
151
|
+
},
|
|
152
|
+
duration: {
|
|
153
|
+
type: 'number',
|
|
154
|
+
description: 'Scroll duration in seconds (default: 12)',
|
|
155
|
+
},
|
|
156
|
+
viewport: {
|
|
157
|
+
type: 'string',
|
|
158
|
+
enum: ['desktop', 'desktop-4k', 'tablet', 'mobile'],
|
|
159
|
+
description: 'Viewport preset (default: desktop)',
|
|
160
|
+
},
|
|
161
|
+
easing: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
enum: ['easeInOutCubic', 'easeInOutQuint', 'easeInOutSine', 'cinematic', 'showcase'],
|
|
164
|
+
description: 'Scroll easing curve (default: showcase)',
|
|
165
|
+
},
|
|
166
|
+
outputPath: {
|
|
167
|
+
type: 'string',
|
|
168
|
+
description: 'Output file path (default: ./output/scroll-{domain}-{timestamp})',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
required: ['url'],
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'record_multi_device',
|
|
176
|
+
description: 'Record the same website in multiple viewports (desktop + tablet + mobile) in one go. Creates separate video files for each device. Great for responsive design showcases.',
|
|
177
|
+
annotations: {
|
|
178
|
+
title: 'Record Multi-Device Video',
|
|
179
|
+
readOnlyHint: false,
|
|
180
|
+
destructiveHint: false,
|
|
181
|
+
openWorldHint: true,
|
|
182
|
+
idempotentHint: true,
|
|
183
|
+
},
|
|
184
|
+
inputSchema: {
|
|
185
|
+
type: 'object' as const,
|
|
186
|
+
properties: {
|
|
187
|
+
url: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'URL of the website to record',
|
|
190
|
+
},
|
|
191
|
+
devices: {
|
|
192
|
+
type: 'array',
|
|
193
|
+
items: {
|
|
194
|
+
type: 'string',
|
|
195
|
+
enum: ['desktop', 'desktop-4k', 'tablet', 'tablet-landscape', 'mobile', 'mobile-landscape'],
|
|
196
|
+
},
|
|
197
|
+
description: 'Device viewports to record (default: ["desktop", "tablet", "mobile"])',
|
|
198
|
+
},
|
|
199
|
+
duration: {
|
|
200
|
+
type: 'number',
|
|
201
|
+
description: 'Scroll duration per device in seconds (default: 10)',
|
|
202
|
+
},
|
|
203
|
+
outputDir: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
description: 'Output directory (default: ./output/)',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ['url'],
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('./lib/logger.js', () => ({
|
|
4
|
+
logger: { info: vi.fn(), logError: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock('./lib/dual-transport.js', () => ({
|
|
8
|
+
startDualTransport: vi.fn().mockResolvedValue({ type: 'stdio' }),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('./lib/types.js', () => ({
|
|
12
|
+
jsonResponse: (data: unknown, isError?: boolean) => ({
|
|
13
|
+
content: [{ type: 'text' as const, text: JSON.stringify(data) }],
|
|
14
|
+
...(isError ? { isError: true } : {}),
|
|
15
|
+
}),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('playwright', () => ({
|
|
19
|
+
chromium: { launch: vi.fn() },
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('createMcpServer', () => {
|
|
23
|
+
it('creates server with 8 tools', async () => {
|
|
24
|
+
const { createMcpServer } = await import('./server.js');
|
|
25
|
+
const server = createMcpServer();
|
|
26
|
+
expect(server).toBeDefined();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('delegate function', () => {
|
|
31
|
+
it('server exports createMcpServer', async () => {
|
|
32
|
+
const mod = await import('./server.js');
|
|
33
|
+
expect(typeof mod.createMcpServer).toBe('function');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('handler registries', () => {
|
|
38
|
+
it('video handlers exist', async () => {
|
|
39
|
+
const { videoHandlers } = await import('./handlers/video.js');
|
|
40
|
+
expect(videoHandlers).toBeDefined();
|
|
41
|
+
expect(videoHandlers.record_website_video).toBeDefined();
|
|
42
|
+
expect(videoHandlers.record_website_scroll).toBeDefined();
|
|
43
|
+
expect(videoHandlers.record_multi_device).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('editing handlers exist', async () => {
|
|
47
|
+
const { editingHandlers } = await import('./handlers/editing.js');
|
|
48
|
+
expect(editingHandlers).toBeDefined();
|
|
49
|
+
expect(editingHandlers.adjust_video_speed).toBeDefined();
|
|
50
|
+
expect(editingHandlers.crop_video).toBeDefined();
|
|
51
|
+
expect(editingHandlers.reverse_clip).toBeDefined();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('capcut handlers exist', async () => {
|
|
55
|
+
const { capcutHandlers } = await import('./handlers/capcut.js');
|
|
56
|
+
expect(capcutHandlers).toBeDefined();
|
|
57
|
+
expect(capcutHandlers.apply_lut_preset).toBeDefined();
|
|
58
|
+
expect(capcutHandlers.sync_to_beat).toBeDefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('post-production handlers exist', async () => {
|
|
62
|
+
const { postProductionHandlers } = await import('./handlers/post-production.js');
|
|
63
|
+
expect(postProductionHandlers).toBeDefined();
|
|
64
|
+
expect(postProductionHandlers.concatenate_videos).toBeDefined();
|
|
65
|
+
expect(postProductionHandlers.convert_social_format).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('tts handlers exist', async () => {
|
|
69
|
+
const { ttsHandlers } = await import('./handlers/tts.js');
|
|
70
|
+
expect(ttsHandlers).toBeDefined();
|
|
71
|
+
expect(ttsHandlers.generate_speech).toBeDefined();
|
|
72
|
+
expect(ttsHandlers.list_voices).toBeDefined();
|
|
73
|
+
expect(ttsHandlers.create_narrated_video).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('screenshot handlers exist', async () => {
|
|
77
|
+
const { smartScreenshotHandlers } = await import('./handlers/smart-screenshot.js');
|
|
78
|
+
expect(smartScreenshotHandlers).toBeDefined();
|
|
79
|
+
expect(smartScreenshotHandlers.screenshot_element).toBeDefined();
|
|
80
|
+
expect(smartScreenshotHandlers.detect_page_features).toBeDefined();
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('tool consolidation', () => {
|
|
85
|
+
it('all handler names are covered by delegate map', () => {
|
|
86
|
+
const allOriginalNames = [
|
|
87
|
+
'record_website_video', 'record_website_scroll', 'record_multi_device',
|
|
88
|
+
'adjust_video_speed', 'crop_video', 'reverse_clip', 'add_keyframe_animation', 'compose_picture_in_pip',
|
|
89
|
+
'apply_color_grade', 'apply_video_effect', 'apply_lut_preset', 'apply_chroma_key',
|
|
90
|
+
'extract_audio', 'add_background_music', 'add_audio_ducking', 'mix_audio_tracks', 'apply_voice_effect',
|
|
91
|
+
'burn_subtitles', 'auto_caption', 'add_text_overlay', 'animate_text',
|
|
92
|
+
'concatenate_videos', 'generate_intro', 'convert_social_format', 'convert_all_social_formats',
|
|
93
|
+
'sync_to_beat', 'list_video_templates', 'render_template',
|
|
94
|
+
'generate_speech', 'list_voices', 'create_narrated_video',
|
|
95
|
+
'screenshot_element', 'detect_page_features',
|
|
96
|
+
];
|
|
97
|
+
expect(allOriginalNames.length).toBe(33);
|
|
98
|
+
});
|
|
99
|
+
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Video Production — MCP Server v1.0.0
|
|
4
|
+
*
|
|
5
|
+
* 8 consolidated tools (from 33+) for recording, editing, color grading,
|
|
6
|
+
* audio, text, compositing, speech/narration, and smart screenshots.
|
|
7
|
+
*
|
|
8
|
+
* Port: 9847 (HTTP mode, configurable via MCP_PORT)
|
|
9
|
+
*/
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
import { logger } from './lib/logger.js';
|
|
13
|
+
import { startDualTransport } from './lib/dual-transport.js';
|
|
14
|
+
import { videoHandlers } from './handlers/video.js';
|
|
15
|
+
import { editingHandlers } from './handlers/editing.js';
|
|
16
|
+
import { capcutHandlers } from './handlers/capcut.js';
|
|
17
|
+
import { postProductionHandlers } from './handlers/post-production.js';
|
|
18
|
+
import { ttsHandlers } from './handlers/tts.js';
|
|
19
|
+
import { smartScreenshotHandlers } from './handlers/smart-screenshot.js';
|
|
20
|
+
|
|
21
|
+
const SERVER_NAME = 'mcp-video';
|
|
22
|
+
const SERVER_VERSION = '1.0.0';
|
|
23
|
+
|
|
24
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
25
|
+
const ALL_HANDLERS: Record<string, (a: any) => any> = {
|
|
26
|
+
...videoHandlers,
|
|
27
|
+
...editingHandlers,
|
|
28
|
+
...capcutHandlers,
|
|
29
|
+
...postProductionHandlers,
|
|
30
|
+
...ttsHandlers,
|
|
31
|
+
...smartScreenshotHandlers,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Delegate to existing handler by original tool name, fix content type literals */
|
|
35
|
+
async function delegate(name: string, args: Record<string, unknown>) {
|
|
36
|
+
const handler = ALL_HANDLERS[name];
|
|
37
|
+
if (!handler) throw new Error(`Unknown handler: ${name}`);
|
|
38
|
+
const result = await handler(args);
|
|
39
|
+
// Ensure content items have literal 'text' type for MCP SDK
|
|
40
|
+
if (result?.content) {
|
|
41
|
+
for (const item of result.content) {
|
|
42
|
+
if (item.type === 'text') item.type = 'text' as const;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createMcpServer() {
|
|
49
|
+
const server = new McpServer(
|
|
50
|
+
{ name: SERVER_NAME, version: SERVER_VERSION },
|
|
51
|
+
{
|
|
52
|
+
instructions: `# Video Server — Tool Selection Guide
|
|
53
|
+
|
|
54
|
+
You have 8 tools for video production. Each has a \`type\` parameter to select the operation.
|
|
55
|
+
|
|
56
|
+
## Recording:
|
|
57
|
+
- **video_record** — Record websites: cinema (full control), scroll (quick scroll-through), multi-device (desktop+tablet+mobile)
|
|
58
|
+
|
|
59
|
+
## Editing:
|
|
60
|
+
- **video_edit** — Edit clips: speed (slow-mo/timelapse), crop, reverse, keyframe (zoom/pan), pip (picture-in-picture)
|
|
61
|
+
- **video_color** — Color & effects: grade (brightness/contrast/etc), effect (blur/sharpen/vignette/etc), lut (22 cinema presets), chroma (green screen)
|
|
62
|
+
- **video_audio** — Audio: extract, music (background), ducking (auto volume), mix (multi-track), voice (effects like echo/robot/whisper)
|
|
63
|
+
- **video_text** — Text & captions: subtitles (burn SRT), caption (Whisper AI auto-caption), overlay (animated text layers), animate (15 text animations)
|
|
64
|
+
|
|
65
|
+
## Post-Production:
|
|
66
|
+
- **video_compose** — Compose: concat (join clips), intro (animated intro/outro), social (format for Instagram/TikTok/etc), social-all (batch all platforms), beat-sync (cuts on music beats), templates (list), render (from template)
|
|
67
|
+
|
|
68
|
+
## Speech & AI:
|
|
69
|
+
- **video_speech** — TTS & narration: generate (ElevenLabs/OpenAI speech), voices (list available), narrated (full narrated video from script)
|
|
70
|
+
|
|
71
|
+
## Screenshots:
|
|
72
|
+
- **video_screenshot** — Smart screenshots: capture (element-aware), detect (page feature analysis)
|
|
73
|
+
|
|
74
|
+
## Decision Flow:
|
|
75
|
+
1. Record website? → video_record
|
|
76
|
+
2. Edit existing video? → video_edit
|
|
77
|
+
3. Color/effects? → video_color
|
|
78
|
+
4. Audio work? → video_audio
|
|
79
|
+
5. Text/captions? → video_text
|
|
80
|
+
6. Join clips/format? → video_compose
|
|
81
|
+
7. Need voiceover? → video_speech
|
|
82
|
+
8. Screenshots? → video_screenshot`,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// ── 1. video_record ────────────────────────────────────
|
|
87
|
+
server.registerTool(
|
|
88
|
+
'video_record',
|
|
89
|
+
{
|
|
90
|
+
title: 'Record Website Video',
|
|
91
|
+
description: 'Record website videos. Types: cinema (full 60fps with scenes/cursor), scroll (quick scroll-through), multi-device (desktop+tablet+mobile).',
|
|
92
|
+
inputSchema: z.object({
|
|
93
|
+
type: z.enum(['cinema', 'scroll', 'multi-device']).describe('Recording type'),
|
|
94
|
+
url: z.string().min(1).describe('Website URL'),
|
|
95
|
+
}).passthrough(),
|
|
96
|
+
annotations: { title: 'Record Video', readOnlyHint: false, openWorldHint: true },
|
|
97
|
+
},
|
|
98
|
+
async (args) => {
|
|
99
|
+
const { type, ...rest } = args;
|
|
100
|
+
const handlerName = type === 'cinema' ? 'record_website_video'
|
|
101
|
+
: type === 'scroll' ? 'record_website_scroll'
|
|
102
|
+
: 'record_multi_device';
|
|
103
|
+
return await delegate(handlerName, rest);
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// ── 2. video_edit ──────────────────────────────────────
|
|
108
|
+
server.registerTool(
|
|
109
|
+
'video_edit',
|
|
110
|
+
{
|
|
111
|
+
title: 'Edit Video',
|
|
112
|
+
description: 'Edit video clips. Types: speed (0.25x-4x), crop (region), reverse (± audio), keyframe (zoom/pan animation), pip (picture-in-picture overlay).',
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
type: z.enum(['speed', 'crop', 'reverse', 'keyframe', 'pip']).describe('Edit operation'),
|
|
115
|
+
}).passthrough(),
|
|
116
|
+
annotations: { title: 'Edit Video', readOnlyHint: false },
|
|
117
|
+
},
|
|
118
|
+
async (args) => {
|
|
119
|
+
const { type, ...rest } = args;
|
|
120
|
+
const map: Record<string, string> = {
|
|
121
|
+
speed: 'adjust_video_speed', crop: 'crop_video', reverse: 'reverse_clip',
|
|
122
|
+
keyframe: 'add_keyframe_animation', pip: 'compose_picture_in_pip',
|
|
123
|
+
};
|
|
124
|
+
return await delegate(map[type], rest);
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// ── 3. video_color ─────────────────────────────────────
|
|
129
|
+
server.registerTool(
|
|
130
|
+
'video_color',
|
|
131
|
+
{
|
|
132
|
+
title: 'Color & Effects',
|
|
133
|
+
description: 'Color grading & visual effects. Types: grade (brightness/contrast/saturation/gamma/temperature), effect (blur/sharpen/vignette/grayscale/sepia/noise/glow), lut (22 cinema presets), chroma (green/blue screen replacement).',
|
|
134
|
+
inputSchema: z.object({
|
|
135
|
+
type: z.enum(['grade', 'effect', 'lut', 'chroma']).describe('Color operation'),
|
|
136
|
+
}).passthrough(),
|
|
137
|
+
annotations: { title: 'Color & Effects', readOnlyHint: false },
|
|
138
|
+
},
|
|
139
|
+
async (args) => {
|
|
140
|
+
const { type, ...rest } = args;
|
|
141
|
+
const map: Record<string, string> = {
|
|
142
|
+
grade: 'apply_color_grade', effect: 'apply_video_effect',
|
|
143
|
+
lut: 'apply_lut_preset', chroma: 'apply_chroma_key',
|
|
144
|
+
};
|
|
145
|
+
return await delegate(map[type], rest);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
// ── 4. video_audio ─────────────────────────────────────
|
|
150
|
+
server.registerTool(
|
|
151
|
+
'video_audio',
|
|
152
|
+
{
|
|
153
|
+
title: 'Audio Tools',
|
|
154
|
+
description: 'Audio operations. Types: extract (MP3/AAC/WAV/FLAC from video), music (add background music with fade), ducking (auto volume reduction), mix (multi-track mixing), voice (9 voice effects: echo/reverb/deep/chipmunk/robot/whisper/radio/megaphone/underwater).',
|
|
155
|
+
inputSchema: z.object({
|
|
156
|
+
type: z.enum(['extract', 'music', 'ducking', 'mix', 'voice']).describe('Audio operation'),
|
|
157
|
+
}).passthrough(),
|
|
158
|
+
annotations: { title: 'Audio', readOnlyHint: false },
|
|
159
|
+
},
|
|
160
|
+
async (args) => {
|
|
161
|
+
const { type, ...rest } = args;
|
|
162
|
+
const map: Record<string, string> = {
|
|
163
|
+
extract: 'extract_audio', music: 'add_background_music',
|
|
164
|
+
ducking: 'add_audio_ducking', mix: 'mix_audio_tracks', voice: 'apply_voice_effect',
|
|
165
|
+
};
|
|
166
|
+
return await delegate(map[type], rest);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// ── 5. video_text ──────────────────────────────────────
|
|
171
|
+
server.registerTool(
|
|
172
|
+
'video_text',
|
|
173
|
+
{
|
|
174
|
+
title: 'Text & Captions',
|
|
175
|
+
description: 'Text overlays & captions. Types: subtitles (burn SRT/ASS), caption (Whisper AI auto-caption), overlay (animated text layers), animate (15 text animation styles).',
|
|
176
|
+
inputSchema: z.object({
|
|
177
|
+
type: z.enum(['subtitles', 'caption', 'overlay', 'animate']).describe('Text operation'),
|
|
178
|
+
}).passthrough(),
|
|
179
|
+
annotations: { title: 'Text & Captions', readOnlyHint: false },
|
|
180
|
+
},
|
|
181
|
+
async (args) => {
|
|
182
|
+
const { type, ...rest } = args;
|
|
183
|
+
const map: Record<string, string> = {
|
|
184
|
+
subtitles: 'burn_subtitles', caption: 'auto_caption',
|
|
185
|
+
overlay: 'add_text_overlay', animate: 'animate_text',
|
|
186
|
+
};
|
|
187
|
+
return await delegate(map[type], rest);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ── 6. video_compose ───────────────────────────────────
|
|
192
|
+
server.registerTool(
|
|
193
|
+
'video_compose',
|
|
194
|
+
{
|
|
195
|
+
title: 'Compose & Export',
|
|
196
|
+
description: 'Compose and export videos. Types: concat (join clips with transitions), intro (animated intro/outro), social (convert to Instagram/TikTok/YouTube/LinkedIn), social-all (batch all platforms), beat-sync (cut on music beats), templates (list available), render (render from template).',
|
|
197
|
+
inputSchema: z.object({
|
|
198
|
+
type: z.enum(['concat', 'intro', 'social', 'social-all', 'beat-sync', 'templates', 'render']).describe('Compose operation'),
|
|
199
|
+
}).passthrough(),
|
|
200
|
+
annotations: { title: 'Compose & Export', readOnlyHint: false },
|
|
201
|
+
},
|
|
202
|
+
async (args) => {
|
|
203
|
+
const { type, ...rest } = args;
|
|
204
|
+
const map: Record<string, string> = {
|
|
205
|
+
concat: 'concatenate_videos', intro: 'generate_intro',
|
|
206
|
+
social: 'convert_social_format', 'social-all': 'convert_all_social_formats',
|
|
207
|
+
'beat-sync': 'sync_to_beat', templates: 'list_video_templates', render: 'render_template',
|
|
208
|
+
};
|
|
209
|
+
return await delegate(map[type], rest);
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// ── 7. video_speech ────────────────────────────────────
|
|
214
|
+
server.registerTool(
|
|
215
|
+
'video_speech',
|
|
216
|
+
{
|
|
217
|
+
title: 'Speech & Narration',
|
|
218
|
+
description: 'Text-to-speech & narrated videos. Types: generate (ElevenLabs/OpenAI TTS), voices (list available voices), narrated (full narrated video: script → TTS → website recording → sync → MP4).',
|
|
219
|
+
inputSchema: z.object({
|
|
220
|
+
type: z.enum(['generate', 'voices', 'narrated']).describe('Speech operation'),
|
|
221
|
+
}).passthrough(),
|
|
222
|
+
annotations: { title: 'Speech & Narration', readOnlyHint: false, openWorldHint: true },
|
|
223
|
+
},
|
|
224
|
+
async (args) => {
|
|
225
|
+
const { type, ...rest } = args;
|
|
226
|
+
const map: Record<string, string> = {
|
|
227
|
+
generate: 'generate_speech', voices: 'list_voices', narrated: 'create_narrated_video',
|
|
228
|
+
};
|
|
229
|
+
return await delegate(map[type], rest);
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// ── 8. video_screenshot ────────────────────────────────
|
|
234
|
+
server.registerTool(
|
|
235
|
+
'video_screenshot',
|
|
236
|
+
{
|
|
237
|
+
title: 'Smart Screenshot',
|
|
238
|
+
description: 'Smart element-aware screenshots. Types: capture (detect & screenshot specific page features: chat, pricing, hero, etc.), detect (analyze page features without screenshotting).',
|
|
239
|
+
inputSchema: z.object({
|
|
240
|
+
type: z.enum(['capture', 'detect']).describe('Screenshot operation'),
|
|
241
|
+
url: z.string().min(1).describe('Website URL'),
|
|
242
|
+
}).passthrough(),
|
|
243
|
+
annotations: { title: 'Smart Screenshot', readOnlyHint: true, openWorldHint: true },
|
|
244
|
+
},
|
|
245
|
+
async (args) => {
|
|
246
|
+
const { type, ...rest } = args;
|
|
247
|
+
const handlerName = type === 'capture' ? 'screenshot_element' : 'detect_page_features';
|
|
248
|
+
return await delegate(handlerName, rest);
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return server;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── Startup Validation ──────────────────────────────────
|
|
256
|
+
|
|
257
|
+
import { execSync } from 'child_process';
|
|
258
|
+
import * as fs from 'fs';
|
|
259
|
+
|
|
260
|
+
function checkDependencies(): void {
|
|
261
|
+
for (const bin of ['ffmpeg', 'ffprobe']) {
|
|
262
|
+
try {
|
|
263
|
+
execSync(`which ${bin}`, { stdio: 'pipe' });
|
|
264
|
+
} catch {
|
|
265
|
+
logger.error(`${bin} not found. Install ffmpeg: https://ffmpeg.org/download.html`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const outputDir = process.env.VIDEO_OUTPUT_DIR || './output';
|
|
271
|
+
if (!fs.existsSync(outputDir)) {
|
|
272
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
273
|
+
logger.info(`Created output directory: ${outputDir}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── Start ────────────────────────────────────────────────
|
|
278
|
+
checkDependencies();
|
|
279
|
+
|
|
280
|
+
startDualTransport(createMcpServer, {
|
|
281
|
+
serverName: SERVER_NAME,
|
|
282
|
+
serverVersion: SERVER_VERSION,
|
|
283
|
+
defaultPort: 9847,
|
|
284
|
+
}).then((result: { type: string; port?: number }) => {
|
|
285
|
+
logger.info(`Video MCP Server running — 8 tools (${result.type}${result.port ? ` :${result.port}` : ''})`);
|
|
286
|
+
}).catch((error) => {
|
|
287
|
+
logger.logError('Fatal error', error);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
});
|