@writepanda/mcp 1.201.0 → 1.207.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
  //
@@ -576,7 +577,7 @@ const MOTION_ELEMENT_PROPS = {
576
577
  content: {
577
578
  type: ["object", "string", "array"],
578
579
  description:
579
- '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.',
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, 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.',
580
581
  },
581
582
  atMs: { type: "number", description: "Start, edited ms. Or wordId." },
582
583
  wordId: {
@@ -634,7 +635,7 @@ const TOOLS = [
634
635
  {
635
636
  name: "system_status",
636
637
  description:
637
- "App version and license; automationGated true = trial expired, every other tool fails; license.socialPublishing.allowed false = the plan has no Instagram/TikTok/social publishing (Creator, Team, Pro only). Call first.",
638
+ "App version and license; automationGated true = trial expired, or the Solo plan (license.agentEditing.allowed false: no agent editing, relay license.agentEditing.message to the user), and every other tool fails; license.socialPublishing.allowed false = the plan has no Instagram/TikTok/social publishing (Creator, Team, Pro only). Call first.",
638
639
  inputSchema: { type: "object", properties: {} },
639
640
  command: "system.status",
640
641
  },
@@ -1299,7 +1300,8 @@ const TOOLS = [
1299
1300
  },
1300
1301
  {
1301
1302
  name: "project_current",
1302
- description: "The project open in the editor, or null.",
1303
+ description:
1304
+ "The project open in the editor, or null, plus the editor's LIVE playhead read at call time: playhead { ms, timeBase: 'edited', durationMs, inCut, playing } (null + playheadNote when unavailable). playhead.ms is EDITED (output) ms, what add-* atMs takes. Call it when the user says 'here' / 'at the playhead' / 'now': the chat context's playhead is from when the message was sent.",
1303
1305
  inputSchema: { type: "object", properties: {} },
1304
1306
  command: "project.current",
1305
1307
  },
@@ -1559,7 +1561,7 @@ const TOOLS = [
1559
1561
  {
1560
1562
  name: "project_add_designed_segment",
1561
1563
  description:
1562
- "Designed segment: host live on one half, a graphic panel on the other, in one call. The panel is a full-frame transparent render with the camera's half left empty (skill 'Designed segments').",
1564
+ "Designed segment: host live on one half, a graphic panel on the other, in one call. The panel is a full-frame transparent render with the camera's half left empty (skill_read file='motion-templates' section='designed segments').",
1563
1565
  inputSchema: {
1564
1566
  type: "object",
1565
1567
  properties: {
@@ -3115,7 +3117,7 @@ const TOOLS = [
3115
3117
  {
3116
3118
  name: "project_auto_reframe",
3117
3119
  description:
3118
- "Active-speaker auto-reframe of landscape multi-person footage for vertical / square: frames whoever talks, shot by shot. Slow (minutes). clear reverts.",
3120
+ "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
3121
  inputSchema: {
3120
3122
  type: "object",
3121
3123
  properties: {
@@ -3126,6 +3128,11 @@ const TOOLS = [
3126
3128
  minShotMs: { type: "number", description: "Default 500." },
3127
3129
  samplesPerShot: { type: "number", description: "Default 3." },
3128
3130
  zoom: { type: "number", description: "Fixed punch-in >= 1; omit = adaptive." },
3131
+ split: {
3132
+ type: "boolean",
3133
+ description:
3134
+ "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.",
3135
+ },
3129
3136
  clear: { type: "boolean" },
3130
3137
  expectedRevision: { type: "number" },
3131
3138
  },
@@ -4514,6 +4521,24 @@ const TOOLS = [
4514
4521
  },
4515
4522
  command: "recipe.list",
4516
4523
  },
4524
+ {
4525
+ name: "recipe_prepare_reference",
4526
+ description:
4527
+ "Prepare evidence for learning a reference video's editing style, without editing any project. Async job: whole-video contact sheets, dense motion samples, transition candidates and mixed-audio excerpts. Inspect the evidence and save a portable reference-specific recipe; do not assume generic effects or exact source sound files.",
4528
+ inputSchema: {
4529
+ type: "object",
4530
+ properties: {
4531
+ file: {
4532
+ type: "string",
4533
+ description: "Absolute local video path (download links with media_download_url first).",
4534
+ },
4535
+ title: { type: "string" },
4536
+ sourceUrl: { type: "string" },
4537
+ },
4538
+ required: ["file"],
4539
+ },
4540
+ command: "recipe.prepare-reference",
4541
+ },
4517
4542
  {
4518
4543
  name: "recipe_pick",
4519
4544
  description:
@@ -4726,7 +4751,8 @@ const TOOLS = [
4726
4751
  },
4727
4752
  {
4728
4753
  name: "motion_craft",
4729
- description: "Launch / promo motion craft library: no id lists topics; id returns the doc.",
4754
+ description:
4755
+ "Launch / promo film craft library (story arcs, blueprints, GSAP rules, design presets): no id lists topics; id returns the doc. An unknown id fails with the closest ids and every valid id. Not the custom motion-graphic HTML contract: that is skill_read section='custom motion graphics'.",
4730
4756
  inputSchema: {
4731
4757
  type: "object",
4732
4758
  properties: {
@@ -4776,7 +4802,7 @@ const TOOLS = [
4776
4802
  {
4777
4803
  name: "motion_render_html",
4778
4804
  description:
4779
- "Render custom HTML to video when no template fits. Contract (skill 'Custom motion graphics'): a paused GSAP timeline at window.__timelines[<data-composition-id>] and a root with data-composition-id / width / height / duration. Lint and STATIC_RENDER failures must be fixed, never placed. ASYNC: { jobId }.",
4805
+ "Render custom HTML to video when no template fits. Contract (skill_read section='custom motion graphics'; render details in skill_read file='custom-html'): a paused GSAP timeline at window.__timelines[<data-composition-id>] and a root with data-composition-id / width / height / duration. Lint and STATIC_RENDER failures must be fixed, never placed. ASYNC: { jobId }.",
4780
4806
  inputSchema: {
4781
4807
  type: "object",
4782
4808
  properties: {
@@ -4856,6 +4882,44 @@ const TOOLS = [
4856
4882
  },
4857
4883
 
4858
4884
  // ── assets ──────────────────────────────────────────────────────
4885
+ {
4886
+ name: "asset_list_sound_patterns",
4887
+ description:
4888
+ "List shared motion sound recipes: counted entrances, scroll ratchets, slides, swipes and presses. Use asset_plan_sound_pattern with actual animation timings.",
4889
+ inputSchema: { type: "object", properties: { query: { type: "string" } } },
4890
+ command: "asset.list-sound-patterns",
4891
+ },
4892
+ {
4893
+ name: "asset_plan_sound_pattern",
4894
+ description:
4895
+ "Plan motion-synced sounds without writing a project. Returns cues for project_add_sound_cues; use a unique group per animation. One cue per actual event, variant rotation, quiet starting gain, tails bounded by settle. Preview below narration.",
4896
+ inputSchema: {
4897
+ type: "object",
4898
+ properties: {
4899
+ pattern: { type: "string", description: "Id from asset_list_sound_patterns." },
4900
+ atMs: { type: "integer", minimum: 0, description: "Edited-timeline motion start." },
4901
+ endMs: { type: "integer", description: "Edited-timeline motion end/settle." },
4902
+ eventOffsetsMs: {
4903
+ type: "array",
4904
+ items: { type: "integer", minimum: 0 },
4905
+ minItems: 1,
4906
+ maxItems: 500,
4907
+ description:
4908
+ "Actual entrance/crossing/gesture offsets relative to atMs; strictly increasing. Follow easing, not an assumed uniform stagger.",
4909
+ },
4910
+ volume: { type: "number", minimum: 0, maximum: 2 },
4911
+ sounds: {
4912
+ type: "array",
4913
+ items: { type: "string" },
4914
+ minItems: 1,
4915
+ maxItems: 500,
4916
+ description: "Optional alternate ids/paths, rotated.",
4917
+ },
4918
+ },
4919
+ required: ["pattern", "atMs", "endMs", "eventOffsetsMs"],
4920
+ },
4921
+ command: "asset.plan-sound-pattern",
4922
+ },
4859
4923
  {
4860
4924
  name: "asset_list_emoji",
4861
4925
  description: "List animated emoji for project_add_emoji.",
@@ -5049,6 +5113,104 @@ const TOOLS = [
5049
5113
  },
5050
5114
  command: "export.set-details",
5051
5115
  },
5116
+ {
5117
+ name: "clips_make",
5118
+ description:
5119
+ "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.",
5120
+ inputSchema: {
5121
+ type: "object",
5122
+ properties: {
5123
+ file: { type: "string", description: "Absolute path of the long video." },
5124
+ length: {
5125
+ type: "string",
5126
+ enum: ["short", "medium", "long"],
5127
+ description: "short 15-30 s (default) | medium 30-60 s | long 60-90 s.",
5128
+ },
5129
+ format: {
5130
+ type: "string",
5131
+ enum: ["9:16", "1:1", "16:9"],
5132
+ description: "Shape of every clip (default 9:16).",
5133
+ },
5134
+ footage: {
5135
+ type: "string",
5136
+ enum: ["camera", "screen"],
5137
+ description: "camera (people talking, default) | screen (screen recording, slides).",
5138
+ },
5139
+ captionTemplate: {
5140
+ type: "string",
5141
+ description: "Caption template id (default glowStack), or none.",
5142
+ },
5143
+ },
5144
+ required: ["file"],
5145
+ },
5146
+ command: "clips.make",
5147
+ },
5148
+ {
5149
+ name: "clips_list",
5150
+ description: "List clip sets from clips_make, newest first, with status and clip counts.",
5151
+ inputSchema: { type: "object", properties: {} },
5152
+ command: "clips.list",
5153
+ },
5154
+ {
5155
+ name: "clips_get",
5156
+ description:
5157
+ "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 }).",
5158
+ inputSchema: {
5159
+ type: "object",
5160
+ properties: { id: { type: "string", description: "Clip set id." } },
5161
+ required: ["id"],
5162
+ },
5163
+ command: "clips.get",
5164
+ },
5165
+ {
5166
+ name: "clips_export",
5167
+ description:
5168
+ "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[] }.",
5169
+ inputSchema: {
5170
+ type: "object",
5171
+ properties: {
5172
+ id: { type: "string", description: "Clip set id." },
5173
+ clipIds: { type: "array", items: { type: "string" }, description: "Only these clips." },
5174
+ },
5175
+ required: ["id"],
5176
+ },
5177
+ command: "clips.export",
5178
+ },
5179
+ {
5180
+ name: "clips_retry",
5181
+ description:
5182
+ "Run a failed or stopped clip set again, reusing its import and transcript and keeping finished clips. clips_get shows why it failed (errorCode).",
5183
+ inputSchema: {
5184
+ type: "object",
5185
+ properties: { id: { type: "string", description: "Clip set id." } },
5186
+ required: ["id"],
5187
+ },
5188
+ command: "clips.retry",
5189
+ },
5190
+ {
5191
+ name: "clips_set_caption",
5192
+ description:
5193
+ "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.",
5194
+ inputSchema: {
5195
+ type: "object",
5196
+ properties: {
5197
+ id: { type: "string", description: "Clip set id." },
5198
+ captionTemplate: { type: "string", description: "Caption template id, or none." },
5199
+ },
5200
+ required: ["id", "captionTemplate"],
5201
+ },
5202
+ command: "clips.set-caption",
5203
+ },
5204
+ {
5205
+ name: "clips_delete",
5206
+ description: "Forget a clip set (its clip projects and exports are kept).",
5207
+ inputSchema: {
5208
+ type: "object",
5209
+ properties: { id: { type: "string", description: "Clip set id." } },
5210
+ required: ["id"],
5211
+ },
5212
+ command: "clips.delete",
5213
+ },
5052
5214
  {
5053
5215
  name: "export_generate_shots",
5054
5216
  description:
@@ -5362,7 +5524,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
5362
5524
  tools: TOOLS.map((t) => ({
5363
5525
  name: t.name,
5364
5526
  description: t.description,
5365
- inputSchema: t.inputSchema,
5527
+ // Gemini rejects multi-type fields that JSON Schema allows.
5528
+ inputSchema: portableSchema(t.inputSchema),
5366
5529
  })),
5367
5530
  };
5368
5531
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.201.0",
3
+ "version": "1.207.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",