@writepanda/mcp 1.222.0 → 1.224.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.
@@ -16,7 +16,7 @@
16
16
  //
17
17
  // Kept free of MCP-sdk imports so the pure parts are unit-testable.
18
18
 
19
- import { readFile } from "node:fs/promises";
19
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
20
20
  import http from "node:http";
21
21
  import os from "node:os";
22
22
  import path from "node:path";
@@ -239,3 +239,171 @@ export function appLaunchEnv(env = process.env) {
239
239
  }
240
240
  return out;
241
241
  }
242
+
243
+ // ── Launching the app (external agents only) ─────────────────────────────
244
+ //
245
+ // Jared (Windows, 2.1.15): "PandaStudio seemed to restart with every
246
+ // request". Every call that didn't find a healthy server at the port file's
247
+ // address launched PandaStudio.exe again: while the app was busy for longer
248
+ // than the busy wait, while it was still booting, and after another process
249
+ // had replaced or deleted the token/port files. Now a launch happens only
250
+ // when nothing says PandaStudio is running, and at most once per
251
+ // LAUNCH_DEBOUNCE_MS across every MCP server / CLI process on the machine.
252
+
253
+ /** Default automation port (Settings → Automation). */
254
+ export const DEFAULT_AUTOMATION_PORT = 7878;
255
+ /** An instance.json heartbeat older than this is a dead (or hung) app. */
256
+ export const INSTANCE_STALE_MS = 20_000;
257
+ /** Don't launch again within this long of the last launch, from anywhere. */
258
+ export const LAUNCH_DEBOUNCE_MS = 90_000;
259
+
260
+ /** Is a process with this pid running? (EPERM: it exists, owned by someone else.) */
261
+ export function pidAlive(pid) {
262
+ if (!Number.isInteger(pid) || pid <= 0) return false;
263
+ try {
264
+ process.kill(pid, 0);
265
+ return true;
266
+ } catch (err) {
267
+ return err?.code === "EPERM";
268
+ }
269
+ }
270
+
271
+ /** The running app's instance.json (pid + heartbeat), or null. */
272
+ export async function readInstanceRecord(dir = configDir()) {
273
+ try {
274
+ const v = JSON.parse(cleanFileValue(await readFile(path.join(dir, "instance.json"), "utf-8")));
275
+ return v && typeof v === "object" ? v : null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+
281
+ /** A fresh heartbeat from a process that still exists. */
282
+ export function instanceAlive(record, now = Date.now(), isAlive = pidAlive) {
283
+ if (!record || !Number.isInteger(record.pid)) return false;
284
+ const beat = Number(record.heartbeatAt);
285
+ if (!Number.isFinite(beat) || now - beat > INSTANCE_STALE_MS) return false;
286
+ return isAlive(record.pid);
287
+ }
288
+
289
+ function launchMarkerPath(dir) {
290
+ return path.join(dir, "launch-requested");
291
+ }
292
+
293
+ /** Milliseconds since any process last launched the app, or null. */
294
+ export async function lastLaunchAgeMs(dir = configDir(), now = Date.now()) {
295
+ try {
296
+ const at = Number(cleanFileValue(await readFile(launchMarkerPath(dir), "utf-8")));
297
+ if (!Number.isFinite(at) || at > now) return null;
298
+ return now - at;
299
+ } catch {
300
+ return null;
301
+ }
302
+ }
303
+
304
+ export async function recordLaunch(dir = configDir(), now = Date.now()) {
305
+ try {
306
+ await mkdir(dir, { recursive: true });
307
+ await writeFile(launchMarkerPath(dir), String(now), "utf-8");
308
+ } catch {
309
+ /* best-effort: the in-process flag still stops a second launch */
310
+ }
311
+ }
312
+
313
+ /**
314
+ * What to do this round:
315
+ * use — the port file's server answered
316
+ * wait — a PandaStudio is running (busy, booting, files being rewritten,
317
+ * or Local automation off) or one was launched moments ago
318
+ * launch — nothing is running and nobody launched it recently
319
+ *
320
+ * healthState / preferredState: probe of the port file's address / of the
321
+ * default port ("up" | "busy" | "down", or null when not probed).
322
+ */
323
+ export function launchDecision({
324
+ healthState = null,
325
+ preferredState = null,
326
+ instanceIsAlive = false,
327
+ launchAgeMs = null,
328
+ debounceMs = LAUNCH_DEBOUNCE_MS,
329
+ }) {
330
+ if (healthState === "up") return "use";
331
+ if (healthState === "busy") return "wait";
332
+ if (instanceIsAlive) return "wait";
333
+ if (preferredState === "up" || preferredState === "busy") return "wait";
334
+ if (launchAgeMs !== null && launchAgeMs < debounceMs) return "wait";
335
+ return "launch";
336
+ }
337
+
338
+ /**
339
+ * Reach the running app, launching it only when none is running. Returns the
340
+ * credentials whose server answered. `launch()` starts the app (throws when
341
+ * it can't). Everything else is injectable for tests.
342
+ */
343
+ export async function ensureApp({
344
+ dir = configDir(),
345
+ launch,
346
+ timeoutMs = 60_000,
347
+ pollMs = 500,
348
+ preferredPort = DEFAULT_AUTOMATION_PORT,
349
+ probe = probeHealth,
350
+ readFiles = fileCredentials,
351
+ readInstance = readInstanceRecord,
352
+ isAlive = pidAlive,
353
+ launchAge = lastLaunchAgeMs,
354
+ markLaunch = recordLaunch,
355
+ now = Date.now,
356
+ sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
357
+ } = {}) {
358
+ const start = now();
359
+ let launched = false;
360
+ let last = { healthState: null, instanceIsAlive: false, recentLaunch: false };
361
+ for (;;) {
362
+ let creds = null;
363
+ let healthState = null;
364
+ try {
365
+ creds = await readFiles(dir);
366
+ healthState = (await probe(creds.port, 1500)).state;
367
+ } catch {
368
+ /* no files yet: not running, booting, or they're being rewritten */
369
+ }
370
+ if (healthState === "up") return creds;
371
+ const alive = instanceAlive(await readInstance(dir), now(), isAlive);
372
+ let preferredState = null;
373
+ if (!alive && healthState !== "busy" && creds?.port !== preferredPort) {
374
+ preferredState = (await probe(preferredPort, 1000)).state;
375
+ }
376
+ const age = launched ? 0 : await launchAge(dir, now());
377
+ const decision = launchDecision({
378
+ healthState,
379
+ preferredState,
380
+ instanceIsAlive: alive,
381
+ launchAgeMs: age,
382
+ });
383
+ last = { healthState, preferredState, instanceIsAlive: alive };
384
+ if (decision === "launch") {
385
+ await markLaunch(dir, now());
386
+ launched = true;
387
+ await launch();
388
+ }
389
+ if (now() - start >= timeoutMs) throw new Error(notReadyMessage(last, launched, timeoutMs));
390
+ await sleep(pollMs);
391
+ }
392
+ }
393
+
394
+ function notReadyMessage({ healthState, preferredState, instanceIsAlive }, launched, timeoutMs) {
395
+ const secs = Math.round(timeoutMs / 1000);
396
+ if (preferredState === "up" && !instanceIsAlive) {
397
+ return `A PandaStudio is answering on 127.0.0.1:${DEFAULT_AUTOMATION_PORT} but its access files in ${configDir()} are missing or stale. Quit PandaStudio completely and open it again.`;
398
+ }
399
+ if (healthState === "busy") {
400
+ return `PandaStudio is running but busy: it didn't answer for ${secs}s (a heavy export or import can do this). Try again in a moment; nothing was relaunched.`;
401
+ }
402
+ if (instanceIsAlive) {
403
+ return `PandaStudio is running but its automation server didn't answer within ${secs}s. In PandaStudio open Settings → Automation and make sure Local automation is on.`;
404
+ }
405
+ if (launched) {
406
+ return `PandaStudio did not become ready within ${secs}s. Open PandaStudio, go to Settings → Automation and turn on Local automation.`;
407
+ }
408
+ return `PandaStudio was started moments ago and isn't ready yet (waited ${secs}s). Open PandaStudio and check Settings → Automation → Local automation is on.`;
409
+ }
package/bin/server.mjs CHANGED
@@ -42,11 +42,10 @@ import {
42
42
  cleanFileValue,
43
43
  configDir,
44
44
  connectInApp,
45
+ ensureApp,
45
46
  fileCredentials,
46
47
  IN_APP_UNAVAILABLE,
47
48
  isInAppCaller,
48
- probeHealth,
49
- waitForHealth,
50
49
  } from "./appConnection.mjs";
51
50
  import { portableSchema } from "./portableSchema.mjs";
52
51
 
@@ -56,10 +55,6 @@ import { portableSchema } from "./portableSchema.mjs";
56
55
 
57
56
  const IN_APP = isInAppCaller();
58
57
 
59
- async function readCredentials() {
60
- return fileCredentials(configDir());
61
- }
62
-
63
58
  function findInstalledApp() {
64
59
  if (process.env.PANDASTUDIO_BIN) return process.env.PANDASTUDIO_BIN;
65
60
  // The app records its own location (it can live anywhere on Windows,
@@ -102,46 +97,34 @@ function findInstalledApp() {
102
97
  return candidates.find((c) => existsSync(c)) ?? candidates[0];
103
98
  }
104
99
 
100
+ // Reach the running app; launch it only when nothing says it is running
101
+ // (ensureApp in appConnection.mjs: port-file health, instance.json heartbeat,
102
+ // the default port, and a machine-wide launch debounce). Launching on every
103
+ // call that missed the server restarted PandaStudio "with every request".
105
104
  async function autoLaunchAndWait(timeoutSec = 60) {
106
- // Try a fast probe in case the user already has it running.
107
- try {
108
- const c = await readCredentials();
109
- // Busy (stalled main process) is not "not running": wait for it
110
- // instead of launching a second copy.
111
- if ((await waitForHealth(c.port)).state === "up") return c;
112
- } catch {
113
- /* not running */
114
- }
115
-
116
- const bin = findInstalledApp();
117
- try {
118
- // appLaunchEnv drops ELECTRON_RUN_AS_NODE: inherited, it made the
119
- // "launch" start a Node REPL that exits instead of PandaStudio.
120
- // windowsHide: false: this is the desktop app, whose window must show.
121
- const child = spawn(bin, [], {
122
- detached: true,
123
- stdio: "ignore",
124
- env: appLaunchEnv(),
125
- windowsHide: false,
126
- });
127
- child.unref();
128
- } catch (err) {
129
- throw new Error(`failed to launch PandaStudio (${bin}): ${err?.message ?? err}`);
130
- }
131
-
132
- const deadline = Date.now() + timeoutSec * 1000;
133
- while (Date.now() < deadline) {
134
- try {
135
- const c = await readCredentials();
136
- if ((await probeHealth(c.port, 2000)).state === "up") return c;
137
- } catch {
138
- /* still booting */
139
- }
140
- await new Promise((r) => setTimeout(r, 500));
141
- }
142
- throw new Error(
143
- `PandaStudio did not become ready within ${timeoutSec}s. Open PandaStudio, go to Settings → Automation and turn on Local automation.`,
144
- );
105
+ return ensureApp({
106
+ timeoutMs: timeoutSec * 1000,
107
+ launch: async () => {
108
+ const bin = findInstalledApp();
109
+ try {
110
+ // appLaunchEnv drops ELECTRON_RUN_AS_NODE: inherited, it made the
111
+ // "launch" start a Node REPL that exits instead of PandaStudio.
112
+ // windowsHide: false: this is the desktop app, whose window must show.
113
+ const child = spawn(bin, [], {
114
+ detached: true,
115
+ stdio: "ignore",
116
+ env: appLaunchEnv(),
117
+ windowsHide: false,
118
+ });
119
+ child.on("error", () => {
120
+ /* reported by the readiness wait */
121
+ });
122
+ child.unref();
123
+ } catch (err) {
124
+ throw new Error(`failed to launch PandaStudio (${bin}): ${err?.message ?? err}`);
125
+ }
126
+ },
127
+ });
145
128
  }
146
129
 
147
130
  // The app answers /v1/call when the command finishes, and some commands run
@@ -1057,6 +1040,77 @@ const TOOLS = [
1057
1040
  },
1058
1041
  command: "workspace.contents",
1059
1042
  },
1043
+ {
1044
+ name: "brand_list_assets",
1045
+ description: "List reusable assets in the active workspace brand library.",
1046
+ inputSchema: {
1047
+ type: "object",
1048
+ properties: {},
1049
+ required: [],
1050
+ },
1051
+ command: "brand.list-assets",
1052
+ },
1053
+ {
1054
+ name: "brand_add_asset",
1055
+ description: "Copy a local image, video or audio file into the workspace brand library.",
1056
+ inputSchema: {
1057
+ type: "object",
1058
+ properties: {
1059
+ path: {
1060
+ type: "string",
1061
+ },
1062
+ name: {
1063
+ type: "string",
1064
+ },
1065
+ role: {
1066
+ type: "string",
1067
+ enum: ["logo", "outro", "intro", "cta", "watermark", "other"],
1068
+ },
1069
+ },
1070
+ required: ["path"],
1071
+ },
1072
+ command: "brand.add-asset",
1073
+ },
1074
+ {
1075
+ name: "brand_remove_asset",
1076
+ description: "Remove an asset from the library. Existing timeline media remains available.",
1077
+ inputSchema: {
1078
+ type: "object",
1079
+ properties: {
1080
+ id: {
1081
+ type: "string",
1082
+ },
1083
+ },
1084
+ required: ["id"],
1085
+ },
1086
+ command: "brand.remove-asset",
1087
+ },
1088
+ {
1089
+ name: "project_add_brand_asset",
1090
+ description:
1091
+ "Insert a brand asset at edited atMs (default 0). Images are overlays, videos clips, audio tracks. Outros append.",
1092
+ inputSchema: {
1093
+ type: "object",
1094
+ properties: {
1095
+ id: {
1096
+ type: "string",
1097
+ },
1098
+ assetId: {
1099
+ type: "string",
1100
+ },
1101
+ atMs: {
1102
+ type: "number",
1103
+ minimum: 0,
1104
+ },
1105
+ placement: {
1106
+ type: "string",
1107
+ enum: ["clip", "overlay", "append"],
1108
+ },
1109
+ },
1110
+ required: ["id", "assetId"],
1111
+ },
1112
+ command: "project.add-brand-asset",
1113
+ },
1060
1114
  {
1061
1115
  name: "workspace_get_brand",
1062
1116
  description:
@@ -1074,6 +1128,7 @@ const TOOLS = [
1074
1128
  brand: {
1075
1129
  type: ["object", "null"],
1076
1130
  properties: {
1131
+ savedColors: { type: "array", items: { type: "string" }, maxItems: 24 },
1077
1132
  name: { type: "string" },
1078
1133
  tagline: { type: "string" },
1079
1134
  colors: {
@@ -1524,6 +1579,35 @@ const TOOLS = [
1524
1579
  },
1525
1580
  command: "matte.bake",
1526
1581
  },
1582
+ {
1583
+ name: "matte_export_cutout",
1584
+ description:
1585
+ "Export the person cut out of a clip as a transparent file for other tools (bakes the matte first if needed). format webm (VP9 + alpha, default; OBS, web), mov (ProRes 4444 + alpha; Premiere, Final Cut, Resolve, After Effects, Keynote) or png (one frame at atMs; thumbnails, Canva). Source size, follows the edit (cuts skipped, 1x speed), clip audio unless audio=false; inverse also writes the background plate. Returns jobId; job_wait gives outputPath, backgroundPath, durationMs, usedIn.",
1586
+ inputSchema: {
1587
+ type: "object",
1588
+ properties: {
1589
+ id: { type: "string" },
1590
+ path: { type: "string" },
1591
+ clip: {
1592
+ type: "string",
1593
+ description: "Clip id. Default: the camera clip / first clip with a person.",
1594
+ },
1595
+ source: { type: "string", enum: ["camera", "main"] },
1596
+ format: { type: "string", enum: ["webm", "mov", "png"] },
1597
+ startMs: { type: "number", description: "Edited-timeline start; default the clip start." },
1598
+ endMs: { type: "number", description: "Edited-timeline end; default the clip end." },
1599
+ atMs: { type: "number", description: "png: edited-timeline time of the frame." },
1600
+ inverse: {
1601
+ type: "boolean",
1602
+ description: "Also write the background with the person removed.",
1603
+ },
1604
+ audio: { type: "boolean", description: "webm/mov: include the clip audio (default true)." },
1605
+ outPath: { type: "string", description: "Absolute output path. Default: beside exports." },
1606
+ },
1607
+ required: ["id"],
1608
+ },
1609
+ command: "matte.export-cutout",
1610
+ },
1527
1611
  {
1528
1612
  name: "project_read",
1529
1613
  description:
@@ -1955,6 +2039,24 @@ const TOOLS = [
1955
2039
  },
1956
2040
  command: "project.set-overlay-chroma-key",
1957
2041
  },
2042
+ {
2043
+ name: "project_remove_overlay_background",
2044
+ description:
2045
+ "Cut the subject out of an IMAGE overlay (on-device; any background). Swaps its source to <name>-cutout.png at the same size and remembers the original; restore=true puts it back.",
2046
+ inputSchema: {
2047
+ type: "object",
2048
+ properties: {
2049
+ id: { type: "string" },
2050
+ path: { type: "string" },
2051
+ regionId: { type: "string" },
2052
+ restore: { type: "boolean" },
2053
+ engine: { type: "string", enum: ["auto", "vision", "birefnet"] },
2054
+ expectedRevision: { type: "number" },
2055
+ },
2056
+ required: ["regionId"],
2057
+ },
2058
+ command: "project.remove-overlay-background",
2059
+ },
1958
2060
  {
1959
2061
  name: "project_add_fx",
1960
2062
  description: "Add an FX overlay (bundled fxId or custom src).",
@@ -2072,6 +2174,40 @@ const TOOLS = [
2072
2174
  },
2073
2175
  command: "project.add-title-behind",
2074
2176
  },
2177
+ {
2178
+ name: "project_add_3d_captions",
2179
+ description:
2180
+ "3D camera captions on a talking-head HOOK (5-20 s, 16:9, locked medium shot): the camera whips in and trucks between caption groups at different depths, a hero word hides behind the head, an optional ring of words turns in front of the speaker, a staircase finale. Rendered from the clip's footage, matte and transcript; placed over [startMs, endMs] (editor captions hidden there). Do it after the cuts. Refuses a moving camera or extreme close-up. ASYNC: job_wait gives overlayId, beats[].still (look at each), qc, warnings. Re-run replaces; reference/3d-captions.md.",
2181
+ inputSchema: {
2182
+ type: "object",
2183
+ properties: {
2184
+ id: { type: "string" },
2185
+ path: { type: "string" },
2186
+ startMs: {
2187
+ type: "number",
2188
+ description: "Edited-timeline start (a pause between sentences).",
2189
+ },
2190
+ endMs: { type: "number", description: "Edited-timeline end, 5-20 s after startMs." },
2191
+ style: { type: "string", enum: ["bold", "editorial"], description: "Default bold." },
2192
+ heroWords: {
2193
+ type: "array",
2194
+ items: { type: "string" },
2195
+ description:
2196
+ "Words/short phrases to feature big. Default: the most stressed word per beat.",
2197
+ },
2198
+ accent: { type: "string", description: "Hero colour, CSS hex." },
2199
+ grade: { type: "boolean", description: "Film finish (curves, grain). Default true." },
2200
+ hideCaptions: {
2201
+ type: "boolean",
2202
+ description: "Hide editor captions over the range. Default true.",
2203
+ },
2204
+ force: { type: "boolean", description: "Run despite a moving camera or extreme close-up." },
2205
+ expectedRevision: { type: "number" },
2206
+ },
2207
+ required: ["id", "startMs", "endMs"],
2208
+ },
2209
+ command: "project.add-3d-captions",
2210
+ },
2075
2211
 
2076
2212
  // ── compose: visual regions ─────────────────────────────────────
2077
2213
  {
@@ -3346,7 +3482,7 @@ const TOOLS = [
3346
3482
  {
3347
3483
  name: "project_update_region",
3348
3484
  description:
3349
- "Patch a region; unspecified fields are kept. Patchable: zoom (depth, focusX, focusY, focusMode static|auto), trim / speed (startMs, endMs, speed, rampIn, rampOut, preservePitch, freezeMs, reverse, reverseAudio), fx (startMs, endMs, opacity, blendMode, speed, zIndex), annotation (startMs, endMs, text = the words shown (content / textContent also work), x, y, width, height, the style fields below, zIndex), overlay (x, y, width, height in canvas %, layer, opacity, blendMode, muted, volume, loop, motionBlur, zIndex), adjustment (startMs, endMs, blendMode), motion (motionBlur), clip-transform (startMs, endMs, preset, transitionMs, easing, bezier), background-effect (as project_add_background_effect), audio-overlay (startMs, endMs, sourceStartMs, volume 0-2, fadeIn, fadeOut). 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.",
3485
+ "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.",
3350
3486
  inputSchema: {
3351
3487
  type: "object",
3352
3488
  properties: {
@@ -4681,6 +4817,24 @@ const TOOLS = [
4681
4817
  inputSchema: {
4682
4818
  type: "object",
4683
4819
  properties: {
4820
+ area: {
4821
+ type: "object",
4822
+ description: "Display-local points. Native region capture. Minimum 32 x 32 points.",
4823
+ properties: {
4824
+ displayId: { type: "number" },
4825
+ x: { type: "number" },
4826
+ y: { type: "number" },
4827
+ width: { type: "number", minimum: 32 },
4828
+ height: { type: "number", minimum: 32 },
4829
+ },
4830
+ required: ["displayId", "x", "y", "width", "height"],
4831
+ additionalProperties: false,
4832
+ },
4833
+ aspectRatio: {
4834
+ type: "string",
4835
+ enum: ["Free", "16:9", "9:16", "1:1", "4:3"],
4836
+ description: "Lock area aspect. Without area, fit on the selected display.",
4837
+ },
4684
4838
  source: { type: "string", description: "From recording_list_sources." },
4685
4839
  fps: { type: "number", description: "1-60, default 30." },
4686
4840
  systemAudio: { type: "boolean", description: "Mic is never captured." },
@@ -5164,6 +5318,25 @@ const TOOLS = [
5164
5318
  },
5165
5319
  command: "media.generate-music",
5166
5320
  },
5321
+ {
5322
+ name: "media_remove_background",
5323
+ description:
5324
+ "Remove a still image's background on this computer (people, products, pets, objects, logos). Writes <name>-cutout.png next to it; returns { path, width, height, engine }.",
5325
+ inputSchema: {
5326
+ type: "object",
5327
+ properties: {
5328
+ path: { type: "string", description: "Absolute image path." },
5329
+ trim: {
5330
+ type: "boolean",
5331
+ description: "Crop the empty margin (default true); false keeps the canvas size.",
5332
+ },
5333
+ outPath: { type: "string", description: "Optional absolute .png path." },
5334
+ engine: { type: "string", enum: ["auto", "vision", "birefnet"] },
5335
+ },
5336
+ required: ["path"],
5337
+ },
5338
+ command: "media.remove-background",
5339
+ },
5167
5340
  {
5168
5341
  name: "media_import",
5169
5342
  description:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.222.0",
3
+ "version": "1.224.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",