@writepanda/mcp 1.19.0 → 1.22.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 +321 -12
  2. package/package.json +1 -1
package/bin/server.mjs CHANGED
@@ -163,6 +163,208 @@ const TOOLS = [
163
163
  command: "system.list",
164
164
  },
165
165
 
166
+ // ── workspaces (v1.19) ──────────────────────────────────────────
167
+ // Multi-workspace isolation: each workspace has its own projects,
168
+ // exports, Replicate key, and (v1.19.0+) YouTube connections.
169
+ // Agents managing an agency account can switch workspaces mid-
170
+ // flow to operate inside a specific client's bucket.
171
+ {
172
+ name: "workspace_list",
173
+ description:
174
+ "List every workspace on this machine. Returns `currentWorkspaceId` + a `limit` object describing the plan's cap (`max: null` for unlimited on Team, 1 on Starter/Trial, 3 on Creator). Call this first to see what workspaces exist and which one every other query defaults to.",
175
+ inputSchema: { type: "object", properties: {} },
176
+ command: "workspace.list",
177
+ },
178
+ {
179
+ name: "workspace_current",
180
+ description: "Return the active workspace (what every workspace-scoped query defaults to).",
181
+ inputSchema: { type: "object", properties: {} },
182
+ command: "workspace.current",
183
+ },
184
+ {
185
+ name: "workspace_create",
186
+ description:
187
+ "Create a new workspace. On plan cap: returns `{ ok: false, details: { code: 'workspace_limit_reached', upgradeTo: 'Creator' | 'Team' | null } }` — surface that to the user instead of retrying. Pass `switchTo: true` to make the new workspace active immediately.",
188
+ inputSchema: {
189
+ type: "object",
190
+ properties: {
191
+ name: { type: "string", description: "Display name, max 80 chars." },
192
+ switchTo: {
193
+ type: "boolean",
194
+ description: "Switch to the new workspace immediately. Default false.",
195
+ },
196
+ },
197
+ required: ["name"],
198
+ },
199
+ command: "workspace.create",
200
+ },
201
+ {
202
+ name: "workspace_rename",
203
+ description: "Rename a workspace. No cross-workspace side effects.",
204
+ inputSchema: {
205
+ type: "object",
206
+ properties: {
207
+ id: { type: "string" },
208
+ name: { type: "string", description: "New name, max 80 chars." },
209
+ },
210
+ required: ["id", "name"],
211
+ },
212
+ command: "workspace.rename",
213
+ },
214
+ {
215
+ name: "workspace_delete",
216
+ description:
217
+ "Hard-delete a workspace and everything inside it: project rows (on-disk .pandastudio files remain), exports, shots, Replicate key + YouTube connections. Refuses to delete the last remaining workspace. Call `workspace.contents` first and show the user what they'll lose.",
218
+ inputSchema: {
219
+ type: "object",
220
+ properties: { id: { type: "string" } },
221
+ required: ["id"],
222
+ },
223
+ command: "workspace.delete",
224
+ },
225
+ {
226
+ name: "workspace_switch",
227
+ description:
228
+ "Change the active workspace. All subsequent project.*/export.*/motion.* queries operate inside the target until the next switch.",
229
+ inputSchema: {
230
+ type: "object",
231
+ properties: { id: { type: "string" } },
232
+ required: ["id"],
233
+ },
234
+ command: "workspace.switch",
235
+ },
236
+ {
237
+ name: "workspace_contents",
238
+ description:
239
+ "Count projects / exports / published YouTube videos in a workspace. Use before `workspace.delete` for the 'you will lose N items' warning.",
240
+ inputSchema: {
241
+ type: "object",
242
+ properties: { id: { type: "string" } },
243
+ required: ["id"],
244
+ },
245
+ command: "workspace.contents",
246
+ },
247
+
248
+ // ── YouTube publishing (v1.19) ──────────────────────────────────
249
+ // Connect Google accounts per workspace, publish exports, edit
250
+ // already-published videos. All verbs operate on the active
251
+ // workspace unless the agent switches first.
252
+ {
253
+ name: "youtube_is_configured",
254
+ description:
255
+ "Return whether this PandaStudio build has Google OAuth credentials baked in. When false, every other `youtube.*` verb will fail — tell the user this build can't publish to YouTube.",
256
+ inputSchema: { type: "object", properties: {} },
257
+ command: "youtube.is-configured",
258
+ },
259
+ {
260
+ name: "youtube_connect",
261
+ description:
262
+ "Start Google OAuth in the user's system browser. Long-running: waits up to 5 minutes for consent. On success, stores refresh token encrypted under the active workspace and returns the account + its channels. INTERACTIVE — only call when the user explicitly asks to connect a YouTube account.",
263
+ inputSchema: { type: "object", properties: {} },
264
+ command: "youtube.connect",
265
+ },
266
+ {
267
+ name: "youtube_disconnect",
268
+ description:
269
+ "Revoke and delete a YouTube account from the active workspace. Published videos stay on YouTube; only the local connection + cached channels + encrypted refresh token are removed.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: { accountId: { type: "string" } },
273
+ required: ["accountId"],
274
+ },
275
+ command: "youtube.disconnect",
276
+ },
277
+ {
278
+ name: "youtube_list_accounts",
279
+ description:
280
+ "List connected Google accounts in the active workspace + cached YouTube channels. Call this before `export.publish-youtube` so the user can pick account + channel.",
281
+ inputSchema: { type: "object", properties: {} },
282
+ command: "youtube.list-accounts",
283
+ },
284
+ {
285
+ name: "youtube_list_channels",
286
+ description:
287
+ "Re-pull the channel list for a connected account from YouTube (bypasses local cache). Useful if the user just created a new channel under the same Google account.",
288
+ inputSchema: {
289
+ type: "object",
290
+ properties: { accountId: { type: "string" } },
291
+ required: ["accountId"],
292
+ },
293
+ command: "youtube.list-channels",
294
+ },
295
+ {
296
+ name: "export_publish_youtube",
297
+ description:
298
+ "Upload an export-library entry to YouTube via resumable upload. Stamps title/description/tags/privacy on the video and (optionally) pushes the entry's thumbnail. Returns `{ videoId, videoUrl }`. LONG-RUNNING (expect ~30 s per 100 MB). The export row is marked `youtubeVideoId` on success, so repeated calls on an already-published entry should check that field first.",
299
+ inputSchema: {
300
+ type: "object",
301
+ properties: {
302
+ id: { type: "string", description: "Export entry id." },
303
+ accountId: { type: "string" },
304
+ channelId: { type: "string" },
305
+ title: { type: "string" },
306
+ description: { type: "string" },
307
+ tags: { type: "array", items: { type: "string" } },
308
+ categoryId: { type: "string" },
309
+ privacyStatus: {
310
+ type: "string",
311
+ description: "public | unlisted | private. Default unlisted.",
312
+ },
313
+ setThumbnail: { type: "boolean" },
314
+ },
315
+ required: ["id", "accountId", "channelId", "title"],
316
+ },
317
+ command: "export.publish-youtube",
318
+ },
319
+ {
320
+ name: "export_list_youtube",
321
+ description:
322
+ "List recently-published videos on a connected account (includes videos not published via PandaStudio). Dashboard data.",
323
+ inputSchema: {
324
+ type: "object",
325
+ properties: {
326
+ accountId: { type: "string" },
327
+ max: { type: "number", description: "Default 25, max 50." },
328
+ },
329
+ required: ["accountId"],
330
+ },
331
+ command: "export.list-youtube",
332
+ },
333
+ {
334
+ name: "export_update_youtube",
335
+ description:
336
+ "Update an already-published video's metadata (title / description / tags / category / privacyStatus). Pass only the fields you want to change. Cannot replace the video file — YouTube requires deleting and re-uploading for that.",
337
+ inputSchema: {
338
+ type: "object",
339
+ properties: {
340
+ accountId: { type: "string" },
341
+ videoId: { type: "string" },
342
+ title: { type: "string" },
343
+ description: { type: "string" },
344
+ tags: { type: "array", items: { type: "string" } },
345
+ categoryId: { type: "string" },
346
+ privacyStatus: { type: "string" },
347
+ },
348
+ required: ["accountId", "videoId"],
349
+ },
350
+ command: "export.update-youtube",
351
+ },
352
+ {
353
+ name: "export_update_youtube_thumbnail",
354
+ description:
355
+ "Replace an already-published video's thumbnail on YouTube via thumbnails.set. Expects a local 1280×720 image (no auto-crop — pre-process or use `export.generate-thumbnail` output).",
356
+ inputSchema: {
357
+ type: "object",
358
+ properties: {
359
+ accountId: { type: "string" },
360
+ videoId: { type: "string" },
361
+ imagePath: { type: "string" },
362
+ },
363
+ required: ["accountId", "videoId", "imagePath"],
364
+ },
365
+ command: "export.update-youtube-thumbnail",
366
+ },
367
+
166
368
  // ── project lifecycle ───────────────────────────────────────────
