@writepanda/mcp 1.9.5 → 1.11.1

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 CHANGED
@@ -48,17 +48,20 @@ After adding, restart your client. The MCP server auto-launches PandaStudio if i
48
48
 
49
49
  ## What the agent gets
50
50
 
51
- 30+ tools covering the full PandaStudio editorial surface:
51
+ 55 tools covering the full PandaStudio editorial surface — complete UI parity:
52
52
 
53
53
  | Category | Tools |
54
54
  |---|---|
55
55
  | Discovery | `system_status`, `system_list_commands` |
56
- | Project lifecycle | `project_list`, `project_show`, `project_read`, `project_new`, `project_open` |
57
- | Composition | `project_add_clip`, `project_add_motion_graphic`, `project_add_fx`, `project_add_lower_third`, `project_add_zoom`, `project_add_trim`, `project_add_speed` |
58
- | Transcript editing | `transcript_transcribe`, `transcript_get`, `transcript_delete_words`, `transcript_remove_fillers`, `transcript_search` |
56
+ | Project lifecycle | `project_list`, `project_show`, `project_read`, `project_new`, `project_open`, `project_save`, `project_delete` |
57
+ | Clips | `project_add_clip`, `project_remove_clip`, `project_split_clip` |
58
+ | Composition | `project_add_motion_graphic`, `project_add_fx`, `project_add_lower_third`, `project_add_zoom`, `project_add_trim`, `project_add_speed`, `project_add_annotation` |
59
+ | Region editing | `project_remove_region`, `project_update_region` |
60
+ | Canvas & style | `project_set_aspect_ratio`, `project_set_wallpaper`, `project_set_style`, `project_set_crop`, `project_set_webcam_layout`, `project_set_export_settings` |
61
+ | Transcript editing | `transcript_transcribe`, `transcript_get`, `transcript_delete_words`, `transcript_remove_fillers`, `transcript_search`, `transcript_find_replace`, `transcript_remove_silences` |
59
62
  | Audio | `audio_clean` (DeepFilter denoising) |
60
- | Captions | `caption_toggle`, `caption_set_template` |
61
- | Motion graphics | `motion_list`, `motion_themes`, `motion_generate` |
63
+ | Captions | `caption_toggle`, `caption_set_template`, `caption_set_style` |
64
+ | Motion graphics | `motion_list`, `motion_themes`, `motion_generate`, `motion_render_html` |
62
65
  | Assets | `asset_list_sounds`, `asset_list_fx` |
63
66
  | AI metadata | `llm_generate_title`, `llm_generate_description`, `llm_generate_timestamps` |
64
67
  | Export | `export_start`, `export_list` |
