ayphic-mcp-server 1.1.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.
Files changed (2) hide show
  1. package/index.js +838 -0
  2. package/package.json +22 -0
package/index.js ADDED
@@ -0,0 +1,838 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Ayphic Model Context Protocol (MCP) Server
4
+ *
5
+ * Enables Claude Desktop, Cursor, and other MCP clients to directly control
6
+ * and edit the active Ayphic AI Video Editor multi-track timeline in real-time.
7
+ *
8
+ * Standard: JSON-RPC 2.0 over stdio
9
+ */
10
+
11
+ import readline from 'readline';
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+ const DOCS_PATH = path.resolve(__dirname, '../server/agent/docs/EDITOR_CAPABILITIES.md');
19
+ const API_BASE_URL = process.env.AYPHIC_API_URL || 'http://localhost:5000';
20
+ const SERVER_NAME = 'ayphic-mcp-server';
21
+ const SERVER_VERSION = '1.1.0';
22
+
23
+ // Supported MCP Tools Definition
24
+ const MCP_TOOLS = [
25
+ {
26
+ name: 'get_timeline',
27
+ description: 'Retrieve the active Ayphic video editor timeline state, canvas dimensions (width, height, FPS), active tracks (V1, V2, A1, C1), and all media items.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ projectId: {
32
+ type: 'string',
33
+ description: 'Optional project ID to retrieve. If omitted, retrieves the currently active open project.'
34
+ }
35
+ }
36
+ }
37
+ },
38
+ {
39
+ name: 'split_item',
40
+ description: 'Split one or more timeline items (video clips, audio, overlays) at specific frame numbers or timestamps (seconds). If id is omitted, automatically splits the primary video clip on track V1.',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ id: {
45
+ type: ['string', 'array'],
46
+ description: 'ID or array of IDs of the timeline items to split. If omitted, targets the main video on track V1.'
47
+ },
48
+ newFrameNumber: {
49
+ type: ['number', 'array'],
50
+ description: 'Frame number(s) at which to split the items (e.g. 90 for 3 seconds at 30 FPS).'
51
+ },
52
+ timestamps: {
53
+ type: ['number', 'array'],
54
+ description: 'Timestamp(s) in seconds at which to split the items (e.g. 2.0 or [2, 5.5]).'
55
+ }
56
+ }
57
+ }
58
+ },
59
+ {
60
+ name: 'trim_item',
61
+ description: 'Trim a timeline item from the start or end to a specific target frame or timestamp. If id is omitted, targets the primary clip on track V1.',
62
+ inputSchema: {
63
+ type: 'object',
64
+ properties: {
65
+ id: {
66
+ type: 'string',
67
+ description: 'ID of the timeline item to trim. If omitted, targets the main video on track V1.'
68
+ },
69
+ from: {
70
+ type: 'string',
71
+ enum: ['start', 'end'],
72
+ description: 'Whether to trim from the "start" or "end".'
73
+ },
74
+ targetFrame: {
75
+ type: 'number',
76
+ description: 'Target frame number on the timeline.'
77
+ },
78
+ timestamps: {
79
+ type: 'number',
80
+ description: 'Target timestamp in seconds on the timeline.'
81
+ }
82
+ },
83
+ required: ['from']
84
+ }
85
+ },
86
+ {
87
+ name: 'delete_item',
88
+ description: 'Permanently delete one or more items (clips, text, shapes, sounds) or an entire track from the timeline.',
89
+ inputSchema: {
90
+ type: 'object',
91
+ properties: {
92
+ id: {
93
+ type: ['string', 'array'],
94
+ description: 'ID or array of IDs of items or tracks to delete (e.g. "item_123" or ["item_1", "item_2"]).'
95
+ }
96
+ },
97
+ required: ['id']
98
+ }
99
+ },
100
+ {
101
+ name: 'move_item',
102
+ description: 'Move or shift a timeline item to a new start frame or move it to a different track layer.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ id: {
107
+ type: ['string', 'array'],
108
+ description: 'ID or array of IDs of items to move.'
109
+ },
110
+ startFrame: {
111
+ type: 'number',
112
+ description: 'New start frame position on the timeline.'
113
+ },
114
+ layer: {
115
+ type: ['string', 'number'],
116
+ description: 'Target track layer ID (e.g. "V1", "V2", "A1").'
117
+ }
118
+ },
119
+ required: ['id', 'startFrame']
120
+ }
121
+ },
122
+ {
123
+ name: 'add_item',
124
+ description: 'Add a new element (video clip, image overlay, sound effect, background music, text, shape, or motion graphic) to the timeline. MANDATORY FOR IMAGES: NEVER hallucinate or guess 3rd-party image URLs (e.g. from pngmart, toppng, etc. which fail CORS/hotlinking). ALWAYS call `stock_search` ({ query: "indian flag", mediaType: "image" }) FIRST to obtain verified, high-speed CDN image URLs, and pass the returned `src` into `url`.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ type: {
129
+ type: 'string',
130
+ enum: ['video', 'image', 'sound', 'overlay', 'text', 'shape', 'motion', 'caption'],
131
+ description: 'Type of timeline element to add.'
132
+ },
133
+ url: {
134
+ type: 'string',
135
+ description: 'Direct URL or path of media asset (required for video, image, sound). Use `src` from stock_search for images/videos.'
136
+ },
137
+ layer: {
138
+ type: ['string', 'number'],
139
+ description: 'Target track layer (e.g. "V1" for main video, "V2" for overlays/text, "A1" for audio).'
140
+ },
141
+ startTime: {
142
+ type: 'number',
143
+ description: 'Start position in seconds.'
144
+ },
145
+ startFrame: {
146
+ type: 'number',
147
+ description: 'Start position in frames.'
148
+ },
149
+ duration: {
150
+ type: 'number',
151
+ description: 'Duration in seconds.'
152
+ },
153
+ backgroundRemoval: {
154
+ type: 'object',
155
+ properties: {
156
+ enabled: { type: 'boolean' }
157
+ },
158
+ description: 'Set { enabled: true } to remove background automatically for image overlays.'
159
+ },
160
+ extra: {
161
+ type: 'object',
162
+ description: 'Additional properties such as text content, shapeType, styles, etc.'
163
+ }
164
+ },
165
+ required: ['type']
166
+ }
167
+ },
168
+ {
169
+ name: 'update_property',
170
+ description: 'Update properties (styles, colors, fonts, animations, opacity, coordinates) of an existing timeline element. CRITICAL: Never guess or invent non-existent properties, animation types, or easing names (e.g. do not invent "slideInUpScale" or "easeOutElastic"). Always check exact names with search_docs first. Supported entrance animations (.in): "fadeIn", "slideUp", "slideDown", "slideLeft", "slideRight", "scaleIn", "popIn", "bounceIn", "rotateIn", "rotate3DIn", "blurIn", "glitchIn", "neonPulse", "revealIn", "slideBlur", "wavyIn", "bubblePopText", "animatedText", "pulsingText", "typewriterSubtitle", "wordSlideBlur", "wordSaasLaunch". Supported loop animations (.loop): "bounce", "float", "pulse", "shake", "spin", "breathe", "glow", "rainbow", "wiggle". Supported exit animations (.out): "fadeOut", "scaleOut", "blurOut", "bounceOut", "slideUp", "slideDown", "slideLeft", "slideRight", "popOut", "rotateOut".',
171
+ inputSchema: {
172
+ type: 'object',
173
+ properties: {
174
+ id: {
175
+ type: ['string', 'array'],
176
+ description: 'ID or array of IDs of elements to update.'
177
+ },
178
+ property: {
179
+ type: 'string',
180
+ description: 'Dot-notation path of property to modify (e.g. "style.opacity", "animation.in", "style.fontSize", "style.color", "style.filter").'
181
+ },
182
+ value: {
183
+ description: 'New value for the property.'
184
+ }
185
+ },
186
+ required: ['id', 'property', 'value']
187
+ }
188
+ },
189
+ {
190
+ name: 'set_position',
191
+ description: 'Position an element on the canvas using standard layout keywords (center, top, bottom, left, right, top-left, top-right, bottom-left, bottom-right).',
192
+ inputSchema: {
193
+ type: 'object',
194
+ properties: {
195
+ target: {
196
+ type: 'string',
197
+ description: 'ID of the element to position.'
198
+ },
199
+ position: {
200
+ type: 'string',
201
+ enum: ['center', 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
202
+ description: 'Target position keyword on the canvas.'
203
+ },
204
+ anchor: {
205
+ type: 'string',
206
+ description: 'Optional anchor element ID. If omitted, positions relative to the entire canvas.'
207
+ }
208
+ },
209
+ required: ['target', 'position']
210
+ }
211
+ },
212
+ {
213
+ name: 'adjust_color',
214
+ description: 'Apply visual color adjustments, filters (grayscale, sepia, invert, fresco), brightness, contrast, and saturation.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ id: {
219
+ type: 'string',
220
+ description: 'ID of the element to adjust.'
221
+ },
222
+ adjustments: {
223
+ type: 'object',
224
+ properties: {
225
+ brightness: { type: 'number' },
226
+ contrast: { type: 'number' },
227
+ saturation: { type: 'number' },
228
+ blur: { type: 'number' },
229
+ opacity: { type: 'number' },
230
+ filter: {
231
+ type: ['string', 'object'],
232
+ description: 'Visual filter preset name or object (e.g. "grayscale" or { type: "grayscale", intensity: 100 }).'
233
+ }
234
+ }
235
+ }
236
+ },
237
+ required: ['id']
238
+ }
239
+ },
240
+ {
241
+ name: 'stock_search',
242
+ description: 'REQUIRED FIRST STEP FOR STOCK IMAGES & B-ROLL: Search Pexels for royalty-free stock photos or video clips by keyword. Always call this tool to find valid high-resolution image/video assets (e.g. `pexels://photo/12345` or `pexels://video/67890`) before calling `add_item`.',
243
+ inputSchema: {
244
+ type: 'object',
245
+ properties: {
246
+ query: {
247
+ type: 'string',
248
+ description: 'Search keyword (e.g. "indian flag", "modern technology", "neon city", "coffee morning").'
249
+ },
250
+ mediaType: {
251
+ type: 'string',
252
+ enum: ['video', 'image'],
253
+ description: 'Filter by "video" or "image" (default: "video").'
254
+ },
255
+ count: {
256
+ type: 'number',
257
+ description: 'Number of results to return (default: 5).'
258
+ }
259
+ },
260
+ required: ['query']
261
+ }
262
+ },
263
+ {
264
+ name: 'get_sfx',
265
+ description: 'Search sound effects library for audio assets matching a description or list all available sound effects. If query is omitted or "all", returns all 32 sound effects in the Ayphic SFX library.',
266
+ inputSchema: {
267
+ type: 'object',
268
+ properties: {
269
+ query: {
270
+ type: 'string',
271
+ description: 'Sound effect query (e.g. "whoosh", "ding", "fail", "boom", "all" to list all sound effects).'
272
+ }
273
+ }
274
+ }
275
+ },
276
+ {
277
+ name: 'motion_designer',
278
+ description: 'Insert a custom React Remotion motion graphic overlay. DIRECT CODE & CONFIG SCHEMA TOOL: You (the MCP agent) generate the full Remotion component code in "code" and provide the configurable variables in "configSchema" so the user can easily customize colors, titles, and values from the UI Properties Panel.',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ code: {
283
+ type: 'string',
284
+ description: 'REQUIRED: Full Remotion React component code (export const GeneratedMotion = (props) => { const frame = Remotion.useCurrentFrame(); const { fps = 30 } = Remotion.useVideoConfig(); ... return <div style={{...}}>...</div>; }). Use Remotion hooks and inline CSS styling.'
285
+ },
286
+ configSchema: {
287
+ type: 'array',
288
+ items: {
289
+ type: 'object',
290
+ properties: {
291
+ key: { type: 'string', description: 'Variable key used in component (e.g. "title", "colorAccent", "count", "showBadge")' },
292
+ label: { type: 'string', description: 'Label shown in the UI properties panel (e.g. "Title Text", "Accent Color")' },
293
+ type: { type: 'string', enum: ['text', 'color', 'number', 'boolean'], description: 'Control type for the UI property panel' },
294
+ default: { description: 'Default value' },
295
+ min: { type: 'number', description: 'Min number (for number type)' },
296
+ max: { type: 'number', description: 'Max number (for number type)' },
297
+ step: { type: 'number', description: 'Step size (for number type)' }
298
+ },
299
+ required: ['key', 'type']
300
+ },
301
+ description: 'Configurable variables array shown in the UI properties panel for user customization.'
302
+ },
303
+ name: {
304
+ type: 'string',
305
+ description: 'Descriptive title of the motion graphic (e.g. "Minimalist Upload Card", "Subscriber Counter").'
306
+ },
307
+ start: {
308
+ type: 'number',
309
+ description: 'Start time on timeline in seconds (e.g. 4.0).'
310
+ },
311
+ dur: {
312
+ type: 'number',
313
+ description: 'Duration on timeline in seconds (default: 3.0).'
314
+ },
315
+ id: {
316
+ type: 'string',
317
+ description: 'Optional ID of existing motion element to update.'
318
+ }
319
+ },
320
+ required: ['code']
321
+ }
322
+ },
323
+ {
324
+ name: 'body_detection',
325
+ description: 'Perception tool: Detect human bodies, faces, postures, bounding boxes, full head-to-toe continuous skeletons, and exact coordinates on canvas.',
326
+ inputSchema: {
327
+ type: 'object',
328
+ properties: {
329
+ part: {
330
+ type: 'string',
331
+ enum: ['face', 'head', 'hair', 'neck', 'torso', 'chest', 'left_arm', 'right_arm', 'left_hand', 'right_hand', 'hands', 'left_leg', 'right_leg', 'legs', 'upper_body', 'lower_body', 'all'],
332
+ description: 'Specific anatomical part to extract coordinates for.'
333
+ },
334
+ frame: {
335
+ type: 'number',
336
+ description: 'Timeline frame number to analyze.'
337
+ },
338
+ timestamp: {
339
+ type: 'number',
340
+ description: 'Timestamp in seconds.'
341
+ }
342
+ }
343
+ }
344
+ },
345
+ {
346
+ name: 'check_face_overlap',
347
+ description: 'Check if timeline elements (CTAs, text, stickers) occlude any detected faces on the video canvas and return safe position coordinates.',
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: {
351
+ elementId: {
352
+ type: 'string',
353
+ description: 'Optional specific element ID to check.'
354
+ },
355
+ frame: {
356
+ type: 'number',
357
+ description: 'Timeline frame number.'
358
+ }
359
+ }
360
+ }
361
+ },
362
+ {
363
+ name: 'inspect_element',
364
+ description: 'Retrieve the complete raw JSON data structure of any timeline element by ID or name.',
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ id: {
369
+ type: 'string',
370
+ description: 'ID of the element to inspect.'
371
+ }
372
+ },
373
+ required: ['id']
374
+ }
375
+ },
376
+ {
377
+ name: 'get_transcript',
378
+ description: 'REQUIRED FIRST STEP FOR VIDEO CAPTIONS: Extract the authentic speech transcript with verbatim word-level timestamps (seconds) from the video/audio clip. When the user asks to add captions/subtitles to a video, you MUST call this tool first to obtain the authentic spoken words and timings from the video before calling captions.',
379
+ inputSchema: {
380
+ type: 'object',
381
+ properties: {
382
+ itemId: {
383
+ type: 'string',
384
+ description: 'Optional ID of the clip to transcribe (e.g. "clip2"). If omitted, automatically targets the main video.'
385
+ },
386
+ mediaUrl: {
387
+ type: 'string',
388
+ description: 'Optional direct media URL.'
389
+ }
390
+ }
391
+ }
392
+ },
393
+ {
394
+ name: 'captions',
395
+ description: 'Add, style, or remove animated captions/subtitles across the video timeline. DIRECT DATA TOOL: When adding captions for a video clip, you MUST first call `get_transcript` to get the real speech timestamps, then pass the `transcription` object directly here. You can also customize `template` ("beast", "hustle", "pop", "soft-ai", "karaoke"), or provide custom Remotion JSX `code`, or use `action: "delete"` to remove captions.',
396
+ inputSchema: {
397
+ type: 'object',
398
+ properties: {
399
+ transcription: {
400
+ type: 'object',
401
+ description: 'The authentic transcription object returned from get_transcript containing { text, words, segments, lines, captions }. Pass this directly from get_transcript.'
402
+ },
403
+ template: {
404
+ type: 'string',
405
+ enum: ['viral-stack', 'viral-stacked', 'stacked', 'beast', 'hustle', 'pop', 'soft-ai', 'gaming-stream', 'karaoke', 'grape', 'poppin', 'kinetic-01', 'kinetic-02', 'aarit', 'podcast'],
406
+ description: 'Visual subtitle preset theme. Recommended: "viral-stack" (stacked kinetic typography with dynamic sizing and slide-up), "beast" (MrBeast style bold uppercase highlight), "hustle" (green glow), "pop" (spring bounce).'
407
+ },
408
+ action: {
409
+ type: 'string',
410
+ enum: ['generate', 'delete', 'remove', 'clear'],
411
+ description: 'Action to perform. Use "generate" to add/update captions, or "delete"/"remove"/"clear" to remove all captions from the timeline.'
412
+ },
413
+ code: {
414
+ type: 'string',
415
+ description: 'Optional custom Remotion React JSX code template for rendering captions.'
416
+ },
417
+ words: {
418
+ type: 'array',
419
+ items: { type: 'object' },
420
+ description: 'Array of word-level timing objects [{ text: string, start: number, end: number }] from get_transcript.'
421
+ },
422
+ captions: {
423
+ type: 'array',
424
+ items: { type: 'object' },
425
+ description: 'Array of timed caption segments [{ text: string, start: number, end: number, words: array }] from get_transcript.'
426
+ },
427
+ fontSize: {
428
+ type: ['number', 'string'],
429
+ description: 'Font size (e.g. 72 or "72px").'
430
+ },
431
+ color: {
432
+ type: 'string',
433
+ description: 'Primary text color hex (e.g. "#ffffff").'
434
+ },
435
+ highlightColor: {
436
+ type: 'string',
437
+ description: 'Highlight active word color hex (e.g. "#ffff00" or "#4ade80").'
438
+ },
439
+ id: {
440
+ type: 'string',
441
+ description: 'Optional specific caption group ID to delete or modify.'
442
+ }
443
+ }
444
+ }
445
+ },
446
+ {
447
+ name: 'search_docs',
448
+ description: 'Search and retrieve Ayphic AI Video Editor documentation, capabilities catalog, schemas, 29 entrance animations, 13 exit animations, 9 loop animations, 11 filters, 20+ fonts, and tracks on-demand without bloating token context. Query with topics like "animations", "fonts", "filters", "text styling", "motion graphics", or "all". MANDATORY RULE: Whenever you need to apply an animation, styling, font, filter, transition, or property and are not 100% certain of the exact valid name, you MUST call this search_docs tool first before applying to avoid guessing or hallucinating non-existent properties.',
449
+ inputSchema: {
450
+ type: 'object',
451
+ properties: {
452
+ query: {
453
+ type: 'string',
454
+ description: 'Topic or keyword to search (e.g. "animations", "fonts", "filters", "text", "all").'
455
+ },
456
+ category: {
457
+ type: 'string',
458
+ description: 'Optional category filter (e.g. "text", "animations", "filters", "shapes", "general").'
459
+ }
460
+ }
461
+ }
462
+ },
463
+ {
464
+ name: 'detect_beats',
465
+ description: 'Analyze an audio track or video clip to detect beats. Returns estimated BPM and a list of timestamps (seconds) where beats occur.',
466
+ inputSchema: {
467
+ type: 'object',
468
+ properties: {
469
+ itemId: { type: 'string', description: 'ID of the clip or sound element in the timeline to analyze (optional, defaults to primary sound track)' },
470
+ mediaUrl: { type: 'string', description: 'Direct URL or path of the audio file to analyze (optional)' }
471
+ }
472
+ }
473
+ },
474
+ {
475
+ name: 'glitch_reveal',
476
+ description: 'Freeze-frame glitch cutout reveal effect with background removal, white camera flash strobe, RGB split distortion, and automatic Shutter Modern sound effect.',
477
+ inputSchema: {
478
+ type: 'object',
479
+ properties: {
480
+ itemId: { type: 'string', description: 'ID of the video clip/overlay to apply the glitch reveal effect to.' },
481
+ timestamp: { type: ['number', 'string'], description: 'Timestamp in seconds or timestamp string where the freeze frame cutout occurs.' },
482
+ duration: { type: 'number', description: 'Duration of freeze-frame cutout in seconds.' },
483
+ style: { type: 'string', description: 'Visual glitch style preset.' },
484
+ includeShutter: { type: 'boolean', description: 'Automatically insert sound effect.' }
485
+ }
486
+ }
487
+ },
488
+ {
489
+ name: 'add_transition',
490
+ description: 'Add one or multiple video transition overlays between clips or across all cut points.',
491
+ inputSchema: {
492
+ type: 'object',
493
+ properties: {
494
+ preset: { type: 'string', description: 'Transition preset style ("filmBurn" or "paperCut").' },
495
+ clipIdA: { type: 'string', description: 'ID of the clip preceding the transition boundary.' },
496
+ clipIdB: { type: 'string', description: 'ID of the clip following the transition boundary.' },
497
+ atFrame: { type: 'number', description: 'Target timeline frame.' },
498
+ timestamp: { type: ['number', 'string'], description: 'Target timestamp.' },
499
+ placement: { type: 'string', description: 'Placement relative to cut point.' },
500
+ duration: { type: 'number', description: 'Custom duration.' }
501
+ }
502
+ }
503
+ },
504
+ {
505
+ name: 'remove_greenscreen',
506
+ description: 'Apply chroma-key (green screen removal) to a clip or overlay by ID.',
507
+ inputSchema: {
508
+ type: 'object',
509
+ properties: {
510
+ itemId: { type: 'string', description: 'The ID of the clip or overlay to update' },
511
+ color: { type: 'string', description: 'Hex color to remove' },
512
+ strength: { type: 'number', description: 'Strength/threshold for removal' },
513
+ smoothness: { type: 'number', description: 'Edge smoothness value' }
514
+ },
515
+ required: ['itemId']
516
+ }
517
+ },
518
+ {
519
+ name: 'remove_silence',
520
+ description: 'Automatically detect and remove silent parts/gaps from a video clip on the timeline.',
521
+ inputSchema: {
522
+ type: 'object',
523
+ properties: {
524
+ clipId: { type: 'string', description: 'ID of the video clip' },
525
+ noiseThreshold: { type: 'number', description: 'Noise threshold in dB' },
526
+ minDuration: { type: 'number', description: 'Minimum duration of silence' }
527
+ },
528
+ required: ['clipId']
529
+ }
530
+ },
531
+ {
532
+ name: 'delete_track',
533
+ description: 'Permanently remove an entire track (like "V2", "A1") from the timeline.',
534
+ inputSchema: {
535
+ type: 'object',
536
+ properties: {
537
+ trackId: { type: ['string', 'array'], description: 'ID or array of IDs of tracks to delete' }
538
+ },
539
+ required: ['trackId']
540
+ }
541
+ }
542
+ ];
543
+
544
+ /**
545
+ * Execute tool against Ayphic backend HTTP API
546
+ */
547
+ async function executeAyphicTool(toolName, params) {
548
+ // Strip 'ayphic_' prefix to get internal tool name
549
+ const cleanName = toolName.startsWith('ayphic_') ? toolName.replace('ayphic_', '') : toolName;
550
+
551
+ // Map naming conventions
552
+ const toolNameMapping = {
553
+ 'get_timeline': 'getTimeline',
554
+ 'split_item': 'splitItem',
555
+ 'trim_item': 'trimItem',
556
+ 'delete_item': 'deleteItem',
557
+ 'move_item': 'moveItem',
558
+ 'add_item': 'addItem',
559
+ 'update_property': 'updateProperty',
560
+ 'set_position': 'setPosition',
561
+ 'adjust_color': 'adjustColor',
562
+ 'delete_track': 'deleteTrack',
563
+ 'inspect_element': 'inspectElement',
564
+ 'stock_search': 'stockSearch',
565
+ 'get_sfx': 'getSFX',
566
+ 'motion_designer': 'motionDesigner',
567
+ 'body_detection': 'body_detection',
568
+ 'check_face_overlap': 'checkFaceOverlap',
569
+ 'get_transcript': 'getTranscript',
570
+ 'captions': 'captions',
571
+ 'search_docs': 'searchDocs',
572
+ 'searchDocs': 'searchDocs',
573
+ 'get_docs': 'searchDocs',
574
+ 'detect_beats': 'detectBeats',
575
+ 'glitch_reveal': 'glitchReveal',
576
+ 'add_transition': 'addTransition',
577
+ 'remove_greenscreen': 'removeGreenscreen',
578
+ 'remove_silence': 'removeSilence'
579
+ };
580
+
581
+ const resolvedToolName = toolNameMapping[cleanName] || cleanName;
582
+
583
+ try {
584
+ const response = await fetch(`${API_BASE_URL}/api/mcp/execute-tool`, {
585
+ method: 'POST',
586
+ headers: {
587
+ 'Content-Type': 'application/json',
588
+ 'x-mcp-client': 'claude-desktop'
589
+ },
590
+ body: JSON.stringify({
591
+ tool: resolvedToolName,
592
+ params: params || {}
593
+ })
594
+ });
595
+
596
+ if (!response.ok) {
597
+ const errorText = await response.text();
598
+ return {
599
+ isError: true,
600
+ content: [
601
+ {
602
+ type: 'text',
603
+ text: `Ayphic Server Error (${response.status}): ${errorText}`
604
+ }
605
+ ]
606
+ };
607
+ }
608
+
609
+ const data = await response.json();
610
+ const isError = data.success === false || data.applied === false || !!data.error;
611
+ return {
612
+ isError,
613
+ content: [
614
+ {
615
+ type: 'text',
616
+ text: JSON.stringify(data, null, 2)
617
+ }
618
+ ]
619
+ };
620
+ } catch (netErr) {
621
+ return {
622
+ isError: true,
623
+ content: [
624
+ {
625
+ type: 'text',
626
+ text: `Failed to connect to Ayphic backend at ${API_BASE_URL}. Ensure Ayphic server is running (npm run dev:server). Error: ${netErr.message}`
627
+ }
628
+ ]
629
+ };
630
+ }
631
+ }
632
+
633
+ /**
634
+ * Handle incoming JSON-RPC 2.0 message
635
+ */
636
+ async function handleRpcMessage(msg) {
637
+ const { id, method, params } = msg;
638
+
639
+ switch (method) {
640
+ case 'initialize':
641
+ return {
642
+ jsonrpc: '2.0',
643
+ id,
644
+ result: {
645
+ protocolVersion: '2024-11-05',
646
+ capabilities: {
647
+ tools: {
648
+ listChanged: false
649
+ },
650
+ resources: {},
651
+ prompts: {}
652
+ },
653
+ serverInfo: {
654
+ name: SERVER_NAME,
655
+ version: SERVER_VERSION
656
+ }
657
+ }
658
+ };
659
+
660
+ case 'notifications/initialized':
661
+ // Notification, no response needed
662
+ return null;
663
+
664
+ case 'ping':
665
+ return {
666
+ jsonrpc: '2.0',
667
+ id,
668
+ result: {}
669
+ };
670
+
671
+ case 'tools/list':
672
+ return {
673
+ jsonrpc: '2.0',
674
+ id,
675
+ result: {
676
+ tools: MCP_TOOLS
677
+ }
678
+ };
679
+
680
+ case 'tools/call': {
681
+ const toolName = params?.name;
682
+ const toolArguments = params?.arguments || {};
683
+ const executionResult = await executeAyphicTool(toolName, toolArguments);
684
+
685
+ return {
686
+ jsonrpc: '2.0',
687
+ id,
688
+ result: executionResult
689
+ };
690
+ }
691
+
692
+ case 'prompts/list':
693
+ return {
694
+ jsonrpc: '2.0',
695
+ id,
696
+ result: {
697
+ prompts: []
698
+ }
699
+ };
700
+
701
+ case 'resources/list':
702
+ return {
703
+ jsonrpc: '2.0',
704
+ id,
705
+ result: {
706
+ resources: [
707
+ {
708
+ uri: 'ayphic://timeline/active',
709
+ name: 'Active Video Timeline',
710
+ description: 'Real-time JSON state of the current video project in Ayphic Editor',
711
+ mimeType: 'application/json'
712
+ },
713
+ {
714
+ uri: 'ayphic://docs/capabilities',
715
+ name: 'Ayphic Video Editor Full Capabilities Documentation',
716
+ description: 'Comprehensive schema & properties reference for text, shapes, motions, SFX, 29 entrance animations, 13 exit animations, 9 loop animations, 11 filters, and 20+ fonts.',
717
+ mimeType: 'text/markdown'
718
+ },
719
+ {
720
+ uri: 'ayphic://docs/summary',
721
+ name: 'Ayphic Master Capabilities Catalog',
722
+ description: 'Concise master catalog of all supported features, animations, filters, transitions, fonts, and tracks.',
723
+ mimeType: 'text/markdown'
724
+ }
725
+ ]
726
+ }
727
+ };
728
+
729
+ case 'resources/read': {
730
+ const uri = params?.uri;
731
+ if (uri === 'ayphic://timeline/active') {
732
+ const timelineResult = await executeAyphicTool('ayphic_get_timeline', {});
733
+ return {
734
+ jsonrpc: '2.0',
735
+ id,
736
+ result: {
737
+ contents: [
738
+ {
739
+ uri,
740
+ mimeType: 'application/json',
741
+ text: timelineResult.content?.[0]?.text || '{}'
742
+ }
743
+ ]
744
+ }
745
+ };
746
+ } else if (uri === 'ayphic://docs/capabilities') {
747
+ let content = '# Ayphic AI Video Editor Capabilities\nDocumentation not found on disk.';
748
+ if (fs.existsSync(DOCS_PATH)) {
749
+ content = fs.readFileSync(DOCS_PATH, 'utf8');
750
+ }
751
+ return {
752
+ jsonrpc: '2.0',
753
+ id,
754
+ result: {
755
+ contents: [
756
+ {
757
+ uri,
758
+ mimeType: 'text/markdown',
759
+ text: content
760
+ }
761
+ ]
762
+ }
763
+ };
764
+ } else if (uri === 'ayphic://docs/summary') {
765
+ const docResult = await executeAyphicTool('search_docs', { query: 'summary' });
766
+ const summaryText = docResult.content?.[0]?.text || '';
767
+ return {
768
+ jsonrpc: '2.0',
769
+ id,
770
+ result: {
771
+ contents: [
772
+ {
773
+ uri,
774
+ mimeType: 'text/markdown',
775
+ text: summaryText
776
+ }
777
+ ]
778
+ }
779
+ };
780
+ }
781
+ return {
782
+ jsonrpc: '2.0',
783
+ id,
784
+ error: {
785
+ code: -32602,
786
+ message: `Resource not found: ${uri}`
787
+ }
788
+ };
789
+ }
790
+
791
+ default:
792
+ if (id !== undefined && id !== null) {
793
+ return {
794
+ jsonrpc: '2.0',
795
+ id,
796
+ error: {
797
+ code: -32601,
798
+ message: `Method '${method}' not found`
799
+ }
800
+ };
801
+ }
802
+ return null;
803
+ }
804
+ }
805
+
806
+ /**
807
+ * Main stdio loop
808
+ */
809
+ function startStdioServer() {
810
+ const rl = readline.createInterface({
811
+ input: process.stdin,
812
+ terminal: false
813
+ });
814
+
815
+ rl.on('line', async (line) => {
816
+ const trimmed = line.trim();
817
+ if (!trimmed) return;
818
+
819
+ try {
820
+ const parsed = JSON.parse(trimmed);
821
+ const response = await handleRpcMessage(parsed);
822
+ if (response) {
823
+ process.stdout.write(JSON.stringify(response) + '\n');
824
+ }
825
+ } catch (parseErr) {
826
+ process.stdout.write(JSON.stringify({
827
+ jsonrpc: '2.0',
828
+ id: null,
829
+ error: {
830
+ code: -32700,
831
+ message: `Parse error: ${parseErr.message}`
832
+ }
833
+ }) + '\n');
834
+ }
835
+ });
836
+ }
837
+
838
+ startStdioServer();
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "ayphic-mcp-server",
3
+ "version": "1.1.0",
4
+ "description": "Model Context Protocol (MCP) Server for Ayphic AI Video Editor",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "ayphic-mcp": "./index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "claude",
17
+ "video-editor",
18
+ "ayphic"
19
+ ],
20
+ "author": "Ayphic",
21
+ "license": "MIT"
22
+ }