@writepanda/mcp 1.276.0 → 1.285.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 +166 -15
  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" },
@@ -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"] },
@@ -8528,7 +8675,8 @@ const TOOLS = [
8528
8675
  // ── project-aware LLM ──────────────────────────────────────────
8529
8676
  {
8530
8677
  name: "llm_generate_title",
8531
- description: "Generate a YouTube title from the project transcript (local LLM).",
8678
+ description:
8679
+ "Generate a YouTube title from what the EDITED video says (cut words left out) with the local LLM. Returns { title }: one complete phrase within maxChars, never cut mid-word.",
8532
8680
  inputSchema: {
8533
8681
  type: "object",
8534
8682
  properties: {
@@ -8541,7 +8689,8 @@ const TOOLS = [
8541
8689
  },
8542
8690
  {
8543
8691
  name: "llm_generate_description",
8544
- description: "Generate a 3-5 sentence YouTube description from the transcript.",
8692
+ description:
8693
+ "Generate a 3-5 sentence YouTube description from what the EDITED video says, with the local LLM. Returns { description, warnings? }: a warning means it repeated the transcript, so write the description yourself.",
8545
8694
  inputSchema: {
8546
8695
  type: "object",
8547
8696
  properties: {
@@ -8554,7 +8703,8 @@ const TOOLS = [
8554
8703
  },
8555
8704
  {
8556
8705
  name: "llm_generate_caption",
8557
- description: "Generate an Instagram Reel caption with hashtags from the transcript.",
8706
+ description:
8707
+ "Generate an Instagram Reel caption with hashtags from what the EDITED video says.",
8558
8708
  inputSchema: {
8559
8709
  type: "object",
8560
8710
  properties: {
@@ -8568,7 +8718,7 @@ const TOOLS = [
8568
8718
  {
8569
8719
  name: "llm_generate_timestamps",
8570
8720
  description:
8571
- "Generate YouTube chapter timestamps for the edited video: edited-timeline times from the words that survive the cuts (first 0:00, each >= 10 s, all before the end); the local LLM only writes labels.",
8721
+ "Generate YouTube chapter timestamps for the edited video: edited-timeline times from the words that survive the cuts (first 0:00, each >= 30 s, starting where a sentence starts, all before the end); the local LLM only writes each label from its own chapter's words.",
8572
8722
  inputSchema: {
8573
8723
  type: "object",
8574
8724
  properties: {
@@ -8856,7 +9006,7 @@ const TOOLS = [
8856
9006
  {
8857
9007
  name: "ideas_research",
8858
9008
  description:
8859
- "Find outliers (videos beating their own channel's median views) across a niche: searches YouTube for the topic (comma-separate several angles) and/or expands from a seed channel the target audience watches, reads every channel found and ranks their recent uploads by multiplier. For what's trending, pass period month and maxAgeDays 30. Async: returns { researchId, jobId }; job.wait the jobId (~20-40 s), then ideas.get.",
9009
+ "Find outliers (videos beating their own channel's median views) across a niche: searches YouTube for the topic (comma-separate several angles) and/or expands from a seed channel the target audience watches, reads every channel found and ranks their recent uploads by multiplier. For what's trending, pass period month and maxAgeDays 30. With maxAgeDays set, an outlier the listing left undated (Shorts, premieres) is kept only once its real publish date is under the limit; ones YouTube won't date are left out and counted in stats.undatedSkipped. Async: returns { researchId, jobId }; job.wait the jobId (~20-40 s), then ideas.get.",
8860
9010
  inputSchema: {
8861
9011
  type: "object",
8862
9012
  properties: {
@@ -9708,6 +9858,7 @@ Publishing has daily limits (YouTube uploads and updates per license, social pos
9708
9858
 
9709
9859
  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.
9710
9860
 
9861
+ 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.
9711
9862
  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.
9712
9863
 
9713
9864
  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.276.0",
3
+ "version": "1.285.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",