@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.
@@ -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? }. Latin script only.',
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
- inputSchema: t.inputSchema,
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.200.0",
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",