package/bin/server.mjs CHANGED
@@ -187,7 +187,7 @@ const TOOLS = [
187
187
  {
188
188
  name: "project_read",
189
189
  description:
190
- "Read a project's full JSON. Returns { path, project }. Pass `project.revision` back as `expectedRevision` on subsequent writes for conflict-safe edits.",
190
+ "Read a project's full JSON. Returns { path, project, clipStates }. `clipStates` is a per-clip summary: { clipId, mediaPath, durationMs, transcribed, wordCount, audioCleaned, cleanedAudioPath? } — use it to decide whether to call transcript_transcribe or audio_clean before editing. Pass `project.revision` back as `expectedRevision` on subsequent writes for conflict-safe edits.",
191
191
  inputSchema: {
192
192
  type: "object",
193
193
  properties: {
@@ -232,13 +232,19 @@ const TOOLS = [
232
232
  // ── compose: clips, motion graphics, FX, lower-thirds ──────────
233
233
  {
234
234
  name: "project_add_clip",
235
- description: "Append a video clip to the project's main track. Probes duration via FFmpeg.",
235
+ description:
236
+ "Append (or insert) a video clip into the project's main track. Probes duration via FFmpeg.",
236
237
  inputSchema: {
237
238
  type: "object",
238
239
  properties: {
239
240
  id: { type: "string" },
240
241
  path: { type: "string" },
241
242
  media: { type: "string", description: "Absolute path to video file" },
243
+ atIndex: {
244
+ type: "number",
245
+ description:
246
+ "Insert before this zero-based clip index. Omit to append at the end.",
247
+ },
242
248
  expectedRevision: { type: "number" },
243
249
  },
244
250
  required: ["media"],
@@ -301,7 +307,18 @@ const TOOLS = [
301
307
  description:
302
308
  "name-bar | slash-reveal | center-stack | minimal-underline | box-reveal | corner-brackets | border-frame | split-horizontal",
303
309
  },
304
- accentColor: { type: "string" },
310
+ accentColor: { type: "string", description: "Hex accent/background color" },
311
+ textColor: { type: "string", description: "Hex text color. Default #ffffff" },
312
+ backgroundColor: {
313
+ type: "string",
314
+ description: "Hex background fill. Default matches accentColor",
315
+ },
316
+ backgroundRadius: {
317
+ type: "number",
318
+ description: "Corner radius in pixels. Default 8",
319
+ },
320
+ fontSize: { type: "number", description: "Primary text font size in px. Default 28" },
321
+ fontFamily: { type: "string", description: "CSS font family name. Default Inter" },
305
322
  expectedRevision: { type: "number" },
306
323
  },
307
324
  required: ["content", "atMs"],
@@ -364,6 +381,309 @@ const TOOLS = [
364
381
  command: "project.add-speed",
365
382
  },
366
383
 
384
+ {
385
+ name: "project_add_annotation",
386
+ description:
387
+ "Drop a text or figure annotation overlay on the canvas for a given time range. Position and size are in percent (0-100) of the canvas dimensions.",
388
+ inputSchema: {
389
+ type: "object",
390
+ properties: {
391
+ id: { type: "string" },
392
+ path: { type: "string" },
393
+ startMs: { type: "number" },
394
+ endMs: { type: "number" },
395
+ type: {
396
+ type: "string",
397
+ description: "text | figure",
398
+ },
399
+ text: { type: "string", description: "Required when type is 'text'" },
400
+ x: { type: "number", description: "Horizontal center, percent 0-100. Default 50." },
401
+ y: { type: "number", description: "Vertical center, percent 0-100. Default 50." },
402
+ width: { type: "number", description: "Width, percent 0-100. Default 30." },
403
+ height: { type: "number", description: "Height, percent 0-100. Default 20." },
404
+ expectedRevision: { type: "number" },
405
+ },
406
+ required: ["startMs", "endMs", "type"],
407
+ },
408
+ command: "project.add-annotation",
409
+ },
410
+
411
+ // ── project lifecycle (extended) ───────────────────────────────
412
+ {
413
+ name: "project_save",
414
+ description:
415
+ "Overwrite a project with the full JSON you supply. Use this when you've built or mutated a project object client-side and want to persist it. Always pass `expectedRevision` (the revision you read) — the server will reject the write if a concurrent edit happened between your read and save, returning { ok: false, details: { code: 'revision_conflict' } }.",
416
+ inputSchema: {
417
+ type: "object",
418
+ properties: {
419
+ id: { type: "string" },
420
+ path: { type: "string" },
421
+ project: { type: "object", description: "Full project JSON to persist" },
422
+ expectedRevision: {
423
+ type: "number",
424
+ description: "Revision from your last project_read. Strongly recommended.",
425
+ },
426
+ },
427
+ required: ["project"],
428
+ },
429
+ command: "project.save",
430
+ },
431
+ {
432
+ name: "project_delete",
433
+ description:
434
+ "Permanently delete a project file from disk. There is no trash — this is irreversible. Use project_list to confirm the id before deleting.",
435
+ inputSchema: {
436
+ type: "object",
437
+ properties: {
438
+ id: { type: "string" },
439
+ path: { type: "string" },
440
+ },
441
+ },
442
+ command: "project.delete",
443
+ },
444
+ {
445
+ name: "project_remove_clip",
446
+ description:
447
+ "Remove a clip from the main track by its clip id. Use project_read → clipStates to find clip ids. All regions whose time falls within the removed clip are also removed.",
448
+ inputSchema: {
449
+ type: "object",
450
+ properties: {
451
+ id: { type: "string" },
452
+ path: { type: "string" },
453
+ clipId: {
454
+ type: "string",
455
+ description: "Clip id to remove (e.g. clip-1). From clipStates in project_read.",
456
+ },
457
+ expectedRevision: { type: "number" },
458
+ },
459
+ required: ["clipId"],
460
+ },
461
+ command: "project.remove-clip",
462
+ },
463
+ {
464
+ name: "project_split_clip",
465
+ description:
466
+ "Split a clip in two at a position in source time (milliseconds from the start of that clip). Produces two clips: the portion before the split point and the portion after. Useful for inserting a motion graphic between two halves of a recording.",
467
+ inputSchema: {
468
+ type: "object",
469
+ properties: {
470
+ id: { type: "string" },
471
+ path: { type: "string" },
472
+ clipId: { type: "string", description: "Clip to split" },
473
+ atSourceMs: {
474
+ type: "number",
475
+ description: "Split position in milliseconds from the clip's own start (source time)",
476
+ },
477
+ expectedRevision: { type: "number" },
478
+ },
479
+ required: ["clipId", "atSourceMs"],
480
+ },
481
+ command: "project.split-clip",
482
+ },
483
+
484
+ // ── compose: edit existing regions ─────────────────────────────
485
+ {
486
+ name: "project_remove_region",
487
+ description:
488
+ "Delete an existing region (zoom, trim, speed, annotation, fx, lower-third, overlay) by its UUID. Use project_read to find region IDs. Returns the updated project.",
489
+ inputSchema: {
490
+ type: "object",
491
+ properties: {
492
+ id: { type: "string" },
493
+ path: { type: "string" },
494
+ regionType: {
495
+ type: "string",
496
+ description: "zoom | trim | speed | annotation | fx | lower-third | overlay",
497
+ },
498
+ regionId: { type: "string", description: "UUID of the region to delete" },
499
+ expectedRevision: { type: "number" },
500
+ },
501
+ required: ["regionType", "regionId"],
502
+ },
503
+ command: "project.remove-region",
504
+ },
505
+ {
506
+ name: "project_update_region",
507
+ description:
508
+ "Patch fields on an existing region without replacing it. Accepts any subset of the region's own properties — only the supplied fields are changed, everything else is preserved. Works on zoom (depth, focusX, focusY), trim (startMs, endMs), speed (startMs, endMs, speed), lower-third (content, subtitle, atMs, durationMs, designType, accentColor, textColor, backgroundColor, backgroundRadius, fontSize, fontFamily), fx (atMs, durationMs), annotation, and overlay regions.",
509
+ inputSchema: {
510
+ type: "object",
511
+ properties: {
512
+ id: { type: "string" },
513
+ path: { type: "string" },
514
+ regionType: {
515
+ type: "string",
516
+ description: "zoom | trim | speed | annotation | fx | lower-third | overlay",
517
+ },
518
+ regionId: { type: "string", description: "UUID of the region to update" },
519
+ patch: {
520
+ type: "object",
521
+ description:
522
+ "Fields to update (partial). E.g. { depth: 4 } to change zoom depth, or { content: 'New Name' } on a lower-third.",
523
+ },
524
+ expectedRevision: { type: "number" },
525
+ },
526
+ required: ["regionType", "regionId", "patch"],
527
+ },
528
+ command: "project.update-region",
529
+ },
530
+
531
+ // ── compose: webcam + crop + export settings ────────────────────
532
+ {
533
+ name: "project_set_webcam_layout",
534
+ description:
535
+ "Set the webcam layout preset and optional position/scale for the current project. Use 'none' to hide the webcam overlay entirely.",
536
+ inputSchema: {
537
+ type: "object",
538
+ properties: {
539
+ id: { type: "string" },
540
+ path: { type: "string" },
541
+ preset: {
542
+ type: "string",
543
+ description:
544
+ "picture-in-picture | vertical-stack | side-by-side | none. 'none' hides the webcam.",
545
+ },
546
+ cx: {
547
+ type: "number",
548
+ description:
549
+ "Horizontal center of the webcam overlay, normalised 0-1. Only for picture-in-picture.",
550
+ },
551
+ cy: {
552
+ type: "number",
553
+ description:
554
+ "Vertical center of the webcam overlay, normalised 0-1. Only for picture-in-picture.",
555
+ },
556
+ scale: {
557
+ type: "number",
558
+ description: "Size multiplier for the webcam overlay. Default 1.0.",
559
+ },
560
+ cropX: { type: "number", description: "Webcam crop left edge, normalised 0-1." },
561
+ cropY: { type: "number", description: "Webcam crop top edge, normalised 0-1." },
562
+ cropWidth: { type: "number", description: "Webcam crop width, normalised 0-1." },
563
+ cropHeight: { type: "number", description: "Webcam crop height, normalised 0-1." },
564
+ expectedRevision: { type: "number" },
565
+ },
566
+ required: ["preset"],
567
+ },
568
+ command: "project.set-webcam-layout",
569
+ },
570
+ {
571
+ name: "project_set_crop",
572
+ description:
573
+ "Set the main video crop region for the project. All values are normalised 0-1 relative to the source frame dimensions. Default is { x:0, y:0, width:1, height:1 } (no crop).",
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: {
577
+ id: { type: "string" },
578
+ path: { type: "string" },
579
+ x: { type: "number", description: "Left edge, normalised 0-1" },
580
+ y: { type: "number", description: "Top edge, normalised 0-1" },
581
+ width: { type: "number", description: "Crop width, normalised 0-1" },
582
+ height: { type: "number", description: "Crop height, normalised 0-1" },
583
+ expectedRevision: { type: "number" },
584
+ },
585
+ required: ["x", "y", "width", "height"],
586
+ },
587
+ command: "project.set-crop",
588
+ },
589
+ {
590
+ name: "project_set_export_settings",
591
+ description:
592
+ "Pre-configure the project's export defaults (quality, format, resolution). These are the settings the Export dialog opens with — they don't affect the CLI export_start quality flag.",
593
+ inputSchema: {
594
+ type: "object",
595
+ properties: {
596
+ id: { type: "string" },
597
+ path: { type: "string" },
598
+ quality: {
599
+ type: "string",
600
+ description: "medium | good | source. Controls CRF/bitrate preset.",
601
+ },
602
+ format: {
603
+ type: "string",
604
+ description: "mp4 | webm | gif. Default mp4.",
605
+ },
606
+ resolution: {
607
+ type: "string",
608
+ description:
609
+ "Output resolution: 4k | 1080p | 720p | 480p | source. Default source.",
610
+ },
611
+ gifFrameRate: {
612
+ type: "number",
613
+ description: "GIF frame rate (fps). Only used when format is gif. Default 15.",
614
+ },
615
+ gifSize: {
616
+ type: "string",
617
+ description:
618
+ "GIF size preset: small | medium | large. Only used when format is gif.",
619
+ },
620
+ expectedRevision: { type: "number" },
621
+ },
622
+ },
623
+ command: "project.set-export-settings",
624
+ },
625
+
626
+ {
627
+ name: "project_set_aspect_ratio",
628
+ description:
629
+ "Change the project's output aspect ratio. Affects the canvas size and how the video is cropped/letterboxed on export. Pick based on the target platform: 16:9 for YouTube/desktop, 9:16 for Shorts/Reels/TikTok, 1:1 for Instagram feed.",
630
+ inputSchema: {
631
+ type: "object",
632
+ properties: {
633
+ id: { type: "string" },
634
+ path: { type: "string" },
635
+ ratio: {
636
+ type: "string",
637
+ description: "16:9 | 9:16 | 1:1 | 4:3 | 3:4",
638
+ },
639
+ expectedRevision: { type: "number" },
640
+ },
641
+ required: ["ratio"],
642
+ },
643
+ command: "project.set-aspect-ratio",
644
+ },
645
+ {
646
+ name: "project_set_wallpaper",
647
+ description:
648
+ "Set the project's background wallpaper. Pass a bundled wallpaper id (e.g. 'gradient-blue', 'dark-mesh') or 'none' to remove the background. The available wallpaper ids match the filenames in the app's /public/wallpapers/ directory.",
649
+ inputSchema: {
650
+ type: "object",
651
+ properties: {
652
+ id: { type: "string" },
653
+ path: { type: "string" },
654
+ wallpaper: {
655
+ type: "string",
656
+ description: "Bundled wallpaper id or 'none' to remove background",
657
+ },
658
+ expectedRevision: { type: "number" },
659
+ },
660
+ required: ["wallpaper"],
661
+ },
662
+ command: "project.set-wallpaper",
663
+ },
664
+ {
665
+ name: "project_set_style",
666
+ description:
667
+ "Adjust the project's cinematic framing style: padding around the recording (0-200), drop shadow intensity (0-100), border radius (0-100), motion blur amount (0-100), and whether to show blur behind the recording. Pass only the fields you want to change — others are left as-is.",
668
+ inputSchema: {
669
+ type: "object",
670
+ properties: {
671
+ id: { type: "string" },
672
+ path: { type: "string" },
673
+ padding: { type: "number", description: "Padding around the recording in px. 0-200." },
674
+ shadowIntensity: { type: "number", description: "Drop shadow strength. 0-100." },
675
+ borderRadius: { type: "number", description: "Corner rounding. 0-100." },
676
+ motionBlurAmount: { type: "number", description: "Motion blur strength. 0-100." },
677
+ showBlur: {
678
+ type: "boolean",
679
+ description: "Whether to show a blurred background behind the recording.",
680
+ },
681
+ expectedRevision: { type: "number" },
682
+ },
683
+ },
684
+ command: "project.set-style",
685
+ },
686
+
367
687
  // ── transcript-based editing ────────────────────────────────────
368
688
  {
369
689
  name: "transcript_transcribe",
@@ -442,6 +762,41 @@ const TOOLS = [
442
762
  },
443
763
  command: "transcript.search",
444
764
  },
765
+ {
766
+ name: "transcript_find_replace",
767
+ description:
768
+ "Find the first occurrence of a phrase in the transcript and replace it with new text. Useful for correcting mis-transcribed names, technical terms, or brand names without re-transcribing. Returns { matched, replacements } — matched is false if the phrase wasn't found.",
769
+ inputSchema: {
770
+ type: "object",
771
+ properties: {
772
+ id: { type: "string" },
773
+ path: { type: "string" },
774
+ find: { type: "string", description: "Phrase to search for (case-insensitive)" },
775
+ replace: { type: "string", description: "Replacement text (replaces the whole phrase)" },
776
+ expectedRevision: { type: "number" },
777
+ },
778
+ required: ["find", "replace"],
779
+ },
780
+ command: "transcript.find-replace",
781
+ },
782
+ {
783
+ name: "transcript_remove_silences",
784
+ description:
785
+ "Detect and trim silent gaps in the recording — leading silence before the first word, trailing silence after the last word, and inter-word gaps longer than `thresholdMs`. Bulk-creates trim regions. Returns { removed, totalTrimmedMs }.",
786
+ inputSchema: {
787
+ type: "object",
788
+ properties: {
789
+ id: { type: "string" },
790
+ path: { type: "string" },
791
+ thresholdMs: {
792
+ type: "number",
793
+ description: "Gaps longer than this are trimmed. Default 700ms.",
794
+ },
795
+ expectedRevision: { type: "number" },
796
+ },
797
+ },
798
+ command: "transcript.remove-silences",
799
+ },
445
800
 
446
801
  // ── audio ───────────────────────────────────────────────────────
447
802
  {
@@ -492,6 +847,39 @@ const TOOLS = [
492
847
  command: "caption.set-template",
493
848
  },
494
849
 
850
+ {
851
+ name: "caption_set_style",
852
+ description:
853
+ "Override specific style properties of the caption overlay without changing the template. All fields are optional — pass only what you want to change. positionY controls vertical placement (0=top, 100=bottom, default 85). fontSize accepts CSS values like '28px' or '1.5em'.",
854
+ inputSchema: {
855
+ type: "object",
856
+ properties: {
857
+ id: { type: "string" },
858
+ path: { type: "string" },
859
+ wordsPerLine: {
860
+ type: "number",
861
+ description: "Max words shown at once. Default varies by template.",
862
+ },
863
+ positionY: { type: "number", description: "Vertical position 0-100. Default 85." },
864
+ color: { type: "string", description: "Caption text color (hex/CSS)" },
865
+ backgroundColor: { type: "string", description: "Caption background color" },
866
+ highlightColor: {
867
+ type: "string",
868
+ description: "Color of the currently-spoken word highlight",
869
+ },
870
+ highlightBackgroundColor: {
871
+ type: "string",
872
+ description: "Background of the currently-spoken word",
873
+ },
874
+ strokeColor: { type: "string", description: "Text stroke/outline color" },
875
+ strokeWidth: { type: "number", description: "Text stroke width in px" },
876
+ fontSize: { type: "string", description: "CSS font-size value, e.g. '28px'" },
877
+ expectedRevision: { type: "number" },
878
+ },
879
+ },
880
+ command: "caption.set-style",
881
+ },
882
+
495
883
  // ── motion graphics ─────────────────────────────────────────────
496
884
  {
497
885
  name: "motion_list",
@@ -719,7 +1107,7 @@ const TOOLS = [
719
1107
  const server = new Server(
720
1108
  {
721
1109
  name: "pandastudio",
722
- version: "1.9.5",
1110
+ version: "1.11.0",
723
1111
  },
724
1112
  {
725
1113
  capabilities: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.9.5",
3
+ "version": "1.11.1",
4
4
  "description": "Model Context Protocol server for PandaStudio. Exposes the desktop video editor's automation surface to Cursor, Continue, Cline, Claude Desktop, and any MCP-compliant client.",
5
5
  "keywords": [
6
6
  "pandastudio",