@engine-room/after-effects-mcp 0.1.2 → 0.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engine-room/after-effects-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Control Adobe After Effects with AI — describe the animation you want and it gets built: layers, keyframes, effects, expressions and text, all editable afterwards.",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "win32"
44
44
  ],
45
45
  "engines": {
46
- "node": ">=20"
46
+ "node": ">=22"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc -p tsconfig.json",
@@ -53,13 +53,13 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@modelcontextprotocol/sdk": "^1.0.4",
56
- "ws": "^8.18.0",
56
+ "ws": "^8.21.3",
57
57
  "zod": "^3.23.8",
58
58
  "zod-to-json-schema": "^3.23.5"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@engineroom/shared": "*",
62
- "@types/node": "^20.14.0",
62
+ "@types/node": "^22",
63
63
  "@types/ws": "^8.5.13",
64
64
  "esbuild": "^0.28.2",
65
65
  "typescript": "^5.6.0"
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.1.2"
2
+ <ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.2.1"
3
3
  ExtensionBundleName="AE MCP Bridge" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4
4
  <ExtensionList>
5
- <Extension Id="games.engine-room.ae-mcp.panel" Version="0.1.2" />
5
+ <Extension Id="games.engine-room.ae-mcp.panel" Version="0.2.1" />
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -11,6 +11,13 @@ CSInterface.prototype.evalScript = function (script, callback) {
11
11
  CSInterface.prototype.getSystemPath = function (pathType) {
12
12
  var path = window.__adobe_cep__.getSystemPath(pathType);
13
13
  if (path && path.indexOf("file://") === 0) path = decodeURIComponent(path.substring(7));
14
+ // CEP hands back a file URL, so on Windows the drive letter arrives still
15
+ // carrying the URL's leading slash: /C:/Users/... Node reads that as
16
+ // root-relative — path.join turns it into \C:\...\jsx\bundle.jsx, which no
17
+ // fs call can open, and the panel reports the bundle missing when it is
18
+ // sitting right there. Strip it here rather than at each call site: this is
19
+ // the one place a URL becomes a native path.
20
+ if (path) path = path.replace(/^\/([A-Za-z]:)/, "$1");
14
21
  return path;
15
22
  };
16
23
 
@@ -10,14 +10,13 @@
10
10
  var fs = require("fs");
11
11
  var os = require("os");
12
12
  var http = require("http");
13
- var WebSocket;
14
- try { WebSocket = require("ws"); }
15
- catch (e) {
16
- // ws is bundled in packages/ae-panel/node_modules; resolve manually if normal require fails.
17
- var alt = path.join(__dirname, "..", "node_modules", "ws");
18
- WebSocket = require(alt);
19
- }
13
+ var crypto = require("crypto");
20
14
 
15
+ // The DOM handles and the logger come first so that everything below is able
16
+ // to report its own failure. `require("ws")` used to run before this point,
17
+ // so when it threw the exception escaped this whole function before a single
18
+ // line could be written — the panel sat on "starting…" indefinitely with the
19
+ // reason nowhere to be seen.
21
20
  var $status = document.getElementById("status");
22
21
  var $port = document.getElementById("port");
23
22
  var $ae = document.getElementById("ae");
@@ -40,6 +39,24 @@
40
39
  while ($log.childNodes.length > 80) $log.removeChild($log.lastChild);
41
40
  }
42
41
 
42
+ var WebSocket;
43
+ try { WebSocket = require("ws"); }
44
+ catch (primary) {
45
+ // ws is bundled in packages/ae-panel/node_modules; resolve manually if normal require fails.
46
+ var alt = path.join(__dirname, "..", "node_modules", "ws");
47
+ try { WebSocket = require(alt); }
48
+ catch (fallback) {
49
+ // Nothing below can be built without ws, so this stops here — but it
50
+ // stops saying why, and naming the fix, rather than looking like a panel
51
+ // that is still starting up.
52
+ setStatus("cannot start — the ws module is missing", "err");
53
+ log("error", "require('ws') failed: " + primary.message);
54
+ log("error", "and " + alt + ": " + fallback.message);
55
+ log("error", "Quit After Effects, run the setup_panel tool, then reopen it.");
56
+ return;
57
+ }
58
+ }
59
+
43
60
  var cs = new CSInterface();
44
61
  var extDir = cs.getSystemPath(SystemPath.EXTENSION);
45
62
  var bundlePath = path.join(extDir, "jsx", "bundle.jsx");
@@ -84,6 +101,24 @@
84
101
  }