167
369
  {
168
370
  name: "project_list",
@@ -558,7 +760,7 @@ const TOOLS = [
558
760
  {
559
761
  name: "project_remove_region",
560
762
  description:
561
- "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.",
763
+ "Delete an existing region (zoom, trim, speed, annotation, fx, lower-third, overlay, audio-overlay) by its id. Use project_read to find region ids — visual regions live under editor.*Regions; audio overlays live under audioOverlays[]. Returns the updated project.",
562
764
  inputSchema: {
563
765
  type: "object",
564
766
  properties: {
@@ -566,9 +768,10 @@ const TOOLS = [
566
768
  path: { type: "string" },
567
769
  regionType: {
568
770
  type: "string",
569
- description: "zoom | trim | speed | annotation | fx | lower-third | overlay",
771
+ description:
772
+ "zoom | trim | speed | annotation | fx | lower-third | overlay | audio-overlay",
570
773
  },
571
- regionId: { type: "string", description: "UUID of the region to delete" },
774
+ regionId: { type: "string", description: "id of the region to delete" },
572
775
  expectedRevision: { type: "number" },
573
776
  },
574
777
  required: ["regionType", "regionId"],
@@ -578,7 +781,7 @@ const TOOLS = [
578
781
  {
579
782
  name: "project_update_region",
580
783
  description:
581
- "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.",
784
+ "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, overlay, and audio-overlay (startMs, endMs, sourceStartMs, volume — lets you drag-trim an audio overlay without removing it).",
582
785
  inputSchema: {
583
786
  type: "object",
584
787
  properties: {
@@ -586,13 +789,14 @@ const TOOLS = [
586
789
  path: { type: "string" },
587
790
  regionType: {
588
791
  type: "string",
589
- description: "zoom | trim | speed | annotation | fx | lower-third | overlay",
792
+ description:
793
+ "zoom | trim | speed | annotation | fx | lower-third | overlay | audio-overlay",
590
794
  },
591
- regionId: { type: "string", description: "UUID of the region to update" },
795
+ regionId: { type: "string", description: "id of the region to update" },
592
796
  patch: {
593
797
  type: "object",
594
798
  description:
595
- "Fields to update (partial). E.g. { depth: 4 } to change zoom depth, or { content: 'New Name' } on a lower-third.",
799
+ "Fields to update (partial). E.g. { depth: 4 } to change zoom depth, { content: 'New Name' } on a lower-third, or { startMs: 2000, endMs: 15000, sourceStartMs: 4000 } to shift + trim an audio overlay (play seconds 4–17 of the source from the 2s mark on the timeline).",
596
800
  },
597
801
  expectedRevision: { type: "number" },
598
802
  },
@@ -758,7 +962,7 @@ const TOOLS = [
758
962
  {
759
963
  name: "project_add_audio",
760
964
  description:
761
- "Add a background audio track (music, VO, ambient sound) to the project. The audio plays over the full mix from startMs and is exported automatically. Use project_remove_audio to remove it. Returns { overlayId } — save this to remove it later.",
965
+ "Add a background audio overlay (music, VO, ambient) as a first-class timeline region. Plays from startMs to endMs on the edited timeline; sourceStartMs is the in-point into the source file. Waveform peaks are extracted automatically and stored on the overlay for the timeline UI. Use project_update_region (regionType=audio-overlay) to drag/trim it afterward. Returns { overlayId } — save this to trim/remove later.",
762
966
  inputSchema: {
763
967
  type: "object",
764
968
  properties: {
@@ -770,12 +974,27 @@ const TOOLS = [
770
974
  },
771
975
  startMs: {
772
976
  type: "number",
773
- description: "Where in the edited timeline playback starts. Default 0.",
977
+ description: "Where on the edited timeline playback starts. Default 0.",
978
+ },
979
+ endMs: {
980
+ type: "number",
981
+ description:
982
+ "Where on the edited timeline playback ends. Precedence: endMs > maxDurationMs > durationMs. If all are omitted, defaults to the source file's real duration.",
983
+ },
984
+ sourceStartMs: {
985
+ type: "number",
986
+ description:
987
+ "In-point into the source file in ms. Default 0. Combine with endMs-startMs to play a window of the source (e.g. startMs=2000, endMs=15000, sourceStartMs=4000 plays seconds 4–17 of the source at timeline position 2–15s).",
988
+ },
989
+ durationMs: {
990
+ type: "number",
991
+ description: "Fallback duration when neither endMs nor maxDurationMs is given.",
774
992
  },
775
993
  volume: { type: "number", description: "Volume multiplier 0–2. Default 0.8." },
776
994
  maxDurationMs: {
777
995
  type: "number",
778
- description: "Clip the audio at this duration. Omit to play the full file.",
996
+ description:
997
+ "[Legacy] Sets endMs = startMs + maxDurationMs when endMs is absent. Prefer endMs.",
779
998
  },
780
999
  expectedRevision: { type: "number" },
781
1000
  },
@@ -1058,7 +1277,7 @@ const TOOLS = [
1058
1277
  {
1059
1278
  name: "motion_render_html",
1060
1279
  description:
1061
- "Render arbitrary HTML/CSS/JS to video. Use this when the bundled motion-graphic templates don't fit the brief — you write the HTML, the Chromium capturePage FFmpeg pipeline does the rest. Pass either inline `html` or a `htmlPath`. Animations should auto-start on DOMContentLoaded. Set `transparent: true` to get a WebM with alpha channel for overlays (lower thirds, watermarks, name plates) that composite over existing video. IMPORTANT: renders are sequential — call job_wait to completion before starting another render, or you'll get a RENDER_BUSY error. Async — returns { jobId, outputPath }; poll job_wait for the final file.",
1280
+ "Render arbitrary HTML/CSS/JS to video via the HyperFrames engine — frame-perfect, seekable capture through chrome-headless-shell's BeginFrame API. Use this when the bundled motion-graphic templates don't fit the brief. The HTML MUST expose a paused GSAP timeline registered as `window.__timelines[<data-composition-id>] = tl` NOT CSS keyframes, NOT setTimeout, NOT `window.__hf.seek` (that lower-level protocol skips the compositor invalidation wrapper and renders with 1-second stalls). See the SKILL.md `Custom motion graphics — HTML authoring` section for the required `data-composition-id`/`data-width`/`data-height`/`data-duration` root element, the canonical template, and pacing rules. Pass either inline `html` or `htmlPath`. Set `transparent: true` for a WebM+alpha overlay (lower thirds, watermarks). IMPORTANT: renders are sequential — call job_wait to completion before starting another render, or you'll get a RENDER_BUSY error. Async — returns { jobId, outputPath }; poll job_wait for the final file.",
1062
1281
  inputSchema: {
1063
1282
  type: "object",
1064
1283
  properties: {
@@ -1177,7 +1396,7 @@ const TOOLS = [
1177
1396
  {
1178
1397
  name: "asset_list_music",
1179
1398
  description:
1180
- "List every bundled background music track (id, title, category, mood, durationMs, absolutePath). Use absolutePath with project_add_audio to attach a track to a project.",
1399
+ "List every bundled background music track. Each track has: id, title, category, mood, durationMs, absolutePath, and agent-routing hints `intents` (e.g. product_video, kinetic_text, promo, intro, outro, tech_review, vlog, generic) and `recommendedFor` (youtube-long | shorts | linkedin | loom). Match on intents first when picking a track — use mood/category as tiebreakers. Use absolutePath with project_add_audio to attach a track to a project.",
1181
1400
  inputSchema: { type: "object", properties: {} },
1182
1401
  command: "asset.list-music",
1183
1402
  },
@@ -1256,6 +1475,96 @@ const TOOLS = [
1256
1475
  inputSchema: { type: "object", properties: {} },
1257
1476
  command: "export.list",
1258
1477
  },
1478
+ {
1479
+ name: "export_generate_thumbnail",
1480
+ description:
1481
+ "Generate a YouTube thumbnail for an export-library entry via Replicate's gpt-image-2 model. If `prompt` isn't supplied, the local LLM writes one from the transcript (an opinionated YouTube-thumbnail art-director prompt targeting 3:2 aspect, photorealistic or bold-graphic, high contrast, optional short overlay text). Requires the user's Replicate API key to be set in Settings → Integrations (the key is stored encrypted via OS keychain; PandaStudio never sees the user's OpenAI bill). Returns { imagePath, prompt, iterations } on success.",
1482
+ inputSchema: {
1483
+ type: "object",
1484
+ properties: {
1485
+ id: { type: "string", description: "Export entry id." },
1486
+ prompt: {
1487
+ type: "string",
1488
+ description:
1489
+ "Optional explicit prompt. If omitted, the local LLM auto-writes one from the transcript.",
1490
+ },
1491
+ referenceImagePath: {
1492
+ type: "string",
1493
+ description:
1494
+ "Optional reference image (absolute path or https URL) passed as gpt-image-2 `input_images`.",
1495
+ },
1496
+ quality: {
1497
+ type: "string",
1498
+ description: "gpt-image-2 quality. 'low' | 'medium' (default) | 'high'.",
1499
+ },
1500
+ },
1501
+ required: ["id"],
1502
+ },
1503
+ command: "export.generate-thumbnail",
1504
+ },
1505
+ {
1506
+ name: "export_edit_thumbnail",
1507
+ description:
1508
+ "Iterate on the existing thumbnail via natural-language edit prompt ('make it more red', 'add bold text at top', 'remove the logo'). The current thumbnail is passed to gpt-image-2 as input_images, and the result replaces it. The previous thumbnail is pushed onto `entry.thumbnailIterations` so the user can revert. Quality is fixed at 'low' on edits — fast and cheap, suitable for rapid iteration.",
1509
+ inputSchema: {
1510
+ type: "object",
1511
+ properties: {
1512
+ id: { type: "string", description: "Export entry id." },
1513
+ editPrompt: {
1514
+ type: "string",
1515
+ description: "What to change. One-sentence natural-language instruction.",
1516
+ },
1517
+ },
1518
+ required: ["id", "editPrompt"],
1519
+ },
1520
+ command: "export.edit-thumbnail",
1521
+ },
1522
+ {
1523
+ name: "export_set_thumbnail",
1524
+ description:
1525
+ "Attach a manually-chosen image file (PNG / JPEG / WebP) as the thumbnail. Use this when the user provides a ready-made image, or when an agent has already generated one elsewhere. Copies the file into the managed thumbnails directory; iteration history is cleared. Prefer `export.generate-thumbnail` for AI-generated thumbnails so iteration history is preserved.",
1526
+ inputSchema: {
1527
+ type: "object",
1528
+ properties: {
1529
+ id: { type: "string", description: "Export entry id." },
1530
+ sourcePath: {
1531
+ type: "string",
1532
+ description: "Absolute path to the local image file (PNG / JPEG / WebP).",
1533
+ },
1534
+ },
1535
+ required: ["id", "sourcePath"],
1536
+ },
1537
+ command: "export.set-thumbnail",
1538
+ },
1539
+ {
1540
+ name: "export_clear_thumbnail",
1541
+ description: "Remove the current thumbnail and wipe its iteration history.",
1542
+ inputSchema: {
1543
+ type: "object",
1544
+ properties: {
1545
+ id: { type: "string", description: "Export entry id." },
1546
+ },
1547
+ required: ["id"],
1548
+ },
1549
+ command: "export.clear-thumbnail",
1550
+ },
1551
+ {
1552
+ name: "export_revert_thumbnail",
1553
+ description:
1554
+ "Revert to a previous thumbnail from the iteration history (`entry.thumbnailIterations[].id`). The current image gets pushed onto history, so the revert itself is undoable.",
1555
+ inputSchema: {
1556
+ type: "object",
1557
+ properties: {
1558
+ id: { type: "string", description: "Export entry id." },
1559
+ iterationId: {
1560
+ type: "string",
1561
+ description: "id of the iteration entry from `entry.thumbnailIterations`.",
1562
+ },
1563
+ },
1564
+ required: ["id", "iterationId"],
1565
+ },
1566
+ command: "export.revert-thumbnail",
1567
+ },
1259
1568
 
1260
1569
  // ── preview overlay (v1.9.2) ────────────────────────────────────
1261
1570
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.19.0",
3
+ "version": "1.22.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",