@writepanda/mcp 1.275.0 → 1.284.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 +161 -12
  2. package/package.json +1 -1
package/bin/server.mjs CHANGED
@@ -53,7 +53,6 @@ import {
53
53
  IN_APP_UNAVAILABLE,
54
54
  isInAppCaller,
55
55
  } from "./appConnection.mjs";
56
- import { frameUntrusted } from "./untrusted.mjs";
57
56
  import {
58
57
  APP_TOOLS,
59
58
  clientSupportsUi,
@@ -63,6 +62,7 @@ import {
63
62
  toolUiMeta,
64
63
  } from "./apps/index.mjs";
65
64
  import { portableSchema } from "./portableSchema.mjs";
65
+ import { frameUntrusted } from "./untrusted.mjs";
66
66
 
67
67
  // ── Credentials + transport ───────────────────────────────────────────
68
68
  //
@@ -1032,7 +1032,7 @@ const TOOLS = [
1032
1032
  {
1033
1033
  name: "connector_list",
1034
1034
  description:
1035
- 'List MCP connectors and their state. If the one needed isn\'t connected, tell the user its howToConnect line; you cannot connect connectors. status "connected, last check failed" (connected:false, lastCheck with reason, host and fix) means PandaStudio\'s own last use of it failed. needsReconnect:true (state needs_auth, status "needs sign-in again", HTTP 401) = the service refused its sign-in (e.g. a stale Replicate API token): nothing on it runs until the user presses Reconnect in Settings; relay howToConnect, don\'t retry. auth "api-key" + needsKey: the user must paste a key in Settings (never ask for it in chat).',
1035
+ 'List MCP connectors and their state. If the one needed isn\'t connected, tell the user its howToConnect line; you cannot connect connectors. status "connected, last check failed" (connected:false, lastCheck with reason, host and fix) means PandaStudio\'s own last use of it failed. needsReconnect:true (state needs_auth, status "needs sign-in again", HTTP 401) = the service refused its sign-in (e.g. a stale Replicate API token): nothing on it runs until the user presses Reconnect in Settings; relay howToConnect, don\'t retry. auth "api-key" + needsKey: the user must paste a key in Settings (never ask for it in chat). native:true (fal): no connector_tools/connector_call; use its usedThrough tools (fal: media_generate_video).',
1036
1036
  inputSchema: {
1037
1037
  type: "object",
1038
1038
  properties: {
@@ -1648,6 +1648,34 @@ const TOOLS = [
1648
1648
  },
1649
1649
  command: "project.duplicate",
1650
1650
  },
1651
+ {
1652
+ name: "project_versions",
1653
+ description:
1654
+ "A project's earlier versions, newest first (a snapshot about every 10 min of editing, kept up to 30 days). For lost edits or 'go back to how it was'. Then project_open_version.",
1655
+ inputSchema: {
1656
+ type: "object",
1657
+ properties: {
1658
+ id: { type: "string" },
1659
+ path: { type: "string" },
1660
+ },
1661
+ },
1662
+ command: "project.versions",
1663
+ },
1664
+ {
1665
+ name: "project_open_version",
1666
+ description:
1667
+ "Bring back an earlier version (from project_versions) as a NEW project beside the original, named '<name> (<date, time>)'. Never changes the original. Returns { id, path, name, savedAt }.",
1668
+ inputSchema: {
1669
+ type: "object",
1670
+ properties: {
1671
+ id: { type: "string" },
1672
+ path: { type: "string" },
1673
+ versionId: { type: "string", description: "From project_versions" },
1674
+ },
1675
+ required: ["versionId"],
1676
+ },
1677
+ command: "project.open-version",
1678
+ },
1651
1679
  {
1652
1680
  name: "project_locate",
1653
1681
  description:
@@ -3005,6 +3033,11 @@ const TOOLS = [
3005
3033
  inputSchema: {
3006
3034
  type: "object",
3007
3035
  properties: {
3036
+ still: {
3037
+ type: "boolean",
3038
+ description:
3039
+ "The graphic is MEANT to hold still (no entrance, no motion): skip the frozen-render check, kept for re-renders. Never fake a slow drift instead.",
3040
+ },
3008
3041
  id: { type: "string" },
3009
3042
  path: { type: "string" },
3010
3043
  overlayId: { type: "string" },
@@ -4107,10 +4140,31 @@ const TOOLS = [
4107
4140
  },
4108
4141
  command: "project.detect-face",
4109
4142
  },
4143
+ {
4144
+ name: "project_review_moment",
4145
+ description:
4146
+ "LOOK AND LISTEN to a moment of the edit as the viewer gets it: a contact sheet of the frames around atMs (edited ms) or wordId, the EXPORTED mix of that stretch transcribed and compared with what the edit should play (extra = leftover of a cut word or line, missing = clipped word), whether each cut lands in quiet or speech, nearby edits and stutters, plus a plain verdict. Use after every cut / pause / insert fix BEFORE telling the user it's fixed, and when the user names a moment ('at 16 seconds'). ASYNC: { jobId }; job_wait.",
4147
+ inputSchema: {
4148
+ type: "object",
4149
+ properties: {
4150
+ id: { type: "string" },
4151
+ path: { type: "string" },
4152
+ atMs: { type: "number", description: "Edited ms at the centre. Or wordId." },
4153
+ wordId: { type: "string", description: "Centre on this word (transcript id or prefix)." },
4154
+ spanMs: {
4155
+ type: "number",
4156
+ description: "Each side of the centre, ms. Default 1500, max 5000.",
4157
+ },
4158
+ frames: { type: "number", description: "Frames on the sheet. Default 12, max 24." },
4159
+ listen: { type: "boolean", description: "Default true; false = frames and edits only." },
4160
+ },
4161
+ },
4162
+ command: "project.review-moment",
4163
+ },
4110
4164
  {
4111
4165
  name: "project_render_sheet",
4112
4166
  description:
4113
- "Render N frames across an edited-time range into one contact-sheet PNG (row-major). Returns { sheetPath, frames[{atMs, path}] }; sheetPath null = read frames[].path.",
4167
+ "Render N frames across an edited-time range into one contact-sheet PNG (row-major). Returns { sheetPath, frames[{atMs, path}] }; sheetPath null (see sheetWarning) = read frames[].path.",
4114
4168
  inputSchema: {
4115
4169
  type: "object",
4116
4170
  properties: {
@@ -4128,7 +4182,7 @@ const TOOLS = [
4128
4182
  {
4129
4183
  name: "project_apply_edit_plan",
4130
4184
  description:
4131
- 'Apply many trim / zoom / speed ops atomically (any invalid op aborts). Ops: {"op":"add-trim",startMs,endMs} | {"op":"add-zoom",atMs,durationMs,depth?,focusX?,focusY?,soundUrl?,anchorSourceMs?} | {"op":"add-speed",startMs,endMs,speed}.',
4185
+ 'Apply many trim / zoom / speed ops atomically (any invalid op aborts). Ops: {"op":"add-trim",startMs,endMs} (SOURCE ms) or by word {"op":"add-trim",afterWordId|fromWordId,beforeWordId|throughWordId}, edges snapped to the audio like project_add_trim | {"op":"add-zoom",atMs,durationMs,depth?,focusX?,focusY?,soundUrl?,anchorSourceMs?} | {"op":"add-speed",startMs,endMs,speed}.',
4132
4186
  inputSchema: {
4133
4187
  type: "object",
4134
4188
  properties: {
@@ -4348,17 +4402,32 @@ const TOOLS = [
4348
4402
  },
4349
4403
  {
4350
4404
  name: "project_add_trim",
4351
- description: "Mark a span the exporter skips (manual cut).",
4405
+ description:
4406
+ "Cut a span the exporter skips. Name the edges by WORD (afterWordId / fromWordId for the start, beforeWordId / throughWordId for the end): no ms maths, no clock mix-ups. Or startMs/endMs in SOURCE ms (a transcript word's sourceStartMs). Every edge snaps to the quietest point nearby without clipping a kept word; result.edges says where each landed and notes say when an edge inside a word cut it. Check the spot with project_review_moment before telling the user it's fixed.",
4352
4407
  inputSchema: {
4353
4408
  type: "object",
4354
4409
  properties: {
4355
4410
  id: { type: "string" },
4356
4411
  path: { type: "string" },
4357
- startMs: { type: "number" },
4358
- endMs: { type: "number" },
4412
+ startMs: { type: "number", description: "SOURCE ms. Or afterWordId / fromWordId." },
4413
+ endMs: { type: "number", description: "SOURCE ms. Or beforeWordId / throughWordId." },
4414
+ afterWordId: {
4415
+ type: "string",
4416
+ description: "Cut STARTS right after this word (kept). Instead of startMs.",
4417
+ },
4418
+ fromWordId: { type: "string", description: "Cut STARTS at this word (cut too)." },
4419
+ beforeWordId: {
4420
+ type: "string",
4421
+ description: "Cut ENDS right before this word (kept). Instead of endMs.",
4422
+ },
4423
+ throughWordId: { type: "string", description: "Cut ENDS after this word (cut too)." },
4424
+ snapToAudio: {
4425
+ type: "boolean",
4426
+ description:
4427
+ "Default true: edges snap to the quietest point nearby without clipping a kept word. false = exact ms.",
4428
+ },
4359
4429
  expectedRevision: { type: "number" },
4360
4430
  },
4361
- required: ["startMs", "endMs"],
4362
4431
  },
4363
4432
  command: "project.add-trim",
4364
4433
  },
@@ -4407,6 +4476,11 @@ const TOOLS = [
4407
4476
  inputSchema: {
4408
4477
  type: "object",
4409
4478
  properties: {
4479
+ snapToAudio: {
4480
+ type: "boolean",
4481
+ description:
4482
+ "lane main: default true, each edge snaps to the quietest point nearby without clipping a kept word (result.edges). false = exact.",
4483
+ },
4410
4484
  id: { type: "string", description: "Project id (or pass path)." },
4411
4485
  path: { type: "string", description: "Project file path (or pass id)." },
4412
4486
  startMs: { type: "number", description: "Gap start, edited ms." },
@@ -4838,10 +4912,25 @@ const TOOLS = [
4838
4912
  {
4839
4913
  name: "project_update_region",
4840
4914
  description:
4841
- "Patch a region; unspecified fields are kept. Put the fields in patch. Every type takes startMs, endMs (edited ms; trim and speed are source ms); a retimed region is re-anchored at its new place, so later cuts and editor reloads keep it there. Patchable: zoom (depth, focusX, focusY, followCursor, focusMode static|auto), trim (startMs, endMs), speed (speed, rampIn, rampOut, preservePitch, freezeMs, holdMs, reverse, reverseAudio), fx (opacity, blendMode, speed, zIndex), annotation (text = the words shown, content, textContent, x, y, width, height, fontSize, fontFamily, textColor, color, backgroundColor, fontWeight, fontStyle, textDecoration, textAlign, style, zIndex), overlay (x, y, width, height in canvas %, layer, opacity, blendMode, muted, volume, loop, motionBlur, zIndex), adjustment (blendMode, fadeIn, fadeOut), motion (motionBlur), clip-transform (preset, transitionMs, easing, bezier, cameraFit, backgroundColor), background-effect (mode, strength, backgroundImage, backgroundFit, outline, outlineWidth, outlineColor, outlineShadow, matteContract, matteFeather), audio-overlay (sourceStartMs, volume 0-2, fadeIn, fadeOut in ms, 0 or null removes; fadeInMs, fadeOutMs are aliases). A key the region type doesn't have is not applied: the result lists it in ignoredKeys with a warning, and the call fails when nothing in the patch applies.",
4915
+ "Patch a region; unspecified fields are kept. Put the fields in patch. Every type takes startMs, endMs (edited ms; trim and speed are source ms); a retimed region is re-anchored at its new place, so later cuts and editor reloads keep it there. Patchable: zoom (depth, focusX, focusY, followCursor, focusMode static|auto), trim (startMs, endMs, afterWordId, fromWordId, beforeWordId, throughWordId), speed (speed, rampIn, rampOut, preservePitch, freezeMs, holdMs, reverse, reverseAudio), fx (opacity, blendMode, speed, zIndex), annotation (text = the words shown, content, textContent, x, y, width, height, fontSize, fontFamily, textColor, color, backgroundColor, fontWeight, fontStyle, textDecoration, textAlign, style, zIndex), overlay (x, y, width, height in canvas %, layer, opacity, blendMode, muted, volume, loop, motionBlur, zIndex), adjustment (blendMode, fadeIn, fadeOut), motion (motionBlur), clip-transform (preset, transitionMs, easing, bezier, cameraFit, backgroundColor), background-effect (mode, strength, backgroundImage, backgroundFit, outline, outlineWidth, outlineColor, outlineShadow, matteContract, matteFeather), audio-overlay (sourceStartMs, volume 0-2, fadeIn, fadeOut in ms, 0 or null removes; fadeInMs, fadeOutMs are aliases). A key the region type doesn't have is not applied: the result lists it in ignoredKeys with a warning, and the call fails when nothing in the patch applies.",
4842
4916
  inputSchema: {
4843
4917
  type: "object",
4844
4918
  properties: {
4919
+ afterWordId: {
4920
+ type: "string",
4921
+ description: "Cut STARTS right after this word (kept). Instead of startMs.",
4922
+ },
4923
+ fromWordId: { type: "string", description: "Cut STARTS at this word (cut too)." },
4924
+ beforeWordId: {
4925
+ type: "string",
4926
+ description: "Cut ENDS right before this word (kept). Instead of endMs.",
4927
+ },
4928
+ throughWordId: { type: "string", description: "Cut ENDS after this word (cut too)." },
4929
+ snapToAudio: {
4930
+ type: "boolean",
4931
+ description:
4932
+ "Default true: edges snap to the quietest point nearby without clipping a kept word. false = exact ms.",
4933
+ },
4845
4934
  id: { type: "string" },
4846
4935
  loop: { type: "boolean", description: "overlay: loop, or hold the last frame." },
4847
4936
  path: { type: "string" },
@@ -5821,7 +5910,7 @@ const TOOLS = [
5821
5910
  {
5822
5911
  name: "transcript_transcribe",
5823
5912
  description:
5824
- "Transcribe clips with word timestamps (default: only clips without words). Engine: language auto/english = Parakeet; any other named language = local Whisper LOCKED to it (engine override). Returns { jobId, plan } (plan = engine + reason, ranges, totalAudioMs: the audio that will be processed, estimate/estimatedMs: how long on this computer, advice: faster options for a long Whisper run; tell the user for long jobs). Long Whisper runs are saved chunk by chunk: stalls restart from the last chunk automatically; after job_cancel, a crash or an app restart, transcribing again resumes there. Progress is real (audio decoded); time left in progress.message and progress.data.etaMs. To REDO a transcript (wrong language, bad run) confirm language, pass per-run language and provider overrides, then force: true (all clips) or clipId. Word fixes are re-applied; tell the user about result droppedWordEdits[] (fixes that may need redoing). Each clip is transcribed only over the part it plays (head trim to tail trim, e.g. one half of a split; only the kept parts when cuts hide most of a long recording; result transcribedRanges[]). Clips with no audio track are skipped (result noAudioClips), not failed. Result warnings[] flags words that can't cover the audio (sound with no words, too few words: music, noise, another language) and a re-run with the same words as before: surface it, and get a second reading with engine whisper or a cloud provider (Parakeet isn't locked by language english, so retrying the same way repeats it). ASYNC: { jobId }, job_wait. Concurrent saves are merged and retried (three attempts). Exhausted conflicts report code revision_conflict / status 409 (job.errorDetails for async jobs).",
5913
+ "Transcribe clips with word timestamps (default: only clips without words). Asking again while the same transcription is still running returns that job (alreadyRunning: true), never a second run: keep waiting on it. Engine: language auto/english = Parakeet; any other named language = local Whisper LOCKED to it (engine override). Returns { jobId, plan } (plan = engine + reason, ranges, totalAudioMs: the audio that will be processed, estimate/estimatedMs: how long on this computer, advice: faster options for a long Whisper run; tell the user for long jobs). Long Whisper runs are saved chunk by chunk: stalls restart from the last chunk automatically; after job_cancel, a crash or an app restart, transcribing again resumes there. Progress is real (audio decoded); time left in progress.message and progress.data.etaMs. To REDO a transcript (wrong language, bad run) confirm language, pass per-run language and provider overrides, then force: true (all clips) or clipId. Word fixes are re-applied; tell the user about result droppedWordEdits[] (fixes that may need redoing). Each clip is transcribed only over the part it plays (head trim to tail trim, e.g. one half of a split; only the kept parts when cuts hide most of a long recording; result transcribedRanges[]). Clips with no audio track are skipped (result noAudioClips), not failed. Result warnings[] flags words that can't cover the audio (sound with no words, too few words: music, noise, another language) and a re-run with the same words as before: surface it, and get a second reading with engine whisper or a cloud provider (Parakeet isn't locked by language english, so retrying the same way repeats it). ASYNC: { jobId }, job_wait. Concurrent saves are merged and retried (three attempts). Exhausted conflicts report code revision_conflict / status 409 (job.errorDetails for async jobs).",
5825
5914
  inputSchema: {
5826
5915
  type: "object",
5827
5916
  properties: {
@@ -5921,7 +6010,7 @@ const TOOLS = [
5921
6010
  {
5922
6011
  name: "transcript_get",
5923
6012
  description:
5924
- "Transcript (default: open project). compact: segments of [id, text, startMs SOURCE, editedStartMs (null = cut)]; ids are unique prefixes, accepted wherever word ids are. text: '[m:ss.s] line' per segment, cheapest to read. words: full ids, end times, speaker. full: legacy.",
6013
+ "Transcript (default: open project). compact: segments of [id, text, editedStartMs, editedEndMs, sourceStartMs] (edited = as it plays, the playhead's clock, null = cut; sourceStartMs for anchorSourceMs; cut by word id, not ms); ids are unique prefixes, accepted wherever word ids are. text: '[m:ss.s] line' per segment, cheapest to read. words: full ids, end times, speaker. full: legacy.",
5925
6014
  inputSchema: {
5926
6015
  type: "object",
5927
6016
  properties: {
@@ -6906,6 +6995,54 @@ const TOOLS = [
6906
6995
  },
6907
6996
  command: "media.generate-presenter",
6908
6997
  },
6998
+ {
6999
+ name: "media_generate_video",
7000
+ description:
7001
+ "Generate an AI video clip with MiniMax H3 on the user's OWN fal account (connect fal in Settings > Connectors; their credits). Text to video, or image to video from a first frame (image) and optional last frame (endImage). PAID: only after the user agreed; never retry a failed job yourself. ASYNC: { jobId, estimatedCostUsd }; job_wait timeoutMs 600000 for { videoPath, durationMs, width, height, model, requestId }. Place with project_add_motion_graphic (overlay, moving B-roll) or project_add_clip (main track). FAL_NOT_CONNECTED: ask the user to connect fal (never ask for the key in chat).",
7002
+ inputSchema: {
7003
+ type: "object",
7004
+ properties: {
7005
+ prompt: {
7006
+ type: "string",
7007
+ description:
7008
+ "The shot: subject, action, camera, light, mood. With an image, describe the motion.",
7009
+ },
7010
+ image: {
7011
+ type: "string",
7012
+ description: "First frame: absolute path (PNG/JPEG/WebP) or https URL.",
7013
+ },
7014
+ endImage: { type: "string", description: "Last frame, same forms. Image to video only." },
7015
+ model: {
7016
+ type: "string",
7017
+ enum: ["h3", "h3-max", "h3-max-turbo"],
7018
+ description: "Default h3. h3-max for 1080P; h3-max-turbo is faster and cheaper.",
7019
+ },
7020
+ durationSec: {
7021
+ type: "number",
7022
+ description: "Default 5. h3: whole seconds 5-15; h3-max/turbo: 0.92-15.",
7023
+ },
7024
+ resolution: {
7025
+ type: "string",
7026
+ enum: ["480P", "768P", "1080P", "2K", "4K"],
7027
+ description: "Default 768P. h3: 480P/768P/2K/4K; h3-max/turbo: 480P/768P/1080P.",
7028
+ },
7029
+ aspectRatio: {
7030
+ type: "string",
7031
+ enum: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"],
7032
+ description: "Text to video only (default 16:9); image to video follows the image.",
7033
+ },
7034
+ promptExpansion: {
7035
+ type: "string",
7036
+ enum: ["balanced", "fast", "quality", "disabled"],
7037
+ description: "fal's prompt rewriting. Default balanced; disabled = word for word.",
7038
+ },
7039
+ seed: { type: "number" },
7040
+ outputDir: { type: "string", description: "Default <userData>/generated-videos." },
7041
+ },
7042
+ required: ["prompt"],
7043
+ },
7044
+ command: "media.generate-video",
7045
+ },
6909
7046
  {
6910
7047
  name: "project_inspect_footage",
6911
7048
  description:
@@ -8179,6 +8316,11 @@ const TOOLS = [
8179
8316
  inputSchema: {
8180
8317
  type: "object",
8181
8318
  properties: {
8319
+ still: {
8320
+ type: "boolean",
8321
+ description:
8322
+ "The graphic is MEANT to hold still (no entrance, no motion): skip the frozen-render check, kept for re-renders. Never fake a slow drift instead.",
8323
+ },
8182
8324
  templateId: { type: "string" },
8183
8325
  slots: { type: "object", description: "By key; image slots take a file path." },
8184
8326
  variables: {
@@ -8328,6 +8470,11 @@ const TOOLS = [
8328
8470
  inputSchema: {
8329
8471
  type: "object",
8330
8472
  properties: {
8473
+ still: {
8474
+ type: "boolean",
8475
+ description:
8476
+ "The graphic is MEANT to hold still (no entrance, no motion): skip the frozen-render check, kept for re-renders. Never fake a slow drift instead.",
8477
+ },
8331
8478
  html: { type: "string", description: "Or htmlPath." },
8332
8479
  htmlPath: { type: "string" },
8333
8480
  aspectRatio: { type: "string", enum: ["16:9", "9:16", "1:1"] },
@@ -8484,7 +8631,8 @@ const TOOLS = [
8484
8631
  },
8485
8632
  {
8486
8633
  name: "asset_list_music",
8487
- description: "List bundled music (pick by intents; add absolutePath with project_add_audio).",
8634
+ description:
8635
+ "List bundled music (pick by intents; add absolutePath with project_add_audio). Cleared for commercial use in the user's videos, paid ads included (see `license` in the reply): don't flag it as a licensing risk.",
8488
8636
  inputSchema: { type: "object", properties: {} },
8489
8637
  command: "asset.list-music",
8490
8638
  },
@@ -9707,6 +9855,7 @@ Publishing has daily limits (YouTube uploads and updates per license, social pos
9707
9855
 
9708
9856
  PandaStudio problems (a capability gap like an imported font the export can't draw, a command failing unexpectedly, a render not matching the project): call system_report_issue yourself, then tell the user you reported it. Not for user mistakes, missing keys or taste.
9709
9857
 
9858
+ Fixing a cut, pause or insert the user points at ("at 16 s it's sloppy"): look and listen with project_review_moment (frames + the exported mix transcribed + whether each cut lands in quiet), fix by WORD (project_add_trim / project_update_region with afterWordId / beforeWordId, never ms worked out by hand; edges snap to the audio), then review again before saying it's fixed. A few frames off is quicker for the user to drag in the timeline: tell them. A stutter in a result's warnings (picture changes a few frames apart) means put them on one frame or keep one.
9710
9859
  Views: in chat apps that show MCP views, preview_show, project_render_frame / _render_sheet and project_edit_summary also show the frames and what the edit contains, and clips_get / clips_make show the clips as cards the user can open, export or remove. Summarise in a line instead of describing each frame or clip; what the user does in a view arrives as context, don't redo it.
9711
9860
 
9712
9861
  Share links: when the user asks to send a video for feedback, share_status then share_create on a finished export gives a link anyone can watch and comment on (async, job_wait; storage is capped per plan, quota_exceeded means ask which link to delete). share_comments brings viewer comments into the project as review pins: apply each, review_resolve it, and it shows as Done to viewers. Never share, delete a link or reply to viewers without the user asking.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.275.0",
3
+ "version": "1.284.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",