@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.
- package/bin/appConnection.mjs +169 -1
- package/bin/server.mjs +219 -46
- package/package.json +1 -1
package/bin/appConnection.mjs
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
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
|
|
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.
|
|
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",
|