@writepanda/mcp 1.221.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.
- package/bin/appConnection.mjs +42 -2
- package/bin/server.mjs +36 -5
- package/package.json +1 -1
package/bin/appConnection.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
);
|
|
@@ -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 (
|
|
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: {
|
|
@@ -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.
|
|
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",
|