@writepanda/mcp 1.262.0 → 1.267.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/apps/index.mjs +15 -1
- package/bin/apps/recorder.mjs +189 -0
- package/bin/server.mjs +300 -9
- package/package.json +1 -1
package/bin/apps/index.mjs
CHANGED
|
@@ -20,8 +20,9 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { CLIPS_URI, clipsHtml } from "./clips.mjs";
|
|
22
22
|
import { EDIT_PREVIEW_URI, editPreviewHtml } from "./editPreview.mjs";
|
|
23
|
+
import { RECORDER_URI, recorderHtml } from "./recorder.mjs";
|
|
23
24
|
|
|
24
|
-
export { CLIPS_URI, EDIT_PREVIEW_URI };
|
|
25
|
+
export { CLIPS_URI, EDIT_PREVIEW_URI, RECORDER_URI };
|
|
25
26
|
export const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
26
27
|
export const UI_MIME = "text/html;profile=mcp-app";
|
|
27
28
|
/** Deprecated flat key some hosts still read; the official helper sets both. */
|
|
@@ -44,6 +45,14 @@ const RESOURCES = [
|
|
|
44
45
|
"A clip set as cards (still, title, hook, score, length) with Open, Export and Remove.",
|
|
45
46
|
html: clipsHtml,
|
|
46
47
|
},
|
|
48
|
+
{
|
|
49
|
+
uri: RECORDER_URI,
|
|
50
|
+
name: "recorder",
|
|
51
|
+
title: "Recorder",
|
|
52
|
+
description:
|
|
53
|
+
"The script loaded into PandaStudio's teleprompter (editable, with reading time), Open the recorder again, and I've recorded it.",
|
|
54
|
+
html: recorderHtml,
|
|
55
|
+
},
|
|
47
56
|
];
|
|
48
57
|
|
|
49
58
|
/** Resource-level UI metadata: no network or extra origins needed (images
|
|
@@ -77,6 +86,11 @@ export const TOOL_VIEWS = {
|
|
|
77
86
|
},
|
|
78
87
|
clips_make: { uri: CLIPS_URI, invoking: "Starting clips…", invoked: "Making clips" },
|
|
79
88
|
clips_get: { uri: CLIPS_URI, invoking: "Loading clips…", invoked: "Clips loaded" },
|
|
89
|
+
recording_open_recorder: {
|
|
90
|
+
uri: RECORDER_URI,
|
|
91
|
+
invoking: "Opening the recorder…",
|
|
92
|
+
invoked: "Recorder open",
|
|
93
|
+
},
|
|
80
94
|
};
|
|
81
95
|
|
|
82
96
|
/** Tools only views call (hidden from the model by UI hosts). */
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Recorder view (ui://pandastudio/recorder.html) for recording_open_recorder:
|
|
2
|
+
// the script the agent loaded into PandaStudio's teleprompter (editable, with
|
|
3
|
+
// a reading-time estimate), Open the recorder again, and "I've recorded it",
|
|
4
|
+
// which tells the agent to carry on with the take. The user always presses
|
|
5
|
+
// Record in PandaStudio itself; this view never starts a camera or mic.
|
|
6
|
+
|
|
7
|
+
import { viewPage } from "./viewKit.mjs";
|
|
8
|
+
|
|
9
|
+
export const RECORDER_URI = "ui://pandastudio/recorder.html";
|
|
10
|
+
|
|
11
|
+
const CSS = `
|
|
12
|
+
.head { margin-bottom: 10px; }
|
|
13
|
+
.status { margin-top: 8px; padding: 9px 11px; border-radius: var(--radius); background: var(--bg2); font-size: 13px; }
|
|
14
|
+
.status.error { color: var(--danger); }
|
|
15
|
+
.modes { display: flex; gap: 4px; margin-top: 10px; flex-wrap: wrap; }
|
|
16
|
+
.modes .btn.on { background: var(--fg); color: var(--bg); border-color: var(--fg); }
|
|
17
|
+
textarea { width: 100%; min-height: 150px; margin-top: 10px; padding: 10px 11px; border-radius: var(--radius);
|
|
18
|
+
border: 1px solid var(--line); background: var(--bg); color: var(--fg); font: 14px/1.5 var(--font); resize: vertical; }
|
|
19
|
+
.meta { margin-top: 4px; }
|
|
20
|
+
.acts { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
/** Runs in the view. Self-contained: serialized into the page. */
|
|
24
|
+
function main(kit, ui) {
|
|
25
|
+
const { h, duration, actionButton, toast } = ui;
|
|
26
|
+
const app = document.getElementById("app");
|
|
27
|
+
const view = {
|
|
28
|
+
script: "",
|
|
29
|
+
mode: "camera",
|
|
30
|
+
orientation: null,
|
|
31
|
+
opened: false,
|
|
32
|
+
error: null,
|
|
33
|
+
sent: false,
|
|
34
|
+
};
|
|
35
|
+
const MODES = [
|
|
36
|
+
["camera", "Camera"],
|
|
37
|
+
["screen", "Screen"],
|
|
38
|
+
["both", "Screen + camera"],
|
|
39
|
+
];
|
|
40
|
+
const ORIENTATIONS = [
|
|
41
|
+
["portrait", "Portrait 9:16"],
|
|
42
|
+
["landscape", "Landscape 16:9"],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const estimate = (text) => {
|
|
46
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
47
|
+
return { words, readSeconds: Math.round((words / 150) * 60) };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
async function open() {
|
|
51
|
+
const { data } = await kit.callTool("recording_open_recorder", {
|
|
52
|
+
...(view.script.trim() ? { script: view.script.trim() } : {}),
|
|
53
|
+
mode: view.mode,
|
|
54
|
+
...(view.orientation ? { orientation: view.orientation } : {}),
|
|
55
|
+
});
|
|
56
|
+
view.opened = !!data?.opened;
|
|
57
|
+
view.error = null;
|
|
58
|
+
toast("The recorder is open in PandaStudio.");
|
|
59
|
+
render();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function render() {
|
|
63
|
+
const est = estimate(view.script);
|
|
64
|
+
const area = h("textarea", {
|
|
65
|
+
"aria-label": "Script for the teleprompter",
|
|
66
|
+
placeholder: "The script to read on camera",
|
|
67
|
+
});
|
|
68
|
+
area.value = view.script;
|
|
69
|
+
area.addEventListener("input", () => {
|
|
70
|
+
view.script = area.value;
|
|
71
|
+
const e = estimate(view.script);
|
|
72
|
+
meta.textContent = e.words
|
|
73
|
+
? `${e.words} words · about ${duration(e.readSeconds * 1000)} to read`
|
|
74
|
+
: "No script: the recorder opens without the teleprompter.";
|
|
75
|
+
});
|
|
76
|
+
const meta = h(
|
|
77
|
+
"div",
|
|
78
|
+
{ class: "small faint meta" },
|
|
79
|
+
est.words
|
|
80
|
+
? `${est.words} words · about ${duration(est.readSeconds * 1000)} to read`
|
|
81
|
+
: "No script: the recorder opens without the teleprompter.",
|
|
82
|
+
);
|
|
83
|
+
app.replaceChildren(
|
|
84
|
+
h(
|
|
85
|
+
"div",
|
|
86
|
+
{ class: "head" },
|
|
87
|
+
h("div", { class: "brand" }, h("i"), "PandaStudio recorder"),
|
|
88
|
+
h(
|
|
89
|
+
"div",
|
|
90
|
+
{ class: view.error ? "status error" : "status" },
|
|
91
|
+
view.error ??
|
|
92
|
+
(view.opened
|
|
93
|
+
? "The recorder is open in PandaStudio with this script in the teleprompter. Press Record in the bar at the bottom of your screen; the script scrolls as you talk."
|
|
94
|
+
: "Opening the recorder in PandaStudio…"),
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
h(
|
|
98
|
+
"div",
|
|
99
|
+
{ class: "modes", role: "radiogroup", "aria-label": "What to record" },
|
|
100
|
+
MODES.map(([id, label]) =>
|
|
101
|
+
h(
|
|
102
|
+
"button",
|
|
103
|
+
{
|
|
104
|
+
type: "button",
|
|
105
|
+
class: view.mode === id ? "btn on" : "btn",
|
|
106
|
+
"aria-pressed": String(view.mode === id),
|
|
107
|
+
onclick: () => {
|
|
108
|
+
view.mode = id;
|
|
109
|
+
render();
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
label,
|
|
113
|
+
),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
h(
|
|
117
|
+
"div",
|
|
118
|
+
{ class: "modes", role: "radiogroup", "aria-label": "Shape" },
|
|
119
|
+
ORIENTATIONS.map(([id, label]) =>
|
|
120
|
+
h(
|
|
121
|
+
"button",
|
|
122
|
+
{
|
|
123
|
+
type: "button",
|
|
124
|
+
class: view.orientation === id ? "btn on" : "btn",
|
|
125
|
+
"aria-pressed": String(view.orientation === id),
|
|
126
|
+
onclick: () => {
|
|
127
|
+
view.orientation = id;
|
|
128
|
+
render();
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
label,
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
area,
|
|
136
|
+
meta,
|
|
137
|
+
h(
|
|
138
|
+
"div",
|
|
139
|
+
{ class: "acts" },
|
|
140
|
+
actionButton(view.opened ? "Update and reopen" : "Open the recorder", open, {
|
|
141
|
+
primary: !view.opened,
|
|
142
|
+
busy: "Opening…",
|
|
143
|
+
}),
|
|
144
|
+
view.opened
|
|
145
|
+
? actionButton(
|
|
146
|
+
view.sent ? "Told the assistant" : "I've recorded it",
|
|
147
|
+
async () => {
|
|
148
|
+
if (view.sent) return;
|
|
149
|
+
await kit.sendMessage(
|
|
150
|
+
"I've finished recording in PandaStudio. Find the new take (project_current) and carry on.",
|
|
151
|
+
);
|
|
152
|
+
view.sent = true;
|
|
153
|
+
render();
|
|
154
|
+
},
|
|
155
|
+
{ primary: true, busy: "Sending…" },
|
|
156
|
+
)
|
|
157
|
+
: null,
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
render();
|
|
163
|
+
kit.on("input", (args) => {
|
|
164
|
+
if (typeof args?.script === "string") view.script = args.script;
|
|
165
|
+
if (args?.mode === "camera" || args?.mode === "screen" || args?.mode === "both")
|
|
166
|
+
view.mode = args.mode;
|
|
167
|
+
else view.mode = view.script.trim() ? "camera" : "both";
|
|
168
|
+
if (args?.orientation === "portrait" || args?.orientation === "landscape")
|
|
169
|
+
view.orientation = args.orientation;
|
|
170
|
+
render();
|
|
171
|
+
});
|
|
172
|
+
kit.on("result", (result) => {
|
|
173
|
+
view.error = kit.resultError(result);
|
|
174
|
+
const data = kit.resultData(result) ?? {};
|
|
175
|
+
view.opened = !view.error && !!data.opened;
|
|
176
|
+
if (data.mode) view.mode = data.mode;
|
|
177
|
+
if (data.orientation) view.orientation = data.orientation;
|
|
178
|
+
render();
|
|
179
|
+
});
|
|
180
|
+
kit.on("context", () => render());
|
|
181
|
+
kit.connect().catch((err) => {
|
|
182
|
+
view.error = `Couldn't connect to the chat: ${err?.message ?? err}`;
|
|
183
|
+
render();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function recorderHtml(version) {
|
|
188
|
+
return viewPage({ title: "PandaStudio recorder", version, css: CSS, main });
|
|
189
|
+
}
|
package/bin/server.mjs
CHANGED
|
@@ -583,13 +583,25 @@ const MOTION_ELEMENT_TYPES = [
|
|
|
583
583
|
"ticker",
|
|
584
584
|
"credits",
|
|
585
585
|
"crawl",
|
|
586
|
+
"reelWall",
|
|
587
|
+
"cardFan",
|
|
588
|
+
"orbit",
|
|
589
|
+
"polaroids",
|
|
590
|
+
"wordWall",
|
|
591
|
+
"neon",
|
|
592
|
+
"bloom",
|
|
593
|
+
"scrapbook",
|
|
594
|
+
"keycaps",
|
|
595
|
+
"productCard",
|
|
596
|
+
"scrollList",
|
|
597
|
+
"splitScreen",
|
|
586
598
|
];
|
|
587
599
|
const MOTION_ELEMENT_PROPS = {
|
|
588
600
|
type: { type: "string", enum: MOTION_ELEMENT_TYPES },
|
|
589
601
|
content: {
|
|
590
602
|
type: ["object", "string", "array"],
|
|
591
603
|
description:
|
|
592
|
-
'keyword/chip/stamp/slam/behind/highlight { text } (or a bare string); count { value, to?, prefix?, suffix?, label?, decimals? } (value "20-30" = range); steps { items[], cues? (ms offsets from start) }; lowerThird { title, subtitle? (<=320 chars, wraps), look?: plate|plain|typewriter, typeSpeed? (typewriter pace, 5-80 chars/s, default ~22) }; endCard { lines: [a, b] }; progress { label?, fromMs?, toMs? }; iconPop { emoji } | { icon }; frame { look: viewfinder | selection | corners, label? }; sticker { image: transparent PNG path, label? }; ticker { text (one story per line, <=2000 chars), label?, speed? (40-600 px/s) }; credits { title?, entries: [{ role?, name }] | "Role: Name" strings (<=200) }; crawl { title?, text (<=6000 chars), tilt? (0-80), turn? (-60..60) }. Any script, symbols and emoji draw.',
|
|
604
|
+
'keyword/chip/stamp/slam/behind/highlight { text } (or a bare string); count { value, to?, prefix?, suffix?, label?, decimals? } (value "20-30" = range); steps { items[], cues? (ms offsets from start) }; lowerThird { title, subtitle? (<=320 chars, wraps), look?: plate|plain|typewriter, typeSpeed? (typewriter pace, 5-80 chars/s, default ~22) }; endCard { lines: [a, b] }; progress { label?, fromMs?, toMs? }; iconPop { emoji } | { icon }; frame { look: viewfinder | selection | corners, label? }; sticker { image: transparent PNG path or "@speaker" (the speaker cut out of her shot), label?, twin? (mirrored pair, one each side) }; ticker { text (one story per line, <=2000 chars), label?, speed? (40-600 px/s) }; credits { title?, entries: [{ role?, name }] | "Role: Name" strings (<=200) }; crawl { title?, text (<=6000 chars), tilt? (0-80), turn? (-60..60) }. Creator effects (picture slots take a path or "@speaker" = the speaker\'s own live shot, the default): reelWall { images?, count? 4-24 } (tiles of her shot behind her); cardFan { images?, count? 3-9 } (card reveal: cards fan in an arc behind her head); orbit { images?, count? 2-6 } (cards circle her, layer around); polaroids { images?, count? 2-8, captions? } (scattered around her, layer around); wordWall { text, rows? 3-14, background? } (word repeated in rows behind her); neon { text, flicker? }; bloom { text } (colour bloom); scrapbook { text } (ransom-note letters); keycaps { text <=24 } (keys falling, pile at the bottom); productCard { title, subtitle?, price?, image?, button? ("Add to cart"), done? ("Added") }; scrollList { title?, items: (string | { title, subtitle?, image? })[] <=30, look?: playlist|feed } (phone list scrolling); splitScreen { panels? 2|3, images?, zooms? 1-4 } (three scenes). Any script, symbols and emoji draw.',
|
|
593
605
|
},
|
|
594
606
|
atMs: { type: "number", description: "Start, edited ms. Or wordId." },
|
|
595
607
|
wordId: {
|
|
@@ -633,7 +645,12 @@ const MOTION_ELEMENT_PROPS = {
|
|
|
633
645
|
description:
|
|
634
646
|
"auto (clear of the face) | top | upper | center | lower | bottom | { x, y, w? } (% of frame; x,y = centre).",
|
|
635
647
|
},
|
|
636
|
-
layer: {
|
|
648
|
+
layer: {
|
|
649
|
+
type: "string",
|
|
650
|
+
enum: ["front", "behind", "around"],
|
|
651
|
+
description:
|
|
652
|
+
"front | behind (behind the speaker's cut-out) | around (orbit, polaroids: the far side behind her, the near side in front; their default). Default per type.",
|
|
653
|
+
},
|
|
637
654
|
sound: {
|
|
638
655
|
type: "string",
|
|
639
656
|
enum: ["auto", "none", "pop", "whoosh", "hit", "tick", "bell"],
|
|
@@ -3072,7 +3089,10 @@ const TOOLS = [
|
|
|
3072
3089
|
type: "boolean",
|
|
3073
3090
|
description: "Follow the recorded cursor (screen recordings).",
|
|
3074
3091
|
},
|
|
3075
|
-
soundUrl: {
|
|
3092
|
+
soundUrl: {
|
|
3093
|
+
type: "string",
|
|
3094
|
+
description: 'Default bundled:sound/motion-swoosh-up; "none".',
|
|
3095
|
+
},
|
|
3076
3096
|
soundVolume: { type: "number", description: "0-1." },
|
|
3077
3097
|
anchorSourceMs: ANCHOR_PROP,
|
|
3078
3098
|
anchorSourceEndMs: ANCHOR_END_PROP,
|
|
@@ -3556,6 +3576,35 @@ const TOOLS = [
|
|
|
3556
3576
|
},
|
|
3557
3577
|
command: "project.add-spotlight",
|
|
3558
3578
|
},
|
|
3579
|
+
{
|
|
3580
|
+
name: "project_set_speaker_background",
|
|
3581
|
+
description:
|
|
3582
|
+
"The speaker's background for the WHOLE video as one look: blur, remove (wallpaper shows; outline on by default), image (studio plate) or none. Replaces the previous whole-video background instead of stacking; partial background-effect regions stay. Camera footage. For one stretch use project_add_background_effect.",
|
|
3583
|
+
inputSchema: {
|
|
3584
|
+
type: "object",
|
|
3585
|
+
properties: {
|
|
3586
|
+
id: { type: "string" },
|
|
3587
|
+
path: { type: "string" },
|
|
3588
|
+
mode: { type: "string", enum: ["none", "blur", "remove", "image"] },
|
|
3589
|
+
strength: { type: "number", description: "blur: 1-120, default 18." },
|
|
3590
|
+
backgroundImage: {
|
|
3591
|
+
type: "string",
|
|
3592
|
+
description:
|
|
3593
|
+
"image: warm-creator, tech-rgb, neutral-grey, podcast-warm, daylight-airy, gradient-gel, cinematic-dark, or an image path.",
|
|
3594
|
+
},
|
|
3595
|
+
outline: {
|
|
3596
|
+
type: "boolean",
|
|
3597
|
+
description: "Keyline around the cutout (on by default for remove).",
|
|
3598
|
+
},
|
|
3599
|
+
outlineWidth: { type: "number" },
|
|
3600
|
+
outlineColor: { type: "string" },
|
|
3601
|
+
outlineShadow: { type: "boolean" },
|
|
3602
|
+
expectedRevision: { type: "number" },
|
|
3603
|
+
},
|
|
3604
|
+
required: ["mode"],
|
|
3605
|
+
},
|
|
3606
|
+
command: "project.set-speaker-background",
|
|
3607
|
+
},
|
|
3559
3608
|
{
|
|
3560
3609
|
name: "project_add_background_effect",
|
|
3561
3610
|
description:
|
|
@@ -4738,7 +4787,8 @@ const TOOLS = [
|
|
|
4738
4787
|
},
|
|
4739
4788
|
frameRate: {
|
|
4740
4789
|
type: ["number", "string"],
|
|
4741
|
-
description:
|
|
4790
|
+
description:
|
|
4791
|
+
"auto (default) | 23.976 | 24 | 25 | 29.97 | 30 | 50 | 59.94 | 60 (25 / 50 for PAL) | source (fastest source).",
|
|
4742
4792
|
},
|
|
4743
4793
|
expectedRevision: { type: "number" },
|
|
4744
4794
|
},
|
|
@@ -5943,6 +5993,30 @@ const TOOLS = [
|
|
|
5943
5993
|
},
|
|
5944
5994
|
command: "caption.set-style",
|
|
5945
5995
|
},
|
|
5996
|
+
{
|
|
5997
|
+
name: "caption_set_look",
|
|
5998
|
+
description:
|
|
5999
|
+
"Set a COMPLETE caption look in one call, replacing the current one: on/off, template, words per line, position and every override (in style). What the look leaves out goes back to the template's own, never what an earlier style left. How a recipe's captions are applied; use it to copy a look exactly. For one tweak use caption_set_style.",
|
|
6000
|
+
inputSchema: {
|
|
6001
|
+
type: "object",
|
|
6002
|
+
properties: {
|
|
6003
|
+
id: { type: "string" },
|
|
6004
|
+
path: { type: "string" },
|
|
6005
|
+
enabled: { type: "boolean" },
|
|
6006
|
+
templateId: { type: "string", description: "A caption_set_template id." },
|
|
6007
|
+
wordsPerLine: { type: "number", description: "1-10. Default the template's own." },
|
|
6008
|
+
positionY: { type: "number", description: "% from the top. Default the project's." },
|
|
6009
|
+
style: {
|
|
6010
|
+
type: "object",
|
|
6011
|
+
description:
|
|
6012
|
+
"caption_set_style's look fields: fontFamily, fontSize, color, backgroundColor, highlightColor, highlightBackgroundColor, strokeColor, strokeWidth, uppercase, highlightMode, emphasisColor, emphasisBackgroundColor, emphasisScale, emphasisItalic, emphasisFontFamily, boxRotation.",
|
|
6013
|
+
},
|
|
6014
|
+
expectedRevision: { type: "number" },
|
|
6015
|
+
},
|
|
6016
|
+
required: ["enabled"],
|
|
6017
|
+
},
|
|
6018
|
+
command: "caption.set-look",
|
|
6019
|
+
},
|
|
5946
6020
|
{
|
|
5947
6021
|
name: "caption_mark_emphasis",
|
|
5948
6022
|
description:
|
|
@@ -6127,6 +6201,36 @@ const TOOLS = [
|
|
|
6127
6201
|
inputSchema: { type: "object", properties: {} },
|
|
6128
6202
|
command: "recording.list-sources",
|
|
6129
6203
|
},
|
|
6204
|
+
{
|
|
6205
|
+
name: "recording_open_recorder",
|
|
6206
|
+
description:
|
|
6207
|
+
"Open PandaStudio's recorder for the USER to record themselves, with the script you wrote loaded in the teleprompter (scrolls with the take, voice tracking available, hidden from the recording). Use after writing a script the user will read on camera; then tell them to press Record in PandaStudio's recorder bar. You never start their camera or microphone. When they stop, the take opens in the editor as a new project (project_current). Returns { opened, mode, teleprompter: { words, readSeconds } | false }. For an unattended screen capture use recording_start.",
|
|
6208
|
+
inputSchema: {
|
|
6209
|
+
type: "object",
|
|
6210
|
+
properties: {
|
|
6211
|
+
script: {
|
|
6212
|
+
type: "string",
|
|
6213
|
+
description:
|
|
6214
|
+
"What the user will read, plain text (no markdown or stage directions), up to 20,000 characters.",
|
|
6215
|
+
},
|
|
6216
|
+
mode: {
|
|
6217
|
+
type: "string",
|
|
6218
|
+
enum: ["camera", "screen", "both"],
|
|
6219
|
+
description:
|
|
6220
|
+
"camera (solo; default with a script), screen (screen only), or both (screen + camera bubble; default without a script).",
|
|
6221
|
+
},
|
|
6222
|
+
orientation: {
|
|
6223
|
+
type: "string",
|
|
6224
|
+
enum: ["landscape", "portrait"],
|
|
6225
|
+
description:
|
|
6226
|
+
"landscape 16:9 (YouTube) or portrait 9:16 (Shorts, Reels, TikTok): the camera shape for a camera take; a screen take's final shape is set while editing.",
|
|
6227
|
+
},
|
|
6228
|
+
speed: { type: "number", description: "Teleprompter scroll speed 0.25-3." },
|
|
6229
|
+
fontSize: { type: "number", description: "Teleprompter text size in px, 16-72." },
|
|
6230
|
+
},
|
|
6231
|
+
},
|
|
6232
|
+
command: "recording.open-recorder",
|
|
6233
|
+
},
|
|
6130
6234
|
{
|
|
6131
6235
|
name: "recording_start",
|
|
6132
6236
|
description:
|
|
@@ -6382,7 +6486,7 @@ const TOOLS = [
|
|
|
6382
6486
|
{
|
|
6383
6487
|
name: "project_add_motion_element",
|
|
6384
6488
|
description:
|
|
6385
|
-
|
|
6489
|
+
'Native motion element: an editable, word-timed graphic the engine draws (preview = export, no HTML render): keyword headline, chip, stamp, count (number/range rolls up), steps, slam (+ camera kick), behind (word behind the speaker), lowerThird, highlight, endCard, progress, iconPop, frame (viewfinder / selection box / corners around the speaker), sticker, ticker, credits, crawl, and the short-form creator effects around the speaker: reelWall, cardFan (card reveal), orbit, polaroids, wordWall, neon, bloom (colour bloom), scrapbook, keycaps (keys falling), productCard (add to cart), scrollList (scroll overlay), splitScreen (three scenes); "@speaker" in any picture slot shows her own live shot. Anchor with wordId (follows the word through cuts) or atMs. Brand kit by default. Returns { elementId, element }.',
|
|
6386
6490
|
inputSchema: {
|
|
6387
6491
|
type: "object",
|
|
6388
6492
|
properties: {
|
|
@@ -7198,6 +7302,151 @@ const TOOLS = [
|
|
|
7198
7302
|
},
|
|
7199
7303
|
command: "recipe.prepare-reference",
|
|
7200
7304
|
},
|
|
7305
|
+
{
|
|
7306
|
+
name: "reference_check_link",
|
|
7307
|
+
description:
|
|
7308
|
+
"Check a pasted reference link without downloading: YouTube, TikTok, Instagram and X video links are accepted. Returns { url, platform } or a clear error (not a supported site, not a single video, local/private address).",
|
|
7309
|
+
inputSchema: {
|
|
7310
|
+
type: "object",
|
|
7311
|
+
properties: { url: { type: "string" } },
|
|
7312
|
+
required: ["url"],
|
|
7313
|
+
},
|
|
7314
|
+
command: "reference.check-link",
|
|
7315
|
+
},
|
|
7316
|
+
{
|
|
7317
|
+
name: "reference_gallery",
|
|
7318
|
+
description:
|
|
7319
|
+
"Curated reference videos (Home → Start from a proven edit → Don't see your style?): real creators' videos worth copying the edit of, from the signed gallery on writepanda.ai. Returns { entries: [{ id, title, creator (@handle), creatorName, avatar, platform, format, views, viewsLabel, durationMs, poster, sourceUrl (the original post), referenceVideo (hover preview, optional), resultVideo, recipeId, tags }] }. Prepare one with reference_prepare { galleryId } (fetches the whole original from sourceUrl on this computer); recipeId = a prebuilt recipe for its style.",
|
|
7320
|
+
inputSchema: { type: "object", properties: {} },
|
|
7321
|
+
command: "reference.gallery",
|
|
7322
|
+
},
|
|
7323
|
+
{
|
|
7324
|
+
name: "reference_prepare",
|
|
7325
|
+
description:
|
|
7326
|
+
"'Make my video like this one': bring in a reference video and study it. Exactly one of url (YouTube / TikTok / Instagram / X video link, fetched on this computer), file (absolute .mp4/.mov/.webm) or galleryId (reference_gallery). Up to 30 minutes. Async: returns { jobId }; job_wait gives { id, title, durationMs, manifestPath (overview + motion sheets, audio excerpts), source }. Clear errors for private / sign-in-only videos and unsupported sites. From then on the reference is GUARDED: every media verb and export refuses it (code REFERENCE_MEDIA). Next: reference_analyze, then reference_render.",
|
|
7327
|
+
inputSchema: {
|
|
7328
|
+
type: "object",
|
|
7329
|
+
properties: {
|
|
7330
|
+
url: { type: "string", description: "YouTube / TikTok / Instagram / X video link." },
|
|
7331
|
+
file: { type: "string", description: "Absolute path of an .mp4 / .mov / .webm." },
|
|
7332
|
+
galleryId: { type: "string", description: "Curated reference id (reference_gallery)." },
|
|
7333
|
+
title: { type: "string" },
|
|
7334
|
+
},
|
|
7335
|
+
},
|
|
7336
|
+
command: "reference.prepare",
|
|
7337
|
+
},
|
|
7338
|
+
{
|
|
7339
|
+
name: "reference_analyze",
|
|
7340
|
+
description:
|
|
7341
|
+
"The setup-screen analysis of a prepared reference: title, format, aspect, summary, the rebuild list (elements: every graphic / transition / caption / effect / camera move with its reference time) and up to 5 slots for the user's material (footage first; logo, screen, product, text). Without `analysis` it runs on the user's own assistant model with the contact sheets attached (async { jobId }; default slots + analysisError when no model). If you looked at the sheets yourself, pass your own `analysis` (same shape): stored at once.",
|
|
7342
|
+
inputSchema: {
|
|
7343
|
+
type: "object",
|
|
7344
|
+
properties: {
|
|
7345
|
+
id: { type: "string", description: "Reference id." },
|
|
7346
|
+
analysis: {
|
|
7347
|
+
type: "object",
|
|
7348
|
+
description:
|
|
7349
|
+
"{ title, format: short|long, aspect: 9:16|16:9|1:1|3:4, summary, elements: [{ name, kind: graphic|transition|caption|effect|camera|sound, atMs, description }], slots: [{ key, kind: footage|logo|screen|product|text, label, hint, required }] }",
|
|
7350
|
+
},
|
|
7351
|
+
providerId: { type: "string" },
|
|
7352
|
+
modelId: { type: "string" },
|
|
7353
|
+
},
|
|
7354
|
+
required: ["id"],
|
|
7355
|
+
},
|
|
7356
|
+
command: "reference.analyze",
|
|
7357
|
+
},
|
|
7358
|
+
{
|
|
7359
|
+
name: "reference_render",
|
|
7360
|
+
description:
|
|
7361
|
+
"The one instruction that makes the user's video in the reference's style, from the filled slots: mode learn (learn the style from the evidence; saveRecipe keeps it as a personal recipe, default true) or recipe (apply recipeId, default the curated entry's prebuilt recipe), then rebuild every element natively, project_render_frame → reference_compare → reference_score each one BEFORE the user's content goes in. mode save: only study the reference and save its style as a personal recipe named recipeTitle, no video now (slots ignored). Returns { prompt, missing } (required slots still empty; always empty for save). Follow the prompt. Changes nothing itself.",
|
|
7362
|
+
inputSchema: {
|
|
7363
|
+
type: "object",
|
|
7364
|
+
properties: {
|
|
7365
|
+
id: { type: "string", description: "Reference id." },
|
|
7366
|
+
slots: {
|
|
7367
|
+
type: "object",
|
|
7368
|
+
description:
|
|
7369
|
+
'{ "<slot key>": { "mode": "file", "path": "/abs" } | { "mode": "files", "paths": ["/abs"] } | { "mode": "text", "text": "..." } | { "mode": "agent" } }',
|
|
7370
|
+
},
|
|
7371
|
+
mode: { type: "string", enum: ["learn", "recipe", "save"] },
|
|
7372
|
+
recipeTitle: { type: "string", description: "mode save: the new recipe's name." },
|
|
7373
|
+
recipeId: { type: "string" },
|
|
7374
|
+
saveRecipe: { type: "boolean" },
|
|
7375
|
+
projectId: { type: "string", description: "Edit this project instead of a new one." },
|
|
7376
|
+
note: { type: "string", description: "The user's own words." },
|
|
7377
|
+
},
|
|
7378
|
+
required: ["id"],
|
|
7379
|
+
},
|
|
7380
|
+
command: "reference.render",
|
|
7381
|
+
},
|
|
7382
|
+
{
|
|
7383
|
+
name: "reference_compare",
|
|
7384
|
+
description:
|
|
7385
|
+
"Side-by-side JPG of the reference at atMs (reference time, left) and a rendered frame of the user's edit (the PNG from project_render_frame, right), to judge one rebuilt graphic / transition / effect. Returns { path, referenceFrame }. Look at it, then reference_score.",
|
|
7386
|
+
inputSchema: {
|
|
7387
|
+
type: "object",
|
|
7388
|
+
properties: {
|
|
7389
|
+
id: { type: "string" },
|
|
7390
|
+
frame: { type: "string", description: "Absolute path of the rendered frame." },
|
|
7391
|
+
atMs: { type: "number", description: "Reference time (ms)." },
|
|
7392
|
+
},
|
|
7393
|
+
required: ["id", "frame", "atMs"],
|
|
7394
|
+
},
|
|
7395
|
+
command: "reference.compare",
|
|
7396
|
+
},
|
|
7397
|
+
{
|
|
7398
|
+
name: "reference_score",
|
|
7399
|
+
description:
|
|
7400
|
+
"Record how close a rebuilt element is to the reference after reference_compare: score 1-5 (4 = close enough to ship, 5 = indistinguishable in look, motion and timing) and a note of what still differs. One score per element name.",
|
|
7401
|
+
inputSchema: {
|
|
7402
|
+
type: "object",
|
|
7403
|
+
properties: {
|
|
7404
|
+
id: { type: "string" },
|
|
7405
|
+
element: { type: "string" },
|
|
7406
|
+
score: { type: "number", minimum: 1, maximum: 5 },
|
|
7407
|
+
note: { type: "string" },
|
|
7408
|
+
atMs: { type: "number" },
|
|
7409
|
+
},
|
|
7410
|
+
required: ["id", "element", "score"],
|
|
7411
|
+
},
|
|
7412
|
+
command: "reference.score",
|
|
7413
|
+
},
|
|
7414
|
+
{
|
|
7415
|
+
name: "reference_attach",
|
|
7416
|
+
description:
|
|
7417
|
+
"Record on a project which reference it follows (and the recipe), so later turns can pick the work up; project_read shows it as `reference`. clear=true removes it. Metadata only: the reference's media never enters the project.",
|
|
7418
|
+
inputSchema: {
|
|
7419
|
+
type: "object",
|
|
7420
|
+
properties: {
|
|
7421
|
+
id: { type: "string", description: "Reference id (not needed with clear)." },
|
|
7422
|
+
projectId: { type: "string" },
|
|
7423
|
+
recipeId: { type: "string" },
|
|
7424
|
+
clear: { type: "boolean" },
|
|
7425
|
+
},
|
|
7426
|
+
required: ["projectId"],
|
|
7427
|
+
},
|
|
7428
|
+
command: "reference.attach",
|
|
7429
|
+
},
|
|
7430
|
+
{
|
|
7431
|
+
name: "reference_get",
|
|
7432
|
+
description:
|
|
7433
|
+
"One prepared reference: { id, title, source, durationMs, manifestPath, analysis, scores, projectIds }.",
|
|
7434
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
7435
|
+
command: "reference.get",
|
|
7436
|
+
},
|
|
7437
|
+
{
|
|
7438
|
+
name: "reference_list",
|
|
7439
|
+
description: "Prepared references, newest first.",
|
|
7440
|
+
inputSchema: { type: "object", properties: {} },
|
|
7441
|
+
command: "reference.list",
|
|
7442
|
+
},
|
|
7443
|
+
{
|
|
7444
|
+
name: "reference_delete",
|
|
7445
|
+
description:
|
|
7446
|
+
"Delete a prepared reference (its downloaded video, study sheets, side-by-sides). An uploaded file stays where the user keeps it; projects made from it are untouched.",
|
|
7447
|
+
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
7448
|
+
command: "reference.delete",
|
|
7449
|
+
},
|
|
7201
7450
|
{
|
|
7202
7451
|
name: "recipe_pick",
|
|
7203
7452
|
description:
|
|
@@ -7246,7 +7495,7 @@ const TOOLS = [
|
|
|
7246
7495
|
{
|
|
7247
7496
|
name: "recipe_apply_style",
|
|
7248
7497
|
description:
|
|
7249
|
-
"Apply a recipe's fixed style to a project, before recipe_render's content work. Returns { applied, failed, pending, notes }: applied / failed are read back from the project, notes = parts left to you. Fails, changing nothing, when the recipe needs footage the project lacks (a camera recipe on an audio-only project).",
|
|
7498
|
+
"Apply a recipe's fixed style to a project, before recipe_render's content work: aspect, the full caption look, speaker background, vertical framing, grade, camera layout. It REPLACES an earlier recipe's look. Returns { applied, failed, pending, notes }: applied / failed are read back from the project, notes = parts left to you. Fails, changing nothing, when the recipe needs footage the project lacks (a camera recipe on an audio-only project).",
|
|
7250
7499
|
inputSchema: {
|
|
7251
7500
|
type: "object",
|
|
7252
7501
|
properties: {
|
|
@@ -7259,6 +7508,17 @@ const TOOLS = [
|
|
|
7259
7508
|
},
|
|
7260
7509
|
command: "recipe.apply-style",
|
|
7261
7510
|
},
|
|
7511
|
+
{
|
|
7512
|
+
name: "recipe_look_frames",
|
|
7513
|
+
description:
|
|
7514
|
+
"The recipe's target look as images (attached): stills of its finished edit at key moments, each with what it shows. Look before editing with a recipe; compare project_render_frame at the matching beats before saying done. Match the look, never the content.",
|
|
7515
|
+
inputSchema: {
|
|
7516
|
+
type: "object",
|
|
7517
|
+
properties: { id: { type: "string", description: "Recipe id." } },
|
|
7518
|
+
required: ["id"],
|
|
7519
|
+
},
|
|
7520
|
+
command: "recipe.look-frames",
|
|
7521
|
+
},
|
|
7262
7522
|
{
|
|
7263
7523
|
name: "recipe_save",
|
|
7264
7524
|
description:
|
|
@@ -7269,7 +7529,7 @@ const TOOLS = [
|
|
|
7269
7529
|
recipe: {
|
|
7270
7530
|
type: "object",
|
|
7271
7531
|
description:
|
|
7272
|
-
"{ id?, title, description, category, footage?, aspectRatio?, fields: [{ key, label, type: text|longtext|select|color|images, default?, options?, required?, help?, minCount?, maxCount?, optional? }], prompt, style?: { aspectRatio?, colorCorrection?, lut?: { preset, intensity? },
|
|
7532
|
+
"{ id?, title, description, category, footage?, aspectRatio?, fields: [{ key, label, type: text|longtext|select|color|images, default?, options?, required?, help?, minCount?, maxCount?, optional? }], prompt, lookFrames?: [{ src: absolute image path, shows: what it shows (position, size, colours), atMs? }] (up to 8 stills of the target look; copied into the recipe), style?: { aspectRatio?, colorCorrection?, lut?: { preset, intensity? }, speakerBackground?: { mode: none|blur|remove|image, image?, strength?, outline?: { enabled, width?, color?, shadow? } }, framing?: { shortsLayout: full|camera-corner }, cameraLayout?, captions?: { enabled, templateId?, wordsPerLine?, positionY?, style?: caption_set_look's style }, broll?: { use: none|ask|generate|user-images, layout?: full|split|card, notes? }, graphics?: { family?, titleFont?, bodyFont?, accentColor?, textColor?, surfaceColor?, notes? }, wallpaper? }, checklist: string[] }. Put the WHOLE look in style (every caption value, the background, framing, B-roll rule, title fonts and colours): it is applied exactly, the prompt only covers what depends on the footage.",
|
|
7273
7533
|
},
|
|
7274
7534
|
},
|
|
7275
7535
|
required: ["recipe"],
|
|
@@ -7842,7 +8102,8 @@ const TOOLS = [
|
|
|
7842
8102
|
},
|
|
7843
8103
|
frameRate: {
|
|
7844
8104
|
type: ["number", "string"],
|
|
7845
|
-
description:
|
|
8105
|
+
description:
|
|
8106
|
+
"23.976 | 24 | 25 | 29.97 | 30 | 50 | 59.94 | 60 | source | auto; omit = project setting.",
|
|
7846
8107
|
},
|
|
7847
8108
|
format: {
|
|
7848
8109
|
type: "string",
|
|
@@ -7897,6 +8158,24 @@ const TOOLS = [
|
|
|
7897
8158
|
},
|
|
7898
8159
|
command: "export.verify",
|
|
7899
8160
|
},
|
|
8161
|
+
{
|
|
8162
|
+
name: "export_timeline",
|
|
8163
|
+
description:
|
|
8164
|
+
"Export the edit as an editable timeline for Premiere Pro / DaVinci Resolve / Final Cut: an FCP 7 XML on the original media (every cut, speed as Time Remap, camera on V2, overlays above, voice + music tracks) plus an .srt of the kept words. Zooms, caption styling, motion graphics, layouts and grade are not in it: tell the user the returned notes. Sync: { xmlPath, srtPath, fps, durationMs, cuts, missingFiles, notes }.",
|
|
8165
|
+
inputSchema: {
|
|
8166
|
+
type: "object",
|
|
8167
|
+
properties: {
|
|
8168
|
+
id: { type: "string" },
|
|
8169
|
+
path: { type: "string" },
|
|
8170
|
+
outputPath: {
|
|
8171
|
+
type: "string",
|
|
8172
|
+
description: "A .xml path or a folder; default the recordings folder.",
|
|
8173
|
+
},
|
|
8174
|
+
captions: { type: "boolean", description: "Write the .srt beside it (default true)." },
|
|
8175
|
+
},
|
|
8176
|
+
},
|
|
8177
|
+
command: "export.timeline",
|
|
8178
|
+
},
|
|
7900
8179
|
{
|
|
7901
8180
|
name: "export_list",
|
|
7902
8181
|
description:
|
|
@@ -8837,10 +9116,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
8837
9116
|
for (const previewPath of previews) {
|
|
8838
9117
|
const fs = await import("node:fs/promises");
|
|
8839
9118
|
const buf = await fs.readFile(previewPath);
|
|
9119
|
+
const ext = previewPath.toLowerCase().split(".").pop();
|
|
8840
9120
|
content.push({
|
|
8841
9121
|
type: "image",
|
|
8842
9122
|
data: buf.toString("base64"),
|
|
8843
|
-
mimeType:
|
|
9123
|
+
mimeType:
|
|
9124
|
+
ext === "jpg" || ext === "jpeg"
|
|
9125
|
+
? "image/jpeg"
|
|
9126
|
+
: ext === "webp"
|
|
9127
|
+
? "image/webp"
|
|
9128
|
+
: "image/png",
|
|
8844
9129
|
});
|
|
8845
9130
|
}
|
|
8846
9131
|
} catch (imgErr) {
|
|
@@ -8901,6 +9186,12 @@ function collectInlinePreviewPaths(command, data) {
|
|
|
8901
9186
|
if (command === "motion.screenshot") {
|
|
8902
9187
|
return typeof data.previewPath === "string" ? [data.previewPath] : [];
|
|
8903
9188
|
}
|
|
9189
|
+
if (command === "recipe.look-frames") {
|
|
9190
|
+
const frames = Array.isArray(data.frames) ? data.frames : [];
|
|
9191
|
+
return frames
|
|
9192
|
+
.map((f) => (f && typeof f.path === "string" ? f.path : null))
|
|
9193
|
+
.filter((p) => p !== null);
|
|
9194
|
+
}
|
|
8904
9195
|
if (command === "motion.verify-frames") {
|
|
8905
9196
|
const frames = Array.isArray(data.frames) ? data.frames : [];
|
|
8906
9197
|
return frames
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writepanda/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.267.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",
|