85
102
 
86
103
  // ---------- Load bundle.jsx ----------
104
+ // Hash of the bundle *actually evaluated into ExtendScript*, reported on
105
+ // /health. The server compares it against the bundle it ships to decide
106
+ // whether the running panel understands the ops it is about to send.
107
+ //
108
+ // It has to be captured here rather than read off disk on demand: after
109
+ // setup_panel refreshes the extension folder, the file on disk is new while
110
+ // this process is still running the old code, and that gap — between
111
+ // installing an update and restarting AE — is exactly when calls fail.
112
+ var loadedBundleHash = null;
113
+
114
+ function hashFile(file) {
115
+ try {
116
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
117
+ } catch (e) {
118
+ return null;
119
+ }
120
+ }
121
+
87
122
  function loadJsxBundle() {
88
123
  setStatus("Loading bundle.jsx…");
89
124
  if (!fs.existsSync(bundlePath)) {
@@ -95,7 +130,12 @@
95
130
  var loadScript = "$.evalFile(" + JSON.stringify(bundlePath) + "); typeof dispatch === 'function' ? 'ok' : 'no-dispatch';";
96
131
  return new Promise(function (resolve, reject) {
97
132
  cs.evalScript(loadScript, function (raw) {
98
- if (raw === "ok") { $jsx.textContent = "loaded"; $jsx.className = "ok"; resolve(); }
133
+ if (raw === "ok") {
134
+ loadedBundleHash = hashFile(bundlePath);
135
+ $jsx.textContent = "loaded";
136
+ $jsx.className = "ok";
137
+ resolve();
138
+ }
99
139
  else { $jsx.textContent = "fail: " + raw; $jsx.className = "err"; reject(new Error("dispatch not defined after loading bundle.jsx: " + raw)); }
100
140
  });
101
141
  });
@@ -245,7 +285,15 @@
245
285
  if (url === "/health") {
246
286
  res.statusCode = 200;
247
287
  res.setHeader("content-type", "application/json");
248
- res.end(JSON.stringify({ ok: true, port: port, bundleLoaded: true, ts: Date.now() }));
288
+ // bundleHash identifies the code this panel is *running*, which is what
289
+ // the server needs to know before sending an op the panel may predate.
290
+ res.end(JSON.stringify({
291
+ ok: true,
292
+ port: port,
293
+ bundleLoaded: true,
294
+ bundleHash: loadedBundleHash,
295
+ ts: Date.now()
296
+ }));
249
297
  return;
250
298
  }
251
299
 
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bundle. Do not edit directly — edit files in packages/jsx/.
2
- // Generated 2026-08-11T09:11:46.741Z
2
+ // Generated 2026-08-12T07:48:29.949Z
3
3
 
4
4
  // ===== core.jsx =====
5
5
 
@@ -1222,6 +1222,123 @@ OPS.screenshot_layer = noUndo(function (args) {
1222
1222
  });
1223
1223
 
1224
1224
 
1225
+ // ===== style.jsx =====
1226
+
1227
+ // style.jsx — the project's house style, read from and written to a plain
1228
+ // markdown file sitting next to the .aep.
1229
+ //
1230
+ // Why here rather than in the MCP server: the bridge into After Effects is the
1231
+ // one channel every client has, because the whole product already depends on it.
1232
+ // A server-side file would need a project folder, and the clients that matter
1233
+ // most here are exactly the ones that do not have one — Claude Desktop starts
1234
+ // its servers at the filesystem root. Reading the style over the bridge needs no
1235
+ // working directory, no filesystem tools on the client, and no configuration.
1236
+ //
1237
+ // The cost is that the project must have been saved once. `app.project.file` is
1238
+ // null until then, and there is no folder to write into. That is reported, never
1239
+ // guessed around.
1240
+
1241
+ var HOUSE_STYLE_FILENAME = "house-style.md";
1242
+
1243
+ /** The .aep's folder, or null when the project has never been saved. */
1244
+ function __projectFolder() {
1245
+ var f = app.project.file;
1246
+ if (!f) return null;
1247
+ return f.parent;
1248
+ }
1249
+
1250
+ function __houseStyleFile() {
1251
+ var folder = __projectFolder();
1252
+ if (!folder) return null;
1253
+ return new File(folder.fsName + "/" + HOUSE_STYLE_FILENAME);
1254
+ }
1255
+
1256
+ /** Shared shape for "there is nowhere to put it", so both ops explain it the same way. */
1257
+ function __unsavedProject() {
1258
+ return {
1259
+ found: false,
1260
+ projectSaved: false,
1261
+ path: null,
1262
+ content: null,
1263
+ reason:
1264
+ "This After Effects project has never been saved, so there is no folder to keep the style guide in. " +
1265
+ "Ask the user to save the project, then try again."
1266
+ };
1267
+ }
1268
+
1269
+ OPS.get_house_style = noUndo(function () {
1270
+ var file = __houseStyleFile();
1271
+ if (!file) return __unsavedProject();
1272
+
1273
+ if (!file.exists) {
1274
+ return {
1275
+ found: false,
1276
+ projectSaved: true,
1277
+ path: file.fsName,
1278
+ content: null,
1279
+ reason: "No style guide for this project yet. Offer to capture one from a comp the user likes."
1280
+ };
1281
+ }
1282
+
1283
+ // UTF-8 explicitly: ExtendScript otherwise decodes with the system encoding,
1284
+ // which mangles any non-ASCII the designer typed (curly quotes, em dashes,
1285
+ // accented font names).
1286
+ file.encoding = "UTF-8";
1287
+ if (!file.open("r")) throw new Error("Could not open " + file.fsName + " for reading");
1288
+ var content;
1289
+ try { content = file.read(); }
1290
+ finally { file.close(); }
1291
+
1292
+ return {
1293
+ found: true,
1294
+ projectSaved: true,
1295
+ path: file.fsName,
1296
+ content: content,
1297
+ bytes: content.length
1298
+ };
1299
+ });
1300
+
1301
+ OPS.set_house_style = noUndo(function (args) {
1302
+ var content = args && args.content;
1303
+ if (typeof content !== "string" || content.length === 0) {
1304
+ throw new Error("content is required and must not be empty");
1305
+ }
1306
+
1307
+ var file = __houseStyleFile();
1308
+ if (!file) {
1309
+ var unsaved = __unsavedProject();
1310
+ throw new Error(unsaved.reason);
1311
+ }
1312
+
1313
+ var existed = file.exists;
1314
+ // Replacing someone's hand-written style guide is not something to do on a
1315
+ // half-remembered version of it, so the caller has to have read it first and
1316
+ // send the whole document back. Refusing here is cheaper than an apology.
1317
+ if (existed && args.overwrite !== true) {
1318
+ throw new Error(
1319
+ "A style guide already exists at " + file.fsName + ". Read it with get_house_style, " +
1320
+ "merge your changes into the full document, and call again with overwrite: true."
1321
+ );
1322
+ }
1323
+
1324
+ file.encoding = "UTF-8";
1325
+ if (!file.open("w")) throw new Error("Could not open " + file.fsName + " for writing");
1326
+ try {
1327
+ if (!file.write(content)) throw new Error("Write failed for " + file.fsName);
1328
+ } finally {
1329
+ file.close();
1330
+ }
1331
+
1332
+ return {
1333
+ ok: true,
1334
+ path: file.fsName,
1335
+ bytes: content.length,
1336
+ created: !existed,
1337
+ replaced: existed
1338
+ };
1339
+ });
1340
+
1341
+
1225
1342
  // ===== batch.jsx =====
1226
1343
 
1227
1344
  // batch.jsx — execute many ops in one ExtendScript pass.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@engineroom/ae-panel",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "private": true,
5
5
  "description": "Invisible CEP extension hosting an HTTP+WS bridge inside After Effects.",
6
6
  "dependencies": {
7
- "ws": "^8.18.0"
7
+ "ws": "^8.21.3"
8
8
  }
9
9
  }