@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.
- package/bin/appConnection.mjs +42 -2
- package/bin/server.mjs +39 -8
- 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
|
);
|
|
@@ -924,7 +938,7 @@ const TOOLS = [
|
|
|
924
938
|
{
|
|
925
939
|
name: "connector_list",
|
|
926
940
|
description:
|
|
927
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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",
|