@writepanda/mcp 1.9.4 → 1.11.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/bin/server.mjs +458 -23
  2. package/package.json +1 -1
package/bin/server.mjs CHANGED
@@ -34,10 +34,7 @@ import path from "node:path";
34
34
  import process from "node:process";
35
35
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
36
36
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
37
- import {
38
- CallToolRequestSchema,
39
- ListToolsRequestSchema,
40
- } from "@modelcontextprotocol/sdk/types.js";
37
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
41
38
 
42
39
  // ── Credentials + transport ───────────────────────────────────────────
43
40
 
@@ -176,7 +173,8 @@ const TOOLS = [
176
173
  },
177
174
  {
178
175
  name: "project_show",
179
- description: "Resolve a project's path + summary metadata by id or path. Cheaper than project_read.",
176
+ description:
177
+ "Resolve a project's path + summary metadata by id or path. Cheaper than project_read.",
180
178
  inputSchema: {
181
179
  type: "object",
182
180
  properties: {
@@ -189,7 +187,7 @@ const TOOLS = [
189
187
  {
190
188
  name: "project_read",
191
189
  description:
192
- "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.",
193
191
  inputSchema: {
194
192
  type: "object",
195
193
  properties: {
@@ -219,7 +217,8 @@ const TOOLS = [
219
217
  },
220
218
  {
221
219
  name: "project_open",
222
- description: "Open the desktop editor focused on a specific project (so the user can take over).",
220
+ description:
221
+ "Open the desktop editor focused on a specific project (so the user can take over).",
223
222
  inputSchema: {
224
223
  type: "object",
225
224
  properties: {
@@ -233,13 +232,19 @@ const TOOLS = [
233
232
  // ── compose: clips, motion graphics, FX, lower-thirds ──────────
234
233
  {
235
234
  name: "project_add_clip",
236
- 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.",
237
237
  inputSchema: {
238
238
  type: "object",
239
239
  properties: {
240
240
  id: { type: "string" },
241
241
  path: { type: "string" },
242
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
+ },
243
248
  expectedRevision: { type: "number" },
244
249
  },
245
250
  required: ["media"],
@@ -272,7 +277,10 @@ const TOOLS = [
272
277
  properties: {
273
278
  id: { type: "string" },
274
279
  path: { type: "string" },
275
- fxId: { type: "string", description: "Bundled FX id (e.g. film-burn). Use asset_list_fx to discover." },
280
+ fxId: {
281
+ type: "string",
282
+ description: "Bundled FX id (e.g. film-burn). Use asset_list_fx to discover.",
283
+ },
276
284
  src: { type: "string", description: "Direct path/URL (alternative to fxId)" },
277
285
  atMs: { type: "number" },
278
286
  durationMs: { type: "number", description: "Default 2500ms for bundled FX" },
@@ -296,9 +304,21 @@ const TOOLS = [
296
304
  durationMs: { type: "number", description: "Default 5000ms" },
297
305
  designType: {
298
306
  type: "string",
299
- description: "name-bar | slash-reveal | center-stack | minimal-underline | box-reveal | corner-brackets | border-frame | split-horizontal",
307
+ description:
308
+ "name-bar | slash-reveal | center-stack | minimal-underline | box-reveal | corner-brackets | border-frame | split-horizontal",
309
+ },
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",
300
315
  },
301
- accentColor: { type: "string" },
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" },
302
322
  expectedRevision: { type: "number" },
303
323
  },
304
324
  required: ["content", "atMs"],
@@ -309,7 +329,8 @@ const TOOLS = [
309
329
  // ── compose: visual regions ─────────────────────────────────────
310
330
  {
311
331
  name: "project_add_zoom",
312
- description: "Add a zoom region — highlight a UI moment. Typical pattern: find 'click X' in transcript, drop zoom there.",
332
+ description:
333
+ "Add a zoom region — highlight a UI moment. Typical pattern: find 'click X' in transcript, drop zoom there.",
313
334
  inputSchema: {
314
335
  type: "object",
315
336
  properties: {
@@ -360,6 +381,309 @@ const TOOLS = [
360
381
  command: "project.add-speed",
361
382
  },
362
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
+
363
687
  // ── transcript-based editing ────────────────────────────────────
364
688
  {
365
689
  name: "transcript_transcribe",
@@ -410,7 +734,8 @@ const TOOLS = [
410
734
  },
411
735
  {
412
736
  name: "transcript_remove_fillers",
413
- description: "Auto-detect filler words ('um', 'uh', 'like', 'you know', ...) plus immediately-repeated words and bulk-trim them.",
737
+ description:
738
+ "Auto-detect filler words ('um', 'uh', 'like', 'you know', ...) plus immediately-repeated words and bulk-trim them.",
414
739
  inputSchema: {
415
740
  type: "object",
416
741
  properties: {
@@ -424,7 +749,8 @@ const TOOLS = [
424
749
  },
425
750
  {
426
751
  name: "transcript_search",
427
- description: "Find every occurrence of a phrase in the merged transcript. Returns matches with their word IDs.",
752
+ description:
753
+ "Find every occurrence of a phrase in the merged transcript. Returns matches with their word IDs.",
428
754
  inputSchema: {
429
755
  type: "object",
430
756
  properties: {
@@ -436,6 +762,41 @@ const TOOLS = [
436
762
  },
437
763
  command: "transcript.search",
438
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
+ },
439
800
 
440
801
  // ── audio ───────────────────────────────────────────────────────
441
802
  {
@@ -471,7 +832,8 @@ const TOOLS = [
471
832
  },
472
833
  {
473
834
  name: "caption_set_template",
474
- description: "Pick a caption template: classic, modern, minimal, bold, spotlight, boxed, neon, or colored.",
835
+ description:
836
+ "Pick a caption template: classic, modern, minimal, bold, spotlight, boxed, neon, or colored.",
475
837
  inputSchema: {
476
838
  type: "object",
477
839
  properties: {
@@ -485,16 +847,51 @@ const TOOLS = [
485
847
  command: "caption.set-template",
486
848
  },
487
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
+
488
883
  // ── motion graphics ─────────────────────────────────────────────
489
884
  {
490
885
  name: "motion_list",
491
- description: "List every motion-graphic template (id, name, slots, defaults, aspectRatios). 19 templates ship today.",
886
+ description:
887
+ "List every motion-graphic template (id, name, slots, defaults, aspectRatios). 19 templates ship today.",
492
888
  inputSchema: { type: "object", properties: {} },
493
889
  command: "motion.list",
494
890
  },
495
891
  {
496
892
  name: "motion_themes",
497
- description: "List every style-pack theme (color overrides). default, mr-beast, mkbhd, kurzgesagt, veritasium.",
893
+ description:
894
+ "List every style-pack theme (color overrides). default, mr-beast, mkbhd, kurzgesagt, veritasium.",
498
895
  inputSchema: { type: "object", properties: {} },
499
896
  command: "motion.themes",
500
897
  },
@@ -514,6 +911,34 @@ const TOOLS = [
514
911
  },
515
912
  command: "motion.generate",
516
913
  },
914
+ {
915
+ name: "motion_render_html",
916
+ description:
917
+ "Render arbitrary HTML/CSS/JS to an MP4. Use this when the bundled motion-graphic templates don't fit the brief — Claude can author full animations and we'll render them. Pass either inline `html` or a `htmlPath`. The HTML loads in a Chromium offscreen window at the requested dimensions; we capture frames for durationMs starting after the first paint. Animations should auto-start on DOMContentLoaded (CSS animations, GSAP, Lottie via CDN, anything Chromium can render). Async — returns { jobId, outputPath }; poll job_wait for the final MP4.",
918
+ inputSchema: {
919
+ type: "object",
920
+ properties: {
921
+ html: {
922
+ type: "string",
923
+ description: "Inline HTML string (mutually exclusive with htmlPath).",
924
+ },
925
+ htmlPath: {
926
+ type: "string",
927
+ description: "Absolute path to .html file (mutually exclusive with html).",
928
+ },
929
+ aspectRatio: {
930
+ type: "string",
931
+ description: "16:9 (default) | 9:16 | 1:1. Ignored if width+height supplied.",
932
+ },
933
+ width: { type: "number" },
934
+ height: { type: "number" },
935
+ durationMs: { type: "number", description: "How long to capture. Default 2500." },
936
+ frameRate: { type: "number", description: "Default 30." },
937
+ outputName: { type: "string", description: "Optional filename stem." },
938
+ },
939
+ },
940
+ command: "motion.render-html",
941
+ },
517
942
 
518
943
  // ── assets ──────────────────────────────────────────────────────
519
944
  {
@@ -524,7 +949,8 @@ const TOOLS = [
524
949
  },
525
950
  {
526
951
  name: "asset_list_fx",
527
- description: "List every bundled FX video overlay (id, title, blendMode, defaults, absolute path).",
952
+ description:
953
+ "List every bundled FX video overlay (id, title, blendMode, defaults, absolute path).",
528
954
  inputSchema: { type: "object", properties: {} },
529
955
  command: "asset.list-fx",
530
956
  },
@@ -532,7 +958,8 @@ const TOOLS = [
532
958
  // ── project-aware LLM ──────────────────────────────────────────
533
959
  {
534
960
  name: "llm_generate_title",
535
- description: "Generate a YouTube-ready title from the project's merged transcript using the bundled local LLM.",
961
+ description:
962
+ "Generate a YouTube-ready title from the project's merged transcript using the bundled local LLM.",
536
963
  inputSchema: {
537
964
  type: "object",
538
965
  properties: {
@@ -580,7 +1007,10 @@ const TOOLS = [
580
1007
  properties: {
581
1008
  id: { type: "string" },
582
1009
  path: { type: "string" },
583
- outputPath: { type: "string", description: "Where to write the MP4. Default: recordings dir" },
1010
+ outputPath: {
1011
+ type: "string",
1012
+ description: "Where to write the MP4. Default: recordings dir",
1013
+ },
584
1014
  quality: { type: "string", description: "draft | standard | high | ultra. Default high" },
585
1015
  },
586
1016
  },
@@ -635,7 +1065,10 @@ const TOOLS = [
635
1065
  type: "object",
636
1066
  properties: {
637
1067
  id: { type: "string", description: "Job id from the async tool's response" },
638
- timeoutMs: { type: "number", description: "Max wait. Default 60_000, hard-capped at 300_000" },
1068
+ timeoutMs: {
1069
+ type: "number",
1070
+ description: "Max wait. Default 60_000, hard-capped at 300_000",
1071
+ },
639
1072
  },
640
1073
  required: ["id"],
641
1074
  },
@@ -674,7 +1107,7 @@ const TOOLS = [
674
1107
  const server = new Server(
675
1108
  {
676
1109
  name: "pandastudio",
677
- version: "1.9.3",
1110
+ version: "1.11.0",
678
1111
  },
679
1112
  {
680
1113
  capabilities: {
@@ -712,7 +1145,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
712
1145
  dispatchArgs = args.args ?? {};
713
1146
  if (typeof command !== "string" || !command.includes(".")) {
714
1147
  return {
715
- content: [{ type: "text", text: "pandastudio_call requires `command` of the form 'verb.noun'" }],
1148
+ content: [
1149
+ { type: "text", text: "pandastudio_call requires `command` of the form 'verb.noun'" },
1150
+ ],
716
1151
  isError: true,
717
1152
  };
718
1153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.9.4",
3
+ "version": "1.11.0",
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",