@writepanda/mcp 1.200.0 → 1.205.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/portableSchema.mjs +91 -0
- package/bin/server.mjs +123 -4
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Tool input schemas in the subset every model provider accepts.
|
|
2
|
+
//
|
|
3
|
+
// Our schemas are written in plain JSON Schema, and some fields allow more
|
|
4
|
+
// than one type (`type: ["object", "null"]`, `["object", "string", "array"]`).
|
|
5
|
+
// Anthropic and OpenAI take that, but Google Gemini rejects the whole tool
|
|
6
|
+
// list when a multi-type field carries `properties` or an array branch has no
|
|
7
|
+
// `items` ("properties: only allowed for OBJECT type", "items: missing
|
|
8
|
+
// field"), so every Gemini session failed before its first reply.
|
|
9
|
+
//
|
|
10
|
+
// portableSchema rewrites each such field, keeping its meaning:
|
|
11
|
+
// - a type plus null becomes that single type; null stays accepted by the
|
|
12
|
+
// handlers and is noted in the description.
|
|
13
|
+
// - several non-null types become anyOf branches, each with only the
|
|
14
|
+
// keywords that belong to its type (properties on the object branch,
|
|
15
|
+
// items on the array branch, enum values of matching type).
|
|
16
|
+
// - every array schema gets an items schema.
|
|
17
|
+
|
|
18
|
+
const OBJECT_KEYS = ["properties", "required", "additionalProperties"];
|
|
19
|
+
const ARRAY_KEYS = ["items", "minItems", "maxItems"];
|
|
20
|
+
const NUMBER_KEYS = ["minimum", "maximum"];
|
|
21
|
+
const STRING_KEYS = ["minLength", "maxLength", "pattern", "format"];
|
|
22
|
+
|
|
23
|
+
function keysFor(type) {
|
|
24
|
+
if (type === "object") return OBJECT_KEYS;
|
|
25
|
+
if (type === "array") return ARRAY_KEYS;
|
|
26
|
+
if (type === "number" || type === "integer") return NUMBER_KEYS;
|
|
27
|
+
if (type === "string") return STRING_KEYS;
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TYPE_SPECIFIC = new Set([...OBJECT_KEYS, ...ARRAY_KEYS, ...NUMBER_KEYS, ...STRING_KEYS]);
|
|
32
|
+
|
|
33
|
+
function enumFor(values, type) {
|
|
34
|
+
const match = (v) =>
|
|
35
|
+
type === "integer"
|
|
36
|
+
? Number.isInteger(v)
|
|
37
|
+
: type === "array"
|
|
38
|
+
? Array.isArray(v)
|
|
39
|
+
: type === "object"
|
|
40
|
+
? v !== null && typeof v === "object" && !Array.isArray(v)
|
|
41
|
+
: typeof v === type;
|
|
42
|
+
return values.filter(match);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** One branch of a multi-type field: the type and only its own keywords. */
|
|
46
|
+
function branch(schema, type) {
|
|
47
|
+
const out = { type };
|
|
48
|
+
for (const k of keysFor(type)) if (k in schema) out[k] = schema[k];
|
|
49
|
+
if (Array.isArray(schema.enum)) {
|
|
50
|
+
const values = enumFor(schema.enum, type);
|
|
51
|
+
if (values.length) out.enum = values;
|
|
52
|
+
}
|
|
53
|
+
return portableSchema(out);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function portableSchema(schema) {
|
|
57
|
+
if (Array.isArray(schema)) return schema.map(portableSchema);
|
|
58
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
59
|
+
let s = { ...schema };
|
|
60
|
+
|
|
61
|
+
if (Array.isArray(s.type)) {
|
|
62
|
+
const nullable = s.type.includes("null");
|
|
63
|
+
const types = s.type.filter((t) => t !== "null");
|
|
64
|
+
if (nullable && s.description && !/\bnull\b/.test(s.description))
|
|
65
|
+
s.description = `${s.description} (null clears it.)`;
|
|
66
|
+
if (Array.isArray(s.enum)) s.enum = s.enum.filter((v) => v !== null);
|
|
67
|
+
if (types.length <= 1) {
|
|
68
|
+
s.type = types[0] ?? "string";
|
|
69
|
+
} else {
|
|
70
|
+
const common = {};
|
|
71
|
+
for (const [k, v] of Object.entries(s))
|
|
72
|
+
if (k !== "type" && k !== "enum" && !TYPE_SPECIFIC.has(k)) common[k] = v;
|
|
73
|
+
return { ...common, anyOf: types.map((t) => branch(s, t)) };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (s.type === "array" && !s.items) s.items = { type: "string" };
|
|
78
|
+
if (s.properties && !s.type && !s.anyOf) s.type = "object";
|
|
79
|
+
|
|
80
|
+
if (s.properties) {
|
|
81
|
+
const props = {};
|
|
82
|
+
for (const [k, v] of Object.entries(s.properties)) props[k] = portableSchema(v);
|
|
83
|
+
s.properties = props;
|
|
84
|
+
}
|
|
85
|
+
if (s.items) s.items = portableSchema(s.items);
|
|
86
|
+
for (const k of ["anyOf", "oneOf", "allOf"])
|
|
87
|
+
if (Array.isArray(s[k])) s[k] = s[k].map(portableSchema);
|
|
88
|
+
if (s.additionalProperties && typeof s.additionalProperties === "object")
|
|
89
|
+
s.additionalProperties = portableSchema(s.additionalProperties);
|
|
90
|
+
return s;
|
|
91
|
+
}
|
package/bin/server.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
probeHealth,
|
|
48
48
|
waitForHealth,
|
|
49
49
|
} from "./appConnection.mjs";
|
|
50
|
+
import { portableSchema } from "./portableSchema.mjs";
|
|
50
51
|
|
|
51
52
|
// ── Credentials + transport ───────────────────────────────────────────
|
|
52
53
|
//
|
|
@@ -567,13 +568,16 @@ const MOTION_ELEMENT_TYPES = [
|
|
|
567
568
|
"iconPop",
|
|
568
569
|
"frame",
|
|
569
570
|
"sticker",
|
|
571
|
+
"ticker",
|
|
572
|
+
"credits",
|
|
573
|
+
"crawl",
|
|
570
574
|
];
|
|
571
575
|
const MOTION_ELEMENT_PROPS = {
|
|
572
576
|
type: { type: "string", enum: MOTION_ELEMENT_TYPES },
|
|
573
577
|
content: {
|
|
574
578
|
type: ["object", "string", "array"],
|
|
575
579
|
description:
|
|
576
|
-
'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? }; endCard { lines: [a, b] }; progress { label?, fromMs?, toMs? }; iconPop { emoji } | { icon }; frame { look: viewfinder | selection | corners, label? }; sticker { image: transparent PNG path, label? }.
|
|
580
|
+
'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 }; 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.',
|
|
577
581
|
},
|
|
578
582
|
atMs: { type: "number", description: "Start, edited ms. Or wordId." },
|
|
579
583
|
wordId: {
|
|
@@ -596,8 +600,19 @@ const MOTION_ELEMENT_PROPS = {
|
|
|
596
600
|
highlight: { type: "string" },
|
|
597
601
|
text: { type: "string" },
|
|
598
602
|
font: { type: "string" },
|
|
599
|
-
size: { type: "string", enum: ["s", "m", "l", "xl"] },
|
|
603
|
+
size: { type: "string", enum: ["xs", "s", "m", "l", "xl"] },
|
|
600
604
|
outline: { type: "boolean" },
|
|
605
|
+
opacity: { type: "number", minimum: 0, maximum: 1, description: "Main text opacity." },
|
|
606
|
+
secondary: {
|
|
607
|
+
type: "object",
|
|
608
|
+
description:
|
|
609
|
+
"The smaller line (count label, lower-third subtitle, ticker label box, credits roles).",
|
|
610
|
+
properties: {
|
|
611
|
+
size: { type: "number", minimum: 0.4, maximum: 2.5, description: "1 = default." },
|
|
612
|
+
opacity: { type: "number", minimum: 0, maximum: 1 },
|
|
613
|
+
color: { type: "string" },
|
|
614
|
+
},
|
|
615
|
+
},
|
|
601
616
|
},
|
|
602
617
|
description: "Default: the brand kit.",
|
|
603
618
|
},
|
|
@@ -3101,7 +3116,7 @@ const TOOLS = [
|
|
|
3101
3116
|
{
|
|
3102
3117
|
name: "project_auto_reframe",
|
|
3103
3118
|
description:
|
|
3104
|
-
"Active-speaker auto-reframe of landscape multi-person footage for vertical / square: frames whoever talks, shot by shot. Slow (minutes). clear reverts.",
|
|
3119
|
+
"Active-speaker auto-reframe of landscape multi-person footage for vertical / square: frames whoever talks, shot by shot. split=true stacks two people (podcast / interview wide) one above the other on a 9:16 canvas. Slow (minutes). clear reverts.",
|
|
3105
3120
|
inputSchema: {
|
|
3106
3121
|
type: "object",
|
|
3107
3122
|
properties: {
|
|
@@ -3112,6 +3127,11 @@ const TOOLS = [
|
|
|
3112
3127
|
minShotMs: { type: "number", description: "Default 500." },
|
|
3113
3128
|
samplesPerShot: { type: "number", description: "Default 3." },
|
|
3114
3129
|
zoom: { type: "number", description: "Fixed punch-in >= 1; omit = adaptive." },
|
|
3130
|
+
split: {
|
|
3131
|
+
type: "boolean",
|
|
3132
|
+
description:
|
|
3133
|
+
"Stack two people: a shot with two people too far apart for one vertical crop becomes two tiles (left person on top), each tracked on its face. Result reframed[].splitShots counts them.",
|
|
3134
|
+
},
|
|
3115
3135
|
clear: { type: "boolean" },
|
|
3116
3136
|
expectedRevision: { type: "number" },
|
|
3117
3137
|
},
|
|
@@ -5035,6 +5055,104 @@ const TOOLS = [
|
|
|
5035
5055
|
},
|
|
5036
5056
|
command: "export.set-details",
|
|
5037
5057
|
},
|
|
5058
|
+
{
|
|
5059
|
+
name: "clips_make",
|
|
5060
|
+
description:
|
|
5061
|
+
"Long video (podcast, webinar, talk) in, scored vertical clips out, no agent needed: imports, transcribes, finds the best sentence-aligned moments across the whole video (local AI + transcript score), and forks each into its own 9:16 project with captions and face-following framing. Async: returns { clipSetId, jobId }; poll clips_get. Then clips_export, or edit a clip's projectPath with any project tool.",
|
|
5062
|
+
inputSchema: {
|
|
5063
|
+
type: "object",
|
|
5064
|
+
properties: {
|
|
5065
|
+
file: { type: "string", description: "Absolute path of the long video." },
|
|
5066
|
+
length: {
|
|
5067
|
+
type: "string",
|
|
5068
|
+
enum: ["short", "medium", "long"],
|
|
5069
|
+
description: "short 15-30 s (default) | medium 30-60 s | long 60-90 s.",
|
|
5070
|
+
},
|
|
5071
|
+
format: {
|
|
5072
|
+
type: "string",
|
|
5073
|
+
enum: ["9:16", "1:1", "16:9"],
|
|
5074
|
+
description: "Shape of every clip (default 9:16).",
|
|
5075
|
+
},
|
|
5076
|
+
footage: {
|
|
5077
|
+
type: "string",
|
|
5078
|
+
enum: ["camera", "screen"],
|
|
5079
|
+
description: "camera (people talking, default) | screen (screen recording, slides).",
|
|
5080
|
+
},
|
|
5081
|
+
captionTemplate: {
|
|
5082
|
+
type: "string",
|
|
5083
|
+
description: "Caption template id (default glowStack), or none.",
|
|
5084
|
+
},
|
|
5085
|
+
},
|
|
5086
|
+
required: ["file"],
|
|
5087
|
+
},
|
|
5088
|
+
command: "clips.make",
|
|
5089
|
+
},
|
|
5090
|
+
{
|
|
5091
|
+
name: "clips_list",
|
|
5092
|
+
description: "List clip sets from clips_make, newest first, with status and clip counts.",
|
|
5093
|
+
inputSchema: { type: "object", properties: {} },
|
|
5094
|
+
command: "clips.list",
|
|
5095
|
+
},
|
|
5096
|
+
{
|
|
5097
|
+
name: "clips_get",
|
|
5098
|
+
description:
|
|
5099
|
+
"One clip set: status (importing, transcribing, finding, building, ready, error), usedModel, sourceProjectPath and clips[] best-first ({ id, title, hook, score 0-100, startMs, endMs, status, projectPath, thumbPath, exportedPath }).",
|
|
5100
|
+
inputSchema: {
|
|
5101
|
+
type: "object",
|
|
5102
|
+
properties: { id: { type: "string", description: "Clip set id." } },
|
|
5103
|
+
required: ["id"],
|
|
5104
|
+
},
|
|
5105
|
+
command: "clips.get",
|
|
5106
|
+
},
|
|
5107
|
+
{
|
|
5108
|
+
name: "clips_export",
|
|
5109
|
+
description:
|
|
5110
|
+
"Export a clip set's finished clips to MP4 (all, or clipIds) into Movies/PandaStudio Clips/<video>/. Async: returns { jobId, outputDir }; job_wait for { exported[] }.",
|
|
5111
|
+
inputSchema: {
|
|
5112
|
+
type: "object",
|
|
5113
|
+
properties: {
|
|
5114
|
+
id: { type: "string", description: "Clip set id." },
|
|
5115
|
+
clipIds: { type: "array", items: { type: "string" }, description: "Only these clips." },
|
|
5116
|
+
},
|
|
5117
|
+
required: ["id"],
|
|
5118
|
+
},
|
|
5119
|
+
command: "clips.export",
|
|
5120
|
+
},
|
|
5121
|
+
{
|
|
5122
|
+
name: "clips_retry",
|
|
5123
|
+
description:
|
|
5124
|
+
"Run a failed or stopped clip set again, reusing its import and transcript and keeping finished clips. clips_get shows why it failed (errorCode).",
|
|
5125
|
+
inputSchema: {
|
|
5126
|
+
type: "object",
|
|
5127
|
+
properties: { id: { type: "string", description: "Clip set id." } },
|
|
5128
|
+
required: ["id"],
|
|
5129
|
+
},
|
|
5130
|
+
command: "clips.retry",
|
|
5131
|
+
},
|
|
5132
|
+
{
|
|
5133
|
+
name: "clips_set_caption",
|
|
5134
|
+
description:
|
|
5135
|
+
"Change every clip's caption style in a set (clips not built yet use it; finished clips are restyled and re-rendered in the background). captionTemplate: template id or none.",
|
|
5136
|
+
inputSchema: {
|
|
5137
|
+
type: "object",
|
|
5138
|
+
properties: {
|
|
5139
|
+
id: { type: "string", description: "Clip set id." },
|
|
5140
|
+
captionTemplate: { type: "string", description: "Caption template id, or none." },
|
|
5141
|
+
},
|
|
5142
|
+
required: ["id", "captionTemplate"],
|
|
5143
|
+
},
|
|
5144
|
+
command: "clips.set-caption",
|
|
5145
|
+
},
|
|
5146
|
+
{
|
|
5147
|
+
name: "clips_delete",
|
|
5148
|
+
description: "Forget a clip set (its clip projects and exports are kept).",
|
|
5149
|
+
inputSchema: {
|
|
5150
|
+
type: "object",
|
|
5151
|
+
properties: { id: { type: "string", description: "Clip set id." } },
|
|
5152
|
+
required: ["id"],
|
|
5153
|
+
},
|
|
5154
|
+
command: "clips.delete",
|
|
5155
|
+
},
|
|
5038
5156
|
{
|
|
5039
5157
|
name: "export_generate_shots",
|
|
5040
5158
|
description:
|
|
@@ -5348,7 +5466,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
5348
5466
|
tools: TOOLS.map((t) => ({
|
|
5349
5467
|
name: t.name,
|
|
5350
5468
|
description: t.description,
|
|
5351
|
-
|
|
5469
|
+
// Gemini rejects multi-type fields that JSON Schema allows.
|
|
5470
|
+
inputSchema: portableSchema(t.inputSchema),
|
|
5352
5471
|
})),
|
|
5353
5472
|
};
|
|
5354
5473
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@writepanda/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.205.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",
|