@writepanda/mcp 1.201.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
  //
@@ -3115,7 +3116,7 @@ const TOOLS = [
3115
3116
  {
3116
3117
  name: "project_auto_reframe",
3117
3118
  description:
3118
- "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.",
3119
3120
  inputSchema: {
3120
3121
  type: "object",
3121
3122
  properties: {
@@ -3126,6 +3127,11 @@ const TOOLS = [
3126
3127
  minShotMs: { type: "number", description: "Default 500." },
3127
3128
  samplesPerShot: { type: "number", description: "Default 3." },
3128
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
+ },
3129
3135
  clear: { type: "boolean" },
3130
3136
  expectedRevision: { type: "number" },
3131
3137
  },
@@ -5049,6 +5055,104 @@ const TOOLS = [
5049
5055
  },
5050
5056
  command: "export.set-details",
5051
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
+ },
5052
5156
  {
5053
5157
  name: "export_generate_shots",
5054
5158
  description:
@@ -5362,7 +5466,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5362
5466
  tools: TOOLS.map((t) => ({
5363
5467
  name: t.name,
5364
5468
  description: t.description,
5365
- inputSchema: t.inputSchema,
5469
+ // Gemini rejects multi-type fields that JSON Schema allows.
5470
+ inputSchema: portableSchema(t.inputSchema),
5366
5471
  })),
5367
5472
  };
5368
5473
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.201.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",