@engine-room/after-effects-mcp 0.1.1 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engine-room/after-effects-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
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.1"
2
+ <ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.2.0"
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.1" />
5
+ <Extension Id="games.engine-room.ae-mcp.panel" Version="0.2.0" />
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,6 +10,7 @@
10
10
  var fs = require("fs");
11
11
  var os = require("os");
12
12
  var http = require("http");
13
+ var crypto = require("crypto");
13
14
  var WebSocket;
14
15
  try { WebSocket = require("ws"); }
15
16
  catch (e) {
@@ -84,6 +85,24 @@
84
85
  }
85
86
 
86
87
  // ---------- Load bundle.jsx ----------
88
+ // Hash of the bundle *actually evaluated into ExtendScript*, reported on
89
+ // /health. The server compares it against the bundle it ships to decide
90
+ // whether the running panel understands the ops it is about to send.
91
+ //
92
+ // It has to be captured here rather than read off disk on demand: after
93
+ // setup_panel refreshes the extension folder, the file on disk is new while
94
+ // this process is still running the old code, and that gap — between
95
+ // installing an update and restarting AE — is exactly when calls fail.
96
+ var loadedBundleHash = null;
97
+
98
+ function hashFile(file) {
99
+ try {
100
+ return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
101
+ } catch (e) {
102
+ return null;
103
+ }
104
+ }
105
+
87
106
  function loadJsxBundle() {
88
107
  setStatus("Loading bundle.jsx…");
89
108
  if (!fs.existsSync(bundlePath)) {
@@ -95,7 +114,12 @@
95
114
  var loadScript = "$.evalFile(" + JSON.stringify(bundlePath) + "); typeof dispatch === 'function' ? 'ok' : 'no-dispatch';";
96
115
  return new Promise(function (resolve, reject) {
97
116
  cs.evalScript(loadScript, function (raw) {
98
- if (raw === "ok") { $jsx.textContent = "loaded"; $jsx.className = "ok"; resolve(); }
117
+ if (raw === "ok") {
118
+ loadedBundleHash = hashFile(bundlePath);
119
+ $jsx.textContent = "loaded";
120
+ $jsx.className = "ok";
121
+ resolve();
122
+ }
99
123
  else { $jsx.textContent = "fail: " + raw; $jsx.className = "err"; reject(new Error("dispatch not defined after loading bundle.jsx: " + raw)); }
100
124
  });
101
125
  });
@@ -245,7 +269,15 @@
245
269
  if (url === "/health") {
246
270
  res.statusCode = 200;
247
271
  res.setHeader("content-type", "application/json");
248
- res.end(JSON.stringify({ ok: true, port: port, bundleLoaded: true, ts: Date.now() }));
272
+ // bundleHash identifies the code this panel is *running*, which is what
273
+ // the server needs to know before sending an op the panel may predate.
274
+ res.end(JSON.stringify({
275
+ ok: true,
276
+ port: port,
277
+ bundleLoaded: true,
278
+ bundleHash: loadedBundleHash,
279
+ ts: Date.now()
280
+ }));
249
281
  return;
250
282
  }
251
283
 
@@ -1,5 +1,5 @@
1
1
  // Auto-generated bundle. Do not edit directly — edit files in packages/jsx/.
2
- // Generated 2026-08-10T15:26:38.133Z
2
+ // Generated 2026-08-12T07:06:29.484Z
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.1",
3
+ "version": "0.2.0",
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
  }