@writepanda/mcp 1.220.0 → 1.222.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.
@@ -144,21 +144,30 @@ export async function connectInApp({
144
144
  busyWaitMs = 20_000,
145
145
  probe = probeHealth,
146
146
  readFiles = fileCredentials,
147
+ excludePort = null,
147
148
  } = {}) {
148
149
  const attempts = [];
149
150
  const candidates = [];
150
151
  const fromEnv = envCredentials(env);
151
- if (fromEnv) candidates.push(fromEnv);
152
+ if (fromEnv && fromEnv.port !== excludePort) candidates.push(fromEnv);
152
153
  let fileError = null;
153
154
  try {
154
155
  const fromFiles = await readFiles(dir);
155
- if (!fromEnv || fromFiles.port !== fromEnv.port || fromFiles.token !== fromEnv.token) {
156
+ if (
157
+ fromFiles.port !== excludePort &&
158
+ (!fromEnv || fromFiles.port !== fromEnv.port || fromFiles.token !== fromEnv.token)
159
+ ) {
156
160
  candidates.push(fromFiles);
157
161
  }
158
162
  } catch (err) {
159
163
  fileError = err?.message ?? String(err);
160
164
  }
161
165
  if (candidates.length === 0) {
166
+ if (excludePort !== null) {
167
+ throw unavailable(
168
+ `nothing is listening on 127.0.0.1:${excludePort} any more and PandaStudio hasn't advertised another address (${fileError ?? `${dir} still names that port`}).`,
169
+ );
170
+ }
162
171
  throw unavailable(
163
172
  `PandaStudio didn't pass its automation address to the tools and ${fileError ?? "the token/port files are missing"}.`,
164
173
  );
@@ -175,6 +184,37 @@ export async function connectInApp({
175
184
  throw unavailable(`PandaStudio's automation server didn't answer. ${attempts.join("; ")}.`);
176
185
  }
177
186
 
187
+ /** A socket error meaning nothing accepted the connection: the request never
188
+ * reached the app, so resending it can't run a command twice. */
189
+ export function isConnectionRefused(err) {
190
+ const code = err?.code ?? err?.cause?.code;
191
+ return code === "ECONNREFUSED" || code === "EADDRNOTAVAIL";
192
+ }
193
+
194
+ /**
195
+ * Send `call(creds)`. When the connection is REFUSED, the port we hold is
196
+ * stale: the app restarted its automation server on another port (a fallback
197
+ * from a busy preferred port, a Settings change, a relaunch) after the
198
+ * credentials were captured, e.g. in the environment of a long-lived agent
199
+ * process. Re-resolve once with `reresolve(staleCreds)` (which re-reads the
200
+ * token/port files) and resend to the new address. Only a refused connection
201
+ * is retried; any other failure may have reached the app.
202
+ *
203
+ * Returns { creds, res } with the credentials that worked. If re-resolving
204
+ * fails, its error (which says why) is thrown; if it yields the same address,
205
+ * the original error is.
206
+ */
207
+ export async function callWithReconnect(creds, call, reresolve) {
208
+ try {
209
+ return { creds, res: await call(creds) };
210
+ } catch (err) {
211
+ if (!isConnectionRefused(err)) throw err;
212
+ const fresh = await reresolve(creds);
213
+ if (!fresh || (fresh.port === creds.port && fresh.token === creds.token)) throw err;
214
+ return { creds: fresh, res: await call(fresh) };
215
+ }
216
+ }
217
+
178
218
  /**
179
219
  * Environment for LAUNCHING the desktop app. This server runs as Electron in
180
220
  * node mode (ELECTRON_RUN_AS_NODE=1), and a child inherits that: spawning
package/bin/server.mjs CHANGED
@@ -38,6 +38,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
38
38
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
39
39
  import {
40
40
  appLaunchEnv,
41
+ callWithReconnect,
41
42
  cleanFileValue,
42
43
  configDir,
43
44
  connectInApp,
@@ -116,7 +117,13 @@ async function autoLaunchAndWait(timeoutSec = 60) {
116
117
  try {
117
118
  // appLaunchEnv drops ELECTRON_RUN_AS_NODE: inherited, it made the
118
119
  // "launch" start a Node REPL that exits instead of PandaStudio.
119
- const child = spawn(bin, [], { detached: true, stdio: "ignore", env: appLaunchEnv() });
120
+ // windowsHide: false: this is the desktop app, whose window must show.
121
+ const child = spawn(bin, [], {
122
+ detached: true,
123
+ stdio: "ignore",
124
+ env: appLaunchEnv(),
125
+ windowsHide: false,
126
+ });
120
127
  child.unref();
121
128
  } catch (err) {
122
129
  throw new Error(`failed to launch PandaStudio (${bin}): ${err?.message ?? err}`);
@@ -180,9 +187,16 @@ async function callPandastudio(command, args = {}) {
180
187
  let creds = IN_APP ? await connectInApp() : await autoLaunchAndWait();
181
188
  let res;
182
189
  try {
183
- res = await postCall(creds, command, args);
190
+ // A refused connection means the address went stale between the
191
+ // health probe and the call (the app moved its automation server):
192
+ // re-read where it is now and resend once.
193
+ ({ creds, res } = await callWithReconnect(
194
+ creds,
195
+ (c) => postCall(c, command, args),
196
+ (stale) => (IN_APP ? connectInApp({ excludePort: stale.port }) : autoLaunchAndWait()),
197
+ ));
184
198
  } catch (err) {
185
- if (!IN_APP) throw err;
199
+ if (!IN_APP || err?.inAppUnavailable) throw err;
186
200
  throw new Error(
187
201
  `${IN_APP_UNAVAILABLE}: the call to 127.0.0.1:${creds.port} failed (${err?.code ?? err?.message ?? err}).`,
188
202
  );
@@ -924,7 +938,7 @@ const TOOLS = [
924
938
  {
925
939
  name: "connector_list",
926
940
  description:
927
- "List MCP connectors and their state. If the one needed isn't connected, tell the user its howToConnect line; you cannot connect connectors. status \"connected, last check failed\" (connected:false, lastCheck with reason, host and fix) means PandaStudio's own last use of it failed. auth \"api-key\" + needsKey: the user must paste a key in Settings (never ask for it in chat).",
941
+ 'List MCP connectors and their state. If the one needed isn\'t connected, tell the user its howToConnect line; you cannot connect connectors. status "connected, last check failed" (connected:false, lastCheck with reason, host and fix) means PandaStudio\'s own last use of it failed. auth "api-key" + needsKey: the user must paste a key in Settings (never ask for it in chat).',
928
942
  inputSchema: {
929
943
  type: "object",
930
944
  properties: {
@@ -3034,7 +3048,7 @@ const TOOLS = [
3034
3048
  fontFamily: {
3035
3049
  type: "string",
3036
3050
  description:
3037
- "text font, default Inter. A family asset_list_fonts returns; an unknown one fails with the families that work.",
3051
+ 'text font, default Inter. Any font that renders: bundled, imported, or installed, including a weight written into the name ("Montserrat ExtraBold") or a PostScript name ("Montserrat-ExtraBold"). A font that draws differently (variable font, nearest weight, unreadable file) passes with a warning; only a name nothing on this computer provides fails, with the families that work.',
3038
3052
  },
3039
3053
  arrowDirection: {
3040
3054
  type: "string",
@@ -3332,7 +3346,7 @@ const TOOLS = [
3332
3346
  {
3333
3347
  name: "project_update_region",
3334
3348
  description:
3335
- "Patch a region; unspecified fields are kept. Patchable: zoom (depth, focusX, focusY, focusMode static|auto), trim / speed (startMs, endMs, speed, rampIn, rampOut, preservePitch, freezeMs, reverse, reverseAudio), fx (atMs, durationMs, opacity, blendMode, speed, zIndex), annotation (zIndex), overlay (x, y, width, height in canvas %, layer, opacity, blendMode, muted, volume, loop, motionBlur, zIndex), adjustment (startMs, endMs, blendMode), motion (motionBlur), clip-transform (startMs, endMs, preset, transitionMs, easing, bezier), background-effect (as project_add_background_effect), audio-overlay (startMs, endMs, sourceStartMs, volume 0-2, fadeIn, fadeOut).",
3349
+ "Patch a region; unspecified fields are kept. Patchable: zoom (depth, focusX, focusY, focusMode static|auto), trim / speed (startMs, endMs, speed, rampIn, rampOut, preservePitch, freezeMs, reverse, reverseAudio), fx (startMs, endMs, opacity, blendMode, speed, zIndex), annotation (startMs, endMs, text = the words shown (content / textContent also work), x, y, width, height, the style fields below, zIndex), overlay (x, y, width, height in canvas %, layer, opacity, blendMode, muted, volume, loop, motionBlur, zIndex), adjustment (startMs, endMs, blendMode), motion (motionBlur), clip-transform (startMs, endMs, preset, transitionMs, easing, bezier), background-effect (as project_add_background_effect), audio-overlay (startMs, endMs, sourceStartMs, volume 0-2, fadeIn, fadeOut). A key the region type doesn't have is not applied: the result lists it in ignoredKeys with a warning, and the call fails when nothing in the patch applies.",
3336
3350
  inputSchema: {
3337
3351
  type: "object",
3338
3352
  properties: {
@@ -3344,7 +3358,7 @@ const TOOLS = [
3344
3358
  patch: {
3345
3359
  type: "object",
3346
3360
  description:
3347
- "e.g. { depth: 4 }. Annotation: fontSize, fontFamily (a family asset_list_fonts returns; an unknown one fails with the families that work), textColor (alias color), backgroundColor, fontWeight, fontStyle, textDecoration, textAlign, flat or nested under style/styles; merges into canonical style, preserving other fields. Flat wins over styles, then style. null clears a value. zIndex (overlay / annotation / fx): stack order, higher on top; overlays, annotations and front motion elements share one stack at group base + zIndex (B-roll 0, overlays 1000, annotations 2000, motion elements 3000).",
3361
+ 'e.g. { depth: 4 }. Annotation: fontSize, fontFamily (any bundled, imported or installed font, including weight-named families like "Montserrat ExtraBold" and PostScript names; only a name nothing here provides fails), textColor (alias color), backgroundColor, fontWeight, fontStyle, textDecoration, textAlign, flat or nested under style/styles; merges into canonical style, preserving other fields. Flat wins over styles, then style. null clears a value. zIndex (overlay / annotation / fx): stack order, higher on top; overlays, annotations and front motion elements share one stack at group base + zIndex (B-roll 0, overlays 1000, annotations 2000, motion elements 3000).',
3348
3362
  },
3349
3363
  expectedRevision: { type: "number" },
3350
3364
  },
@@ -5689,7 +5703,7 @@ const TOOLS = [
5689
5703
  {
5690
5704
  name: "asset_list_fonts",
5691
5705
  description:
5692
- "Font families text can use: bundled, scriptFallbacks (Noto, used per character for Indic/Arabic/Hebrew/Thai text), custom (the user's imported fonts; prefer one covering the video's language), system with includeSystem. Pass one to caption_set_style fontFamily.",
5706
+ "Font families text can use (a missing one: asset_import_font): bundled, scriptFallbacks (Noto, used per character for Indic/Arabic/Hebrew/Thai text), custom (the user's imported fonts; prefer one covering the video's language), system with includeSystem. Pass one to caption_set_style fontFamily.",
5693
5707
  inputSchema: {
5694
5708
  type: "object",
5695
5709
  properties: {
@@ -5698,6 +5712,23 @@ const TOOLS = [
5698
5712
  },
5699
5713
  command: "asset.list-fonts",
5700
5714
  },
5715
+ {
5716
+ name: "asset_import_font",
5717
+ description:
5718
+ 'Import a font like the font picker does, so captions, annotations and motion elements can use it: a local file (path: .ttf/.otf/.woff/.woff2, 30 MB max) or a Google Fonts family (google: "Montserrat", downloaded now). Only fonts the user owns or openly licensed ones. Returns { family } to pass as fontFamily; an already imported family is reused.',
5719
+ inputSchema: {
5720
+ type: "object",
5721
+ properties: {
5722
+ path: { type: "string", description: "Absolute font file path. Give path or google." },
5723
+ google: { type: "string", description: "Google Fonts family name. Give path or google." },
5724
+ family: {
5725
+ type: "string",
5726
+ description: "path only: name to register it under (default: the font's own family).",
5727
+ },
5728
+ },
5729
+ },
5730
+ command: "asset.import-font",
5731
+ },
5701
5732
 
5702
5733
  // ── project-aware LLM ──────────────────────────────────────────
5703
5734
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.220.0",
3
+ "version": "1.222.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",